Skip to content

Commit a8093d6

Browse files
committed
Merge branch 'master' into EXUI-3457-manage-caseLink-event-not-working
2 parents 959eaed + 403eded commit a8093d6

File tree

8 files changed

+226
-14
lines changed

8 files changed

+226
-14
lines changed

RELEASE-NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## RELEASE NOTES
22

3+
### Version 7.2.47
4+
**EXUI-2916** Welsh Translation - issues seen during Testing of EXUI-1848
5+
36
### Version 7.2.46
47
**EXUI-2836** PED: Duplicate calls being received when users leave
58

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hmcts/ccd-case-ui-toolkit",
3-
"version": "7.2.47-manage-caseLink-event",
3+
"version": "7.2.48-manage-caseLink-event",
44
"engines": {
55
"node": ">=18.19.0"
66
},

projects/ccd-case-ui-toolkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hmcts/ccd-case-ui-toolkit",
3-
"version": "7.2.47-manage-caseLink-event",
3+
"version": "7.2.48-manage-caseLink-event",
44
"engines": {
55
"node": ">=18.19.0"
66
},

projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/utils/field-label.pipe.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,8 @@ export class FieldLabelPipe implements PipeTransform {
3030
return field.label;
3131
}
3232
}
33+
34+
public getOriginalLabelForYesNoTranslation(field: CaseField): string {
35+
return field.originalLabel || field.label;
36+
}
3337
}

projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/yes-no/write-yes-no-field.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<div [id]="createElementId('radio')">
1212
<div class="multiple-choice" *ngFor="let value of yesNoValues" [ngClass]="{selected: yesNoControl.value === value}">
1313
<input class="form-control" [id]="createElementId(value)" [attr.name]="id()" [name]="id()" type="radio" [formControl]="yesNoControl" [value]="value">
14-
<label class="form-label" [for]="createElementId(value)">{{caseField.label ? (caseField.label | rpxTranslate:null:value) : value}}</label>
14+
<label class="form-label" [for]="createElementId(value)">{{caseField.label ? ((caseField.originalLabel || caseField.label) | rpxTranslate:null:value) : value}}</label>
1515
</div>
1616
</div>
1717
</fieldset>

projects/ccd-case-ui-toolkit/src/lib/shared/directives/substitutor/label-substitutor.directive.spec.ts

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChangeDetectorRef, Component, DebugElement, Input } from '@angular/core';
2-
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
33
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
44
import { By } from '@angular/platform-browser';
55
import { CaseField } from '../../domain/definition/case-field.model';
@@ -10,6 +10,27 @@ import { PlaceholderService } from './services/placeholder.service';
1010
import createSpyObj = jasmine.createSpyObj;
1111
import { RpxTranslatePipe, RpxTranslationConfig, RpxTranslationService } from 'rpx-xui-translation';
1212
import { HttpClient, HttpHandler } from '@angular/common/http';
13+
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
14+
15+
class MockRpxTranslationService {
16+
private langSubject = new BehaviorSubject<string>('en');
17+
private _language = 'en';
18+
language$ = this.langSubject.asObservable();
19+
20+
get language() {
21+
return this._language;
22+
}
23+
24+
set language(lang: string) {
25+
this._language = lang;
26+
this.langSubject.next(lang);
27+
}
28+
29+
setLanguage(lang: string) {
30+
this._language = lang;
31+
this.langSubject.next(lang);
32+
}
33+
}
1334

