Skip to content

Commit

Permalink
EUI-9020: Case Flags v2 fix - Reset flag changes when user starts ove…
Browse files Browse the repository at this point in the history
…r while updating a flag

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.
  • Loading branch information
Daniel-Lam committed Nov 28, 2023
1 parent 6029336 commit 5e43a24
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 33 deletions.
3 changes: 3 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion projects/ccd-case-ui-toolkit/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -388,23 +388,23 @@ 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;
}
});
}
});
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;
Expand All @@ -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;
Expand Down

0 comments on commit 5e43a24

Please sign in to comment.