Skip to content

Commit

Permalink
fix: prototype duplicate prevention
Browse files Browse the repository at this point in the history
  • Loading branch information
ChinHairSaintClair committed Jan 15, 2025
1 parent cfa682f commit 9eb471f
Show file tree
Hide file tree
Showing 15 changed files with 749 additions and 16 deletions.
27 changes: 27 additions & 0 deletions webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
"select2": "4.0.3",
"signature_pad": "2.3.x",
"tslib": "^2.5.3",
"zone.js": "^0.14.4"
"zone.js": "^0.14.4",
"levenshtein": "1.0.5",
"@types/levenshtein": "1.0.4"
},
"overrides": {
"minimist": ">=1.2.6"
Expand Down
52 changes: 52 additions & 0 deletions webapp/src/css/enketo/medic.less
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,58 @@
.pages.or .or-repeat-info[role="page"] {
display: block;
}

#duplicate_info {
width: 100%;
min-height: 20px;
padding-left: 20px;
padding-right: 20px;
background-color: #ffe7e8;

.results_header {
font-size: large;
color: #e33030;
}

.acknowledge_label {
-webkit-user-select: none; -ms-user-select: none; user-select: none;
}

.acknowledge_checkbox {
margin-right: 5px;
}

.divider {
background-color: #e33030;
height: 1px;
margin-top: 5px;
margin-bottom: 5px;
}

.card {
border: 1px solid #ddd;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 5px;
}

.nested-section {
margin-left: 1.5rem;
}

.toggle-button {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-weight: bold;
padding-left: 0px;
}

.toggle-button:hover {
text-decoration: underline;
}
}
}

@media (max-width: @media-mobile) {
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/ts/components/components.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c
import { SidebarMenuComponent } from '@mm-components/sidebar-menu/sidebar-menu.component';
import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component';
import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/training-cards-form.component';
import {DuplicateInfoComponent} from '@mm-components/duplicate-info/duplicate-info.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -78,6 +79,7 @@ import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/t
SidebarMenuComponent,
TrainingCardsFormComponent,
ToolBarComponent,
DuplicateInfoComponent,
],
imports: [
CommonModule,
Expand Down Expand Up @@ -122,6 +124,7 @@ import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/t
SidebarMenuComponent,
TrainingCardsFormComponent,
ToolBarComponent,
DuplicateInfoComponent,
]
})
export class ComponentsModule { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<div
id="duplicate_info"
[ngStyle]="{ display: duplicates.length > 0 ? 'block' : 'none' }">
<p class="results_header">{{ duplicates.length }} {{'potential duplicate item(s) found:' | translate }}</p>
<div class="divider"></div>
<div>
<label for="check" class="acknowledge_label">
<input
id="check"
type="checkbox"
[checked]="acknowledged"
(change)="toggleAcknowledged()"
class="acknowledge_checkbox"/>
{{'Acknowledge duplicate siblings and proceed with submission' | translate }}
</label>
</div>
<div class="divider"></div>
<div *ngFor="let duplicate of duplicates; let i = index" >
<div class="card">
<strong>{{ 'Item number:' | translate }}</strong> {{ i + 1 }}
<hr>
<p>
<strong>{{ 'Name:' | translate }}</strong> {{ duplicate.name }}
<br>
<strong>{{ 'Created on:' | translate }}</strong> {{ duplicate.reported_date | date: 'EEE MMM dd yyyy HH:mm:ss' }}
</p>
<button class="toggle-button" (click)="toggleSection(getPath('', duplicate._id))">
{{ isExpanded(getPath('', duplicate._id)) ? '▼' : '▶' }}
{{ (isExpanded(getPath('', duplicate._id)) ? 'Show less details' : 'Show more details') | translate }}
</button>
<div *ngIf="isExpanded(getPath('', duplicate._id))" class="nested-section">
<ng-container *ngTemplateOutlet="renderObject; context: { obj: duplicate, path: duplicate._id }"></ng-container>
</div>
<hr>
<button class="btn submit btn-primary" (click)="_navigateToDuplicate(duplicate._id)">{{ 'Take me there' | translate }}</button>
</div>
</div>
<ng-template #renderObject let-obj="obj" let-path="path">
<div *ngFor="let key of obj | keyvalue">
<div *ngIf="isObject(key.value); else primitiveValue">
<button class="toggle-button" (click)="toggleSection(getPath(path, key.key))">
{{ isExpanded(getPath(path, key.key)) ? '▼' : '▶' }} {{ key.key }}
</button>
<div *ngIf="isExpanded(getPath(path, key.key))" class="nested-section">
<ng-container *ngTemplateOutlet="renderObject; context: { obj: key.value, path: getPath(path, key.key) }"></ng-container>
</div>
</div>
<ng-template #primitiveValue>
<strong>{{ key.key }}:</strong> {{ key.value }}
</ng-template>
</div>
</ng-template>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
selector: 'mm-duplicate-info',
templateUrl: './duplicate-info.component.html',
})
export class DuplicateInfoComponent {
@Input() acknowledged: boolean = false;
@Output() acknowledgedChange = new EventEmitter<boolean>();
@Output() navigateToDuplicate = new EventEmitter<string>();
@Input() duplicates: { _id: string; name: string; reported_date: string | Date; [key: string]: string | Date }[] = [];

toggleAcknowledged() {
this.acknowledged = !this.acknowledged;
this.acknowledgedChange.emit(this.acknowledged);
}

_navigateToDuplicate(_id: string){
this.navigateToDuplicate.emit(_id);
}

// Handles collapse / expand of duplicate doc details
expandedSections = new Map<string, boolean>();

toggleSection(path: string): void {
this.expandedSections.set(path, !this.expandedSections.get(path));
}

isExpanded(path: string): boolean {
return this.expandedSections.get(path) || false;
}

isObject(value: any): boolean {
return value && typeof value === 'object' && !Array.isArray(value);
}

getPath(parentPath: string, key: string): string {
return parentPath ? `${parentPath}.${key}` : key;
}
}
1 change: 1 addition & 0 deletions webapp/src/ts/components/enketo/enketo.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div [attr.id]="formId" class="enketo" [attr.data-editing]="editing">
<div class="container pages"></div>
<ng-content select="[duplicate-info]"></ng-content> <!-- placeholder -->
<div class="form-footer">
<button (click)="onCancel.emit()" class="btn btn-link cancel" [disabled]="status?.saving">{{'Cancel' | translate}}</button>
<div class="loader inline small" *ngIf="status?.saving"></div>
Expand Down
6 changes: 5 additions & 1 deletion webapp/src/ts/modules/contacts/contacts-edit.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
</div>
<div class="col-sm-8 item-content material" [hidden]="loadingContent || contentError">
<div class="card">
<mm-enketo formId="contact-form" [editing]="enketoContact?.docId" [status]="enketoStatus" (onSubmit)="save()" (onCancel)="navigationCancel()"></mm-enketo>
<mm-enketo formId="contact-form" [editing]="enketoContact?.docId" [status]="enketoStatus" (onSubmit)="save()" (onCancel)="navigationCancel()">
<div duplicate-info>
<mm-duplicate-info [acknowledged]="acknowledged" [duplicates]="duplicates" (acknowledgedChange)="onAcknowledgeChange($event)" (navigateToDuplicate)="onNavigateToDuplicate($event)"></mm-duplicate-info>
</div>
</mm-enketo>
</div>
</div>
</div>
28 changes: 26 additions & 2 deletions webapp/src/ts/modules/contacts/contacts-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isEqual as _isEqual } from 'lodash-es';
import { ActivatedRoute, Router } from '@angular/router';

