From 58a25fe19e8e4a1a2f8f33419491545cc5588592 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Thu, 20 Nov 2025 11:43:30 +0000 Subject: [PATCH] AAE-40136 Migrate locale settings to signals (#11361) --- .../action.yml | 6 ++ .../core/services/user-preferences.service.md | 75 +++++++++++++++++-- .../search-date-range.component.ts | 16 ++-- .../card-view-dateitem.component.ts | 18 ++--- .../services/user-preferences.service.ts | 73 +++++++++++++++++- .../src/lib/common/utils/date-fns-adapter.ts | 25 +++++-- .../lib/common/utils/moment-date-adapter.ts | 9 ++- .../date-cell/date-cell.component.ts | 28 ++++--- lib/core/src/lib/pipes/decimal-number.pipe.ts | 23 ++---- lib/core/src/lib/pipes/localized-date.pipe.ts | 36 +++------ lib/core/src/lib/pipes/time-ago.pipe.ts | 14 +--- .../lib/translation/translation.service.ts | 9 ++- .../date-range-filter.component.spec.ts | 5 +- .../edit-process-filter-cloud.component.ts | 21 +++--- .../base-edit-task-filter-cloud.component.ts | 19 +++-- 15 files changed, 253 insertions(+), 124 deletions(-) diff --git a/.github/actions/download-node-modules-and-artifacts/action.yml b/.github/actions/download-node-modules-and-artifacts/action.yml index c67b9179d7..d8fc02af5d 100644 --- a/.github/actions/download-node-modules-and-artifacts/action.yml +++ b/.github/actions/download-node-modules-and-artifacts/action.yml @@ -8,6 +8,8 @@ runs: with: path: dist key: dist-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + dist-${{ github.run_id }}- fail-on-cache-miss: true - name: Restore nxcache from cache @@ -15,6 +17,8 @@ runs: with: path: nxcache key: nxcache-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + nxcache-${{ github.run_id }}- fail-on-cache-miss: true - name: Restore node_modules from cache @@ -22,6 +26,8 @@ runs: with: path: node_modules key: node-modules-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + node-modules-${{ github.run_id }}- fail-on-cache-miss: true - name: show files diff --git a/docs/core/services/user-preferences.service.md b/docs/core/services/user-preferences.service.md index a898c08bf4..6225d706ae 100644 --- a/docs/core/services/user-preferences.service.md +++ b/docs/core/services/user-preferences.service.md @@ -2,10 +2,10 @@ Title: User Preferences Service Added: v2.0.0 Status: Active -Last reviewed: 2019-01-16 +Last reviewed: 2025-11-20 --- -# [User Preferences Service](../../../lib/core/src/lib/common/services/user-preferences.service.ts "Defined in user-preferences.service.ts") +# User Preferences Service Stores preferences for the app and for individual components. @@ -32,10 +32,10 @@ Stores preferences for the app and for individual components. Check if an item is present in the storage - _property:_ `string` - Name of the property - **Returns** `boolean` - True if the item is present, false otherwise -- **select**(property: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)``
+- **select**(property: `string`): `Observable`
Sets up a callback to notify when a property has changed. - _property:_ `string` - The property to watch - - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - Notification callback + - **Returns** `Observable` - Notification callback - **set**(property: `string`, value: `any`)
Sets a preference property. - _property:_ `string` - Name of the property @@ -71,7 +71,7 @@ class AppComponent { } ``` -As soon as you assign the storage prefix, all settings that you get or set via the [`UserPreferencesService`](../../core/services/user-preferences.service.md) will be saved to a dedicated profile. +As soon as you assign the storage prefix, all settings that you get or set via the `UserPreferencesService` will be saved to a dedicated profile. You can import the service into your controller and use its APIs as shown below: @@ -96,7 +96,7 @@ The service also provides quick access to a set of the "known" properties used a | ---- | ---- | ----------- | | authType | `string` | Authorization type (can be "ECM", "BPM" or "ALL"). | | disableCSRF | `boolean` | Prevents the CSRF Token from being submitted if true. Only valid for Process Services. | -| paginationSize | `number` | [`Pagination`](../../../lib/content-services/document-list/models/document-library.model.ts) size. | +| paginationSize | `number` | `Pagination` size. | | locale | `string` | Current locale setting. | ## User Preference onChange Stream @@ -112,7 +112,13 @@ whole set of user properties. This is useful when a component needs to react to ``` You can also use the `select` method to get notification when a particular property is changed. -A set of basic properties is added into the enumeration [`UserPreferenceValues`](lib/core/src/lib/services/user-preferences.service.ts) which gives you the key value to access the standard user preference service properties : **PaginationSize**, **DisableCSRF**, **Locale**, **SupportedPageSizes** and **ExpandedSideNavStatus**. +A set of basic properties is added into the enumeration `UserPreferenceValues` which gives you the key value to access the standard user preference service properties: + +- `PaginationSize` +- `DisableCSRF` +- `Locale` +- `SupportedPageSizes` +- `ExpandedSideNavStatus` ```ts userPreferences.disableCSRF = true; @@ -120,3 +126,58 @@ A set of basic properties is added into the enumeration [`UserPreferenceValues`] console.log(CSRFflag); //this will be true; }); ``` + +### Convenience Observables and Signals + +For commonly accessed preferences like `locale`, the service provides both observables and signals that simplify access patterns. + +#### Using Signals (Recommended - No Subscription Needed!) + +Signals automatically handle cleanup and don't require manual unsubscription: + +```ts +export class MyComponent { + private userPreferences = inject(UserPreferencesService); + + // Signal - automatically reactive, no subscription needed! + currentLocale = this.userPreferences.localeSignal; + + // Use in template or computed values + displayText = computed(() => `Current locale: ${this.currentLocale()}`); +} +``` + +Available signals: + +- `localeSignal` - Current locale value +- `paginationSizeSignal` - Current pagination size +- `supportedPageSizesSignal` - Supported page sizes array + +**Benefits of signals:** + +- ✅ No manual subscription/unsubscription needed +- ✅ Automatic cleanup when component is destroyed +- ✅ Better performance with fine-grained reactivity +- ✅ Simpler code - just read the value with `()` + +#### Using Observables (For Advanced Cases) + +If you need RxJS operators or imperative subscriptions: + +```ts +constructor(private userPreferences: UserPreferencesService) { + // Observable - requires takeUntilDestroyed() to prevent memory leaks + this.userPreferences.locale$ + .pipe(takeUntilDestroyed()) + .subscribe(locale => { + this.currentLocale = locale; + }); +} +``` + +Available observables: +- `locale$` - Observable for locale changes +- `paginationSize$` - Observable for pagination size changes +- `supportedPageSizes$` - Observable for supported page sizes changes + +**Note:** When subscribing to observables from a singleton service in a component, always use `takeUntilDestroyed()` or `takeUntil()` to prevent memory leaks. diff --git a/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts b/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts index caeab9406a..a338a5842c 100644 --- a/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts +++ b/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, DestroyRef, EventEmitter, inject, Inject, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, effect, EventEmitter, inject, Inject, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { endOfDay, isAfter, isBefore, isValid, parse } from 'date-fns'; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core'; import { DateFnsAdapter, MAT_DATE_FNS_FORMATS } from '@angular/material-date-fns-adapter'; @@ -23,7 +23,7 @@ import { InLastDateType } from './in-last-date-type'; import { DateRangeType } from './date-range-type'; import { SearchDateRange } from './search-date-range'; import { FormBuilder, ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/forms'; -import { DateFnsUtils, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core'; +import { DateFnsUtils, UserPreferencesService } from '@alfresco/adf-core'; import { CommonModule } from '@angular/common'; import { MatRadioModule } from '@angular/material/radio'; import { TranslatePipe } from '@ngx-translate/core'; @@ -100,7 +100,13 @@ export class SearchDateRangeComponent implements OnInit { private userPreferencesService: UserPreferencesService, private dateAdapter: DateAdapter, @Inject(MAT_DATE_FORMATS) private dateFormatConfig: MatDateFormats - ) {} + ) { + // Use effect to react to locale signal changes (must be in injection context) + effect(() => { + const locale = this.userPreferencesService.localeSignal(); + this.dateAdapter.setLocale(DateFnsUtils.getLocaleFromString(locale)); + }); + } readonly endDateValidator = (formControl: UntypedFormControl): { [key: string]: boolean } | null => { if (isBefore(formControl.value, this.betweenStartDateFormControl.value) || isAfter(formControl.value, this.convertedMaxDate)) { @@ -114,10 +120,6 @@ export class SearchDateRangeComponent implements OnInit { ngOnInit(): void { this.dateFormatConfig.display.dateInput = this.dateFormat; this.convertedMaxDate = endOfDay(this.maxDate && this.maxDate !== 'today' ? parse(this.maxDate, this.dateFormat, new Date()) : new Date()); - this.userPreferencesService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((locale) => this.dateAdapter.setLocale(DateFnsUtils.getLocaleFromString(locale))); this.form.controls.dateRangeType.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((dateRangeType) => this.updateValidators(dateRangeType)); diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts index 278b021d92..ed52b1d3c1 100644 --- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts +++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, DestroyRef, inject, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Component, effect, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DatetimeAdapter, @@ -25,7 +25,7 @@ import { MatDatetimepickerModule } from '@mat-datetimepicker/core'; import { CardViewDateItemModel } from '../../models/card-view-dateitem.model'; -import { UserPreferencesService, UserPreferenceValues } from '../../../common/services/user-preferences.service'; +import { UserPreferencesService } from '../../../common/services/user-preferences.service'; import { BaseCardView } from '../base-card-view'; import { ClipboardService } from '../../../clipboard/clipboard.service'; import { TranslationService } from '../../../translation/translation.service'; @@ -40,7 +40,6 @@ import { MatChipsModule } from '@angular/material/chips'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; @@ -83,8 +82,6 @@ export class CardViewDateItemComponent extends BaseCardView = new FormControl(null); - private readonly destroyRef = inject(DestroyRef); - constructor( private dateAdapter: DateAdapter, private userPreferencesService: UserPreferencesService, @@ -92,16 +89,13 @@ export class CardViewDateItemComponent extends BaseCardView { + this.property.locale = this.userPreferencesService.localeSignal(); + }); } ngOnInit() { - this.userPreferencesService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((locale) => { - this.property.locale = locale; - }); - (this.dateAdapter as AdfDateFnsAdapter).displayFormat = 'MMM DD'; if (this.property.multivalued) { diff --git a/lib/core/src/lib/common/services/user-preferences.service.ts b/lib/core/src/lib/common/services/user-preferences.service.ts index c171646869..6fe0e99b0c 100644 --- a/lib/core/src/lib/common/services/user-preferences.service.ts +++ b/lib/core/src/lib/common/services/user-preferences.service.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { inject, Injectable, RendererFactory2 } from '@angular/core'; +import { inject, Injectable, RendererFactory2, Signal } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Observable, BehaviorSubject } from 'rxjs'; import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; @@ -25,6 +25,7 @@ import { LanguageItem } from './language-item.interface'; import { DOCUMENT } from '@angular/common'; import { Directionality, Direction } from '@angular/cdk/bidi'; import { DEFAULT_LANGUAGE_LIST } from '../models/default-languages.model'; +import { toSignal } from '@angular/core/rxjs-interop'; // eslint-disable-next-line no-shadow export enum UserPreferenceValues { @@ -53,10 +54,78 @@ export class UserPreferencesService { private onChangeSubject: BehaviorSubject; onChange: Observable; - constructor(public translate: TranslateService, private appConfig: AppConfigService, private storage: StorageService) { + /** + * Observable that emits the current locale whenever it changes. + * This is a convenience property that simplifies subscribing to locale changes. + * + * @example Observable usage (requires manual unsubscription): + * ```typescript + * constructor(private userPreferencesService: UserPreferencesService) { + * this.userPreferencesService.locale$ + * .pipe(takeUntilDestroyed()) + * .subscribe(locale => { + * this.currentLocale = locale; + * }); + * } + * ``` + * + * @example Signal usage (automatic cleanup, recommended): + * ```typescript + * export class MyComponent { + * private userPreferencesService = inject(UserPreferencesService); + * currentLocale = this.userPreferencesService.localeSignal; // Signal - no subscription needed! + * } + * ``` + */ + readonly locale$: Observable; + + /** + * Signal that provides the current locale value. + * Automatically handles cleanup - no need for takeUntilDestroyed or manual unsubscription. + * This is the recommended way to access locale in components. + */ + readonly localeSignal: Signal; + + /** + * Observable that emits the current pagination size whenever it changes. + */ + readonly paginationSize$: Observable; + + /** + * Signal that provides the current pagination size value. + */ + readonly paginationSizeSignal: Signal; + + /** + * Observable that emits the supported page sizes whenever they change. + */ + readonly supportedPageSizes$: Observable; + + /** + * Signal that provides the supported page sizes array. + */ + readonly supportedPageSizesSignal: Signal; + + constructor( + public translate: TranslateService, + private appConfig: AppConfigService, + private storage: StorageService + ) { this.onChangeSubject = new BehaviorSubject(this.userPreferenceStatus); this.onChange = this.onChangeSubject.asObservable(); + // Initialize convenience observables + this.locale$ = this.select(UserPreferenceValues.Locale); + this.paginationSize$ = this.select(UserPreferenceValues.PaginationSize); + this.supportedPageSizes$ = this.select(UserPreferenceValues.SupportedPageSizes).pipe( + map((value) => (value ? JSON.parse(value) : this.defaults.supportedPageSizes)) + ); + + // Initialize convenience signals (automatically handle cleanup) + this.localeSignal = toSignal(this.locale$, { initialValue: this.defaults.locale }); + this.paginationSizeSignal = toSignal(this.paginationSize$, { initialValue: this.defaults.paginationSize }); + this.supportedPageSizesSignal = toSignal(this.supportedPageSizes$, { initialValue: this.defaults.supportedPageSizes }); + this.appConfig.onLoad.subscribe(() => { this.initUserPreferenceStatus(); }); diff --git a/lib/core/src/lib/common/utils/date-fns-adapter.ts b/lib/core/src/lib/common/utils/date-fns-adapter.ts index ec48f58116..a951f4b394 100644 --- a/lib/core/src/lib/common/utils/date-fns-adapter.ts +++ b/lib/core/src/lib/common/utils/date-fns-adapter.ts @@ -17,10 +17,11 @@ import { DateFnsAdapter } from '@angular/material-date-fns-adapter'; import { DateFnsUtils } from './date-fns-utils'; -import { Inject, Injectable, Optional } from '@angular/core'; +import { effect, Inject, Injectable, Optional } from '@angular/core'; import { MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core'; -import { UserPreferenceValues, UserPreferencesService } from '../services/user-preferences.service'; +import { UserPreferencesService } from '../services/user-preferences.service'; import { isValid, Locale, parse } from 'date-fns'; +import { enUS } from 'date-fns/locale'; /** * Date-fns adapter with moment-to-date-fns conversion. @@ -82,10 +83,24 @@ export class AdfDateFnsAdapter extends DateFnsAdapter { @Optional() @Inject(MAT_DATE_FORMATS) private formats: MatDateFormats, preferences: UserPreferencesService ) { - super(matDateLocale); + // Ensure we have a valid locale for the base class + // If matDateLocale is not provided, use enUS as default + super(matDateLocale || enUS); - preferences.select(UserPreferenceValues.Locale).subscribe((locale: string) => { - this.setLocale(DateFnsUtils.getLocaleFromString(locale)); + // Initialize locale synchronously from signal's initial value + // This ensures locale is set before any format() calls + const initialLocale = preferences.localeSignal(); + if (initialLocale) { + this.setLocale(DateFnsUtils.getLocaleFromString(initialLocale)); + } + + // Use effect to reactively update locale when signal changes + // Note: This adapter is a singleton service, so no cleanup needed + effect(() => { + const locale = preferences.localeSignal(); + if (locale) { + this.setLocale(DateFnsUtils.getLocaleFromString(locale)); + } }); } diff --git a/lib/core/src/lib/common/utils/moment-date-adapter.ts b/lib/core/src/lib/common/utils/moment-date-adapter.ts index 59814a1a6d..f29a0a4613 100644 --- a/lib/core/src/lib/common/utils/moment-date-adapter.ts +++ b/lib/core/src/lib/common/utils/moment-date-adapter.ts @@ -15,9 +15,9 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { effect, Injectable } from '@angular/core'; import { DateAdapter } from '@angular/material/core'; -import { UserPreferencesService, UserPreferenceValues } from '../services/user-preferences.service'; +import { UserPreferencesService } from '../services/user-preferences.service'; // Stub for the moment.js integration. // While this dependency is no longer used by the libraries, the moment adapter can still discover the moment.js linked to the application @@ -37,7 +37,10 @@ export class MomentDateAdapter extends DateAdapter { constructor(preferences: UserPreferencesService) { super(); - preferences.select(UserPreferenceValues.Locale).subscribe((locale: string) => { + // Use effect to reactively update locale when signal changes + // Note: This adapter is a singleton service, so no cleanup needed + effect(() => { + const locale = preferences.localeSignal(); this.setLocale(locale); }); } diff --git a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts index d321d3d2ef..7853d63eb3 100644 --- a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts +++ b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.ts @@ -15,14 +15,13 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation, inject, ChangeDetectorRef } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation, inject, ChangeDetectorRef, effect } from '@angular/core'; import { DataTableCellComponent } from '../datatable-cell/datatable-cell.component'; import { AppConfigService } from '../../../app-config/app-config.service'; import { DateConfig } from '../../data/data-column.model'; import { LocalizedDatePipe, TimeAgoPipe } from '../../../pipes'; import { AsyncPipe } from '@angular/common'; -import { UserPreferencesService, UserPreferenceValues } from '../../../common/services/user-preferences.service'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { UserPreferencesService } from '../../../common/services/user-preferences.service'; @Component({ imports: [LocalizedDatePipe, TimeAgoPipe, AsyncPipe], @@ -52,19 +51,18 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit locale: undefined }; - ngOnInit(): void { - // Subscribe to locale changes - this.userPreferencesService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((locale) => { - this.userLocale = locale || 'en'; - this.setConfig(); - this.updateValue(); // Recalculate computedTitle with new locale - this.cdr.markForCheck(); - }); + constructor() { + super(); + // Use effect to react to locale signal changes (must be in injection context) + effect(() => { + this.userLocale = this.userPreferencesService.localeSignal() || 'en'; + this.setConfig(); + this.updateValue(); // Recalculate computedTitle with new locale + this.cdr.markForCheck(); + }); + } - this.setConfig(); + ngOnInit(): void { super.ngOnInit(); } diff --git a/lib/core/src/lib/pipes/decimal-number.pipe.ts b/lib/core/src/lib/pipes/decimal-number.pipe.ts index 4b250d542d..e4c68aabd6 100644 --- a/lib/core/src/lib/pipes/decimal-number.pipe.ts +++ b/lib/core/src/lib/pipes/decimal-number.pipe.ts @@ -18,9 +18,8 @@ import { DecimalPipe } from '@angular/common'; import { Pipe, PipeTransform } from '@angular/core'; import { AppConfigService } from '../app-config/app-config.service'; -import { UserPreferencesService, UserPreferenceValues } from '../common/services/user-preferences.service'; +import { UserPreferencesService } from '../common/services/user-preferences.service'; import { DecimalNumberModel } from '../models/decimal-number.model'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Pipe({ name: 'adfDecimalNumber', @@ -32,22 +31,14 @@ export class DecimalNumberPipe implements PipeTransform { static DEFAULT_MIN_FRACTION_DIGITS = 0; static DEFAULT_MAX_FRACTION_DIGITS = 2; - defaultLocale: string = DecimalNumberPipe.DEFAULT_LOCALE; defaultMinIntegerDigits: number = DecimalNumberPipe.DEFAULT_MIN_INTEGER_DIGITS; defaultMinFractionDigits: number = DecimalNumberPipe.DEFAULT_MIN_FRACTION_DIGITS; defaultMaxFractionDigits: number = DecimalNumberPipe.DEFAULT_MAX_FRACTION_DIGITS; - constructor(public userPreferenceService?: UserPreferencesService, public appConfig?: AppConfigService) { - if (this.userPreferenceService) { - this.userPreferenceService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed()) - .subscribe((locale) => { - if (locale) { - this.defaultLocale = locale; - } - }); - } + constructor( + public userPreferenceService?: UserPreferencesService, + public appConfig?: AppConfigService + ) { if (this.appConfig) { this.defaultMinIntegerDigits = this.appConfig.get('decimalValues.minIntegerDigits', DecimalNumberPipe.DEFAULT_MIN_INTEGER_DIGITS); this.defaultMinFractionDigits = this.appConfig.get( @@ -67,7 +58,9 @@ export class DecimalNumberPipe implements PipeTransform { const actualMaxFractionDigits: number = digitsInfo?.maxFractionDigits ? digitsInfo.maxFractionDigits : this.defaultMaxFractionDigits; const actualDigitsInfo = `${actualMinIntegerDigits}.${actualMinFractionDigits}-${actualMaxFractionDigits}`; - const actualLocale = locale || this.defaultLocale; + // Use signal directly - no subscription needed! + const defaultLocale = this.userPreferenceService?.localeSignal() || DecimalNumberPipe.DEFAULT_LOCALE; + const actualLocale = locale || defaultLocale; const decimalPipe: DecimalPipe = new DecimalPipe(actualLocale); diff --git a/lib/core/src/lib/pipes/localized-date.pipe.ts b/lib/core/src/lib/pipes/localized-date.pipe.ts index 1f34dc2bf2..03f265b3f7 100644 --- a/lib/core/src/lib/pipes/localized-date.pipe.ts +++ b/lib/core/src/lib/pipes/localized-date.pipe.ts @@ -16,38 +16,25 @@ */ import { DatePipe } from '@angular/common'; -import { Pipe, PipeTransform, OnDestroy } from '@angular/core'; +import { Pipe, PipeTransform } from '@angular/core'; import { AppConfigService } from '../app-config/app-config.service'; -import { UserPreferencesService, UserPreferenceValues } from '../common/services/user-preferences.service'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { UserPreferencesService } from '../common/services/user-preferences.service'; @Pipe({ standalone: true, name: 'adfLocalizedDate', pure: false }) -export class LocalizedDatePipe implements PipeTransform, OnDestroy { +export class LocalizedDatePipe implements PipeTransform { static DEFAULT_LOCALE = 'en-US'; static DEFAULT_DATE_FORMAT = 'mediumDate'; - defaultLocale: string = LocalizedDatePipe.DEFAULT_LOCALE; defaultFormat: string = LocalizedDatePipe.DEFAULT_DATE_FORMAT; - private onDestroy$ = new Subject(); - - constructor(public userPreferenceService?: UserPreferencesService, public appConfig?: AppConfigService) { - if (this.userPreferenceService) { - this.userPreferenceService - .select(UserPreferenceValues.Locale) - .pipe(takeUntil(this.onDestroy$)) - .subscribe((locale) => { - if (locale) { - this.defaultLocale = locale; - } - }); - } - + constructor( + public userPreferenceService?: UserPreferencesService, + public appConfig?: AppConfigService + ) { if (this.appConfig) { this.defaultFormat = this.appConfig.get('dateValues.defaultDateFormat', LocalizedDatePipe.DEFAULT_DATE_FORMAT); } @@ -55,13 +42,10 @@ export class LocalizedDatePipe implements PipeTransform, OnDestroy { transform(value: Date | string | number, format?: string, locale?: string, timezone?: string): string { const actualFormat = format || this.defaultFormat; - const actualLocale = locale || this.defaultLocale; + // Use signal directly - no subscription needed! + const defaultLocale = this.userPreferenceService?.localeSignal() || LocalizedDatePipe.DEFAULT_LOCALE; + const actualLocale = locale || defaultLocale; const datePipe = timezone ? new DatePipe(actualLocale, timezone) : new DatePipe(actualLocale); return datePipe.transform(value, actualFormat); } - - ngOnDestroy() { - this.onDestroy$.next(true); - this.onDestroy$.complete(); - } } diff --git a/lib/core/src/lib/pipes/time-ago.pipe.ts b/lib/core/src/lib/pipes/time-ago.pipe.ts index 094a004ab4..cd35ddcbab 100644 --- a/lib/core/src/lib/pipes/time-ago.pipe.ts +++ b/lib/core/src/lib/pipes/time-ago.pipe.ts @@ -17,10 +17,9 @@ import { Pipe, PipeTransform } from '@angular/core'; import { AppConfigService } from '../app-config/app-config.service'; -import { UserPreferencesService, UserPreferenceValues } from '../common/services/user-preferences.service'; +import { UserPreferencesService } from '../common/services/user-preferences.service'; import { DatePipe } from '@angular/common'; import { differenceInDays, formatDistance } from 'date-fns'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DateFnsUtils } from '../common/utils/date-fns-utils'; @Pipe({ @@ -32,25 +31,20 @@ export class TimeAgoPipe implements PipeTransform { static DEFAULT_LOCALE = 'en-US'; static DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm'; - defaultLocale: string; defaultDateTimeFormat: string; constructor( public userPreferenceService: UserPreferencesService, public appConfig: AppConfigService ) { - this.userPreferenceService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed()) - .subscribe((locale) => { - this.defaultLocale = locale || TimeAgoPipe.DEFAULT_LOCALE; - }); this.defaultDateTimeFormat = this.appConfig.get('dateValues.defaultDateTimeFormat', TimeAgoPipe.DEFAULT_DATE_TIME_FORMAT); } transform(value: Date, locale?: string) { if (value !== null && value !== undefined) { - const actualLocale = locale || this.defaultLocale; + // Use signal directly - no subscription needed! + const defaultLocale = this.userPreferenceService.localeSignal() || TimeAgoPipe.DEFAULT_LOCALE; + const actualLocale = locale || defaultLocale; const diff = differenceInDays(new Date(), new Date(value)); if (diff > 7) { const datePipe: DatePipe = new DatePipe(actualLocale); diff --git a/lib/core/src/lib/translation/translation.service.ts b/lib/core/src/lib/translation/translation.service.ts index b2f2b88798..dfdf37fa51 100644 --- a/lib/core/src/lib/translation/translation.service.ts +++ b/lib/core/src/lib/translation/translation.service.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; +import { effect, Inject, Injectable, InjectionToken, Optional } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { TranslateLoaderService } from './translate-loader.service'; -import { UserPreferencesService, UserPreferenceValues } from '../common/services/user-preferences.service'; +import { UserPreferencesService } from '../common/services/user-preferences.service'; export const TRANSLATION_PROVIDER = new InjectionToken('Injection token for translation providers.'); @@ -71,7 +71,10 @@ export class TranslationService { } } - userPreferencesService.select(UserPreferenceValues.Locale).subscribe((locale) => { + // Use effect to reactively update translations when locale signal changes + // Note: This is a singleton service, so no cleanup needed + effect(() => { + const locale = userPreferencesService.localeSignal(); if (locale) { this.userLang = locale; this.use(this.userLang); diff --git a/lib/process-services-cloud/src/lib/common/date-range-filter/date-range-filter.component.spec.ts b/lib/process-services-cloud/src/lib/common/date-range-filter/date-range-filter.component.spec.ts index a5a60633f6..047100e47c 100644 --- a/lib/process-services-cloud/src/lib/common/date-range-filter/date-range-filter.component.spec.ts +++ b/lib/process-services-cloud/src/lib/common/date-range-filter/date-range-filter.component.spec.ts @@ -22,12 +22,14 @@ import { DateCloudFilterType } from '../../models/date-cloud-filter.model'; import { DateRangeFilterService } from './date-range-filter.service'; import { mockFilterProperty } from '../mock/date-range-filter.mock'; import { add, endOfDay } from 'date-fns'; +import { enUS } from 'date-fns/locale'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSelectHarness } from '@angular/material/select/testing'; import { MatFormFieldHarness } from '@angular/material/form-field/testing'; import { MatDateRangeInputHarness } from '@angular/material/datepicker/testing'; import { NoopTranslateModule } from '@alfresco/adf-core'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; describe('DateRangeFilterComponent', () => { let component: DateRangeFilterComponent; @@ -37,7 +39,8 @@ describe('DateRangeFilterComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NoopTranslateModule, DateRangeFilterComponent] + imports: [NoopTranslateModule, DateRangeFilterComponent], + providers: [{ provide: MAT_DATE_LOCALE, useValue: enUS }] }); fixture = TestBed.createComponent(DateRangeFilterComponent); component = fixture.componentInstance; diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.ts index 94faa4a035..cd9e71c133 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, effect, EventEmitter, inject, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { DateAdapter } from '@angular/material/core'; import { MatDialog } from '@angular/material/dialog'; @@ -29,7 +29,7 @@ import { ProcessFilterProperties, ProcessSortFilterProperty } from '../../models/process-filter-cloud.model'; -import { DateFnsUtils, IconComponent, TranslationService, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core'; +import { DateFnsUtils, IconComponent, TranslationService, UserPreferencesService } from '@alfresco/adf-core'; import { ProcessFilterCloudService } from '../../services/process-filter-cloud.service'; import { ProcessFilterDialogCloudComponent } from '../process-filter-dialog/process-filter-dialog-cloud.component'; import { ProcessCloudService } from '../../../services/process-cloud.service'; @@ -101,7 +101,7 @@ interface ProcessFilterFormProps { styleUrls: ['./edit-process-filter-cloud.component.scss'], encapsulation: ViewEncapsulation.None }) -export class EditProcessFilterCloudComponent implements OnInit, OnChanges { +export class EditProcessFilterCloudComponent implements OnChanges { /** The name of the application. */ @Input() appName: string = ''; @@ -228,13 +228,14 @@ export class EditProcessFilterCloudComponent implements OnInit, OnChanges { private processFilterCloudService: ProcessFilterCloudService, private appsProcessCloudService: AppsProcessCloudService, private processCloudService: ProcessCloudService - ) {} - - ngOnInit() { - this.userPreferencesService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((locale) => this.dateAdapter.setLocale(locale)); + ) { + // Use effect to react to locale signal changes (must be in injection context) + effect(() => { + const locale = this.userPreferencesService.localeSignal(); + if (locale) { + this.dateAdapter.setLocale(DateFnsUtils.getLocaleFromString(locale)); + } + }); } ngOnChanges(changes: SimpleChanges) { diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/base-edit-task-filter-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/base-edit-task-filter-cloud.component.ts index 34a6d23d7b..f69278a0cf 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/base-edit-task-filter-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/base-edit-task-filter-cloud.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { DestroyRef, Directive, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { DestroyRef, Directive, effect, EventEmitter, inject, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { AssignmentType, FilterOptions, TaskFilterAction, TaskFilterProperties, TaskStatusFilter } from '../../models/filter-cloud.model'; import { TaskCloudService } from './../../../services/task-cloud.service'; import { AppsProcessCloudService } from './../../../../app/services/apps-process-cloud.service'; @@ -24,7 +24,7 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/ import { debounceTime, filter, finalize, switchMap } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { DateAdapter } from '@angular/material/core'; -import { DateFnsUtils, TranslationService, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core'; +import { DateFnsUtils, TranslationService, UserPreferencesService } from '@alfresco/adf-core'; import { TaskFilterDialogCloudComponent } from '../task-filter-dialog/task-filter-dialog-cloud.component'; import { MatDialog } from '@angular/material/dialog'; import { IdentityUserModel } from '../../../../people/models/identity-user.model'; @@ -55,7 +55,7 @@ const ORDER_PROPERTY = 'order'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix -export abstract class BaseEditTaskFilterCloudComponent implements OnInit, OnChanges { +export abstract class BaseEditTaskFilterCloudComponent implements OnChanges { public static ACTIONS_DISABLED_BY_DEFAULT = [ACTION_SAVE, ACTION_DELETE]; /** (required) Name of the app. */ @@ -145,11 +145,14 @@ export abstract class BaseEditTaskFilterCloudComponent implements OnInit, OnC protected formBuilder = inject(UntypedFormBuilder); protected dateAdapter = inject>(DateAdapter); - ngOnInit() { - this.userPreferencesService - .select(UserPreferenceValues.Locale) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((locale) => this.dateAdapter.setLocale(locale)); + constructor() { + // Use effect to react to locale signal changes (must be in injection context) + effect(() => { + const locale = this.userPreferencesService.localeSignal(); + if (locale) { + this.dateAdapter.setLocale(DateFnsUtils.getLocaleFromString(locale)); + } + }); } ngOnChanges(changes: SimpleChanges) {