From e0f482205bb78f00329f5338aceed9eb1e685d1c Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 16 Apr 2026 11:58:04 +0100 Subject: [PATCH 01/10] fix for populating nested dynamicmultiselectlists --- .../shared/services/fields/fields.utils.ts | 60 +++++++++++++------ .../services/form/field-type-sanitiser.ts | 59 ++++++++++++------ 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts index fbdb8303ef..bf6714e4de 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts @@ -23,6 +23,7 @@ export class FieldsUtils { public static readonly SERVER_RESPONSE_FIELD_TYPE_COLLECTION = 'Collection'; public static readonly SERVER_RESPONSE_FIELD_TYPE_COMPLEX = 'Complex'; public static readonly SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_LIST_TYPE: FieldTypeEnum[] = ['DynamicList', 'DynamicRadioList']; + public static readonly SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_MULTISELECT_LIST_TYPE: FieldTypeEnum = 'DynamicMultiSelectList'; public static readonly defaultTabList = { "PRLAPPS": "Summary" } @@ -304,32 +305,57 @@ export class FieldsUtils { return `{ Invalid ${type}: ${invalidValue} }`; } - private static setDynamicListDefinition(caseField: CaseField, caseFieldType: FieldType, rootCaseField: CaseField) { + private static setDynamicListDefinition(caseField: CaseField, caseFieldType: FieldType, + rootCaseField: CaseField, isWithinCollection = false) { if (caseFieldType.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COMPLEX) { caseFieldType.complex_fields.forEach(field => { try { const isDynamicField = FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_LIST_TYPE.indexOf(field.field_type.type) !== -1; + const isDynamicMultiSelectField = + field.field_type.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_MULTISELECT_LIST_TYPE; - if (isDynamicField) { + if (isDynamicField || isDynamicMultiSelectField) { const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); if (dynamicListValue) { - const list_items = dynamicListValue[0].list_items; - const complexValue = dynamicListValue.map(data => data.value); - const value = { - list_items, - value: complexValue.length > 0 ? complexValue : undefined - }; - field.value = { - ...value - }; - field.formatted_value = { - ...field.formatted_value, - ...value - }; + const list_items = dynamicListValue.find(data => data?.list_items !== undefined)?.list_items; + + if (list_items !== undefined) { + field.list_items = list_items; + field.formatted_value = { + ...field.formatted_value, + list_items + }; + } + + if (isDynamicMultiSelectField) { + if (!isWithinCollection) { + const value = dynamicListValue[0]?.value; + if (value !== undefined) { + field.value = value; + field.formatted_value = { + ...field.formatted_value, + value + }; + } + } + } else { + const complexValue = dynamicListValue.map(data => data.value); + const value = { + list_items, + value: complexValue.length > 0 ? complexValue : undefined + }; + field.value = { + ...value + }; + field.formatted_value = { + ...field.formatted_value, + ...value + }; + } } } else { - this.setDynamicListDefinition(field, field.field_type, rootCaseField); + this.setDynamicListDefinition(field, field.field_type, rootCaseField, isWithinCollection); } } catch (error) { console.log(error); @@ -337,7 +363,7 @@ export class FieldsUtils { }); } else if (caseFieldType.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { if (caseFieldType.collection_field_type) { - this.setDynamicListDefinition(caseField, caseFieldType.collection_field_type, rootCaseField); + this.setDynamicListDefinition(caseField, caseFieldType.collection_field_type, rootCaseField, true); } } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts index 6d2a8b7932..a03f326e95 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts @@ -58,39 +58,62 @@ export class FieldTypeSanitiser { public ensureDynamicMultiSelectListPopulated(caseFields: CaseField[]): CaseField[] { return caseFields.map((field) => { - if (field.field_type.type !== 'Complex') { + const fieldData = field._value || field.value; + + if (field.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { + this.checkNestedDynamicList(field, fieldData); + } else if (field.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COLLECTION && + field.field_type.collection_field_type?.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX + ) { + this.checkNestedDynamicList(field, fieldData, true); + } else { return field; } - const caseFieldData = field._value; - // Process each complex field - field.field_type.complex_fields.forEach((complexField) => { - if (complexField.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { - this.checkNestedDynamicList(complexField, caseFieldData?.[complexField.id]); - } else if (this.isDynamicList(complexField.field_type.type) && - complexField.display_context !== 'HIDDEN' && - field._value?.[complexField.id] - ) { - complexField.list_items = field._value[complexField.id]?.list_items; - } - }); // Final transformation: construct updated field object return { ...field, field_type: { ...field?.field_type } } as CaseField; }); } - private checkNestedDynamicList(caseField: CaseField, fieldData: any = null): void { - caseField.field_type.complex_fields.forEach((complexField) => { + private checkNestedDynamicList(caseField: CaseField, fieldData: any = null, isCollection = false): void { + const complexFields = isCollection + ? caseField.field_type.collection_field_type?.complex_fields || [] + : caseField.field_type.complex_fields; + + complexFields.forEach((complexField) => { + const childData = isCollection + ? this.getFirstCollectionFieldData(fieldData, complexField.id) + : fieldData?.[complexField.id]; + if (complexField.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { - this.checkNestedDynamicList(complexField, fieldData?.[complexField.id]); + this.checkNestedDynamicList(complexField, childData); + } else if (complexField.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COLLECTION && + complexField.field_type.collection_field_type?.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX + ) { + this.checkNestedDynamicList(complexField, childData, true); } else if (this.isDynamicList(complexField.field_type.type) && complexField.display_context !== 'HIDDEN' && - fieldData?.[complexField.id] + childData ) { - complexField.list_items = fieldData?.[complexField.id]?.list_items; + complexField.list_items = childData.list_items; } }); } + private getFirstCollectionFieldData(collectionData: any[], fieldId: string): any { + if (!Array.isArray(collectionData)) { + return null; + } + + for (const item of collectionData) { + const value = item?.value || item; + if (value?.[fieldId] !== undefined) { + return value[fieldId]; + } + } + + return null; + } + private isDynamicList(fieldType: FieldTypeEnum): boolean { return FieldTypeSanitiser.DYNAMIC_LIST_TYPE.indexOf(fieldType) !== -1; } From 6f0e06fc3fc1d4ac396b3d4e66a5ee202d364576 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 16 Apr 2026 12:00:17 +0100 Subject: [PATCH 02/10] added unit tests --- .../write-collection-field.component.spec.ts | 140 ++++++++++++++++++ .../services/fields/fields.utils.spec.ts | 52 +++++++ .../form/field-type-sanitiser.spec.ts | 110 ++++++++++++++ 3 files changed, 302 insertions(+) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts index 316e1b1a97..80f668fd20 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts @@ -311,6 +311,146 @@ describe('WriteCollectionFieldComponent', () => { }); }); +describe('WriteCollectionFieldComponent with hydrated dynamic multiselect children', () => { + let fixture: ComponentFixture; + let component: WriteCollectionFieldComponent; + let formValidatorService: any; + let dialog: any; + let dialogRef: any; + let scrollToService: any; + let profileNotifier: any; + let caseField: CaseField; + let formGroup: FormGroup; + let collectionCreateCheckerService: CollectionCreateCheckerService; + + beforeEach(waitForAsync(() => { + formValidatorService = createSpyObj('formValidatorService', ['addValidators']); + dialogRef = createSpyObj>('MatDialogRef', ['afterClosed']); + dialogRef.afterClosed.and.returnValue(of()); + dialog = createSpyObj('MatDialog', ['open']); + dialog.open.and.returnValue(dialogRef); + scrollToService = createSpyObj('scrollToService', ['scrollTo']); + scrollToService.scrollTo.and.returnValue(of()); + caseField = ({ + id: 'stmtOfServiceAddRecipient', + label: 'Recipient', + display_context: 'OPTIONAL', + display_context_parameter: '#COLLECTION(allowInsert)', + field_type: { + id: 'StmtOfServiceAddRecipient', + type: 'Collection', + collection_field_type: { + id: 'StmtOfServiceAddRecipient', + type: 'Complex', + complex_fields: [ + ({ + id: 'servedParty', + label: 'Who was served?', + display_context: 'OPTIONAL', + field_type: { + id: 'DynamicList', + type: 'DynamicList' + } + }) as CaseField, + ({ + id: 'orderList', + label: 'Statement of service orders', + display_context: 'OPTIONAL', + field_type: { + id: 'DynamicMultiSelectList', + type: 'DynamicMultiSelectList' + }, + list_items: [ + { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' }, + { code: 'order-2', label: 'Parental responsibility order (C45A) - 15 Apr 2026' } + ] + }) as CaseField + ] + } + } as FieldType, + value: [ + { + id: 'recipient-1', + value: { + servedParty: 'John Doe (Applicant 1)', + orderList: [ + { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' } + ] + } + } + ], + acls: [ + { + role: 'caseworker-divorce', + create: true, + read: true, + update: true, + delete: true + } + ] + }) as CaseField; + formGroup = new FormGroup({ + field1: new FormControl() + }); + + profileNotifier = new ProfileNotifier(); + profileNotifier.profile = new BehaviorSubject(createAProfile()).asObservable(); + + collectionCreateCheckerService = new CollectionCreateCheckerService(); + + TestBed + .configureTestingModule({ + imports: [ + ReactiveFormsModule, + PaletteUtilsModule, + fieldWriteComponent, + fieldReadComponent + ], + declarations: [ + WriteCollectionFieldComponent, + MockRpxTranslatePipe, + MockFieldLabelPipe + ], + providers: [ + { provide: FormValidatorsService, useValue: formValidatorService }, + { provide: MatDialog, useValue: dialog }, + { provide: ScrollToService, useValue: scrollToService }, + { provide: ProfileNotifier, useValue: profileNotifier }, + { provide: CollectionCreateCheckerService, useValue: collectionCreateCheckerService }, + RemoveDialogComponent + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(WriteCollectionFieldComponent); + component = fixture.componentInstance; + component.caseField = caseField; + component.caseFields = [caseField]; + component.formGroup = formGroup; + component.ngOnInit(); + fixture.detectChanges(); + })); + + afterEach(() => { + fixture.destroy(); + }); + + it('should retain orderList options for a new recipient without inheriting another recipient\'s selections', () => { + component.addItem(false); + fixture.detectChanges(); + + const newRecipient = component.collItems[1].caseField; + const orderListField = newRecipient.field_type.complex_fields.find((field) => field.id === 'orderList'); + + expect(orderListField.list_items).toEqual([ + { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' }, + { code: 'order-2', label: 'Parental responsibility order (C45A) - 15 Apr 2026' } + ]); + expect(orderListField.value).toBeUndefined(); + expect(newRecipient.value).toBeNull(); + }); +}); + describe('WriteCollectionFieldComponent CRUD impact', () => { const collectionValues = [ { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts index 4cf8e6dd00..ea6b16f1bc 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts @@ -559,6 +559,10 @@ describe('FieldsUtils', () => { type: 'DynamicList' }, id: 'complex_dl', + list_items: [ + {code: '1', value: '1'}, + {code: '2', value: '2'} + ], value: { list_items: [ {code: '1', value: '1'}, @@ -590,6 +594,54 @@ describe('FieldsUtils', () => { expect(callbackResponse).toEqual(expected); }); + + it('should hydrate list_items for collection dynamic multiselects without copying selected values', () => { + const listItems = [ + {code: '1', value: '1'}, + {code: '2', value: '2'} + ]; + const callbackResponse = { + field_type: { + type: 'Collection', + collection_field_type: { + type: 'Complex', + complex_fields: [ + { + field_type: { + type: 'DynamicMultiSelectList' + }, + id: 'orderList', + formatted_value: {} + } + ] + } + }, + value: [ + { + id: 'recipient-1', + value: { + orderList: { + list_items: listItems, + value: [ + {code: '2', value: '2'} + ] + } + } + } + ] + }; + + (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); + + expect(callbackResponse.field_type.collection_field_type.complex_fields[0]).toEqual(jasmine.objectContaining({ + id: 'orderList', + list_items: listItems, + formatted_value: { + list_items: listItems + } + })); + expect((callbackResponse.field_type.collection_field_type.complex_fields[0] as any).value).toBeUndefined(); + }); }); describe('isFlagsCaseField() function test', () => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts index 44bf4be5a6..c8528b66af 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts @@ -137,6 +137,72 @@ describe('FieldTypeSanitiser', () => { expect(editForm.data.dynamicList).toEqual(EXPECTED_VALUE_DYNAMIC_LIST); }); + it('should preserve collection recipient DynamicMultiSelectList selections for each row during sanitisation', () => { + const orderListItems = [ + { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' }, + { code: 'order-2', label: 'Parental responsibility order (C45A) - 15 Apr 2026' } + ]; + const collectionCaseFields = [{ + id: 'stmtOfServiceAddRecipient', + value: [ + { + id: 'recipient-1', + value: { + orderList: { + list_items: orderListItems, + value: [orderListItems[0]] + } + } + } + ], + field_type: { + id: 'StmtOfServiceAddRecipient', + type: 'Collection', + collection_field_type: { + id: 'StmtOfServiceAddRecipient', + type: 'Complex', + complex_fields: [ + { + id: 'orderList', + field_type: { + id: 'DynamicMultiSelectList', + type: 'DynamicMultiSelectList' + }, + display_context: 'OPTIONAL' + } + ] + } + } + }] as unknown as CaseField[]; + const formData = { + stmtOfServiceAddRecipient: [ + { + id: 'recipient-1', + value: { + orderList: [orderListItems[0]] + } + }, + { + id: 'recipient-2', + value: { + orderList: [orderListItems[1]] + } + } + ] + }; + + new FieldTypeSanitiser().sanitiseLists(collectionCaseFields, formData); + + expect(formData.stmtOfServiceAddRecipient[0].value.orderList as any).toEqual({ + value: [orderListItems[0]], + list_items: orderListItems + }); + expect(formData.stmtOfServiceAddRecipient[1].value.orderList as any).toEqual({ + value: [orderListItems[1]], + list_items: orderListItems + }); + }); + describe('ensureDynamicMultiSelectListPopulated', () => { let fieldTypeSanitiser: FieldTypeSanitiser; let mockCaseFields: CaseField[]; @@ -327,5 +393,49 @@ describe('FieldTypeSanitiser', () => { const result = fieldTypeSanitiser.ensureDynamicMultiSelectListPopulated(mockCaseFields); expect(result[2].field_type.complex_fields[0].field_type.complex_fields[0].list_items).toBeUndefined(); }); + + it('should populate list_items for DynamicMultiSelectList within collection complex fields', () => { + mockCaseFields.push({ + id: 'stmtOfServiceAddRecipient', + value: [ + { + id: 'recipient-1', + value: { + orderList: { + list_items: [ + { code: 'order-1', label: 'Order 1' }, + { code: 'order-2', label: 'Order 2' } + ] + } + } + } + ], + field_type: { + id: 'StmtOfServiceAddRecipient', + type: 'Collection', + collection_field_type: { + id: 'StmtOfServiceAddRecipient', + type: 'Complex', + complex_fields: [{ + id: 'orderList', + field_type: { + id: 'DynamicMultiSelectList', + type: 'DynamicMultiSelectList' + }, + display_context: 'OPTIONAL' + }] + } + } + } as unknown as CaseField); + + const result = fieldTypeSanitiser.ensureDynamicMultiSelectListPopulated(mockCaseFields); + const collectionField = result.find((field) => field.id === 'stmtOfServiceAddRecipient'); + const complexField = collectionField.field_type.collection_field_type.complex_fields.find((field) => field.id === 'orderList'); + + expect(complexField.list_items).toEqual([ + { code: 'order-1', label: 'Order 1' }, + { code: 'order-2', label: 'Order 2' } + ]); + }); }); }); From 28764c4eaa112f183b57ed968fdcd977367c8723 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 16 Apr 2026 13:20:07 +0100 Subject: [PATCH 03/10] fix for midevent validation --- .../case-edit/case-edit.component.ts | 9 ++++---- .../services/form/form-value.service.ts | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts index 110ee3ea52..a4e26000f7 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts @@ -320,12 +320,13 @@ export class CaseEditComponent implements OnInit, OnDestroy { } private generateCaseEventData({ eventTrigger, form }: CaseEditGenerateCaseEventData ): CaseEventData { + const formData = this.replaceHiddenFormValuesWithOriginalCaseData( + form.get('data') as FormGroup, eventTrigger.case_fields); + this.formValueService.sanitiseDynamicLists(eventTrigger.case_fields, { data: formData }); + const caseEventData: CaseEventData = { data: this.replaceEmptyComplexFieldValues( - this.formValueService.sanitise( - this.replaceHiddenFormValuesWithOriginalCaseData( - form.get('data') as FormGroup, eventTrigger.case_fields), - this.isCaseFlagSubmission)), + this.formValueService.sanitise(formData, this.isCaseFlagSubmission)), event: form.value.event } as CaseEventData; this.formValueService.clearNonCaseFields(caseEventData.data, eventTrigger.case_fields); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts index 716808e91e..660be1e64e 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts @@ -291,7 +291,7 @@ export class FormValueService { * @param clearNonCase Whether or not we should clear out non-case fields at the top level. */ public removeUnnecessaryFields(data: object, caseFields: CaseField[], clearEmpty = false, clearNonCase = false, - fromPreviousPage = false, currentPageCaseFields = []): void { + fromPreviousPage = false, currentPageCaseFields = [], isNested = false): void { if (data && caseFields && caseFields.length > 0) { // check if there is any data at the top level of the form that's not in the caseFields if (clearNonCase) { @@ -302,9 +302,9 @@ export class FormValueService { // Retain anything that is readonly and not a label. continue; } - if (field.hidden === true && field.display_context !== 'HIDDEN' && field.display_context !== 'HIDDEN_TEMP' && field.id !== 'caseLinks' && !field.retain_hidden_value) { + if (this.shouldRemoveHiddenField(field, isNested)) { // Delete anything that is hidden (that is NOT readonly), and that - // hasn't had its display_context overridden to make it hidden. + // is not explicitly retained. Nested hidden fields should be dropped by default. delete data[field.id]; } else if (field.field_type) { switch (field.field_type.type) { @@ -318,7 +318,8 @@ export class FormValueService { } break; case 'Complex': - this.removeUnnecessaryFields(data[field.id], field.field_type.complex_fields, clearEmpty); + this.removeUnnecessaryFields(data[field.id], field.field_type.complex_fields, clearEmpty, + false, false, [], true); // Also remove any optional complex objects that are completely empty. // EUI-4244: Ritesh's fix, passing true instead of clearEmpty. if (FormValueService.clearOptionalEmpty(true, data[field.id], field)) { @@ -341,8 +342,10 @@ export class FormValueService { if (field.field_type.collection_field_type.type === 'Complex') { // Iterate through the elements and remove any unnecessary fields within. for (const item of collection) { - this.removeUnnecessaryFields(item, field.field_type.collection_field_type.complex_fields, clearEmpty); - this.removeUnnecessaryFields(item.value, field.field_type.collection_field_type.complex_fields, false); + this.removeUnnecessaryFields(item, field.field_type.collection_field_type.complex_fields, clearEmpty, + false, false, [], true); + this.removeUnnecessaryFields(item.value, field.field_type.collection_field_type.complex_fields, false, + false, false, [], true); } } } @@ -358,6 +361,13 @@ export class FormValueService { FormValueService.removeMultiSelectLabels(data); } + private shouldRemoveHiddenField(field: CaseField, isNested: boolean): boolean { + return field.hidden === true + && field.id !== 'caseLinks' + && !field.retain_hidden_value + && (isNested || (field.display_context !== 'HIDDEN' && field.display_context !== 'HIDDEN_TEMP')); + } + public removeInvalidCollectionData(data: object, field: CaseField) { if (data[field.id] && data[field.id].length > 0) { for (const objCollection of data[field.id]) { From 8d97e82d34fb9b170d064e09dc792d8a645dd127 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 16 Apr 2026 13:29:39 +0100 Subject: [PATCH 04/10] updated version number --- RELEASE-NOTES.md | 3 +++ package.json | 2 +- projects/ccd-case-ui-toolkit/package.json | 2 +- yarn-audit-known-issues | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 51b8f0b719..fd52bb8054 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,8 @@ ## RELEASE NOTES +### Version 7.3.48-xui-3740 +**EXUI-3740** Dynamic MultiSelect Box doesn't retain it's original data in Collection on Add new button click + ### Version 7.3.48 **EXUI-4278** Suppressions-lodash-minor diff --git a/package.json b/package.json index 1624b5fa6e..66e147dcbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.48", + "version": "7.3.48-xui-3740", "engines": { "node": ">=20.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index 3dd0b3daba..eb131a7447 100644 --- a/projects/ccd-case-ui-toolkit/package.json +++ b/projects/ccd-case-ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.48", + "version": "7.3.48-xui-3740", "engines": { "node": ">=20.19.0" }, diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 70ad98116d..3d43858668 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -2,6 +2,7 @@ {"value":"@tootallnate/once","children":{"ID":1113977,"Issue":"@tootallnate/once vulnerable to Incorrect Control Flow Scoping","URL":"https://github.com/advisories/GHSA-vpq2-c234-7xj6","Severity":"low","Vulnerable Versions":"<3.0.1","Tree Versions":["2.0.0"],"Dependents":["http-proxy-agent@npm:5.0.0"]}} {"value":"abab","children":{"ID":"abab (deprecation)","Issue":"Use your platform's native atob() and btoa() methods instead","Severity":"moderate","Vulnerable Versions":"2.0.6","Tree Versions":["2.0.6"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} {"value":"domexception","children":{"ID":"domexception (deprecation)","Issue":"Use your platform's native DOMException instead","Severity":"moderate","Vulnerable Versions":"4.0.0","Tree Versions":["4.0.0"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} +{"value":"dompurify","children":{"ID":1116663,"Issue":"DOMPurify's ADD_TAGS function form bypasses FORBID_TAGS due to short-circuit evaluation","URL":"https://github.com/advisories/GHSA-39q2-94rc-95cp","Severity":"moderate","Vulnerable Versions":"<=3.3.3","Tree Versions":["3.3.3"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"govuk-elements-sass","children":{"ID":"govuk-elements-sass (deprecation)","Issue":"GOV.UK Elements is no longer maintained. Use the GOV.UK Design System instead: https://frontend.design-system.service.gov.uk/v4/migrating-from-legacy-products/","Severity":"moderate","Vulnerable Versions":"3.1.3","Tree Versions":["3.1.3"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} {"value":"govuk_frontend_toolkit","children":{"ID":"govuk_frontend_toolkit (deprecation)","Issue":"GOV.UK Frontend Toolkit is no longer maintained. Use the GOV.UK Design System instead: https://frontend.design-system.service.gov.uk/v4/migrating-from-legacy-products/","Severity":"moderate","Vulnerable Versions":"7.6.0","Tree Versions":["7.6.0"],"Dependents":["govuk-elements-sass@npm:3.1.3"]}} {"value":"lodash","children":{"ID":1115806,"Issue":"lodash vulnerable to Code Injection via `_.template` imports key names","URL":"https://github.com/advisories/GHSA-r5fr-rjxr-66jc","Severity":"high","Vulnerable Versions":">=4.0.0 <=4.17.23","Tree Versions":["4.17.23"],"Dependents":["@edium/fsm@npm:2.1.5"]}} From 56a1f38cde3887f07108c66cab50362d9e6c92c3 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 14 May 2026 16:09:36 +0100 Subject: [PATCH 05/10] updated yarn-audit-known-issues --- yarn-audit-known-issues | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 2c768b22c2..1bc429f7ac 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -2,7 +2,6 @@ {"value":"@tootallnate/once","children":{"ID":1113977,"Issue":"@tootallnate/once vulnerable to Incorrect Control Flow Scoping","URL":"https://github.com/advisories/GHSA-vpq2-c234-7xj6","Severity":"low","Vulnerable Versions":"<3.0.1","Tree Versions":["2.0.0"],"Dependents":["http-proxy-agent@npm:5.0.0"]}} {"value":"abab","children":{"ID":"abab (deprecation)","Issue":"Use your platform's native atob() and btoa() methods instead","Severity":"moderate","Vulnerable Versions":"2.0.6","Tree Versions":["2.0.6"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} {"value":"domexception","children":{"ID":"domexception (deprecation)","Issue":"Use your platform's native DOMException instead","Severity":"moderate","Vulnerable Versions":"4.0.0","Tree Versions":["4.0.0"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} -{"value":"dompurify","children":{"ID":1115529,"Issue":"DOMPurify is vulnerable to mutation-XSS via Re-Contextualization ","URL":"https://github.com/advisories/GHSA-h8r8-wccr-v5f2","Severity":"moderate","Vulnerable Versions":"<3.3.2","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"dompurify","children":{"ID":1115668,"Issue":"DOMPurify contains a Cross-site Scripting vulnerability","URL":"https://github.com/advisories/GHSA-v2wj-7wpq-c8vv","Severity":"moderate","Vulnerable Versions":">=3.1.3 <=3.3.1","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"dompurify","children":{"ID":1115921,"Issue":"DOMPurify ADD_ATTR predicate skips URI validation","URL":"https://github.com/advisories/GHSA-cjmm-f4jc-qw8r","Severity":"moderate","Vulnerable Versions":"<=3.3.1","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"dompurify","children":{"ID":1115922,"Issue":"DOMPurify USE_PROFILES prototype pollution allows event handlers","URL":"https://github.com/advisories/GHSA-cj63-jhhr-wcxv","Severity":"moderate","Vulnerable Versions":"<=3.3.1","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} @@ -10,13 +9,18 @@ {"value":"dompurify","children":{"ID":1117138,"Issue":"DOMPurify: FORBID_TAGS bypassed by function-based ADD_TAGS predicate (asymmetry with FORBID_ATTR fix)","URL":"https://github.com/advisories/GHSA-h7mw-gpvr-xq4m","Severity":"moderate","Vulnerable Versions":"<3.4.0","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"dompurify","children":{"ID":1117139,"Issue":"DOMPurify has a SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode","URL":"https://github.com/advisories/GHSA-crv5-9vww-q3g8","Severity":"moderate","Vulnerable Versions":">=1.0.10 <3.4.0","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"dompurify","children":{"ID":1117140,"Issue":"DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback","URL":"https://github.com/advisories/GHSA-v9jr-rg53-9pgp","Severity":"moderate","Vulnerable Versions":">=3.0.1 <3.4.0","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} +{"value":"dompurify","children":{"ID":1117795,"Issue":"DOMPurify is vulnerable to mutation-XSS via Re-Contextualization ","URL":"https://github.com/advisories/GHSA-h8r8-wccr-v5f2","Severity":"moderate","Vulnerable Versions":"<3.3.2","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} {"value":"govuk-elements-sass","children":{"ID":"govuk-elements-sass (deprecation)","Issue":"GOV.UK Elements is no longer maintained. Use the GOV.UK Design System instead: https://frontend.design-system.service.gov.uk/v4/migrating-from-legacy-products/","Severity":"moderate","Vulnerable Versions":"3.1.3","Tree Versions":["3.1.3"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} {"value":"govuk_frontend_toolkit","children":{"ID":"govuk_frontend_toolkit (deprecation)","Issue":"GOV.UK Frontend Toolkit is no longer maintained. Use the GOV.UK Design System instead: https://frontend.design-system.service.gov.uk/v4/migrating-from-legacy-products/","Severity":"moderate","Vulnerable Versions":"7.6.0","Tree Versions":["7.6.0"],"Dependents":["govuk-elements-sass@npm:3.1.3"]}} +{"value":"mermaid","children":{"ID":1117991,"Issue":"Mermaid Gantt Charts are vulnerable to an Infinite Loop DoS","URL":"https://github.com/advisories/GHSA-6m6c-36f7-fhxh","Severity":"moderate","Vulnerable Versions":">=11.0.0-alpha.1 <=11.14.0","Tree Versions":["11.12.2"],"Dependents":["ngx-markdown@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:20.1.0"]}} +{"value":"mermaid","children":{"ID":1117995,"Issue":"Mermaid: Improper sanitization of `classDefs` in diagrams leads to CSS injection","URL":"https://github.com/advisories/GHSA-xcj9-5m2h-648r","Severity":"moderate","Vulnerable Versions":">=11.0.0-alpha.1 <=11.14.0","Tree Versions":["11.12.2"],"Dependents":["ngx-markdown@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:20.1.0"]}} +{"value":"mermaid","children":{"ID":1118513,"Issue":"Mermaid: Improper sanitization of configuration leads to CSS injection","URL":"https://github.com/advisories/GHSA-87f9-hvmw-gh4p","Severity":"moderate","Vulnerable Versions":">=11.0.0-alpha.1 <=11.14.0","Tree Versions":["11.12.2"],"Dependents":["ngx-markdown@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:20.1.0"]}} +{"value":"mermaid","children":{"ID":1118515,"Issue":"Mermaid: Improper sanitization of `classDef` in state diagrams leads to HTML injection","URL":"https://github.com/advisories/GHSA-ghcm-xqfw-q4vr","Severity":"moderate","Vulnerable Versions":">=11.0.0-alpha.1 <=11.14.0","Tree Versions":["11.12.2"],"Dependents":["ngx-markdown@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:20.1.0"]}} {"value":"picomatch","children":{"ID":1115549,"Issue":"Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching","URL":"https://github.com/advisories/GHSA-3v7f-55p6-f55p","Severity":"moderate","Vulnerable Versions":"<2.3.2","Tree Versions":["2.3.1"],"Dependents":["jest-util@npm:29.7.0"]}} {"value":"picomatch","children":{"ID":1115551,"Issue":"Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching","URL":"https://github.com/advisories/GHSA-3v7f-55p6-f55p","Severity":"moderate","Vulnerable Versions":">=4.0.0 <4.0.4","Tree Versions":["4.0.3"],"Dependents":["tinyglobby@npm:0.2.15"]}} {"value":"picomatch","children":{"ID":1115552,"Issue":"Picomatch has a ReDoS vulnerability via extglob quantifiers","URL":"https://github.com/advisories/GHSA-c2c7-rcm5-vvqj","Severity":"high","Vulnerable Versions":"<2.3.2","Tree Versions":["2.3.1"],"Dependents":["jest-util@npm:29.7.0"]}} {"value":"picomatch","children":{"ID":1115554,"Issue":"Picomatch has a ReDoS vulnerability via extglob quantifiers","URL":"https://github.com/advisories/GHSA-c2c7-rcm5-vvqj","Severity":"high","Vulnerable Versions":">=4.0.0 <4.0.4","Tree Versions":["4.0.3"],"Dependents":["tinyglobby@npm:0.2.15"]}} {"value":"socket.io-parser","children":{"ID":1115154,"Issue":"socket.io allows an unbounded number of binary attachments","URL":"https://github.com/advisories/GHSA-677m-j7p3-52f9","Severity":"high","Vulnerable Versions":">=4.0.0 <4.2.6","Tree Versions":["4.2.4"],"Dependents":["socket.io-client@npm:4.8.1"]}} -{"value":"underscore","children":{"ID":1113950,"Issue":"Underscore has unlimited recursion in _.flatten and _.isEqual, potential for DoS attack","URL":"https://github.com/advisories/GHSA-qpx9-hpmf-5gmw","Severity":"high","Vulnerable Versions":"<=1.13.7","Tree Versions":["1.13.7"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} -{"value":"uuid","children":{"ID":1116970,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":"<14.0.0","Tree Versions":["11.1.0"],"Dependents":["@hmcts/media-viewer@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:4.2.16"]}} +{"value":"underscore","children":{"ID":1117689,"Issue":"Underscore has unlimited recursion in _.flatten and _.isEqual, potential for DoS attack","URL":"https://github.com/advisories/GHSA-qpx9-hpmf-5gmw","Severity":"high","Vulnerable Versions":"<=1.13.7","Tree Versions":["1.13.7"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} +{"value":"uuid","children":{"ID":1117637,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":">=11.0.0 <11.1.1","Tree Versions":["11.1.0"],"Dependents":["@hmcts/media-viewer@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:4.2.16"]}} {"value":"whatwg-encoding","children":{"ID":"whatwg-encoding (deprecation)","Issue":"Use @exodus/bytes instead for a more spec-conformant and faster implementation","Severity":"moderate","Vulnerable Versions":"2.0.0","Tree Versions":["2.0.0"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} From ca07ea9add7a52a37cd5ff1720b675cd72448fc4 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 19 May 2026 13:39:22 +0100 Subject: [PATCH 06/10] updated yarn-audit-known-issues --- yarn-audit-known-issues | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 1bc429f7ac..326049281c 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -24,3 +24,4 @@ {"value":"underscore","children":{"ID":1117689,"Issue":"Underscore has unlimited recursion in _.flatten and _.isEqual, potential for DoS attack","URL":"https://github.com/advisories/GHSA-qpx9-hpmf-5gmw","Severity":"high","Vulnerable Versions":"<=1.13.7","Tree Versions":["1.13.7"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} {"value":"uuid","children":{"ID":1117637,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":">=11.0.0 <11.1.1","Tree Versions":["11.1.0"],"Dependents":["@hmcts/media-viewer@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:4.2.16"]}} {"value":"whatwg-encoding","children":{"ID":"whatwg-encoding (deprecation)","Issue":"Use @exodus/bytes instead for a more spec-conformant and faster implementation","Severity":"moderate","Vulnerable Versions":"2.0.0","Tree Versions":["2.0.0"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} +{"value":"ws","children":{"ID":1119108,"Issue":"ws: Uninitialized memory disclosure","URL":"https://github.com/advisories/GHSA-58qx-3vcg-4xpx","Severity":"moderate","Vulnerable Versions":">=8.0.0 <8.20.1","Tree Versions":["8.17.1","8.18.3"],"Dependents":["engine.io-client@npm:6.6.3","jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} From 112bd62cc9c2c13d7bfff2440e8c31a80495bea3 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 19 May 2026 13:52:28 +0100 Subject: [PATCH 07/10] updated case edit tests for dynamic list sanitisation --- .../case-editor/case-edit/case-edit.component.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts index b82c403ec1..fab5b413de 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts @@ -279,6 +279,7 @@ describe('CaseEditComponent', () => { formValueService = createSpyObj('formValueService', [ 'sanitise', + 'sanitiseDynamicLists', 'clearNonCaseFields', 'removeNullLabels', 'removeEmptyDocuments', @@ -1223,6 +1224,10 @@ describe('CaseEditComponent', () => { }); expect(component.isSubmitting).toEqual(false); + expect(formValueService.sanitiseDynamicLists).toHaveBeenCalledWith( + component.eventTrigger.case_fields, + { data: jasmine.any(Object) } + ); expect(formValueService.sanitise).toHaveBeenCalled(); }); }); @@ -1700,7 +1705,7 @@ describe('CaseEditComponent', () => { { provide: ConditionalShowRegistrarService, useValue: new ConditionalShowRegistrarService() }, { provide: ValidPageListCaseFieldsService, useValue: new ValidPageListCaseFieldsService(new FieldsUtils()) }, { provide: FormErrorService, useValue: jasmine.createSpyObj('FormErrorService', ['mapFieldErrors']) }, - { provide: FormValueService, useValue: jasmine.createSpyObj('FormValueService', ['sanitise']) }, + { provide: FormValueService, useValue: jasmine.createSpyObj('FormValueService', ['sanitise', 'sanitiseDynamicLists']) }, { provide: LoadingService, useValue: jasmine.createSpyObj('LoadingService', ['register','unregister']) }, { provide: WorkAllocationService, useValue: jasmine.createSpyObj('WorkAllocationService', ['assignAndCompleteTask','completeTask']) }, { provide: AlertService, useValue: jasmine.createSpyObj('AlertService', ['error','setPreserveAlerts']) }, @@ -1800,7 +1805,7 @@ describe('CaseEditComponent', () => { wizard.pages = []; formErrorService = createSpyObj('formErrorService', ['mapFieldErrors']); - formValueService = createSpyObj('formValueService', ['sanitise']); + formValueService = createSpyObj('formValueService', ['sanitise', 'sanitiseDynamicLists']); const snapshotNoProfile = { pathFromRoot: [ From 04c802398a7983072eecdf5eaeab63acffa0410c Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 19 May 2026 15:30:28 +0100 Subject: [PATCH 08/10] refactoring of preserve collection dynamic multiselect values --- .../write-collection-field.component.spec.ts | 156 +++--------------- .../services/fields/fields.utils.spec.ts | 47 ++---- .../shared/services/fields/fields.utils.ts | 62 ++++--- .../form/field-type-sanitiser.spec.ts | 109 +++--------- .../services/form/field-type-sanitiser.ts | 84 +++++----- .../services/form/form-value.service.spec.ts | 31 ++++ .../services/form/form-value.service.ts | 22 +-- 7 files changed, 171 insertions(+), 340 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts index 89ca82e88b..1ccc471d1b 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.spec.ts @@ -1,6 +1,6 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatLegacyDialog as MatDialog, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; import { By } from '@angular/platform-browser'; import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; @@ -337,143 +337,29 @@ describe('WriteCollectionFieldComponent', () => { const fields = de.queryAll($WRITE_FIELDS); expect(fields.length).toBe(0); }); -}); - -describe('WriteCollectionFieldComponent with hydrated dynamic multiselect children', () => { - let fixture: ComponentFixture; - let component: WriteCollectionFieldComponent; - let formValidatorService: any; - let dialog: any; - let dialogRef: any; - let scrollToService: any; - let profileNotifier: any; - let caseField: CaseField; - let formGroup: FormGroup; - let collectionCreateCheckerService: CollectionCreateCheckerService; - - beforeEach(waitForAsync(() => { - formValidatorService = createSpyObj('formValidatorService', ['addValidators']); - dialogRef = createSpyObj>('MatDialogRef', ['afterClosed']); - dialogRef.afterClosed.and.returnValue(of()); - dialog = createSpyObj('MatDialog', ['open']); - dialog.open.and.returnValue(dialogRef); - scrollToService = createSpyObj('scrollToService', ['scrollTo']); - scrollToService.scrollTo.and.returnValue(of()); - caseField = ({ - id: 'stmtOfServiceAddRecipient', - label: 'Recipient', - display_context: 'OPTIONAL', - display_context_parameter: '#COLLECTION(allowInsert)', - field_type: { - id: 'StmtOfServiceAddRecipient', - type: 'Collection', - collection_field_type: { - id: 'StmtOfServiceAddRecipient', - type: 'Complex', - complex_fields: [ - ({ - id: 'servedParty', - label: 'Who was served?', - display_context: 'OPTIONAL', - field_type: { - id: 'DynamicList', - type: 'DynamicList' - } - }) as CaseField, - ({ - id: 'orderList', - label: 'Statement of service orders', - display_context: 'OPTIONAL', - field_type: { - id: 'DynamicMultiSelectList', - type: 'DynamicMultiSelectList' - }, - list_items: [ - { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' }, - { code: 'order-2', label: 'Parental responsibility order (C45A) - 15 Apr 2026' } - ] - }) as CaseField - ] - } - } as FieldType, - value: [ - { - id: 'recipient-1', - value: { - servedParty: 'John Doe (Applicant 1)', - orderList: [ - { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' } - ] - } - } - ], - acls: [ - { - role: 'caseworker-divorce', - create: true, - read: true, - update: true, - delete: true - } - ] - }) as CaseField; - formGroup = new FormGroup({ - field1: new FormControl() - }); - - profileNotifier = new ProfileNotifier(); - profileNotifier.profile = new BehaviorSubject(createAProfile()).asObservable(); - - collectionCreateCheckerService = new CollectionCreateCheckerService(); - - TestBed - .configureTestingModule({ - imports: [ - ReactiveFormsModule, - PaletteUtilsModule, - fieldWriteComponent, - fieldReadComponent - ], - declarations: [ - WriteCollectionFieldComponent, - MockRpxTranslatePipe, - MockFieldLabelPipe - ], - providers: [ - { provide: FormValidatorsService, useValue: formValidatorService }, - { provide: MatDialog, useValue: dialog }, - { provide: ScrollToService, useValue: scrollToService }, - { provide: ProfileNotifier, useValue: profileNotifier }, - { provide: CollectionCreateCheckerService, useValue: collectionCreateCheckerService }, - RemoveDialogComponent - ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(WriteCollectionFieldComponent); - component = fixture.componentInstance; - component.caseField = caseField; - component.caseFields = [caseField]; - component.formGroup = formGroup; - component.ngOnInit(); - fixture.detectChanges(); - })); - - afterEach(() => { - fixture.destroy(); - }); - - it('should retain orderList options for a new recipient without inheriting another recipient\'s selections', () => { - component.addItem(false); - fixture.detectChanges(); - const newRecipient = component.collItems[1].caseField; + it('should retain dynamic multiselect options for a new complex collection item', () => { + const orderListItems = [ + { code: 'order-1', label: 'Order 1' }, + { code: 'order-2', label: 'Order 2' } + ]; + caseField.field_type = { + type: 'Collection', + collection_field_type: { + type: 'Complex', + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + list_items: orderListItems + } as CaseField] + } + } as FieldType; + component.formArray = new FormArray([]); + + const newRecipient = component.buildCaseField({ value: null }, 1, true); const orderListField = newRecipient.field_type.complex_fields.find((field) => field.id === 'orderList'); - expect(orderListField.list_items).toEqual([ - { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' }, - { code: 'order-2', label: 'Parental responsibility order (C45A) - 15 Apr 2026' } - ]); + expect(orderListField.list_items).toEqual(orderListItems); expect(orderListField.value).toBeUndefined(); expect(newRecipient.value).toBeNull(); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts index f17e1e7483..f0bb6374f2 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts @@ -559,10 +559,6 @@ describe('FieldsUtils', () => { type: 'DynamicList' }, id: 'complex_dl', - list_items: [ - {code: '1', value: '1'}, - {code: '2', value: '2'} - ], value: { list_items: [ {code: '1', value: '1'}, @@ -605,42 +601,29 @@ describe('FieldsUtils', () => { type: 'Collection', collection_field_type: { type: 'Complex', - complex_fields: [ - { - field_type: { - type: 'DynamicMultiSelectList' - }, - id: 'orderList', - formatted_value: {} - } - ] + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + formatted_value: {} + }] } }, - value: [ - { - id: 'recipient-1', - value: { - orderList: { - list_items: listItems, - value: [ - {code: '2', value: '2'} - ] - } + value: [{ + value: { + orderList: { + list_items: listItems, + value: [{code: '2', value: '2'}] } } - ] + }] }; (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); - expect(callbackResponse.field_type.collection_field_type.complex_fields[0]).toEqual(jasmine.objectContaining({ - id: 'orderList', - list_items: listItems, - formatted_value: { - list_items: listItems - } - })); - expect((callbackResponse.field_type.collection_field_type.complex_fields[0] as any).value).toBeUndefined(); + const orderListField = callbackResponse.field_type.collection_field_type.complex_fields[0] as any; + expect(orderListField.list_items).toEqual(listItems); + expect(orderListField.formatted_value).toEqual({ list_items: listItems }); + expect(orderListField.value).toBeUndefined(); }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts index 4a4b2a7e6d..72d148a9d8 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts @@ -24,7 +24,6 @@ export class FieldsUtils { public static readonly SERVER_RESPONSE_FIELD_TYPE_COLLECTION = 'Collection'; public static readonly SERVER_RESPONSE_FIELD_TYPE_COMPLEX = 'Complex'; public static readonly SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_LIST_TYPE: FieldTypeEnum[] = ['DynamicList', 'DynamicRadioList']; - public static readonly SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_MULTISELECT_LIST_TYPE: FieldTypeEnum = 'DynamicMultiSelectList'; public static readonly defaultTabList = { "PRLAPPS": "Summary" } @@ -306,21 +305,18 @@ export class FieldsUtils { return `{ Invalid ${type}: ${invalidValue} }`; } - private static setDynamicListDefinition(caseField: CaseField, caseFieldType: FieldType, - rootCaseField: CaseField, isWithinCollection = false) { + private static setDynamicListDefinition(caseField: CaseField, caseFieldType: FieldType, rootCaseField: CaseField) { if (caseFieldType.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COMPLEX) { caseFieldType.complex_fields.forEach(field => { try { const isDynamicField = FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_LIST_TYPE.indexOf(field.field_type.type) !== -1; - const isDynamicMultiSelectField = - field.field_type.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_DYNAMIC_MULTISELECT_LIST_TYPE; + const isDynamicMultiSelectField = field.field_type.type === 'DynamicMultiSelectList'; - if (isDynamicField || isDynamicMultiSelectField) { + if (isDynamicMultiSelectField) { const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); if (dynamicListValue) { const list_items = dynamicListValue.find(data => data?.list_items !== undefined)?.list_items; - if (list_items !== undefined) { field.list_items = list_items; field.formatted_value = { @@ -329,34 +325,36 @@ export class FieldsUtils { }; } - if (isDynamicMultiSelectField) { - if (!isWithinCollection) { - const value = dynamicListValue[0]?.value; - if (value !== undefined) { - field.value = value; - field.formatted_value = { - ...field.formatted_value, - value - }; - } + if (rootCaseField.field_type.type !== FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { + const value = dynamicListValue[0]?.value; + if (value !== undefined) { + field.value = value; + field.formatted_value = { + ...field.formatted_value, + value + }; } - } else { - const complexValue = dynamicListValue.map(data => data.value); - const value = { - list_items, - value: complexValue.length > 0 ? complexValue : undefined - }; - field.value = { - ...value - }; - field.formatted_value = { - ...field.formatted_value, - ...value - }; } } + } else if (isDynamicField) { + const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); + if (dynamicListValue) { + const list_items = dynamicListValue[0].list_items; + const complexValue = dynamicListValue.map(data => data.value); + const value = { + list_items, + value: complexValue.length > 0 ? complexValue : undefined + }; + field.value = { + ...value + }; + field.formatted_value = { + ...field.formatted_value, + ...value + }; + } } else { - this.setDynamicListDefinition(field, field.field_type, rootCaseField, isWithinCollection); + this.setDynamicListDefinition(field, field.field_type, rootCaseField); } } catch (error) { console.log(error); @@ -364,7 +362,7 @@ export class FieldsUtils { }); } else if (caseFieldType.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { if (caseFieldType.collection_field_type) { - this.setDynamicListDefinition(caseField, caseFieldType.collection_field_type, rootCaseField, true); + this.setDynamicListDefinition(caseField, caseFieldType.collection_field_type, rootCaseField); } } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts index c8528b66af..a84b092bde 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.spec.ts @@ -137,69 +137,50 @@ describe('FieldTypeSanitiser', () => { expect(editForm.data.dynamicList).toEqual(EXPECTED_VALUE_DYNAMIC_LIST); }); - it('should preserve collection recipient DynamicMultiSelectList selections for each row during sanitisation', () => { - const orderListItems = [ - { code: 'order-1', label: 'Blank order or directions (C21) - 15 Apr 2026' }, - { code: 'order-2', label: 'Parental responsibility order (C45A) - 15 Apr 2026' } - ]; + it('should use collection row DynamicMultiSelectList options without leaking selections to new rows', () => { + const defaultItems = [{ code: 'default-1', label: 'Default 1' }, { code: 'default-2', label: 'Default 2' }]; + const recipientOneItems = [{ code: 'recipient-1-order-1', label: 'Recipient 1 order 1' }]; + const recipientTwoItems = [{ code: 'recipient-2-order-1', label: 'Recipient 2 order 1' }]; const collectionCaseFields = [{ id: 'stmtOfServiceAddRecipient', value: [ - { - id: 'recipient-1', - value: { - orderList: { - list_items: orderListItems, - value: [orderListItems[0]] - } - } - } + { id: 'recipient-1', value: { orderList: { list_items: recipientOneItems, value: [recipientOneItems[0]] } } }, + { id: 'recipient-2', value: { orderList: { list_items: recipientTwoItems, value: [recipientTwoItems[0]] } } } ], field_type: { - id: 'StmtOfServiceAddRecipient', type: 'Collection', collection_field_type: { - id: 'StmtOfServiceAddRecipient', type: 'Complex', - complex_fields: [ - { - id: 'orderList', - field_type: { - id: 'DynamicMultiSelectList', - type: 'DynamicMultiSelectList' - }, - display_context: 'OPTIONAL' - } - ] + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + display_context: 'OPTIONAL', + list_items: defaultItems + }] } } }] as unknown as CaseField[]; const formData = { stmtOfServiceAddRecipient: [ - { - id: 'recipient-1', - value: { - orderList: [orderListItems[0]] - } - }, - { - id: 'recipient-2', - value: { - orderList: [orderListItems[1]] - } - } + { id: 'recipient-1', value: { orderList: [recipientOneItems[0]] } }, + { id: 'recipient-2', value: { orderList: [recipientTwoItems[0]] } }, + { id: 'recipient-3', value: { orderList: [defaultItems[1]] } } ] }; new FieldTypeSanitiser().sanitiseLists(collectionCaseFields, formData); expect(formData.stmtOfServiceAddRecipient[0].value.orderList as any).toEqual({ - value: [orderListItems[0]], - list_items: orderListItems + value: [recipientOneItems[0]], + list_items: recipientOneItems }); expect(formData.stmtOfServiceAddRecipient[1].value.orderList as any).toEqual({ - value: [orderListItems[1]], - list_items: orderListItems + value: [recipientTwoItems[0]], + list_items: recipientTwoItems + }); + expect(formData.stmtOfServiceAddRecipient[2].value.orderList as any).toEqual({ + value: [defaultItems[1]], + list_items: defaultItems }); }); @@ -393,49 +374,5 @@ describe('FieldTypeSanitiser', () => { const result = fieldTypeSanitiser.ensureDynamicMultiSelectListPopulated(mockCaseFields); expect(result[2].field_type.complex_fields[0].field_type.complex_fields[0].list_items).toBeUndefined(); }); - - it('should populate list_items for DynamicMultiSelectList within collection complex fields', () => { - mockCaseFields.push({ - id: 'stmtOfServiceAddRecipient', - value: [ - { - id: 'recipient-1', - value: { - orderList: { - list_items: [ - { code: 'order-1', label: 'Order 1' }, - { code: 'order-2', label: 'Order 2' } - ] - } - } - } - ], - field_type: { - id: 'StmtOfServiceAddRecipient', - type: 'Collection', - collection_field_type: { - id: 'StmtOfServiceAddRecipient', - type: 'Complex', - complex_fields: [{ - id: 'orderList', - field_type: { - id: 'DynamicMultiSelectList', - type: 'DynamicMultiSelectList' - }, - display_context: 'OPTIONAL' - }] - } - } - } as unknown as CaseField); - - const result = fieldTypeSanitiser.ensureDynamicMultiSelectListPopulated(mockCaseFields); - const collectionField = result.find((field) => field.id === 'stmtOfServiceAddRecipient'); - const complexField = collectionField.field_type.collection_field_type.complex_fields.find((field) => field.id === 'orderList'); - - expect(complexField.list_items).toEqual([ - { code: 'order-1', label: 'Order 1' }, - { code: 'order-2', label: 'Order 2' } - ]); - }); }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts index a03f326e95..8b3c2ec61a 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/field-type-sanitiser.ts @@ -47,8 +47,16 @@ export class FieldTypeSanitiser { case FieldTypeSanitiser.FIELD_TYPE_COLLECTION: if (Array.isArray(data[caseField.id])) { - data[caseField.id].forEach((formElement: any) => { - this.sanitiseLists(caseField.field_type.collection_field_type.complex_fields, formElement.value); + data[caseField.id].forEach((formElement: any, index: number) => { + const matchingItem = Array.isArray(caseField.value) && formElement?.id !== undefined + ? caseField.value.find((item) => item?.id === formElement.id) + : null; + const collectionItem = matchingItem || (Array.isArray(caseField.value) ? caseField.value[index] : null); + this.sanitiseLists( + this.copyCaseFieldsWithCollectionData(caseField.field_type.collection_field_type.complex_fields, + collectionItem?.value || collectionItem), + formElement.value + ); }); } break; @@ -58,60 +66,58 @@ export class FieldTypeSanitiser { public ensureDynamicMultiSelectListPopulated(caseFields: CaseField[]): CaseField[] { return caseFields.map((field) => { - const fieldData = field._value || field.value; - - if (field.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { - this.checkNestedDynamicList(field, fieldData); - } else if (field.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COLLECTION && - field.field_type.collection_field_type?.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX - ) { - this.checkNestedDynamicList(field, fieldData, true); - } else { + if (field.field_type.type !== 'Complex') { return field; } + const caseFieldData = field._value; + // Process each complex field + field.field_type.complex_fields.forEach((complexField) => { + if (complexField.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { + this.checkNestedDynamicList(complexField, caseFieldData?.[complexField.id]); + } else if (this.isDynamicList(complexField.field_type.type) && + complexField.display_context !== 'HIDDEN' && + field._value?.[complexField.id] + ) { + complexField.list_items = field._value[complexField.id]?.list_items; + } + }); // Final transformation: construct updated field object return { ...field, field_type: { ...field?.field_type } } as CaseField; }); } - private checkNestedDynamicList(caseField: CaseField, fieldData: any = null, isCollection = false): void { - const complexFields = isCollection - ? caseField.field_type.collection_field_type?.complex_fields || [] - : caseField.field_type.complex_fields; - - complexFields.forEach((complexField) => { - const childData = isCollection - ? this.getFirstCollectionFieldData(fieldData, complexField.id) - : fieldData?.[complexField.id]; - + private checkNestedDynamicList(caseField: CaseField, fieldData: any = null): void { + caseField.field_type.complex_fields.forEach((complexField) => { if (complexField.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { - this.checkNestedDynamicList(complexField, childData); - } else if (complexField.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COLLECTION && - complexField.field_type.collection_field_type?.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX - ) { - this.checkNestedDynamicList(complexField, childData, true); + this.checkNestedDynamicList(complexField, fieldData?.[complexField.id]); } else if (this.isDynamicList(complexField.field_type.type) && complexField.display_context !== 'HIDDEN' && - childData + fieldData?.[complexField.id] ) { - complexField.list_items = childData.list_items; + complexField.list_items = fieldData?.[complexField.id]?.list_items; } }); } - private getFirstCollectionFieldData(collectionData: any[], fieldId: string): any { - if (!Array.isArray(collectionData)) { - return null; - } - - for (const item of collectionData) { - const value = item?.value || item; - if (value?.[fieldId] !== undefined) { - return value[fieldId]; + private copyCaseFieldsWithCollectionData(caseFields: CaseField[], collectionItemData: any): CaseField[] { + return caseFields.map((field) => { + const fieldData = collectionItemData?.[field.id]; + if (field.field_type.type === FieldTypeSanitiser.FIELD_TYPE_COMPLEX) { + return { + ...field, + field_type: { + ...field.field_type, + complex_fields: this.copyCaseFieldsWithCollectionData(field.field_type.complex_fields, fieldData) + } + } as CaseField; } - } - return null; + return this.isDynamicList(field.field_type.type) && + field.display_context !== 'HIDDEN' && + fieldData?.list_items !== undefined + ? { ...field, list_items: fieldData.list_items } as CaseField + : field; + }); } private isDynamicList(fieldType: FieldTypeEnum): boolean { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.spec.ts index ffdc3f7355..c4e1318fa2 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.spec.ts @@ -895,6 +895,37 @@ describe('FormValueService', () => { }; expect(JSON.stringify(data)).toEqual(JSON.stringify(actual)); }); + + it('should retain nested fields hidden with HIDDEN_TEMP display context', () => { + const data = { + complexField: { hiddenChild: 'keep me' }, + collectionField: [{ id: 'row-1', value: { hiddenChild: 'keep me' } }] + }; + const childField = { + id: 'hiddenChild', + hidden: true, + display_context: 'HIDDEN_TEMP', + retain_hidden_value: false, + field_type: { type: 'Text' } + }; + const caseFields = [{ + id: 'complexField', + field_type: { type: 'Complex', complex_fields: [childField] } + }, { + id: 'collectionField', + field_type: { + type: 'Collection', + collection_field_type: { type: 'Complex', complex_fields: [childField] } + } + }] as unknown as CaseField[]; + + formValueService.removeUnnecessaryFields(data, caseFields); + + expect(data).toEqual({ + complexField: { hiddenChild: 'keep me' }, + collectionField: [{ id: 'row-1', value: { hiddenChild: 'keep me' } }] + }); + }); }); describe('removeInvalidCollectionData', () => { it('should empty the collection field if it contains only id', () => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts index 1452a37b1a..552e2cc3f5 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/form/form-value.service.ts @@ -294,7 +294,7 @@ export class FormValueService { * @param clearNonCase Whether or not we should clear out non-case fields at the top level. */ public removeUnnecessaryFields(data: object, caseFields: CaseField[], clearEmpty = false, clearNonCase = false, - fromPreviousPage = false, currentPageCaseFields = [], isNested = false): void { + fromPreviousPage = false, currentPageCaseFields = []): void { if (data && caseFields && caseFields.length > 0) { // check if there is any data at the top level of the form that's not in the caseFields if (clearNonCase) { @@ -305,9 +305,9 @@ export class FormValueService { // Retain anything that is readonly and not a label. continue; } - if (this.shouldRemoveHiddenField(field, isNested)) { + if (field.hidden === true && field.display_context !== 'HIDDEN' && field.display_context !== 'HIDDEN_TEMP' && field.id !== 'caseLinks' && !field.retain_hidden_value) { // Delete anything that is hidden (that is NOT readonly), and that - // is not explicitly retained. Nested hidden fields should be dropped by default. + // hasn't had its display_context overridden to make it hidden. delete data[field.id]; } else if (field.field_type) { switch (field.field_type.type) { @@ -321,8 +321,7 @@ export class FormValueService { } break; case 'Complex': - this.removeUnnecessaryFields(data[field.id], field.field_type.complex_fields, clearEmpty, - false, false, [], true); + this.removeUnnecessaryFields(data[field.id], field.field_type.complex_fields, clearEmpty); // Also remove any optional complex objects that are completely empty. // EUI-4244: Ritesh's fix, passing true instead of clearEmpty. if (FormValueService.clearOptionalEmpty(true, data[field.id], field)) { @@ -345,10 +344,8 @@ export class FormValueService { if (field.field_type.collection_field_type.type === 'Complex') { // Iterate through the elements and remove any unnecessary fields within. for (const item of collection) { - this.removeUnnecessaryFields(item, field.field_type.collection_field_type.complex_fields, clearEmpty, - false, false, [], true); - this.removeUnnecessaryFields(item.value, field.field_type.collection_field_type.complex_fields, false, - false, false, [], true); + this.removeUnnecessaryFields(item, field.field_type.collection_field_type.complex_fields, clearEmpty); + this.removeUnnecessaryFields(item.value, field.field_type.collection_field_type.complex_fields, false); } } } @@ -364,13 +361,6 @@ export class FormValueService { FormValueService.removeMultiSelectLabels(data); } - private shouldRemoveHiddenField(field: CaseField, isNested: boolean): boolean { - return field.hidden === true - && field.id !== 'caseLinks' - && !field.retain_hidden_value - && (isNested || (field.display_context !== 'HIDDEN' && field.display_context !== 'HIDDEN_TEMP')); - } - public removeInvalidCollectionData(data: object, field: CaseField) { if (data[field.id] && data[field.id].length > 0) { for (const objCollection of data[field.id]) { From eb36f225291f2b7cf3b6fcde67e6b9dfec22b2f5 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 19 May 2026 16:15:54 +0100 Subject: [PATCH 09/10] added additional unit tests --- .../services/fields/fields.utils.spec.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts index f0bb6374f2..7d4b0d9d05 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.spec.ts @@ -625,6 +625,144 @@ describe('FieldsUtils', () => { expect(orderListField.formatted_value).toEqual({ list_items: listItems }); expect(orderListField.value).toBeUndefined(); }); + + it('should hydrate list_items and selected values for complex dynamic multiselects', () => { + const listItems = [ + {code: '1', value: '1'}, + {code: '2', value: '2'} + ]; + const selectedValue = [{code: '2', value: '2'}]; + const callbackResponse = { + field_type: { + type: 'Complex', + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + formatted_value: { + label: 'Order list' + } + }] + }, + value: { + orderList: { + list_items: listItems, + value: selectedValue + } + } + }; + + (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); + + const orderListField = callbackResponse.field_type.complex_fields[0] as any; + expect(orderListField.list_items).toEqual(listItems); + expect(orderListField.value).toEqual(selectedValue); + expect(orderListField.formatted_value).toEqual({ + label: 'Order list', + list_items: listItems, + value: selectedValue + }); + }); + + it('should persist multiple selected values for complex dynamic multiselects', () => { + const listItems = [ + {code: '1', value: '1'}, + {code: '2', value: '2'}, + {code: '3', value: '3'} + ]; + const selectedValue = [ + {code: '1', value: '1'}, + {code: '3', value: '3'} + ]; + const callbackResponse = { + field_type: { + type: 'Complex', + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + formatted_value: {} + }] + }, + value: { + orderList: { + list_items: listItems, + value: selectedValue + } + } + }; + + (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); + + const orderListField = callbackResponse.field_type.complex_fields[0] as any; + expect(orderListField.value).toEqual(selectedValue); + expect(orderListField.formatted_value.value).toEqual(selectedValue); + }); + + it('should hydrate list_items from a later nested dynamic multiselect while using the first selected value', () => { + const listItems = [ + {code: '1', value: '1'}, + {code: '2', value: '2'} + ]; + const selectedValue = [{code: '1', value: '1'}]; + const callbackResponse = { + field_type: { + type: 'Complex', + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + formatted_value: {} + }] + }, + value: { + recipients: [{ + orderList: { + value: selectedValue + } + }, { + orderList: { + list_items: listItems, + value: [{code: '2', value: '2'}] + } + }] + } + }; + + (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); + + const orderListField = callbackResponse.field_type.complex_fields[0] as any; + expect(orderListField.list_items).toEqual(listItems); + expect(orderListField.value).toEqual(selectedValue); + expect(orderListField.formatted_value).toEqual({ + list_items: listItems, + value: selectedValue + }); + }); + + it('should leave dynamic multiselects unchanged when matching data has no list_items or selected value', () => { + const callbackResponse = { + field_type: { + type: 'Complex', + complex_fields: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + formatted_value: { + label: 'Order list' + } + }] + }, + value: { + orderList: {} + } + }; + + (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); + + const orderListField = callbackResponse.field_type.complex_fields[0] as any; + expect(orderListField.list_items).toBeUndefined(); + expect(orderListField.value).toBeUndefined(); + expect(orderListField.formatted_value).toEqual({ + label: 'Order list' + }); + }); }); describe('isFlagsCaseField() function test', () => { From 90659372169106447b6f2020ee01b9689624d9cd Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 19 May 2026 16:29:04 +0100 Subject: [PATCH 10/10] refactored to fix sonar complexity issue --- .../shared/services/fields/fields.utils.ts | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts index 72d148a9d8..51891f9238 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts @@ -314,28 +314,7 @@ export class FieldsUtils { const isDynamicMultiSelectField = field.field_type.type === 'DynamicMultiSelectList'; if (isDynamicMultiSelectField) { - const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); - if (dynamicListValue) { - const list_items = dynamicListValue.find(data => data?.list_items !== undefined)?.list_items; - if (list_items !== undefined) { - field.list_items = list_items; - field.formatted_value = { - ...field.formatted_value, - list_items - }; - } - - if (rootCaseField.field_type.type !== FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { - const value = dynamicListValue[0]?.value; - if (value !== undefined) { - field.value = value; - field.formatted_value = { - ...field.formatted_value, - value - }; - } - } - } + this.setDynamicMultiSelectListDefinition(field, rootCaseField); } else if (isDynamicField) { const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); if (dynamicListValue) { @@ -367,6 +346,31 @@ export class FieldsUtils { } } + private static setDynamicMultiSelectListDefinition(field: CaseField, rootCaseField: CaseField) { + const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); + if (dynamicListValue) { + const list_items = dynamicListValue.find(data => data?.list_items !== undefined)?.list_items; + if (list_items !== undefined) { + field.list_items = list_items; + field.formatted_value = { + ...field.formatted_value, + list_items + }; + } + + if (rootCaseField.field_type.type !== FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { + const value = dynamicListValue[0]?.value; + if (value !== undefined) { + field.value = value; + field.formatted_value = { + ...field.formatted_value, + value + }; + } + } + } + } + private static getDynamicListValue(jsonBlock: any, key: string) { const data = jsonBlock ? this.getNestedFieldValues(jsonBlock, key, []) : [];