import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service';
import { FormService } from '@mm-services/form.service';
import { FormService, DuplicatesFoundError, Duplicate } from '@mm-services/form.service';
import { EnketoFormContext } from '@mm-services/enketo.service';
import { ContactTypesService } from '@mm-services/contact-types.service';
import { DbService } from '@mm-services/db.service';
Expand Down Expand Up @@ -55,6 +55,18 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
private trackSave;
private trackMetadata = { action: '', form: '' };

private duplicateCheck;
acknowledged = false;
onAcknowledgeChange(value: boolean) {
this.acknowledged = value;
}

onNavigateToDuplicate(_id: string){
this.router.navigate(['/contacts', _id, 'edit']);
}

duplicates: Duplicate[] = [];

ngOnInit() {
this.trackRender = this.performanceService.track();
this.subscribeToStore();
Expand Down Expand Up @@ -153,6 +165,10 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
this.contentError = false;
this.errorTranslationKey = false;

// Reset when when navigated to duplicate
this.duplicates = [];
this.acknowledged = false;

try {
const contact = await this.getContact();
const contactTypeId = this.contactTypesService.getTypeId(contact) || this.routeSnapshot.params?.type;
Expand Down Expand Up @@ -272,6 +288,7 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
private async renderForm(formId: string, titleKey: string) {
const formDoc = await this.dbService.get().get(formId);
this.xmlVersion = formDoc.xmlVersion;
this.duplicateCheck = formDoc.context?.duplicate_check;

this.globalActions.setEnketoEditedStatus(false);

Expand Down Expand Up @@ -326,7 +343,9 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
$('form.or').trigger('beforesave');

return this.formService
.saveContact(form, docId, this.enketoContact.type, this.xmlVersion)
.saveContact({
form, docId, type: this.enketoContact.type, xmlVersion: this.xmlVersion
}, this.duplicateCheck, this.acknowledged)
.then((result) => {
console.debug('saved contact', result);

Expand All @@ -345,6 +364,11 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
this.router.navigate(['/contacts', result.docId]);
})
.catch((err) => {
if (err instanceof DuplicatesFoundError){
this.duplicates = err.duplicates;
err = Error(err.message);
}

console.error('Error submitting form data', err);

this.globalActions.setEnketoSavingStatus(false);
Expand Down
Loading

0 comments on commit 9eb471f

Please sign in to comment.