Skip to content

Commit 9eb471f

Browse files
fix: prototype duplicate prevention
1 parent cfa682f commit 9eb471f

15 files changed

+749
-16
lines changed

webapp/package-lock.json

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webapp/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@
6161
"select2": "4.0.3",
6262
"signature_pad": "2.3.x",
6363
"tslib": "^2.5.3",
64-
"zone.js": "^0.14.4"
64+
"zone.js": "^0.14.4",
65+
"levenshtein": "1.0.5",
66+
"@types/levenshtein": "1.0.4"
6567
},
6668
"overrides": {
6769
"minimist": ">=1.2.6"

webapp/src/css/enketo/medic.less

+52
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,58 @@
443443
.pages.or .or-repeat-info[role="page"] {
444444
display: block;
445445
}
446+
447+
#duplicate_info {
448+
width: 100%;
449+
min-height: 20px;
450+
padding-left: 20px;
451+
padding-right: 20px;
452+
background-color: #ffe7e8;
453+
454+
.results_header {
455+
font-size: large;
456+
color: #e33030;
457+
}
458+
459+
.acknowledge_label {
460+
-webkit-user-select: none; -ms-user-select: none; user-select: none;
461+
}
462+
463+
.acknowledge_checkbox {
464+
margin-right: 5px;
465+
}
466+
467+
.divider {
468+
background-color: #e33030;
469+
height: 1px;
470+
margin-top: 5px;
471+
margin-bottom: 5px;
472+
}
473+
474+
.card {
475+
border: 1px solid #ddd;
476+
padding: 1rem;
477+
margin-bottom: 1rem;
478+
border-radius: 5px;
479+
}
480+
481+
.nested-section {
482+
margin-left: 1.5rem;
483+
}
484+
485+
.toggle-button {
486+
background: none;
487+
border: none;
488+
color: #007bff;
489+
cursor: pointer;
490+
font-weight: bold;
491+
padding-left: 0px;
492+
}
493+
494+
.toggle-button:hover {
495+
text-decoration: underline;
496+
}
497+
}
446498
}
447499

448500
@media (max-width: @media-mobile) {

webapp/src/ts/components/components.module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { PanelHeaderComponent } from '@mm-components/panel-header/panel-header.c
4747
import { SidebarMenuComponent } from '@mm-components/sidebar-menu/sidebar-menu.component';
4848
import { ToolBarComponent } from '@mm-components/tool-bar/tool-bar.component';
4949
import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/training-cards-form.component';
50+
import {DuplicateInfoComponent} from '@mm-components/duplicate-info/duplicate-info.component';
5051

5152
@NgModule({
5253
declarations: [
@@ -78,6 +79,7 @@ import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/t
7879
SidebarMenuComponent,
7980
TrainingCardsFormComponent,
8081
ToolBarComponent,
82+
DuplicateInfoComponent,
8183
],
8284
imports: [
8385
CommonModule,
@@ -122,6 +124,7 @@ import { TrainingCardsFormComponent } from '@mm-components/training-cards-form/t
122124
SidebarMenuComponent,
123125
TrainingCardsFormComponent,
124126
ToolBarComponent,
127+
DuplicateInfoComponent,
125128
]
126129
})
127130
export class ComponentsModule { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<div
2+
id="duplicate_info"
3+
[ngStyle]="{ display: duplicates.length > 0 ? 'block' : 'none' }">
4+
<p class="results_header">{{ duplicates.length }} {{'potential duplicate item(s) found:' | translate }}</p>
5+
<div class="divider"></div>
6+
<div>
7+
<label for="check" class="acknowledge_label">
8+
<input
9+
id="check"
10+
type="checkbox"
11+
[checked]="acknowledged"
12+
(change)="toggleAcknowledged()"
13+
class="acknowledge_checkbox"/>
14+
{{'Acknowledge duplicate siblings and proceed with submission' | translate }}
15+
</label>
16+
</div>
17+
<div class="divider"></div>
18+
<div *ngFor="let duplicate of duplicates; let i = index" >
19+
<div class="card">
20+
<strong>{{ 'Item number:' | translate }}</strong> {{ i + 1 }}
21+
<hr>
22+
<p>
23+
<strong>{{ 'Name:' | translate }}</strong> {{ duplicate.name }}
24+
<br>
25+
<strong>{{ 'Created on:' | translate }}</strong> {{ duplicate.reported_date | date: 'EEE MMM dd yyyy HH:mm:ss' }}
26+
</p>
27+
<button class="toggle-button" (click)="toggleSection(getPath('', duplicate._id))">
28+
{{ isExpanded(getPath('', duplicate._id)) ? '▼' : '▶' }}
29+
{{ (isExpanded(getPath('', duplicate._id)) ? 'Show less details' : 'Show more details') | translate }}
30+
</button>
31+
<div *ngIf="isExpanded(getPath('', duplicate._id))" class="nested-section">
32+
<ng-container *ngTemplateOutlet="renderObject; context: { obj: duplicate, path: duplicate._id }"></ng-container>
33+
</div>
34+
<hr>
35+
<button class="btn submit btn-primary" (click)="_navigateToDuplicate(duplicate._id)">{{ 'Take me there' | translate }}</button>
36+
</div>
37+
</div>
38+
<ng-template #renderObject let-obj="obj" let-path="path">
39+
<div *ngFor="let key of obj | keyvalue">
40+
<div *ngIf="isObject(key.value); else primitiveValue">
41+
<button class="toggle-button" (click)="toggleSection(getPath(path, key.key))">
42+
{{ isExpanded(getPath(path, key.key)) ? '▼' : '▶' }} {{ key.key }}
43+
</button>
44+
<div *ngIf="isExpanded(getPath(path, key.key))" class="nested-section">
45+
<ng-container *ngTemplateOutlet="renderObject; context: { obj: key.value, path: getPath(path, key.key) }"></ng-container>
46+
</div>
47+
</div>
48+
<ng-template #primitiveValue>
49+
<strong>{{ key.key }}:</strong> {{ key.value }}
50+
</ng-template>
51+
</div>
52+
</ng-template>
53+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Component, EventEmitter, Input, Output } from '@angular/core';
2+
3+
@Component({
4+
selector: 'mm-duplicate-info',
5+
templateUrl: './duplicate-info.component.html',
6+
})
7+
export class DuplicateInfoComponent {
8+
@Input() acknowledged: boolean = false;
9+
@Output() acknowledgedChange = new EventEmitter<boolean>();
10+
@Output() navigateToDuplicate = new EventEmitter<string>();
11+
@Input() duplicates: { _id: string; name: string; reported_date: string | Date; [key: string]: string | Date }[] = [];
12+
13+
toggleAcknowledged() {
14+
this.acknowledged = !this.acknowledged;
15+
this.acknowledgedChange.emit(this.acknowledged);
16+
}
17+
18+
_navigateToDuplicate(_id: string){
19+
this.navigateToDuplicate.emit(_id);
20+
}
21+
22+
// Handles collapse / expand of duplicate doc details
23+
expandedSections = new Map<string, boolean>();
24+
25+
toggleSection(path: string): void {
26+
this.expandedSections.set(path, !this.expandedSections.get(path));
27+
}
28+
29+
isExpanded(path: string): boolean {
30+
return this.expandedSections.get(path) || false;
31+
}
32+
33+
isObject(value: any): boolean {
34+
return value && typeof value === 'object' && !Array.isArray(value);
35+
}
36+
37+
getPath(parentPath: string, key: string): string {
38+
return parentPath ? `${parentPath}.${key}` : key;
39+
}
40+
}

webapp/src/ts/components/enketo/enketo.component.html

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<div [attr.id]="formId" class="enketo" [attr.data-editing]="editing">
22
<div class="container pages"></div>
3+
<ng-content select="[duplicate-info]"></ng-content> <!-- placeholder -->
34
<div class="form-footer">
45
<button (click)="onCancel.emit()" class="btn btn-link cancel" [disabled]="status?.saving">{{'Cancel' | translate}}</button>
56
<div class="loader inline small" *ngIf="status?.saving"></div>

webapp/src/ts/modules/contacts/contacts-edit.component.html

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
</div>
1010
<div class="col-sm-8 item-content material" [hidden]="loadingContent || contentError">
1111
<div class="card">
12-
<mm-enketo formId="contact-form" [editing]="enketoContact?.docId" [status]="enketoStatus" (onSubmit)="save()" (onCancel)="navigationCancel()"></mm-enketo>
12+
<mm-enketo formId="contact-form" [editing]="enketoContact?.docId" [status]="enketoStatus" (onSubmit)="save()" (onCancel)="navigationCancel()">
13+
<div duplicate-info>
14+
<mm-duplicate-info [acknowledged]="acknowledged" [duplicates]="duplicates" (acknowledgedChange)="onAcknowledgeChange($event)" (navigateToDuplicate)="onNavigateToDuplicate($event)"></mm-duplicate-info>
15+
</div>
16+
</mm-enketo>
1317
</div>
1418
</div>
1519
</div>

webapp/src/ts/modules/contacts/contacts-edit.component.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isEqual as _isEqual } from 'lodash-es';
55
import { ActivatedRoute, Router } from '@angular/router';
66

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

