Skip to content

Commit

Permalink
Merge branch 'master' into fix/exui-2595
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh-HMCTS committed Jan 2, 2025
2 parents 2b30b9a + ada884f commit c76ddd8
Show file tree
Hide file tree
Showing 68 changed files with 6,151 additions and 2,410 deletions.
8 changes: 7 additions & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
## RELEASE NOTES

### Version 7.0.75-exui-2575
**EXUI-2536** issue with DynamicMultiSelectList
**EXUI-2431** Special characters stopping events
**EXUI-2537** upgrade pdf-dist in media viewer
**EXUI-2389** PED and Media Viewer

### Version 7.0.75-exui-2315
**EXUI-2315** etrieve current user language selection
**EXUI-2315** retrieve current user language selection

### Version 7.0.75-exui-2515
**EXUI-2515** case-link-issue
Expand Down
3 changes: 2 additions & 1 deletion browserslist
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.
not op_mini all
not kaios 2.5
9 changes: 8 additions & 1 deletion demo/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export class AppConfig extends AbstractAppConfig {
'access_management_mode': true,
'refunds_url': '/api/refunds',
'payment_return_url': 'https://paymentoutcome-web.demo.platform.hmcts.net/',
'case_flags_refdata_api_url': '/refdata/commondata/caseflags/service-id=:sid'
'case_flags_refdata_api_url': '/refdata/commondata/caseflags/service-id=:sid',
'events_to_hide': [
'queryManagementRespondQuery'
]
};

