AAE-40136 Migrate locale settings to signals (#11361)

This commit is contained in:
Denys Vuika
2025-11-20 06:43:30 -05:00
committed by GitHub
parent 2f5139df73
commit 58a25fe19e
15 changed files with 253 additions and 124 deletions
@@ -8,6 +8,8 @@ runs:
with: with:
path: dist path: dist
key: dist-${{ github.run_id }}-${{ github.run_attempt }} key: dist-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
dist-${{ github.run_id }}-
fail-on-cache-miss: true fail-on-cache-miss: true
- name: Restore nxcache from cache - name: Restore nxcache from cache
@@ -15,6 +17,8 @@ runs:
with: with:
path: nxcache path: nxcache
key: nxcache-${{ github.run_id }}-${{ github.run_attempt }} key: nxcache-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
nxcache-${{ github.run_id }}-
fail-on-cache-miss: true fail-on-cache-miss: true
- name: Restore node_modules from cache - name: Restore node_modules from cache
@@ -22,6 +26,8 @@ runs:
with: with:
path: node_modules path: node_modules
key: node-modules-${{ github.run_id }}-${{ github.run_attempt }} key: node-modules-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
node-modules-${{ github.run_id }}-
fail-on-cache-miss: true fail-on-cache-miss: true
- name: show files - name: show files
+68 -7
View File
@@ -2,10 +2,10 @@
Title: User Preferences Service Title: User Preferences Service
Added: v2.0.0 Added: v2.0.0
Status: Active 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. 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 Check if an item is present in the storage
- _property:_ `string` - Name of the property - _property:_ `string` - Name of the property
- **Returns** `boolean` - True if the item is present, false otherwise - **Returns** `boolean` - True if the item is present, false otherwise
- **select**(property: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<any>`<br/> - **select**(property: `string`): `Observable<any>`<br/>
Sets up a callback to notify when a property has changed. Sets up a callback to notify when a property has changed.
- _property:_ `string` - The property to watch - _property:_ `string` - The property to watch
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<any>` - Notification callback - **Returns** `Observable<any>` - Notification callback
- **set**(property: `string`, value: `any`)<br/> - **set**(property: `string`, value: `any`)<br/>
Sets a preference property. Sets a preference property.
- _property:_ `string` - Name of the 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: 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"). | | 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. | | 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. | | locale | `string` | Current locale setting. |
## User Preference onChange Stream ## 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. 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 ```ts
userPreferences.disableCSRF = true; 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; 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.
@@ -15,7 +15,7 @@
* limitations under the License. * 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 { endOfDay, isAfter, isBefore, isValid, parse } from 'date-fns';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core'; 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'; 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 { DateRangeType } from './date-range-type';
import { SearchDateRange } from './search-date-range'; import { SearchDateRange } from './search-date-range';
import { FormBuilder, ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/forms'; 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 { CommonModule } from '@angular/common';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { TranslatePipe } from '@ngx-translate/core'; import { TranslatePipe } from '@ngx-translate/core';
@@ -100,7 +100,13 @@ export class SearchDateRangeComponent implements OnInit {
private userPreferencesService: UserPreferencesService, private userPreferencesService: UserPreferencesService,
private dateAdapter: DateAdapter<DateFnsAdapter>, private dateAdapter: DateAdapter<DateFnsAdapter>,
@Inject(MAT_DATE_FORMATS) private dateFormatConfig: MatDateFormats @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 => { readonly endDateValidator = (formControl: UntypedFormControl): { [key: string]: boolean } | null => {
if (isBefore(formControl.value, this.betweenStartDateFormControl.value) || isAfter(formControl.value, this.convertedMaxDate)) { if (isBefore(formControl.value, this.betweenStartDateFormControl.value) || isAfter(formControl.value, this.convertedMaxDate)) {
@@ -114,10 +120,6 @@ export class SearchDateRangeComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.dateFormatConfig.display.dateInput = this.dateFormat; this.dateFormatConfig.display.dateInput = this.dateFormat;
this.convertedMaxDate = endOfDay(this.maxDate && this.maxDate !== 'today' ? parse(this.maxDate, this.dateFormat, new Date()) : new Date()); 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 this.form.controls.dateRangeType.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((dateRangeType) => this.updateValidators(dateRangeType)); .subscribe((dateRangeType) => this.updateValidators(dateRangeType));
@@ -15,7 +15,7 @@
* limitations under the License. * 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 { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { import {
DatetimeAdapter, DatetimeAdapter,
@@ -25,7 +25,7 @@ import {
MatDatetimepickerModule MatDatetimepickerModule
} from '@mat-datetimepicker/core'; } from '@mat-datetimepicker/core';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model'; 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 { BaseCardView } from '../base-card-view';
import { ClipboardService } from '../../../clipboard/clipboard.service'; import { ClipboardService } from '../../../clipboard/clipboard.service';
import { TranslationService } from '../../../translation/translation.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 { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
@@ -83,8 +82,6 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
cardViewDateTimeControl: FormControl<Date> = new FormControl<Date>(null); cardViewDateTimeControl: FormControl<Date> = new FormControl<Date>(null);
private readonly destroyRef = inject(DestroyRef);
constructor( constructor(
private dateAdapter: DateAdapter<Date>, private dateAdapter: DateAdapter<Date>,
private userPreferencesService: UserPreferencesService, private userPreferencesService: UserPreferencesService,
@@ -92,16 +89,13 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
private translateService: TranslationService private translateService: TranslationService
) { ) {
super(); super();
// Use effect to react to locale signal changes (must be in injection context)
effect(() => {
this.property.locale = this.userPreferencesService.localeSignal();
});
} }
ngOnInit() { ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((locale) => {
this.property.locale = locale;
});
(this.dateAdapter as AdfDateFnsAdapter).displayFormat = 'MMM DD'; (this.dateAdapter as AdfDateFnsAdapter).displayFormat = 'MMM DD';
if (this.property.multivalued) { if (this.property.multivalued) {
@@ -15,7 +15,7 @@
* limitations under the License. * 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 { TranslateService } from '@ngx-translate/core';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { AppConfigService, AppConfigValues } from '../../app-config/app-config.service'; 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 { DOCUMENT } from '@angular/common';
import { Directionality, Direction } from '@angular/cdk/bidi'; import { Directionality, Direction } from '@angular/cdk/bidi';
import { DEFAULT_LANGUAGE_LIST } from '../models/default-languages.model'; import { DEFAULT_LANGUAGE_LIST } from '../models/default-languages.model';
import { toSignal } from '@angular/core/rxjs-interop';
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
export enum UserPreferenceValues { export enum UserPreferenceValues {
@@ -53,10 +54,78 @@ export class UserPreferencesService {
private onChangeSubject: BehaviorSubject<any>; private onChangeSubject: BehaviorSubject<any>;
onChange: Observable<any>; onChange: Observable<any>;
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<string>;
/**
* 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<string>;
/**
* Observable that emits the current pagination size whenever it changes.
*/
readonly paginationSize$: Observable<number>;
/**
* Signal that provides the current pagination size value.
*/
readonly paginationSizeSignal: Signal<number>;
/**
* Observable that emits the supported page sizes whenever they change.
*/
readonly supportedPageSizes$: Observable<number[]>;
/**
* Signal that provides the supported page sizes array.
*/
readonly supportedPageSizesSignal: Signal<number[]>;
constructor(
public translate: TranslateService,
private appConfig: AppConfigService,
private storage: StorageService
) {
this.onChangeSubject = new BehaviorSubject(this.userPreferenceStatus); this.onChangeSubject = new BehaviorSubject(this.userPreferenceStatus);
this.onChange = this.onChangeSubject.asObservable(); this.onChange = this.onChangeSubject.asObservable();
// Initialize convenience observables
this.locale$ = this.select<string>(UserPreferenceValues.Locale);
this.paginationSize$ = this.select<number>(UserPreferenceValues.PaginationSize);
this.supportedPageSizes$ = this.select<string>(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.appConfig.onLoad.subscribe(() => {
this.initUserPreferenceStatus(); this.initUserPreferenceStatus();
}); });
@@ -17,10 +17,11 @@
import { DateFnsAdapter } from '@angular/material-date-fns-adapter'; import { DateFnsAdapter } from '@angular/material-date-fns-adapter';
import { DateFnsUtils } from './date-fns-utils'; 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 { 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 { isValid, Locale, parse } from 'date-fns';
import { enUS } from 'date-fns/locale';
/** /**
* Date-fns adapter with moment-to-date-fns conversion. * 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, @Optional() @Inject(MAT_DATE_FORMATS) private formats: MatDateFormats,
preferences: UserPreferencesService 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) => { // 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)); this.setLocale(DateFnsUtils.getLocaleFromString(locale));
}
}); });
} }
@@ -15,9 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { Injectable } from '@angular/core'; import { effect, Injectable } from '@angular/core';
import { DateAdapter } from '@angular/material/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. // 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 // 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<Moment> {
constructor(preferences: UserPreferencesService) { constructor(preferences: UserPreferencesService) {
super(); 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); this.setLocale(locale);
}); });
} }
@@ -15,14 +15,13 @@
* limitations under the License. * 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 { DataTableCellComponent } from '../datatable-cell/datatable-cell.component';
import { AppConfigService } from '../../../app-config/app-config.service'; import { AppConfigService } from '../../../app-config/app-config.service';
import { DateConfig } from '../../data/data-column.model'; import { DateConfig } from '../../data/data-column.model';
import { LocalizedDatePipe, TimeAgoPipe } from '../../../pipes'; import { LocalizedDatePipe, TimeAgoPipe } from '../../../pipes';
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { UserPreferencesService, UserPreferenceValues } from '../../../common/services/user-preferences.service'; import { UserPreferencesService } from '../../../common/services/user-preferences.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
imports: [LocalizedDatePipe, TimeAgoPipe, AsyncPipe], imports: [LocalizedDatePipe, TimeAgoPipe, AsyncPipe],
@@ -52,19 +51,18 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit
locale: undefined locale: undefined
}; };
ngOnInit(): void { constructor() {
// Subscribe to locale changes super();
this.userPreferencesService // Use effect to react to locale signal changes (must be in injection context)
.select(UserPreferenceValues.Locale) effect(() => {
.pipe(takeUntilDestroyed(this.destroyRef)) this.userLocale = this.userPreferencesService.localeSignal() || 'en';
.subscribe((locale) => {
this.userLocale = locale || 'en';
this.setConfig(); this.setConfig();
this.updateValue(); // Recalculate computedTitle with new locale this.updateValue(); // Recalculate computedTitle with new locale
this.cdr.markForCheck(); this.cdr.markForCheck();
}); });
}
this.setConfig(); ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
} }
+8 -15
View File
@@ -18,9 +18,8 @@
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { AppConfigService } from '../app-config/app-config.service'; 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 { DecimalNumberModel } from '../models/decimal-number.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Pipe({ @Pipe({
name: 'adfDecimalNumber', name: 'adfDecimalNumber',
@@ -32,22 +31,14 @@ export class DecimalNumberPipe implements PipeTransform {
static DEFAULT_MIN_FRACTION_DIGITS = 0; static DEFAULT_MIN_FRACTION_DIGITS = 0;
static DEFAULT_MAX_FRACTION_DIGITS = 2; static DEFAULT_MAX_FRACTION_DIGITS = 2;
defaultLocale: string = DecimalNumberPipe.DEFAULT_LOCALE;
defaultMinIntegerDigits: number = DecimalNumberPipe.DEFAULT_MIN_INTEGER_DIGITS; defaultMinIntegerDigits: number = DecimalNumberPipe.DEFAULT_MIN_INTEGER_DIGITS;
defaultMinFractionDigits: number = DecimalNumberPipe.DEFAULT_MIN_FRACTION_DIGITS; defaultMinFractionDigits: number = DecimalNumberPipe.DEFAULT_MIN_FRACTION_DIGITS;
defaultMaxFractionDigits: number = DecimalNumberPipe.DEFAULT_MAX_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) { if (this.appConfig) {
this.defaultMinIntegerDigits = this.appConfig.get<number>('decimalValues.minIntegerDigits', DecimalNumberPipe.DEFAULT_MIN_INTEGER_DIGITS); this.defaultMinIntegerDigits = this.appConfig.get<number>('decimalValues.minIntegerDigits', DecimalNumberPipe.DEFAULT_MIN_INTEGER_DIGITS);
this.defaultMinFractionDigits = this.appConfig.get<number>( this.defaultMinFractionDigits = this.appConfig.get<number>(
@@ -67,7 +58,9 @@ export class DecimalNumberPipe implements PipeTransform {
const actualMaxFractionDigits: number = digitsInfo?.maxFractionDigits ? digitsInfo.maxFractionDigits : this.defaultMaxFractionDigits; const actualMaxFractionDigits: number = digitsInfo?.maxFractionDigits ? digitsInfo.maxFractionDigits : this.defaultMaxFractionDigits;
const actualDigitsInfo = `${actualMinIntegerDigits}.${actualMinFractionDigits}-${actualMaxFractionDigits}`; 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); const decimalPipe: DecimalPipe = new DecimalPipe(actualLocale);
+10 -26
View File
@@ -16,38 +16,25 @@
*/ */
import { DatePipe } from '@angular/common'; 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 { 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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Pipe({ @Pipe({
standalone: true, standalone: true,
name: 'adfLocalizedDate', name: 'adfLocalizedDate',
pure: false pure: false
}) })
export class LocalizedDatePipe implements PipeTransform, OnDestroy { export class LocalizedDatePipe implements PipeTransform {
static DEFAULT_LOCALE = 'en-US'; static DEFAULT_LOCALE = 'en-US';
static DEFAULT_DATE_FORMAT = 'mediumDate'; static DEFAULT_DATE_FORMAT = 'mediumDate';
defaultLocale: string = LocalizedDatePipe.DEFAULT_LOCALE;
defaultFormat: string = LocalizedDatePipe.DEFAULT_DATE_FORMAT; defaultFormat: string = LocalizedDatePipe.DEFAULT_DATE_FORMAT;
private onDestroy$ = new Subject<boolean>(); constructor(
public userPreferenceService?: UserPreferencesService,
constructor(public userPreferenceService?: UserPreferencesService, public appConfig?: AppConfigService) { public appConfig?: AppConfigService
if (this.userPreferenceService) { ) {
this.userPreferenceService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe((locale) => {
if (locale) {
this.defaultLocale = locale;
}
});
}
if (this.appConfig) { if (this.appConfig) {
this.defaultFormat = this.appConfig.get<string>('dateValues.defaultDateFormat', LocalizedDatePipe.DEFAULT_DATE_FORMAT); this.defaultFormat = this.appConfig.get<string>('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 { transform(value: Date | string | number, format?: string, locale?: string, timezone?: string): string {
const actualFormat = format || this.defaultFormat; 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); const datePipe = timezone ? new DatePipe(actualLocale, timezone) : new DatePipe(actualLocale);
return datePipe.transform(value, actualFormat); return datePipe.transform(value, actualFormat);
} }
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
} }
+4 -10
View File
@@ -17,10 +17,9 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { AppConfigService } from '../app-config/app-config.service'; 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 { DatePipe } from '@angular/common';
import { differenceInDays, formatDistance } from 'date-fns'; import { differenceInDays, formatDistance } from 'date-fns';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DateFnsUtils } from '../common/utils/date-fns-utils'; import { DateFnsUtils } from '../common/utils/date-fns-utils';
@Pipe({ @Pipe({
@@ -32,25 +31,20 @@ export class TimeAgoPipe implements PipeTransform {
static DEFAULT_LOCALE = 'en-US'; static DEFAULT_LOCALE = 'en-US';
static DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm'; static DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm';
defaultLocale: string;
defaultDateTimeFormat: string; defaultDateTimeFormat: string;
constructor( constructor(
public userPreferenceService: UserPreferencesService, public userPreferenceService: UserPreferencesService,
public appConfig: AppConfigService public appConfig: AppConfigService
) { ) {
this.userPreferenceService
.select(UserPreferenceValues.Locale)
.pipe(takeUntilDestroyed())
.subscribe((locale) => {
this.defaultLocale = locale || TimeAgoPipe.DEFAULT_LOCALE;
});
this.defaultDateTimeFormat = this.appConfig.get<string>('dateValues.defaultDateTimeFormat', TimeAgoPipe.DEFAULT_DATE_TIME_FORMAT); this.defaultDateTimeFormat = this.appConfig.get<string>('dateValues.defaultDateTimeFormat', TimeAgoPipe.DEFAULT_DATE_TIME_FORMAT);
} }
transform(value: Date, locale?: string) { transform(value: Date, locale?: string) {
if (value !== null && value !== undefined) { 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)); const diff = differenceInDays(new Date(), new Date(value));
if (diff > 7) { if (diff > 7) {
const datePipe: DatePipe = new DatePipe(actualLocale); const datePipe: DatePipe = new DatePipe(actualLocale);
@@ -15,11 +15,11 @@
* limitations under the License. * 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 { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { TranslateLoaderService } from './translate-loader.service'; 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.'); 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) { if (locale) {
this.userLang = locale; this.userLang = locale;
this.use(this.userLang); this.use(this.userLang);
@@ -22,12 +22,14 @@ import { DateCloudFilterType } from '../../models/date-cloud-filter.model';
import { DateRangeFilterService } from './date-range-filter.service'; import { DateRangeFilterService } from './date-range-filter.service';
import { mockFilterProperty } from '../mock/date-range-filter.mock'; import { mockFilterProperty } from '../mock/date-range-filter.mock';
import { add, endOfDay } from 'date-fns'; import { add, endOfDay } from 'date-fns';
import { enUS } from 'date-fns/locale';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing'; import { MatSelectHarness } from '@angular/material/select/testing';
import { MatFormFieldHarness } from '@angular/material/form-field/testing'; import { MatFormFieldHarness } from '@angular/material/form-field/testing';
import { MatDateRangeInputHarness } from '@angular/material/datepicker/testing'; import { MatDateRangeInputHarness } from '@angular/material/datepicker/testing';
import { NoopTranslateModule } from '@alfresco/adf-core'; import { NoopTranslateModule } from '@alfresco/adf-core';
import { MAT_DATE_LOCALE } from '@angular/material/core';
describe('DateRangeFilterComponent', () => { describe('DateRangeFilterComponent', () => {
let component: DateRangeFilterComponent; let component: DateRangeFilterComponent;
@@ -37,7 +39,8 @@ describe('DateRangeFilterComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [NoopTranslateModule, DateRangeFilterComponent] imports: [NoopTranslateModule, DateRangeFilterComponent],
providers: [{ provide: MAT_DATE_LOCALE, useValue: enUS }]
}); });
fixture = TestBed.createComponent(DateRangeFilterComponent); fixture = TestBed.createComponent(DateRangeFilterComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@@ -15,7 +15,7 @@
* limitations under the License. * 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 { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { DateAdapter } from '@angular/material/core'; import { DateAdapter } from '@angular/material/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@@ -29,7 +29,7 @@ import {
ProcessFilterProperties, ProcessFilterProperties,
ProcessSortFilterProperty ProcessSortFilterProperty
} from '../../models/process-filter-cloud.model'; } 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 { ProcessFilterCloudService } from '../../services/process-filter-cloud.service';
import { ProcessFilterDialogCloudComponent } from '../process-filter-dialog/process-filter-dialog-cloud.component'; import { ProcessFilterDialogCloudComponent } from '../process-filter-dialog/process-filter-dialog-cloud.component';
import { ProcessCloudService } from '../../../services/process-cloud.service'; import { ProcessCloudService } from '../../../services/process-cloud.service';
@@ -101,7 +101,7 @@ interface ProcessFilterFormProps {
styleUrls: ['./edit-process-filter-cloud.component.scss'], styleUrls: ['./edit-process-filter-cloud.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class EditProcessFilterCloudComponent implements OnInit, OnChanges { export class EditProcessFilterCloudComponent implements OnChanges {
/** The name of the application. */ /** The name of the application. */
@Input() @Input()
appName: string = ''; appName: string = '';
@@ -228,13 +228,14 @@ export class EditProcessFilterCloudComponent implements OnInit, OnChanges {
private processFilterCloudService: ProcessFilterCloudService, private processFilterCloudService: ProcessFilterCloudService,
private appsProcessCloudService: AppsProcessCloudService, private appsProcessCloudService: AppsProcessCloudService,
private processCloudService: ProcessCloudService private processCloudService: ProcessCloudService
) {} ) {
// Use effect to react to locale signal changes (must be in injection context)
ngOnInit() { effect(() => {
this.userPreferencesService const locale = this.userPreferencesService.localeSignal();
.select(UserPreferenceValues.Locale) if (locale) {
.pipe(takeUntilDestroyed(this.destroyRef)) this.dateAdapter.setLocale(DateFnsUtils.getLocaleFromString(locale));
.subscribe((locale) => this.dateAdapter.setLocale(locale)); }
});
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
@@ -15,7 +15,7 @@
* limitations under the License. * 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 { AssignmentType, FilterOptions, TaskFilterAction, TaskFilterProperties, TaskStatusFilter } from '../../models/filter-cloud.model';
import { TaskCloudService } from './../../../services/task-cloud.service'; import { TaskCloudService } from './../../../services/task-cloud.service';
import { AppsProcessCloudService } from './../../../../app/services/apps-process-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 { debounceTime, filter, finalize, switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DateAdapter } from '@angular/material/core'; 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 { TaskFilterDialogCloudComponent } from '../task-filter-dialog/task-filter-dialog-cloud.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { IdentityUserModel } from '../../../../people/models/identity-user.model'; import { IdentityUserModel } from '../../../../people/models/identity-user.model';
@@ -55,7 +55,7 @@ const ORDER_PROPERTY = 'order';
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix
export abstract class BaseEditTaskFilterCloudComponent<T> implements OnInit, OnChanges { export abstract class BaseEditTaskFilterCloudComponent<T> implements OnChanges {
public static ACTIONS_DISABLED_BY_DEFAULT = [ACTION_SAVE, ACTION_DELETE]; public static ACTIONS_DISABLED_BY_DEFAULT = [ACTION_SAVE, ACTION_DELETE];
/** (required) Name of the app. */ /** (required) Name of the app. */
@@ -145,11 +145,14 @@ export abstract class BaseEditTaskFilterCloudComponent<T> implements OnInit, OnC
protected formBuilder = inject(UntypedFormBuilder); protected formBuilder = inject(UntypedFormBuilder);
protected dateAdapter = inject<DateAdapter<Date>>(DateAdapter); protected dateAdapter = inject<DateAdapter<Date>>(DateAdapter);
ngOnInit() { constructor() {
this.userPreferencesService // Use effect to react to locale signal changes (must be in injection context)
.select(UserPreferenceValues.Locale) effect(() => {
.pipe(takeUntilDestroyed(this.destroyRef)) const locale = this.userPreferencesService.localeSignal();
.subscribe((locale) => this.dateAdapter.setLocale(locale)); if (locale) {
this.dateAdapter.setLocale(DateFnsUtils.getLocaleFromString(locale));
}
});
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {