Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EUI-9020: Case Flags v2 fix - Reset flag changes when user starts over while updating a flag #1625

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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