Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 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
dd3c717
Minor refactors from code review.
jkuester Jul 27, 2026
9a88818
Fix findNodeWithTextContent to support special chars
jkuester Jul 27, 2026
c513f16
Replace getNodeByXpath with new findReferencedDoc logic in EnketoService
jkuester Jul 27, 2026
c03b1b4
Clean up and streamline xml-forms logic
jkuester Jul 27, 2026
6c8924b
Clean up cht-form logic
jkuester Jul 27, 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 thread
jkuester marked this conversation as resolved.
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!);
Comment thread
jkuester marked this conversation as resolved.
Outdated
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 thread
jkuester marked this conversation as resolved.
if (this.duplicatesAcknowledged && this.duplicates.length) {
this.telemetryService.record(
['enketo', 'contacts', this.enketoContact.type, 'duplicates_acknowledged'].join(':')
);
}
Comment thread
jkuester marked this conversation as resolved.

// Updating fields before save. Ref: #6670.
form.view.html.dispatchEvent(events.BeforeSave());
return this.formService
Comment thread
jkuester marked this conversation as resolved.
Outdated
.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