diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 8878e6c0d8..ff68ab3d57 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,8 @@ ## RELEASE NOTES +### Version 7.3.54-exui-3740 +**EXUI-3740** Dynamic MultiSelect Box doesn't retain it's original data in Collection on Add new button click + ### Version 7.3.54 **EXUI-4636** Revert CME-220 and EXUI-2111 **EXUI-4637** Revert EXUI-2645 diff --git a/package.json b/package.json index 38447a3b4c..5b6ad9eb4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.54", + "version": "7.3.54-exui-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 6fdf4141f8..b92657f607 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.54", + "version": "7.3.54-exui-3740", "engines": { "node": ">=20.19.0" }, 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: [ 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 139b1f3fa1..4caf838d3a 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 @@ -317,12 +317,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/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 57402618e3..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,6 +337,32 @@ describe('WriteCollectionFieldComponent', () => { const fields = de.queryAll($WRITE_FIELDS); expect(fields.length).toBe(0); }); + + 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(orderListItems); + expect(orderListField.value).toBeUndefined(); + expect(newRecipient.value).toBeNull(); + }); }); describe('WriteCollectionFieldComponent CRUD impact', () => { 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 a5ab358a37..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 @@ -590,6 +590,179 @@ 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: [{ + id: 'orderList', + field_type: { type: 'DynamicMultiSelectList' }, + formatted_value: {} + }] + } + }, + value: [{ + value: { + orderList: { + list_items: listItems, + value: [{code: '2', value: '2'}] + } + } + }] + }; + + (FieldsUtils as any).setDynamicListDefinition(callbackResponse, callbackResponse.field_type, callbackResponse); + + 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(); + }); + + 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', () => { 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 2ab42b88a3..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 @@ -311,8 +311,11 @@ export class FieldsUtils { 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 === 'DynamicMultiSelectList'; - if (isDynamicField) { + if (isDynamicMultiSelectField) { + this.setDynamicMultiSelectListDefinition(field, rootCaseField); + } else if (isDynamicField) { const dynamicListValue = this.getDynamicListValue(rootCaseField.value, field.id); if (dynamicListValue) { const list_items = dynamicListValue[0].list_items; @@ -343,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, []) : []; 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..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,6 +137,53 @@ describe('FieldTypeSanitiser', () => { expect(editForm.data.dynamicList).toEqual(EXPECTED_VALUE_DYNAMIC_LIST); }); + 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: recipientOneItems, value: [recipientOneItems[0]] } } }, + { id: 'recipient-2', value: { orderList: { list_items: recipientTwoItems, value: [recipientTwoItems[0]] } } } + ], + field_type: { + type: 'Collection', + collection_field_type: { + type: 'Complex', + 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: [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: [recipientOneItems[0]], + list_items: recipientOneItems + }); + expect(formData.stmtOfServiceAddRecipient[1].value.orderList as any).toEqual({ + value: [recipientTwoItems[0]], + list_items: recipientTwoItems + }); + expect(formData.stmtOfServiceAddRecipient[2].value.orderList as any).toEqual({ + value: [defaultItems[1]], + list_items: defaultItems + }); + }); + describe('ensureDynamicMultiSelectListPopulated', () => { let fieldTypeSanitiser: FieldTypeSanitiser; let mockCaseFields: CaseField[]; 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..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; @@ -91,6 +99,27 @@ export class FieldTypeSanitiser { }); } + 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 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 { return FieldTypeSanitiser.DYNAMIC_LIST_TYPE.indexOf(fieldType) !== -1; } 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', () => {