58+
private duplicateCheck;
59+
acknowledged = false;
60+
onAcknowledgeChange(value: boolean) {
61+
this.acknowledged = value;
62+
}
63+
64+
onNavigateToDuplicate(_id: string){
65+
this.router.navigate(['/contacts', _id, 'edit']);
66+
}
67+
68+
duplicates: Duplicate[] = [];
69+
5870
ngOnInit() {
5971
this.trackRender = this.performanceService.track();
6072
this.subscribeToStore();
@@ -153,6 +165,10 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
153165
this.contentError = false;
154166
this.errorTranslationKey = false;
155167

168+
// Reset when when navigated to duplicate
169+
this.duplicates = [];
170+
this.acknowledged = false;
171+
156172
try {
157173
const contact = await this.getContact();
158174
const contactTypeId = this.contactTypesService.getTypeId(contact) || this.routeSnapshot.params?.type;
@@ -272,6 +288,7 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
272288
private async renderForm(formId: string, titleKey: string) {
273289
const formDoc = await this.dbService.get().get(formId);
274290
this.xmlVersion = formDoc.xmlVersion;
291+
this.duplicateCheck = formDoc.context?.duplicate_check;
275292

276293
this.globalActions.setEnketoEditedStatus(false);
277294

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

328345
return this.formService
329-
.saveContact(form, docId, this.enketoContact.type, this.xmlVersion)
346+
.saveContact({
347+
form, docId, type: this.enketoContact.type, xmlVersion: this.xmlVersion
348+
}, this.duplicateCheck, this.acknowledged)
330349
.then((result) => {
331350
console.debug('saved contact', result);
332351

@@ -345,6 +364,11 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
345364
this.router.navigate(['/contacts', result.docId]);
346365
})
347366
.catch((err) => {
367+
if (err instanceof DuplicatesFoundError){
368+
this.duplicates = err.duplicates;
369+
err = Error(err.message);
370+
}
371+
348372
console.error('Error submitting form data', err);
349373

350374
this.globalActions.setEnketoSavingStatus(false);

0 commit comments

Comments
 (0)