fix(#9601): prototype duplicate prevention#9609
Conversation
48d32cc to
e266863
Compare
| queryParams: { | ||
| valuePaths: ['/data/health_center/is_user_flagged_duplicate', '/data/health_center/duplicate/action'], | ||
| // eslint-disable-next-line eqeqeq | ||
| query: (duplicate, action) => duplicate === 'yes' && action != null |
There was a problem hiding this comment.
@jkuester @ChinHairSaintClair My feeling here is that this would need to end up with some kind of operator syntax support that lets the user do logic, maybe something like (though doesn't have to be) JsonLogic: https://jsonlogic.com/
The way I see it there are two point of config -- the excel files and the configuration json -- and probably neither should take serialised JS (?).
If we return the key => { key, value } as a tuple or object on key lookup, we could let the user express logic maybe like this (with the value found at the key tested against the value provided (with) using the operator):
let andOnly = {
logic: [
[
// array bracket denotes a logical grouping
{
key1: { with: value, op: $in },
key2: { with: value, op: $contains },
key3: { with: value, op: $eq },
},
{
key1: { with: value, op: $in },
key2: { with: value, op: $ne },
key3: { with: value, op: $startsWith },
},
],
],
};
let orWithNestedAnds = {
logic: [
[
{
key1: { with: value, op: $in },
},
{
key2: { with: value, op: $eq },
},
],
// each grouping in its own bracket
[
{
key1: { with: value, op: $in },
},
{
key2: { with: value, op: $in },
},
],
],
};
let orWithNestedOrWithNestedAnds = {
logic: [
[
{
key1: { with: value, op: $in },
},
{
key2: { with: value, op: $in },
},
],
// OR
[
[
// nested groupings acceptable
{
key1: { with: value, op: $ne },
},
// AND
{
key2: { with: value, op: $ne },
},
],
// OR
[
{
key1: { with: value, op: $contains },
},
// AND
{
key1: { with: value, op: $startsWith },
},
],
],
],
};The other stuff seems easier to move to config.
| EnketoForm:any; | ||
| _phdcChanges: { // Additional namespace | ||
| // Specify your own contact_types here | ||
| hierarchyDuplicatePrevention: Partial<{[key in 'person' | 'health_center']: Strategy;}>; |
There was a problem hiding this comment.
@jkuester @ChinHairSaintClair Following on from my above comment:
The way I see it there are two point of config -- the excel files and the configuration json.
We probably couldn't expect the user to do config here. I agree the type safety is lovely and I'm not sure whether it's possible to generate a type from the config, but if not I don't see how we can keep the type safety and still maintain the existing configuration contract.
| }; | ||
| } | ||
|
|
||
| private parseXmlForm(form): Document | undefined { |
There was a problem hiding this comment.
I'd move as much of this sort of stuff as is possible to a lib/file so it can be unit tested easily and then just called internally. Not sure if private is needed here.
(I know this was just a conceptual prototype not productionised, but for forward looking feedback.)
| } | ||
| } | ||
|
|
||
| return count > 0 ? totalScore / count : null; |
There was a problem hiding this comment.
Does the alternative return type need to be null or is there an appropriate default value of type int?
| } | ||
|
|
||
| // Promise.allSettled is not available due to the app's javascript version | ||
| private allSettledFallback(promises: Promise<Exclude<any, null | undefined>>[]): Promise<ReturnType[]> { |
There was a problem hiding this comment.
@jkuester @ChinHairSaintClair Would promise allSettled not be transpiled to a target version?
| const $duplicateInfoElement = $('#contact-form').find('#duplicate_info'); | ||
| $duplicateInfoElement.empty(); // Remove all child nodes | ||
| $duplicateInfoElement.show(); | ||
| // TODO: create a template component where these values are fed into. |
There was a problem hiding this comment.
Does this need DOM mutation? Can the angular template not handle it?
| count++; | ||
| } | ||
| $duplicateInfoElement.append(content); | ||
| $duplicateInfoElement.on('click', '.duplicate-navigate-link', () => { |
There was a problem hiding this comment.
No anon functions on event listeners please. They don't get properly cleaned up by the browser. They need to be named.
|
|
||
| export const NormalizedLevenshtein: Strategy = { | ||
| type: 'NormalizedLevenshtein', | ||
| threshold: 0.334, |
There was a problem hiding this comment.
You'll probably want to set this in config
| formService.saveContact.resolves({ docId: 'new_clinic_id' }); | ||
|
|
||
| // TODO: figure out why this test's dbLookupRef is null despite being set in the beforeEach | ||
| component.dbLookupRef = Promise.resolve({ |
There was a problem hiding this comment.
You may want sinon.resolves
| {form_prop_path: `/data/health_center/name`, db_doc_ref: 'name'}, | ||
| {form_prop_path: '/data/health_center/external_id', db_doc_ref: 'external_id'} | ||
| ], | ||
| queryParams: { |
There was a problem hiding this comment.
maybe formQuestion? Name is still bothering me.
| health_center: { | ||
| ...Levenshtein, | ||
| props: [ | ||
| {form_prop_path: `/data/health_center/name`, db_doc_ref: 'name'}, |
There was a problem hiding this comment.
camelCase is the js standard.
| ], | ||
| queryParams: { | ||
| valuePaths: ['/data/health_center/is_user_flagged_duplicate', '/data/health_center/duplicate/action'], | ||
| // eslint-disable-next-line eqeqeq |
There was a problem hiding this comment.
@jkuester Continuing to lobby for != as the most excellent exception to the strict equality lint rule. Checks for both null and undefined.
5442b21 to
9eb471f
Compare
There was a problem hiding this comment.
Wow, this is amazing! I think we are 100% on the right track here and I am excited to get this functionality released and in the hands of users.
I have not had a chance to look at all the changes yet, but I wanted to go ahead and share my comments so far (mostly focused on the internal service stuff and have not looked much yet at the UI code). Mostly just a bunch of minor questions/suggestions.
I provided some suggestions regarding the Sonar complaints for hasOwnProperty, but I have not had a chance to dig into the circular navigation issue yet.
Thank you for the great PR and the patience to work with us and get the across the finish line!
(FYI, I am following conventional comments for my PR comments, so that is where the comment prefixes are coming from...)
jkuester
left a comment
There was a problem hiding this comment.
Okay, I made it through the rest of the files. Added a few more comments, but all in all this is good stuff! I really appreciate the quality unit tests!
I am going to make a post out on the forum that demos the current "Duplicates Found" UI we have here. The idea would be to maybe crowd-source some UX design in case other folks have input on the look/feel/function.
Finally, before we merge this code (but probably after we finalize the UX) we will need to add some e2e tests. Since there are a lot of moving parts here (config + db docs + forms), we should probably be thorough in validating the main flows. My current thought is to add some new tests cases to tests/e2e/default/contacts/edit.wdio-spec.js and maybe create a new tests/e2e/default/contacts/create.wdio-spec.js.
|
Okay, I created a forum post demoing some of the functionality we are building here. Would love any UX feedback from your team on the look/feel/functionality! 🙏 |
07c4acc to
0201642
Compare
|
Sorry for the delayed response here! I will not be able to have a look at the revisions this week, but I plan to do so early next week. 👍 |
91be783 to
495dccf
Compare
jkuester
left a comment
There was a problem hiding this comment.
Things are coming along great here! 🤩
|
I've pushed up the UI changes, translation strings, duplicate-contacts expand/collapse functionality, and the shift of state management to the child component for your review @jkuester. I'll finish up the deduplication and form service next week. |
eddda4b to
6b6ca2d
Compare
jkuester
left a comment
There was a problem hiding this comment.
This is coming along great! Thank you for the updates!
264292f to
9220d81
Compare
|
@jkuester I ran into a bug in the top-level place duplicate check. Create two top-level places with the name 'test'. On the second creation, you will NOT be prompted with a duplicate check. Logging out/in fixes this. I believe this might be related to the cache used in the |
|
Oh wow @ChinHairSaintClair this is a good find! After debugging this, it seems like the cache invalidation for the The good news is that the fix should be simple. Basically, the So, line invalidate: ({ doc }) => type.id === this.contactTypesService.getTypeId(doc), |
9220d81 to
50cb4d0
Compare
50cb4d0 to
18bf65e
Compare
|
@jkuester , the PR should now be ready for the telemetry additions and a bundle size bump. |
- Added telemetry and performance tracking for duplicate contact lookup - Made dupe display more responsive to user selections - Added and fixed unit and e2e tests (including Enketo widgets) - Fixed linting issues - Minor UI tweaks and bug fixes - Bumped bundle size
|
Alright we are very close to getting this landed! 🎉 Here are the remaining tasks before I think we can consider this done:
|
|
Here is the docs PR: medic/cht-docs#1819 |
Better to default to English than to provide a potentially inaccurate local translation. Also updated the Nepali translation based on Binod's suggestion.
There was a problem hiding this comment.
Code is done! Tests are done! Docs are done! cht-conf updates are done! We were only missing translations for two languages and those langs are not required for merging/releasing this PR (can add the translations later when they are available).
So, I think I am going to hit the merge button on this and call it complete! 🚀
@ChinHairSaintClair thank you for your tireless efforts here to get this right! I am super proud of what we have built! (Also a big thank you to @fardarter for your support and assistance!)
|
wow! This PR was an amazing collaborative journey and a thing of real beauty to see in open source and to help the CHT. Thanks all!! |
…edic#9609) Co-authored-by: Joshua Kuestersteffen <jkuester@kuester7.com>
Description
This feature prevents duplicate hierarchy contact siblings from being created, as discussed with @jkuester and @mrjones-plip in a "technical working session" and through interactions on the linked issue thread.
To achieve this, we hook into duplicate detection strategies through "configuration", updated the
saveContactmethod in theform.service.tsfile to include an additional check, and display potential duplicates in aduplicate_infosection added to theenketo.component.htmlfile. Each duplicate is displayed as a card with expandible/collapsible segments, accompanied by an "acknowledgement" prompt to allow form submission.Configuration:
{ "expression":"levenshteinEq(3, current.name, existing.name)" }Currently two strategies are supported:
levenshteinEqandnormalizedLevenshteinEq, with the ability to customize properties based on implementation needs.Example implementation:
{ "title": [ { "locale": "en", "content": "New Household" }, ], "icon": "household-1", "context": { "expression": "contact.type === 'Indawo'", "permission": "can_register_household", "duplicate_check": { "expression":"levenshteinEq(4, current.name, existing.name)" } } }Here, the
duplicate_check.expressiondefines the logic for comparing the current record with its sibling. If noduplicate_checkis provided, the system defaults to evaluating thenamefield. E.g:street_number,street_name, andpostal_code:name,sex, anddate_of_birth:In these expressions,
currentrefers to the created/edited form, whileexistingrefers to a "sibling" loaded from the database.Conditional Duplicate Check:
This is a key requirement. For now, the
is_canonicalform question will be used to control conditional duplicate checking. When the backend flags a record as a duplicate, the CHW can mark the record accordingly. Downstream it will allow us tomergeordeletethe record based on the specified action.Opt-out:
Use the following configuration to disable the duplicate-checking functionality for a specific form:
{ "duplicate_check":{ "disabled":true } }Misc:
We use the CHT provided
medic-client/contacts_by_parentview to query for siblings.@kennsippell, since we've touched on the duplicate topic before, it would be great to get your thoughts as this as well.
#Issue
Closes #9601
Code review checklist
License
The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.