diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 396fcb1da4..fcf6e48c00 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,4 +1,7 @@ ## RELEASE NOTES +### Version 6.19.13-case-flags-v2-manage-case-flags-value-caching-fix +**EUI-9020** Fix bug where previous changes to a flag are retained in the UI when the user starts over and selects the same flag to update again + ### Version 6.16.0-follow-up-on-query-tab-v2 **EUI-8608** Query management follow up on the query tab diff --git a/package.json b/package.json index f83c5ebf6d..68062f1c23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "6.19.13-case-flags-v2-update-flag-show-other-description", + "version": "6.19.13-case-flags-v2-manage-case-flags-value-caching-fix", "engines": { "yarn": "^3.5.0", "npm": "^8.10.0" diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index 32b7b8483a..130be13db6 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": "6.19.13-case-flags-v2-update-flag-show-other-description", + "version": "6.19.13-case-flags-v2-manage-case-flags-value-caching-fix", "engines": { "yarn": "^3.5.0", "npm": "^8.10.0" diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.spec.ts index 1ef39d6212..5b7ea32c5a 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.spec.ts @@ -78,6 +78,28 @@ describe('ManageCaseFlagsComponent', () => { subTypeValue: 'Dummy subtype value', subTypeValue_cy: 'Dummy subtype value - Welsh' }; + const activeFlagWithOtherDescription = { + name: 'Flag 1', + flagComment: 'First flag', + flagComment_cy: 'Cymraeg', + dateTimeCreated: new Date(), + path: [{id: null, value: 'Reasonable adjustment'}], + hearingRelevant: false, + flagCode: 'OT0001', + status: 'Active', + otherDescription: 'Description' + } as FlagDetail; + const activeFlagWithOtherDescriptionCy = { + name: 'Flag 1', + flagComment: 'First flag', + flagComment_cy: 'Cymraeg', + dateTimeCreated: new Date(), + path: [{id: null, value: 'Reasonable adjustment'}], + hearingRelevant: false, + flagCode: 'OT0001', + status: 'Active', + otherDescription_cy: 'Description (Welsh)' + } as FlagDetail; const flagsData = [ { flags: { @@ -450,6 +472,124 @@ describe('ManageCaseFlagsComponent', () => { expect(flagDetailMappedForDisplay.originalStatus).toEqual(flagsInstance.caseField.formatted_value.partyFlags.details[0].value.status); }); + it('should not cache any changes to the comments and description fields (English and Welsh) if the user starts over', () => { + const flagWithOtherDescriptionDetail = { + id: 'a6073742-f616-4a6a-a412-a0e60f47dc32', + ...activeFlagWithOtherDescription + } as FlagDetail; + const flagsInstance = { + flags: { + partyName: 'Rose Bank', + details: [flagWithOtherDescriptionDetail] as FlagDetail[], + flagsCaseFieldId: 'CaseFlag' + }, + pathToFlagsFormGroup: 'CaseFlag', + caseField: { + id: 'CaseFlag', + field_type: { + id: 'Flags', + type: 'Complex' + } as FieldType, + formatted_value: { + partyName: 'Rose Bank', + details: [ + { + id: 'a6073742-f616-4a6a-a412-a0e60f47dc32', + value: { ...activeFlagWithOtherDescription } + } + ] + }, + value: { + partyName: 'Rose Bank', + details: [ + { + id: 'a6073742-f616-4a6a-a412-a0e60f47dc32', + value: { ...activeFlagWithOtherDescription } + } + ] + } + } as CaseField + } as FlagsWithFormGroupPath; + // Make some changes to comments and description fields in the flag instance, simulating previous updates via the UI + flagsInstance.flags.details[0].flagComment = 'A new comment'; + flagsInstance.flags.details[0].flagComment_cy = 'A new comment in Welsh'; + flagsInstance.flags.details[0].otherDescription = 'A new description'; + flagsInstance.flags.details[0].otherDescription_cy = 'A new description in Welsh'; + const flagDetailMappedForDisplay = component.mapFlagDetailForDisplay(flagWithOtherDescriptionDetail, flagsInstance); + // Expect all four fields to have been reset to the original persisted values + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.flagComment).toEqual( + flagsInstance.caseField.formatted_value.details[0].value.flagComment); + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.flagComment_cy).toEqual( + flagsInstance.caseField.formatted_value.details[0].value.flagComment_cy); + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.otherDescription).toEqual( + flagsInstance.caseField.formatted_value.details[0].value.otherDescription); + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.otherDescription_cy).toEqual( + flagsInstance.caseField.formatted_value.details[0].value.otherDescription_cy); + }); + + it('should not cache changes to comments and description fields where flag is within a Complex type, if the user starts over', () => { + const flagWithOtherDescriptionCyDetail = { + id: 'b43d9d2c-9f9f-4514-b182-508cfe19550e', + ...activeFlagWithOtherDescriptionCy + } as FlagDetail; + const flagsInstance = { + flags: { + partyName: 'Rose Bank', + details: [flagWithOtherDescriptionCyDetail] as FlagDetail[], + flagsCaseFieldId: 'Witness1' + }, + pathToFlagsFormGroup: 'Witness1.partyFlags', + caseField: { + id: 'Witness1', + field_type: { + id: 'Witness', + type: 'Complex' + } as FieldType, + formatted_value: { + firstName: 'Rose', + lastName: 'Bank', + partyFlags: { + partyName: 'Rose Bank', + details: [ + { + id: 'b43d9d2c-9f9f-4514-b182-508cfe19550e', + value: { ...activeFlagWithOtherDescriptionCy } + } + ] + } + }, + value: { + firstName: 'Rose', + lastName: 'Bank', + partyFlags: { + partyName: 'Rose Bank', + details: [ + { + id: 'b43d9d2c-9f9f-4514-b182-508cfe19550e', + value: { ...activeFlagWithOtherDescriptionCy } + } + ] + } + } + } as CaseField + } as FlagsWithFormGroupPath; + // Make some changes to comments and description fields in the flag instance, simulating previous updates via the UI + flagsInstance.flags.details[0].flagComment = 'A new comment'; + flagsInstance.flags.details[0].flagComment_cy = 'A new comment in Welsh'; + flagsInstance.flags.details[0].otherDescription = 'A new description'; + flagsInstance.flags.details[0].otherDescription_cy = 'A new description in Welsh'; + const flagDetailMappedForDisplay = component.mapFlagDetailForDisplay(flagWithOtherDescriptionCyDetail, flagsInstance); + // Expect all four fields to have been reset to the original persisted values + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.flagComment).toEqual( + flagsInstance.caseField.formatted_value.partyFlags.details[0].value.flagComment); + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.flagComment_cy).toEqual( + flagsInstance.caseField.formatted_value.partyFlags.details[0].value.flagComment_cy); + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.otherDescription).toEqual( + flagsInstance.caseField.formatted_value.partyFlags.details[0].value.otherDescription); + expect(flagDetailMappedForDisplay.flagDetailDisplay.flagDetail.otherDescription_cy).toEqual( + flagsInstance.caseField.formatted_value.partyFlags.details[0].value.otherDescription_cy); + }); + it('should emit to parent with the selected party and flag details if the validation succeeds', () => { spyOn(component, 'onNext').and.callThrough(); spyOn(component.caseFlagStateEmitter, 'emit'); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.ts index c5ffae5453..1f2db295a1 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/components/manage-case-flags/manage-case-flags.component.ts @@ -57,14 +57,17 @@ export class ManageCaseFlagsComponent implements OnInit { } public mapFlagDetailForDisplay(flagDetail: FlagDetail, flagsInstance: FlagsWithFormGroupPath): FlagDetailDisplayWithFormGroupPath { - // Cache the *original* status of the flag before it is modified. This is needed because ngOnInit() needs to filter + // Reset the flag status with the original persisted status. This is needed because ngOnInit() needs to filter // out any "Inactive" or "Not approved" flags based on their status *before* modification. If the user changes a - // flag's status then decides to return to the start of the flag update journey, the flag's status no longer - // reflects its actual *persisted* status + // flag's status then decides to return to the start of the flag update journey, the flag's status would no + // longer reflect its actual *persisted* status + // Also reset comments and description fields (both English and Welsh) with the original persisted data, to avoid + // the UI caching any changes that the user might not want persisted, if they start over and don't intend to add + // translations subsequently let originalStatus: string; - let formattedValue = flagsInstance.caseField.formatted_value; + let formattedValue = flagsInstance.caseField?.formatted_value; // Use the pathToFlagsFormGroup property from the selected flag location to drill down to the correct part of the - // CaseField formatted_value from which to get the original status + // CaseField formatted_value from which to get the original persisted data const pathToValue = flagsInstance.pathToFlagsFormGroup; // Root-level Flags CaseFields don't have a dot-delimited path - just the CaseField ID itself - so don't drill down if (pathToValue.indexOf('.') > -1) { @@ -75,21 +78,25 @@ export class ManageCaseFlagsComponent implements OnInit { }); } if (formattedValue && FieldsUtils.isNonEmptyObject(formattedValue)) { - const originalFlagDetail = formattedValue.details.find((detail) => detail.id === flagDetail.id); + const originalFlagDetail = formattedValue.details?.find((detail) => detail.id === flagDetail.id); if (originalFlagDetail) { - originalStatus = originalFlagDetail.value.status; + originalStatus = originalFlagDetail.value?.status; + flagDetail.flagComment = originalFlagDetail.value?.flagComment; + flagDetail.flagComment_cy = originalFlagDetail.value?.flagComment_cy; + flagDetail.otherDescription = originalFlagDetail.value?.otherDescription; + flagDetail.otherDescription_cy = originalFlagDetail.value?.otherDescription_cy; } } return { flagDetailDisplay: { - partyName: flagsInstance.flags.partyName, + partyName: flagsInstance.flags?.partyName, flagDetail, - flagsCaseFieldId: flagsInstance.caseField.id, - visibility: flagsInstance.flags.visibility + flagsCaseFieldId: flagsInstance.caseField?.id, + visibility: flagsInstance.flags?.visibility }, pathToFlagsFormGroup: flagsInstance.pathToFlagsFormGroup, caseField: flagsInstance.caseField, - roleOnCase: flagsInstance.flags.roleOnCase, + roleOnCase: flagsInstance.flags?.roleOnCase, originalStatus }; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/read-case-flag-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/read-case-flag-field.component.ts index 4b8122c74f..1f9bec9add 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/read-case-flag-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/read-case-flag-field.component.ts @@ -134,7 +134,7 @@ export class ReadCaseFlagFieldComponent extends AbstractFieldReadComponent imple private extractNewFlagToFlagDetailDisplayObject(selectedFlagsLocation: FlagsWithFormGroupPath): FlagDetailDisplay { // Use the pathToFlagsFormGroup property from the selected flag location to drill down to the correct part of the // CaseField value containing the new flag - let flagsCaseFieldValue = selectedFlagsLocation.caseField.value; + let flagsCaseFieldValue = selectedFlagsLocation.caseField?.value; const path = selectedFlagsLocation.pathToFlagsFormGroup; // Root-level Flags CaseFields don't have a dot-delimited path - just the CaseField ID itself - so don't drill down if (path.indexOf('.') > -1) { @@ -144,7 +144,7 @@ export class ReadCaseFlagFieldComponent extends AbstractFieldReadComponent imple return { partyName: flagsCaseFieldValue.partyName, // Look in the details array for the object that does *not* have an id - this indicates it is the new flag - flagDetail: flagsCaseFieldValue.details.find(element => !element.hasOwnProperty('id'))?.value + flagDetail: flagsCaseFieldValue.details?.find(element => !element.hasOwnProperty('id'))?.value } as FlagDetailDisplay; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/write-case-flag-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/write-case-flag-field.component.ts index 672427c58f..59ee58bf43 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/write-case-flag-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/case-flag/write-case-flag-field.component.ts @@ -245,13 +245,13 @@ export class WriteCaseFlagFieldComponent extends AbstractFieldWriteComponent imp this.flagsData.forEach(instance => { // Use the pathToFlagsFormGroup property for each Flags case field to drill down to the correct part of the // CaseField value to remove the new value from - let value = instance.caseField.value; + let value = instance.caseField?.value; const pathToValue = instance.pathToFlagsFormGroup; // Root-level Flags CaseFields don't have a dot-delimited path - just the CaseField ID itself - so don't drill down if (pathToValue.indexOf('.') > -1) { pathToValue.slice(pathToValue.indexOf('.') + 1).split('.').forEach(part => value = value[part]); } - if (value && value.details && value.details.length > 0) { + if (value?.details?.length > 0) { const indexOfNewFlagDetail = value.details.findIndex(element => !element.hasOwnProperty('id')); if (indexOfNewFlagDetail > -1) { value.details.splice(indexOfNewFlagDetail, 1); @@ -271,7 +271,7 @@ export class WriteCaseFlagFieldComponent extends AbstractFieldWriteComponent imp if (this.determinedLocation) { const path = this.determinedLocation.pathToFlagsFormGroup; const flagDataRef = this.flagsData.find(item => item.pathToFlagsFormGroup === path); - let flagsCaseFieldValue = flagDataRef.caseField.value; + let flagsCaseFieldValue = flagDataRef.caseField?.value; // Use the pathToFlagsFormGroup property from the selected flag location to drill down to the correct part of the // CaseField value to apply changes to // Root-level Flags CaseFields don't have a dot-delimited path - just the CaseField ID itself - so don't drill down @@ -374,8 +374,8 @@ export class WriteCaseFlagFieldComponent extends AbstractFieldWriteComponent imp this.flagsData.forEach(instance => { // Use the pathToFlagsFormGroup property for each Flags case field to drill down to the correct part of the // CaseField value for which to restore the original values - let value = instance.caseField.value; - let formattedValue = instance.caseField.formatted_value; + let value = instance.caseField?.value; + let formattedValue = instance.caseField?.formatted_value; const pathToValue = instance.pathToFlagsFormGroup; // Root-level Flags CaseFields don't have a dot-delimited path - just the CaseField ID itself - so don't drill down if (pathToValue.indexOf('.') > -1) { @@ -388,15 +388,15 @@ export class WriteCaseFlagFieldComponent extends AbstractFieldWriteComponent imp } if (value?.details?.length > 0 && formattedValue && FieldsUtils.isNonEmptyObject(formattedValue)) { value.details.forEach(flagDetail => { - const originalFlagDetail = formattedValue.details.find(detail => detail.id === flagDetail.id); + const originalFlagDetail = formattedValue.details?.find(detail => detail.id === flagDetail.id); if (originalFlagDetail) { - flagDetail.value.otherDescription = originalFlagDetail.value.otherDescription || null; - flagDetail.value.otherDescription_cy = originalFlagDetail.value.otherDescription_cy || null; - flagDetail.value.flagComment = originalFlagDetail.value.flagComment || null; - flagDetail.value.flagComment_cy = originalFlagDetail.value.flagComment_cy || null; - flagDetail.value.flagUpdateComment = originalFlagDetail.value.flagUpdateComment || null; - flagDetail.value.status = originalFlagDetail.value.status; - flagDetail.value.dateTimeModified = originalFlagDetail.value.dateTimeModified || null; + flagDetail.value.otherDescription = originalFlagDetail.value?.otherDescription || null; + flagDetail.value.otherDescription_cy = originalFlagDetail.value?.otherDescription_cy || null; + flagDetail.value.flagComment = originalFlagDetail.value?.flagComment || null; + flagDetail.value.flagComment_cy = originalFlagDetail.value?.flagComment_cy || null; + flagDetail.value.flagUpdateComment = originalFlagDetail.value?.flagUpdateComment || null; + flagDetail.value.status = originalFlagDetail.value?.status; + flagDetail.value.dateTimeModified = originalFlagDetail.value?.dateTimeModified || null; } }); } @@ -404,7 +404,7 @@ export class WriteCaseFlagFieldComponent extends AbstractFieldWriteComponent imp if (!this.selectedFlag) { this.selectedFlag = this.formGroup.get(this.selectedManageCaseLocation).value as FlagDetailDisplayWithFormGroupPath; } - let flagsCaseFieldValue = this.selectedFlag.caseField.value; + let flagsCaseFieldValue = this.selectedFlag.caseField?.value; // Use the pathToFlagsFormGroup property from the selected flag location to drill down to the correct part of the // CaseField value to apply changes to const path = this.selectedFlag.pathToFlagsFormGroup; @@ -413,17 +413,17 @@ export class WriteCaseFlagFieldComponent extends AbstractFieldWriteComponent imp path.slice(path.indexOf('.') + 1).split('.').forEach(part => flagsCaseFieldValue = flagsCaseFieldValue[part]); } if (flagsCaseFieldValue) { - const flagDetailToUpdate = flagsCaseFieldValue.details.find( - detail => detail.id === this.selectedFlag.flagDetailDisplay.flagDetail.id); + const flagDetailToUpdate = flagsCaseFieldValue.details?.find( + detail => detail.id === this.selectedFlag.flagDetailDisplay?.flagDetail?.id); if (flagDetailToUpdate) { // Cache the *original* status of the flag before it is modified. This is needed if the user changes the flag status // then decides to return to any part of the flag update journey. The ManageCaseFlagsComponent and UpdateFlagComponent // should refer to a flag's original status, not the one set via the UI because this hasn't been persisted yet - this.selectedFlag.originalStatus = flagDetailToUpdate.value.status; + this.selectedFlag.originalStatus = flagDetailToUpdate.value?.status; // Update description fields only if flag type is "Other" (flag code OT0001); these fields apply only to that flag type // If their FormControls don't exist, it means these fields weren't visited as part of the "Update Flag" journey, so do // *not* update their values (otherwise they will become undefined) - if (flagDetailToUpdate.value.flagCode === this.otherFlagTypeCode) { + if (flagDetailToUpdate.value?.flagCode === this.otherFlagTypeCode) { if (this.caseFlagParentFormGroup.get(CaseFlagFormFields.OTHER_FLAG_DESCRIPTION)) { flagDetailToUpdate.value.otherDescription = this.caseFlagParentFormGroup.get( CaseFlagFormFields.OTHER_FLAG_DESCRIPTION).value;