1435
@Component({
1536
template: `
@@ -73,7 +94,7 @@ describe('LabelSubstitutorDirective', () => {
7394
FieldsUtils,
7495
FormatTranslatorService,
7596
{ provide: RpxTranslatePipe, useValue: mockTranslationPipe },
76-
RpxTranslationService,
97+
{ provide: RpxTranslationService, useClass: MockRpxTranslationService },
7798
RpxTranslationConfig,
7899
HttpClient,
79100
HttpHandler,
@@ -728,4 +749,160 @@ describe('LabelSubstitutorDirective', () => {
728749
expect(placeholderService.resolvePlaceholders).toHaveBeenCalledWith({ LabelB: '', LabelA: TRANSFORMED_VALUES }, label);
729750
});
730751
});
731-
});
752+
753+
describe('Language change and translation functionality', () => {
754+
let translationService: MockRpxTranslationService;
755+
756+
beforeEach(() => {
757+
translationService = TestBed.inject(RpxTranslationService) as any;
758+
});
759+
760+
it('should set isTranslated to false when language is not cy', fakeAsync(() => {
761+
const label = 'English label with ${placeholder}';
762+
comp.caseField = textField('LabelB', '', label);
763+
comp.caseFields = [comp.caseField, textField('LabelA', 'ValueA', '')];
764+
765+
placeholderService.resolvePlaceholders.and.returnValues('English label with ValueA', '', '');
766+
fixture.detectChanges();
767+
768+
placeholderService.resolvePlaceholders.calls.reset();
769+
placeholderService.resolvePlaceholders.and.returnValues('English label with ValueA', '', '');
770+
771+
translationService.setLanguage('en');
772+
773+
tick(100);
774+
fixture.detectChanges();
775+
776+
expect(comp.caseField.isTranslated).toBe(false);
777+
}));
778+
779+
it('should translate unsubstituted text first then apply substitutions', () => {
780+
const label = 'English text with ${LabelA} placeholder';
781+
const translatedTemplate = 'Welsh text with ${LabelA} placeholder';
782+
const finalText = 'Welsh text with ValueA placeholder';
783+
784+
comp.caseField = textField('LabelB', '', label);
785+
comp.caseFields = [comp.caseField, textField('LabelA', 'ValueA', '')];
786+
787+
placeholderService.resolvePlaceholders.and.returnValues(
788+
'English text with ValueA placeholder',
789+
finalText,
790+
'',
791+
''
792+
);
793+
mockTranslationPipe.transform.and.returnValue(translatedTemplate);
794+
795+
fixture.detectChanges();
796+
797+
expect(mockTranslationPipe.transform).toHaveBeenCalledWith(label);
798+
expect(comp.caseField.label).toBe(finalText);
799+
expect(placeholderService.resolvePlaceholders).toHaveBeenCalledTimes(4);
800+
expect(placeholderService.resolvePlaceholders).toHaveBeenCalledWith(
801+
jasmine.objectContaining({ LabelA: 'ValueA' }),
802+
translatedTemplate
803+
);
804+
});
805+
806+
it('should handle hint_text during language changes', fakeAsync(() => {
807+
const initialHint = 'Initial hint with ${placeholder}';
808+
comp.caseField = textField('LabelB', '', '', initialHint);
809+
comp.caseFields = [comp.caseField, textField('LabelA', 'ValueA', '')];
810+
811+
placeholderService.resolvePlaceholders.and.callFake((_fields: any, str: any) => {
812+
if (str === initialHint) return 'Initial hint with ValueA';
813+
if (str === '') return '';
814+
return str;
815+
});
816+
fixture.detectChanges();
817+
818+
comp.caseField.hint_text = 'Modified hint';
819+
820+
placeholderService.resolvePlaceholders.calls.reset();
821+
placeholderService.resolvePlaceholders.and.callFake((_fields: any, str: any) => {
822+
if (str === initialHint) return 'Initial hint with ValueA after change';
823+
if (str === '') return '';
824+
return str;
825+
});
826+
827+
translationService.setLanguage('en');
828+
829+
tick(100);
830+
fixture.detectChanges();
831+
832+
expect(comp.caseField.hint_text).toBe('Initial hint with ValueA after change');
833+
expect(hintEl.innerText).toBe('Initial hint with ValueA after change');
834+
}));
835+
836+
it('should not call translation when label has no placeholders', () => {
837+
const label = 'Simple label without placeholders';
838+
comp.caseField = textField('LabelB', '', label);
839+
comp.caseFields = [comp.caseField];
840+
841+
placeholderService.resolvePlaceholders.and.returnValue(label);
842+
fixture.detectChanges();
843+
844+
expect(mockTranslationPipe.transform).not.toHaveBeenCalled();
845+
expect(comp.caseField.label).toBe(label);
846+
expect(comp.caseField.isTranslated).toBe(false);
847+
});
848+
849+
it('should handle language change timeout correctly', fakeAsync(() => {
850+
const label = 'Label with ${placeholder}';
851+
comp.caseField = textField('LabelB', '', label);
852+
comp.caseFields = [comp.caseField, textField('LabelA', 'ValueA', '')];
853+
854+
placeholderService.resolvePlaceholders.and.returnValues('Label with ValueA', '', '');
855+
fixture.detectChanges();
856+
857+
placeholderService.resolvePlaceholders.calls.reset();
858+
859+
translationService.setLanguage('cy');
860+
861+
expect(placeholderService.resolvePlaceholders).not.toHaveBeenCalled();
862+
863+
tick(50);
864+
expect(placeholderService.resolvePlaceholders).not.toHaveBeenCalled();
865+
866+
tick(50);
867+
fixture.detectChanges();
868+
expect(placeholderService.resolvePlaceholders).toHaveBeenCalled();
869+
}));
870+
871+
it('should clean up language subscription on destroy', () => {
872+
const label = 'Test label';
873+
comp.caseField = textField('LabelB', '', label);
874+
comp.caseFields = [comp.caseField];
875+
876+
placeholderService.resolvePlaceholders.and.returnValues(label, '', '');
877+
fixture.detectChanges();
878+
879+
const directiveEl = de.query(By.directive(LabelSubstitutorDirective));
880+
const directive = directiveEl.injector.get(LabelSubstitutorDirective);
881+
spyOn(directive['languageSubscription'], 'unsubscribe');
882+
883+
fixture.destroy();
884+
885+
expect(directive['languageSubscription'].unsubscribe).toHaveBeenCalled();
886+
});
887+
888+
it('should restore initial values on destroy', () => {
889+
const initialLabel = 'Initial label';
890+
const initialHint = 'Initial hint';
891+
comp.caseField = textField('LabelB', '', initialLabel, initialHint);
892+
comp.caseFields = [comp.caseField];
893+
894+
placeholderService.resolvePlaceholders.and.returnValues('Modified label', 'Modified hint', '');
895+
fixture.detectChanges();
896+
897+
comp.caseField.label = 'Changed label';
898+
comp.caseField.hint_text = 'Changed hint';
899+
comp.caseField.isTranslated = true;
900+
901+
fixture.destroy();
902+
903+
expect(comp.caseField.label).toBe('Modified label');
904+
expect(comp.caseField.hint_text).toBe(initialHint);
905+
expect(comp.caseField.isTranslated).toBe(false);
906+
});
907+
});
908+
});

projects/ccd-case-ui-toolkit/src/lib/shared/directives/substitutor/label-substitutor.directive.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { FormGroup } from '@angular/forms';
44
import { CaseField } from '../../domain/definition/case-field.model';
55
import { FieldsUtils } from '../../services/fields/fields.utils';
66
import { PlaceholderService } from './services/placeholder.service';
7-
import { RpxTranslatePipe } from 'rpx-xui-translation';
7+
import { RpxTranslatePipe, RpxTranslationService } from 'rpx-xui-translation';
8+
import { Subscription } from 'rxjs';
89

910
@Directive({ selector: '[ccdLabelSubstitutor]' })
1011
/**
@@ -17,31 +18,44 @@ export class LabelSubstitutorDirective implements OnInit, OnDestroy {
1718
@Input() public formGroup: FormGroup;
1819
@Input() public elementsToSubstitute: string[] = ['label', 'hint_text'];
1920

20-
private initialLabel: string;
2121
private initialHintText: string;
22+
private languageSubscription: Subscription
2223

2324
constructor(
2425
private readonly fieldsUtils: FieldsUtils,
2526
private readonly placeholderService: PlaceholderService,
26-
private readonly rpxTranslationPipe: RpxTranslatePipe
27+
private readonly rpxTranslationPipe: RpxTranslatePipe,
28+
private readonly rpxTranslationService: RpxTranslationService
2729
) {}
2830

2931
public ngOnInit(): void {
30-
this.initialLabel = this.caseField.label;
3132
this.initialHintText = this.caseField.hint_text;
33+
this.caseField.originalLabel = this.caseField.label;
3234
this.formGroup = this.formGroup || new FormGroup({});
3335

36+
this.languageSubscription = this.rpxTranslationService.language$.subscribe(() => {
37+
// timeout is required to prevent race conditions with translation pipe
38+
setTimeout(() => {
39+
this.onLanguageChange();
40+
}, 100);
41+
});
42+
43+
this.applySubstitutions()
44+
}
45+
46+
private applySubstitutions(): void {
3447
const fields: object = this.getReadOnlyAndFormFields();
3548

3649
if (this.shouldSubstitute('label')) {
3750
const oldLabel = this.caseField.label;
3851
const substitutedLabel = this.resolvePlaceholders(fields, this.caseField.label);
3952
if (oldLabel && oldLabel !== substitutedLabel) {
4053
// we need to translate the uninterpolated data then substitute the values in translated string
54+
this.caseField.originalLabel = substitutedLabel;
4155
const translated = this.rpxTranslationPipe.transform(oldLabel)
4256
const transSubstitutedLabel = this.resolvePlaceholders(fields, translated);
4357
this.caseField.label = transSubstitutedLabel;
44-
this.caseField.isTranslated = true;
58+
this.caseField.isTranslated = this.rpxTranslationService.language === 'cy' && translated !== oldLabel;
4559
} else {
4660
this.caseField.label = substitutedLabel;
4761
this.caseField.isTranslated = false;
@@ -55,13 +69,26 @@ export class LabelSubstitutorDirective implements OnInit, OnDestroy {
5569
}
5670
}
5771

58-
public ngOnDestroy(): void {
59-
if (this.initialLabel) {
60-
this.caseField.label = this.initialLabel;
72+
private onLanguageChange(): void {
73+
this.resetToInitialValues();
74+
this.applySubstitutions();
75+
}
76+
77+
private resetToInitialValues(): void {
78+
if (this.caseField?.originalLabel) {
79+
this.caseField.label = this.caseField.originalLabel;
6180
}
6281
if (this.initialHintText) {
6382
this.caseField.hint_text = this.initialHintText;
6483
}
84+
this.caseField.isTranslated = false;
85+
}
86+
87+
public ngOnDestroy(): void {
88+
this.resetToInitialValues();
89+
if (this.languageSubscription) {
90+
this.languageSubscription.unsubscribe();
91+
}
6592
}
6693

6794
private shouldSubstitute(element: string): boolean {

projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class CaseField implements Orderable {
1313
public hidden: boolean;
1414
public hiddenCannotChange: boolean;
1515
public label: string;
16+
public originalLabel?: string;
1617
public order?: number;
1718
@Exclude()
1819
public parent?: CaseField;

0 commit comments

Comments
 (0)