Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6df4caf
Initilize NewEnketoService
jkuester Jul 12, 2026
426cc44
try to work in contextElement
jkuester Jul 12, 2026
df13baf
Initial complete
jkuester Jul 12, 2026
2cefd19
Creating new reports working
jkuester Jul 13, 2026
04dc956
Edit reports seems to also work
jkuester Jul 13, 2026
8aac455
Add support for contacts
jkuester Jul 16, 2026
e2c6fad
Fix some bugs
jkuester Jul 16, 2026
c8572c8
Narrow the CI checks for now
jkuester Jul 16, 2026
ce42f0f
Update service to remove legacy `content`.
jkuester Jul 16, 2026
56e5eb0
Avoid writing empty attachments object
jkuester Jul 16, 2026
ac02e3d
Fix attachment handling on edits
jkuester Jul 16, 2026
eeeb217
Update whole flow to use FormConfig
jkuester Jul 17, 2026
b7c6cb8
Fix minor issues
jkuester Jul 17, 2026
b009775
Fix issue with form validation
jkuester Jul 17, 2026
171a25d
Fix contact type issue
jkuester Jul 17, 2026
e533acd
Diable transpiling webapp tests
jkuester Jul 17, 2026
ed3024d
Fix FormConfig import to not leak into cht-form
jkuester Jul 17, 2026
bf5286e
Final clean up for NewEnketoService
jkuester Jul 17, 2026
ec882d7
Slot everything into place and remove old code.
jkuester Jul 17, 2026
6f0c8cd
Fix the cht-form build
jkuester Jul 17, 2026
d354e56
Fix the sonar issues
jkuester Jul 17, 2026
e4b7e32
Lock in the cht-form
jkuester Jul 17, 2026
9e5fa1c
Lock in module unit tests
jkuester Jul 17, 2026
f10f949
Lock in more unit tests
jkuester Jul 17, 2026
046178a
Lock in prepop tests
jkuester Jul 17, 2026
8975432
Lock in form tests
jkuester Jul 17, 2026
2fd0db6
Lock in enekto tests
jkuester Jul 18, 2026
e0e4e72
First round of changes to form-data unit tests
jkuester Jul 18, 2026
cd23e70
Lock in form-data
jkuester Jul 18, 2026
d09a4db
Fix sonar
jkuester Jul 20, 2026
6844aaf
Update cht-form to clear undefined properties
jkuester Jul 20, 2026
f9ab731
Update cht-form unit tests
jkuester Jul 20, 2026
e5cdabc
Final cleanup
jkuester Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions tests/integration/cht-form/default/person-edit.wdio-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ describe('cht-form web component - Edit Person Form', () => {
role: 'chw',
external_id: '12345',
notes: 'Test notes',
contact: undefined,
parent: undefined,
Comment on lines -34 to -35

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here are actually due to the updates I made in the cht-form/src/app.component.ts. When we write a doc to Couch, properties set to undefined are not included in the JSON data. cht-form returns the JS objects directly and was not doing anything to account for undefined properties. Unfortunately, Chai treats an object with a property set to undefined as not deep equal to an object that just does not have the property at all. So, these values were previously needed here to account for these props getting set in the Enekto service, etc.

user_for_contact: {
create: 'true'
},
Expand Down Expand Up @@ -82,14 +80,12 @@ describe('cht-form web component - Edit Person Form', () => {
role: 'patient',
external_id: '54321',
notes: 'Updated notes',
contact: undefined,
parent: '',
meta: {
...initialPerson.meta,
last_edited_by: '',
last_edited_by_person_uuid: 'default_user',
last_edited_by_place_uuid: ''
},
}
};

await mockConfig.loadForm('default', 'contact', 'person-edit');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TranslateFromService } from '@mm-services/translate-from.service';
import { NgIf } from '@angular/common';
import { EnketoComponent } from '@mm-components/enketo/enketo.component';
import { TranslatePipe } from '@ngx-translate/core';
import { FormConfig } from '@mm-services/form/form-config';

@Component({
selector: 'training-cards-form',
Expand All @@ -33,7 +34,7 @@ export class TrainingCardsFormComponent implements OnInit, OnDestroy {
private trackRender;
private trackEditDuration;
private trackSave;
private trackMetadata = { action: '', form: '' };
private readonly trackMetadata = { action: '', form: '' };
readonly FORM_WRAPPER_ID = 'training-cards-form';
trainingCardFormId: null | string = null;
formNoTitle = false;
Expand Down Expand Up @@ -112,23 +113,23 @@ export class TrainingCardsFormComponent implements OnInit, OnDestroy {
this.loadingContent = true;
this.geoHandle?.cancel();
this.geoHandle = this.geolocationService.init();
const form = await this.xmlFormsService.get(this.trainingCardFormId);
await this.ngZone.run(() => this.renderForm(form));
const formConfig = await this.xmlFormsService.getFormConfig('training-card', this.trainingCardFormId!);
await this.ngZone.run(() => this.renderForm(formConfig));
} catch (error) {
this.setError(error);
console.error('TrainingCardsFormComponent :: Error fetching form.', error);
}
}

private async renderForm(formDoc) {
private async renderForm(formConfig: FormConfig) {
try {
const formContext = new WebappEnketoFormContext(`#${this.FORM_WRAPPER_ID}`, 'training-card', formDoc);
const formContext = new WebappEnketoFormContext(`#${this.FORM_WRAPPER_ID}`, formConfig);
formContext.isFormInModal = !this.isEmbedded;
formContext.valuechangeListener = this.resetFormError.bind(this);

this.form = await this.formService.render(formContext);
this.formNoTitle = !formDoc?.title;
this.setNavigationTitle(formDoc);
this.formNoTitle = !formConfig.doc.title;
this.setNavigationTitle(formConfig.doc);
this.showContent();
this.recordPerformancePostRender();
} catch (error) {
Expand Down Expand Up @@ -195,7 +196,7 @@ export class TrainingCardsFormComponent implements OnInit, OnDestroy {
this.resetFormError();

try {
const docs = await this.formService.save(this.trainingCardFormId, this.form, this.geoHandle);
const docs = await this.formService.save(this.form, this.geoHandle);
console.debug('Saved form and associated docs', docs);
const snackText = await this.translateService.get('training_cards.form.saved');
this.globalActions.setSnackbarContent(snackText);
Expand Down
115 changes: 51 additions & 64 deletions webapp/src/ts/modules/contacts/contacts-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import { MatAccordion } from '@angular/material/expansion';
import { EnketoComponent } from '@mm-components/enketo/enketo.component';
import { TranslatePipe } from '@ngx-translate/core';
import { DuplicateContactsComponent } from '@mm-components/duplicate-contacts/duplicate-contacts.component';
import { DuplicateCheck } from '@mm-services/deduplicate.service';
import { Contact, Qualifier } from '@medic/cht-datasource';
import { TelemetryService } from '@mm-services/telemetry.service';
import { CHTDatasourceService } from '@mm-services/cht-datasource.service';
import events from 'enketo-core/src/js/event';
import { XmlFormsService } from '@mm-services/xml-forms.service';
import { FormValidationError } from '@mm-services/enketo.service';

@Component({
templateUrl: './contacts-edit.component.html',
Expand All @@ -37,6 +37,7 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
private readonly formService: FormService,
private readonly contactTypesService: ContactTypesService,
private readonly dbService: DbService,
private readonly xmlFormsService: XmlFormsService,
private readonly performanceService: PerformanceService,
private readonly telemetryService: TelemetryService,
readonly chtDatasourceService: CHTDatasourceService,
Expand All @@ -50,8 +51,7 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {

subscription = new Subscription();
translationsLoadedSubscription;
private globalActions;
private xmlVersion;
private readonly globalActions;
private readonly getContactFromDatasource: ReturnType<typeof Contact.v1.get>;

enketoStatus;
Expand All @@ -70,9 +70,8 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
private trackRender;
private trackEditDuration;
private trackSave;
private trackMetadata = { action: '', form: '' };
private readonly trackMetadata = { action: '', form: '' };

private duplicateCheck?: DuplicateCheck;
duplicatesAcknowledged = false;

duplicates: Contact.v1.Contact[] = [];
Expand Down Expand Up @@ -323,13 +322,10 @@ 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.duplicate_check;

const formConfig = await this.xmlFormsService.getFormConfig('contact', formId);
this.globalActions.setEnketoEditedStatus(false);

const formContext = new WebappEnketoFormContext('#contact-form', 'contact', formDoc, this.getFormInstanceData());
const formContext = new WebappEnketoFormContext('#contact-form', formConfig, this.getFormInstanceData());
formContext.editedListener = this.markFormEdited.bind(this);
formContext.valuechangeListener = this.resetFormError.bind(this);
formContext.titleKey = titleKey;
Expand Down Expand Up @@ -434,64 +430,55 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit {
this.globalActions.setEnketoSavingStatus(true);
this.globalActions.setEnketoError(null);

return Promise
.resolve(form.validate())
.then((valid) => {
if (!valid) {
throw new Error('Validation failed.');
}
Comment on lines -438 to -442

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now done in the EnketoService.saveContact.

if (this.duplicatesAcknowledged && this.duplicates.length) {
this.telemetryService.record(
['enketo', 'contacts', this.enketoContact.type, 'duplicates_acknowledged'].join(':')
);
}
Comment on lines +433 to +437

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most important thing is that this runs before the dupchecking logic in formService.saveContact. I do not think it is an issue that this now runs before the Enketo validation for the form. The Enketo validation is still going to run before the dup checking.


// Updating fields before save. Ref: #6670.
form.view.html.dispatchEvent(events.BeforeSave());
return this.formService
.saveContact(
{ docId, type: this.enketoContact.type },
form,
this.duplicatesAcknowledged
)
.then((result) => {
console.debug('saved contact', result);

if (this.duplicatesAcknowledged && this.duplicates.length) {
this.telemetryService.record(
['enketo', 'contacts', this.enketoContact.type, 'duplicates_acknowledged'].join(':')
);
}
this.globalActions.setEnketoSavingStatus(false);
this.globalActions.setEnketoEditedStatus(false);

this.trackSave?.stop({
name: ['enketo', 'contacts', this.trackMetadata.form, this.trackMetadata.action, 'save'].join(':'),
recordApdex: true,
});

this.translateService
.get(docId ? 'contact.updated' : 'contact.created')
.then(snackBarContent => this.globalActions.setSnackbarContent(snackBarContent));

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

this.globalActions.setEnketoSavingStatus(false);
this.globalActions.setEnketoEditedStatus(false);

this.trackSave?.stop({
name: ['enketo', 'contacts', this.trackMetadata.form, this.trackMetadata.action, 'save'].join(':'),
recordApdex: true,
});

this.translateService
.get(docId ? 'contact.updated' : 'contact.created')
.then(snackBarContent => this.globalActions.setSnackbarContent(snackBarContent));

this.router.navigate(['/contacts', result.docId]);
})
.catch((err) => {
if (err instanceof DuplicatesFoundError) {
this.duplicates = err.duplicates;
} else {
this.duplicates = [];
}

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

this.globalActions.setEnketoSavingStatus(false);
return this.translateService
.get('Error updating contact')
.then(error => this.globalActions.setEnketoError(error));
});
this.router.navigate(['/contacts', result.docId]);
})
.catch(() => {
// validation messages will be displayed for individual fields.
// That's all we want, really.
.catch((err) => {
this.globalActions.setEnketoSavingStatus(false);

if (err instanceof FormValidationError) {
// validation messages will be displayed for individual fields.
// That's all we want, really.
return;
}

if (err instanceof DuplicatesFoundError) {
this.duplicates = err.duplicates;
} else {
this.duplicates = [];
}

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

return this.translateService
.get('Error updating contact')
.then(error => this.globalActions.setEnketoError(error));
});
}

Expand Down
42 changes: 21 additions & 21 deletions webapp/src/ts/modules/contacts/contacts-report.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,44 @@ import { TranslateService } from '@mm-services/translate.service';
import { NgIf } from '@angular/common';
import { EnketoComponent } from '@mm-components/enketo/enketo.component';
import { TranslatePipe } from '@ngx-translate/core';
import { EnketoForm } from '@mm-services/enketo.service';

@Component({
templateUrl: './contacts-report.component.html',
imports: [NgIf, EnketoComponent, TranslatePipe]
})
export class ContactsReportComponent implements OnInit, OnDestroy, AfterViewInit {
private globalActions;
private readonly globalActions;
private geoHandle: any;
private routeSnapshot;
private trackRender;
private trackEditDuration;
private trackSave;
private trackMetadata = { form: '' };
private readonly trackMetadata = { form: '' };

subscription: Subscription = new Subscription();
enketoEdited;
enketoStatus;
enketoSaving;
enketoError;
form;
form?: EnketoForm;
loadingForm;
errorTranslationKey;
contentError;
cancelCallback;

constructor(
private store: Store,
private formService: FormService,
private geolocationService: GeolocationService,
private performanceService: PerformanceService,
private xmlFormsService: XmlFormsService,
private translateFromService: TranslateFromService,
private router: Router,
private route: ActivatedRoute,
private translateService: TranslateService,
private contactViewModelGeneratorService: ContactViewModelGeneratorService,
private ngZone: NgZone,
private readonly store: Store,
private readonly formService: FormService,
private readonly geolocationService: GeolocationService,
private readonly performanceService: PerformanceService,
private readonly xmlFormsService: XmlFormsService,
private readonly translateFromService: TranslateFromService,
private readonly router: Router,
private readonly route: ActivatedRoute,
private readonly translateService: TranslateService,
private readonly contactViewModelGeneratorService: ContactViewModelGeneratorService,
private readonly ngZone: NgZone,
) {
this.globalActions = new GlobalActions(store);
}
Expand All @@ -64,7 +65,7 @@ export class ContactsReportComponent implements OnInit, OnDestroy, AfterViewInit

this.geoHandle = this.geolocationService.init();
this.resetFormError();
this.form = null;
this.form = undefined;
this.loadingForm = true;
this.globalActions.setShowContent(true);
this.setCancelCallback();
Expand Down Expand Up @@ -115,7 +116,7 @@ export class ContactsReportComponent implements OnInit, OnDestroy, AfterViewInit
return Promise
.all([
this.getContact(),
this.xmlFormsService.get(this.routeSnapshot.params?.formId),
this.xmlFormsService.getFormConfig('report', this.routeSnapshot.params?.formId),
]);
}

Expand All @@ -129,15 +130,14 @@ export class ContactsReportComponent implements OnInit, OnDestroy, AfterViewInit

return this
.getContactAndForm()
.then(([ contact, formDoc ]) => {
.then(([ contact, formConfig ]) => {
this.globalActions.setEnketoEditedStatus(false);
this.globalActions.setTitle(this.translateFromService.get(formDoc.title));
this.globalActions.setTitle(this.translateFromService.get(formConfig.doc.title));
this.setCancelCallback();

const formContext = new WebappEnketoFormContext(
'#contact-report',
'report',
formDoc,
formConfig,
{ source: 'contact', contact }
);
formContext.editedListener = this.markFormEdited.bind(this);
Expand Down Expand Up @@ -228,7 +228,7 @@ export class ContactsReportComponent implements OnInit, OnDestroy, AfterViewInit
this.globalActions.setEnketoSavingStatus(true);
this.resetFormError();
this.formService
.save(this.routeSnapshot.params.formId, this.form, this.geoHandle)
.save(this.form!, this.geoHandle)
.then((docs) => {
console.debug('saved report and associated docs', docs);
this.globalActions.setEnketoSavingStatus(false);
Expand Down
Loading
Loading