constructor(private http: HttpClient) {
Expand Down Expand Up @@ -187,4 +190,8 @@ export class AppConfig extends AbstractAppConfig {
public getCaseFlagsRefdataApiUrl(): string {
return this.config.case_flags_refdata_api_url;
}

public getEventsToHide(): string[] {
return this.config.events_to_hide;
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hmcts/ccd-case-ui-toolkit",
"version": "7.0.75-event-spinner",
"version": "7.1.23-event-spinner",
"engines": {
"node": ">=18.19.0"
},
Expand Down Expand Up @@ -64,7 +64,7 @@
"@edium/fsm": "^2.1.2",
"@hmcts/ccpay-web-component": "6.2.1",
"@hmcts/frontend": "0.0.50-alpha",
"@hmcts/media-viewer": "4.0.8",
"@hmcts/media-viewer": "4.0.9",
"@ngrx/effects": "17.2.0",
"@ngrx/store": "^17.2.0",
"@nicky-lenaers/ngx-scroll-to": "^14.0.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": "7.0.75-event-spinner",
"version": "7.1.23-event-spinner",
"engines": {
"node": ">=18.19.0"
},
Expand Down
10 changes: 8 additions & 2 deletions projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,26 @@ export class AppMockConfig implements AbstractAppConfig {
return '';
}

public getEventsToHide(): string[] {
return [];
}

public getEnableRestrictedCaseAccessConfig(): boolean {
return true;
}

public getEnableCaseFileViewVersion1_1(): boolean {
return true;
}

public getIcpEnable(): boolean {
return false;
}

public getIcpJurisdictions(): string[] {
return ['', ''];
}
public logMessage(): void {

public logMessage(msg: string): void {
console.log(msg);
}
}
2 changes: 2 additions & 0 deletions projects/ccd-case-ui-toolkit/src/lib/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export abstract class AbstractAppConfig {
public abstract getCaseFlagsRefdataApiUrl(): string;
public abstract getRDCommonDataApiUrl(): string;
public abstract getCaseDataStoreApiUrl(): string;
public abstract getEventsToHide(): string[];
public abstract getEnableRestrictedCaseAccessConfig(): boolean;
public abstract getEnableCaseFileViewVersion1_1(): boolean;
}
Expand Down Expand Up @@ -194,4 +195,5 @@ export class CaseEditorConfig {
public enable_case_file_view_version_1_1: boolean;
public icp_enabled: boolean;
public icp_jurisdictions: string[];
public events_to_hide: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CaseEditUtils, convertNonASCIICharacter } from "./case-edit.utils";

describe('CaseEditUtils', () => {
const caseUtils: CaseEditUtils = new CaseEditUtils();
const LONG_ASCII_STRING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@£$%^&*()-=[];\,./`<>?:"|{}_+';
const LONG_PRE_STRING: string = 'Examples of non-ASCII characters: éこ¥🌍';
const LONG_POST_STRING: string = 'Examples of non-ASCII characters: &#233;&#12371;&#165;&#55356;&#57101;';

describe('editNonASCIICharacters', () => {

it('should not edit undefined', () => {
// Note: Should never happen
const response = caseUtils.convertNonASCIICharacters(undefined);
expect(response).toEqual('');
});

it('should not edit an empty string', () => {
const mockString = '';
const response = caseUtils.convertNonASCIICharacters(mockString);
expect(response).toEqual(mockString);
});

it('should note edit ASCII characters', () => {
// note: string includes £ (non-ASCII) which should not be edited
const response = caseUtils.convertNonASCIICharacters(LONG_ASCII_STRING);
expect(response).toEqual(LONG_ASCII_STRING);
});

it('should not edit £ (non ASCII)', () => {
const mockString = 'Cost: £2.50';
const response = caseUtils.convertNonASCIICharacters(mockString);
expect(response).toEqual(mockString);
});

it('should edit ASCII characters', () => {
// Summarises with copied mock string
const response = caseUtils.convertNonASCIICharacters(LONG_PRE_STRING);
expect(response).toEqual(LONG_POST_STRING);

// Goes deeper into what should be happening just in case
const chineseCharacter = '漢';
const secondMockString = 'Examples of non-ASCII characters: ' + chineseCharacter;
const editedSecondMockString =
`Examples of non-ASCII characters: ${CaseEditUtils.PREFIX + chineseCharacter.charCodeAt(0) + CaseEditUtils.SUFFIX}`;
const secondResponse = caseUtils.convertNonASCIICharacters(secondMockString);
expect(secondResponse).toEqual(editedSecondMockString);
});
});

describe('revertEditNonASCIICharacters', () => {

it('should not revert strings without the prefix and/or suffix', () => {
const mockString = 'Hello World!';
const response = caseUtils.convertHTMLEntities(mockString);
expect(response).toEqual(mockString);
});

it('should revert relevant strings', () => {
const response = caseUtils.convertHTMLEntities(LONG_POST_STRING);
expect(response).toEqual(LONG_PRE_STRING);
});
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

export function convertNonASCIICharacter(character: string): string {
if (character === '£') {
// pound sign will be frequently used and works for btoa despite being non-ASCII
// note: this could be done for other characters provided they work for btoa()
return character;
}
// Note: Will convert to HTML entity
return CaseEditUtils.PREFIX + character.charCodeAt(0) + CaseEditUtils.SUFFIX;
}

export class CaseEditUtils {

public static readonly PREFIX = '&#';
public static readonly SUFFIX = ';';

public convertNonASCIICharacters(rawString: string): string {
return rawString ? rawString.replace(/[^\x20-\x7E]/g, function (c) {
return convertNonASCIICharacter(c);
}) : '';
}

public convertHTMLEntities(editedString: string): string {
const revertedCharacterList = editedString.split(CaseEditUtils.PREFIX);
let rawString = revertedCharacterList[0];
for (let index = 1; index < revertedCharacterList.length; index++) {
const currentSection = revertedCharacterList[index];
if (!currentSection.includes(CaseEditUtils.SUFFIX)) {
return rawString.concat(currentSection);
} else {
const suffixSplitList = currentSection.split(CaseEditUtils.SUFFIX);
const characterCode = Number(suffixSplitList[0]);
rawString = rawString.concat(String.fromCharCode(characterCode), suffixSplitList[1]);
}
}
return rawString;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,46 @@ describe('CaseEditComponent', () => {
expect(mockWorkAllocationService.completeTask).not.toHaveBeenCalled();
});

it('should submit the case for a Case Flags submission', () => {
const mockClass = {
submit: () => of({})
};
spyOn(mockClass, 'submit').and.returnValue(of({
id: 'id',
/* tslint:disable:object-literal-key-quotes */
'callback_response_status': 'CALLBACK_HASNOT_COMPLETED',
/* tslint:disable:object-literal-key-quotes */
'after_submit_callback_response': {
/* tslint:disable:object-literal-key-quotes */
'confirmation_header': 'confirmation_header',
/* tslint:disable:object-literal-key-quotes */
'confirmation_body': 'confirmation_body'
}
}));

spyOn(component, 'confirm');

component.isCaseFlagSubmission = true;
component.confirmation = {} as unknown as Confirmation;

formValueService.sanitise.and.returnValue({name: 'sweet'});
component.onEventCanBeCompleted({
eventTrigger: component.eventTrigger,
eventCanBeCompleted: true,
caseDetails: component.caseDetails,
form: component.form,
submit: mockClass.submit,
});

expect(component.confirm).toHaveBeenCalled();
expect(formValueService.populateLinkedCasesDetailsFromCaseFields).toHaveBeenCalled();
expect(formValueService.removeCaseFieldsOfType)
.toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Array), ['FlagLauncher', 'ComponentLauncher']);
expect(formValueService.repopulateFormDataFromCaseFieldValues).toHaveBeenCalled();
expect(validPageListCaseFieldsService.deleteNonValidatedFields).toHaveBeenCalled();
expect(formValueService.removeUnnecessaryFields).not.toHaveBeenCalled();
});

it('should NOT submit the case due to error', () => {
const mockClass = {
submit: () => of({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,43 +404,51 @@ export class CaseEditComponent implements OnInit, OnDestroy {
if (caseField && caseField.retain_hidden_value &&
(caseField.hidden || (caseField.hidden !== false && parentField && parentField.hidden))) {
if (caseField.field_type.type === 'Complex') {
// Note: Deliberate use of equality (==) and non-equality (!=) operators for null checks throughout, to
// handle both null and undefined values
if (caseField.value != null) {
// Call this function recursively to replace the Complex field's sub-fields as necessary, passing the
// CaseField itself (the sub-fields do not contain any values, so these need to be obtained from the
// parent)
// Update rawFormValueData for this field
// creating form group and adding control into it in case caseField is of complext type and and part of formGroup
const form: FormGroup = new FormGroup({});
if (formGroup.controls[key].value) {
Object.keys(formGroup.controls[key].value).forEach((item) => {
form.addControl(item, new FormControl(formGroup.controls[key].value[item]));
});
}
rawFormValueData[key] = this.replaceHiddenFormValuesWithOriginalCaseData(
form, caseField.field_type.complex_fields, caseField);
}
this.handleComplexField(caseField, formGroup, key, rawFormValueData);
} else {
// Default case also handles collections of *all* types; the entire collection in rawFormValueData will be
// replaced with the original from formatted_value
// Use the CaseField's existing *formatted_value* from the parent, if available. (This is necessary for
// Complex fields, whose sub-fields do not hold any values in the model.) Otherwise, use formatted_value
// from the CaseField itself.
if (parentField && parentField.formatted_value) {
rawFormValueData[key] = parentField.formatted_value[caseField.id];
} else {
if (!(caseField.hidden && caseField.retain_hidden_value)) {
rawFormValueData[key] = caseField.formatted_value;
}
}
this.handleNonComplexField(parentField, rawFormValueData, key, caseField);
}
}
});

return rawFormValueData;
}

private handleNonComplexField(parentField: CaseField, rawFormValueData, key: string, caseField: CaseField) {
// Default case also handles collections of *all* types; the entire collection in rawFormValueData will be
// replaced with the original from formatted_value
// Use the CaseField's existing *formatted_value* from the parent, if available. (This is necessary for
// Complex fields, whose sub-fields do not hold any values in the model.) Otherwise, use formatted_value
// from the CaseField itself.
if (parentField && parentField.formatted_value) {
rawFormValueData[key] = parentField.formatted_value[caseField.id];
} else {
if (!(caseField.hidden && caseField.retain_hidden_value)) {
rawFormValueData[key] = caseField.formatted_value;
}
}
}

private handleComplexField(caseField: CaseField, formGroup: FormGroup<any>, key: string, rawFormValueData) {
// Note: Deliberate use of equality (==) and non-equality (!=) operators for null checks throughout, to
// handle both null and undefined values
if (caseField.value != null) {
// Call this function recursively to replace the Complex field's sub-fields as necessary, passing the
// CaseField itself (the sub-fields do not contain any values, so these need to be obtained from the
// parent)
// Update rawFormValueData for this field
// creating form group and adding control into it in case caseField is of complext type and and part of formGroup
const form: FormGroup = new FormGroup({});
if (formGroup.controls[key].value) {
Object.keys(formGroup.controls[key].value).forEach((item) => {
form.addControl(item, new FormControl(formGroup.controls[key].value[item]));
});
}
rawFormValueData[key] = this.replaceHiddenFormValuesWithOriginalCaseData(
form, caseField.field_type.complex_fields, caseField);
}
}

private caseSubmit({ form, caseEventData, submit }: CaseEditCaseSubmit): void {
const loadingSpinnerToken = this.loadingService.register();
// keep the initial event response to finalise process after task completion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { LinkedCasesResponse } from '../../palette/linked-cases/domain/linked-ca
import { CaseAccessUtils } from '../case-access-utils';
import { WizardPage } from '../domain';
import { WizardPageFieldToCaseFieldMapper } from './wizard-page-field-to-case-field.mapper';
import { CaseEditUtils } from '../case-edit-utils/case-edit.utils';

@Injectable()
export class CasesService {
Expand Down Expand Up @@ -396,8 +397,10 @@ export class CasesService {
private addClientContextHeader(headers: HttpHeaders): HttpHeaders {
const clientContextDetails = this.sessionStorageService.getItem('clientContext');
if (clientContextDetails) {
// may require URI encoding in certain circumstances
const clientContext = window.btoa(clientContextDetails);
const caseEditUtils = new CaseEditUtils();
// below changes non-ASCII characters
const editedClientContext = caseEditUtils.convertNonASCIICharacters(clientContextDetails);
const clientContext = window.btoa(editedClientContext);
if (clientContext) {
headers = headers.set('Client-Context', clientContext);
}
Expand All @@ -407,8 +410,11 @@ export class CasesService {

private updateClientContextStorage(headers: HttpHeaders): void {
if (headers && headers.get('Client-Context')) {
const caseEditUtils = new CaseEditUtils();
const clientContextString = window.atob(headers.get('Client-Context'));
this.sessionStorageService.setItem('clientContext', clientContextString);
// below reverts non-ASCII characters
const editedClientContextString = caseEditUtils.convertHTMLEntities(clientContextString);
this.sessionStorageService.setItem('clientContext', editedClientContextString);
}
}
}
Loading

0 comments on commit c76ddd8

Please sign in to comment.