diff --git a/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.spec.ts b/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.spec.ts index 74666a60d9..44686746fa 100644 --- a/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.spec.ts +++ b/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.spec.ts @@ -31,9 +31,23 @@ describe('DataTableCellComponent', () => { let testingUtils: UnitTestingUtils; const renderTextCell = (value: string, tooltip: string) => { + // Set up mock data if not already set + if (!component.data) { + component.data = { + getValue: () => value + } as any; + } + if (!component.row) { + component.row = { id: '1', getValue: () => value } as any; + } + if (!component.column) { + component.column = { key: 'text' } as any; + } + component.value$ = new BehaviorSubject(value); component.tooltip = tooltip; + component.ngOnInit(); fixture.detectChanges(); }; @@ -136,7 +150,7 @@ describe('DataTableCellComponent', () => { component.row = row; expect(() => fixture.detectChanges()).not.toThrow(); - expect(component.computedTitle).toBe(''); + expect(component.title()).toBe(''); expect(testingUtils.getByCSS('span').nativeElement.title).toBe(''); }); }); diff --git a/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts b/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts index 3e838c01ef..db9ac2dc7f 100644 --- a/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts +++ b/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, DestroyRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, inject, Input, OnInit, ViewEncapsulation, signal, computed } from '@angular/core'; import { DataColumn } from '../../data/data-column.model'; import { DataRow } from '../../data/data-row.model'; import { DataTableAdapter } from '../../data/datatable-adapter'; @@ -31,22 +31,21 @@ import { TruncatePipe } from '../../../pipes/truncate.pipe'; imports: [CommonModule, ClipboardDirective, TruncatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - + @let value = value$ | async; + @let displayValue = column?.maxTextLength ? (value | truncate: column?.maxTextLength) : value; + + @if (copyContent) { {{ column?.maxTextLength ? (value$ | async | truncate: column?.maxTextLength) : (value$ | async) }}{{ displayValue }} - - - {{ - column?.maxTextLength ? (value$ | async | truncate: column?.maxTextLength) : (value$ | async) - }} - + } @else { + {{ displayValue }} + } `, encapsulation: ViewEncapsulation.None, host: { class: 'adf-datatable-content-cell' } @@ -79,7 +78,12 @@ export class DataTableCellComponent implements OnInit { protected destroyRef = inject(DestroyRef); protected dataTableService = inject(DataTableService, { optional: true }); value$ = new BehaviorSubject(''); - computedTitle: string = ''; + + // Signal to track the raw computed title (without tooltip override) + protected rawComputedTitle = signal(''); + + // Computed signal that automatically combines tooltip input with computed title + title = computed(() => this.tooltip || this.rawComputedTitle()); ngOnInit() { this.updateValue(); @@ -90,7 +94,7 @@ export class DataTableCellComponent implements OnInit { if (this.column?.key && this.row && this.data) { const value = this.data.getValue(this.row, this.column, this.resolverFn); this.value$.next(value); - this.computedTitle = this.computeTitle(value); + this.rawComputedTitle.set(this.computeTitle(value)); } } @@ -113,10 +117,15 @@ export class DataTableCellComponent implements OnInit { return path.split('.').reduce((source, key) => (source ? source[key] : ''), obj); } - private computeTitle(value: string): string { - if (this.tooltip) { - return this.tooltip; - } + /** + * Computes the title/tooltip for the cell based on the value. + * Override this in derived classes to provide custom tooltip logic. + * Note: The tooltip input always takes precedence (handled by title signal). + * + * @param value - The cell value to compute the title for + * @returns The computed title string, or empty string if no title should be shown + */ + protected computeTitle(value: string): string { const rawValue = value; const max = this.column?.maxTextLength; diff --git a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.html b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.html index 398963c834..abac2de12b 100644 --- a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.html +++ b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.html @@ -1,15 +1,19 @@ - - - {{ date | adfTimeAgo: config.locale }} - - - +@let date = value$ | async; + +@if (date) { + + @if (config.format === 'timeAgo') { + @if (config.locale) { + {{ date | adfTimeAgo: config.locale }} + } @else { + {{ date | adfTimeAgo }} + } + } @else { + @if (config.locale) { {{ date | adfLocalizedDate: config.format: config.locale }} - - - + } @else { + {{ date | adfLocalizedDate: config.format }} + } + } + +} diff --git a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts index aad8a968c6..d7e57c4b6f 100644 --- a/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts +++ b/lib/core/src/lib/datatable/components/date-cell/date-cell.component.spec.ts @@ -31,18 +31,33 @@ let fixture: ComponentFixture; let testingUtils: UnitTestingUtils; let mockDate; -let mockTooltip = ''; const mockColumn: DataColumn = { key: 'mock-date', type: 'date', format: 'full' }; -const renderDateCell = (dateConfig: DateConfig, value: number | string | Date, tooltip: string) => { +const renderDateCell = (dateConfig: DateConfig, value: number | string | Date, tooltip?: string) => { + // Set up mock data if not already set + if (!component.data) { + component.data = { + getValue: () => value + } as any; + } + if (!component.row) { + component.row = { id: '1', getValue: () => value } as any; + } + if (!component.column) { + component.column = { key: 'date' } as any; + } + component.value$ = new BehaviorSubject(value); component.dateConfig = dateConfig; - component.tooltip = tooltip; + if (tooltip) { + component.tooltip = tooltip; + } + component.ngOnInit(); fixture.detectChanges(); }; @@ -85,7 +100,6 @@ describe('DateCellComponent', () => { registerLocaleData(localePL); configureTestingModule([]); mockDate = new Date('2023-10-25T00:00:00'); - mockTooltip = mockDate.toISOString(); }); it('should set default date config', () => { @@ -103,7 +117,7 @@ describe('DateCellComponent', () => { const expectedDate = '10/25/23, 12:00 AM'; const expectedTooltip = '10/25/23'; - renderDateCell(mockDateConfig, mockDate, mockTooltip); + renderDateCell(mockDateConfig, mockDate); checkDisplayedDate(expectedDate); checkDisplayedTooltip(expectedTooltip); }); @@ -113,7 +127,7 @@ describe('DateCellComponent', () => { const expectedDate = 'Oct 25, 2023'; const expectedTooltip = 'October 25, 2023 at 12:00:00 AM GMT+0'; - renderDateCell(mockDateConfig, mockDate, mockTooltip); + renderDateCell(mockDateConfig, mockDate); checkDisplayedDate(expectedDate); checkDisplayedTooltip(expectedTooltip); @@ -131,7 +145,7 @@ describe('DateCellComponent', () => { const expectedDate = 'Oct 25, 2023, 12:00:00 AM'; const expectedTooltip = expectedDate; - renderDateCell(mockDateConfig, mockDate, mockTooltip); + renderDateCell(mockDateConfig, mockDate); checkDisplayedDate(expectedDate); checkDisplayedTooltip(expectedTooltip); }); @@ -146,7 +160,7 @@ describe('DateCellComponent', () => { const expectedDate = '1 day ago'; - renderDateCell(mockDateConfig, yesterday, mockTooltip); + renderDateCell(mockDateConfig, yesterday); checkDisplayedDate(expectedDate); }); @@ -158,7 +172,7 @@ describe('DateCellComponent', () => { yesterday.setDate(today.getDate() - 1); const expectedDate = '1 day ago'; - renderDateCell(mockDateConfig, yesterday, mockTooltip); + renderDateCell(mockDateConfig, yesterday); checkDisplayedDate(expectedDate); }); //eslint-disable-next-line @@ -170,7 +184,7 @@ describe('DateCellComponent', () => { const expectedDate = 'Wednesday, October 25, 2023 at 12:00:00 AM GMT+00:00'; - renderDateCell(mockDateConfig, mockDate, mockTooltip); + renderDateCell(mockDateConfig, mockDate); checkDisplayedDate(expectedDate); }); @@ -182,7 +196,7 @@ describe('DateCellComponent', () => { const expectedDate = '10/25/23, 12:00 AM'; - renderDateCell(mockDateConfig, mockDate, mockTooltip); + renderDateCell(mockDateConfig, mockDate); checkDisplayedDate(expectedDate); }); @@ -195,7 +209,7 @@ describe('DateCellComponent', () => { const expectedDate = '10/25/23, 12:00 AM'; - renderDateCell(mockDateConfig, mockStringDate, mockTooltip); + renderDateCell(mockDateConfig, mockStringDate); checkDisplayedDate(expectedDate); }); @@ -208,7 +222,7 @@ describe('DateCellComponent', () => { const expectedDate = '10/25/23, 12:00 AM'; - renderDateCell(mockDateConfig, mockTimestamp, mockTooltip); + renderDateCell(mockDateConfig, mockTimestamp); checkDisplayedDate(expectedDate); }); }); @@ -227,7 +241,7 @@ describe('DateCellComponent locale', () => { const expectedDate = '25.10.2023, 00:00'; const expectedTooltip = '25 paź 2023, 00:00:00'; - renderDateCell(mockDateConfig, mockDate, mockTooltip); + renderDateCell(mockDateConfig, mockDate); checkDisplayedDate(expectedDate); checkDisplayedTooltip(expectedTooltip); }); 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 d0b57a3d2e..d321d3d2ef 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,20 +15,23 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation, inject, ChangeDetectorRef } 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 { CommonModule } from '@angular/common'; 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'; @Component({ - imports: [CommonModule, LocalizedDatePipe, TimeAgoPipe], + imports: [LocalizedDatePipe, TimeAgoPipe, AsyncPipe], selector: 'adf-date-cell', templateUrl: './date-cell.component.html', encapsulation: ViewEncapsulation.None, host: { class: 'adf-datatable-content-cell' }, - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [LocalizedDatePipe] }) export class DateCellComponent extends DataTableCellComponent implements OnInit { @Input() @@ -37,6 +40,11 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit config: DateConfig = {}; private readonly appConfig: AppConfigService = inject(AppConfigService); + private readonly localizedDatePipe: LocalizedDatePipe = inject(LocalizedDatePipe); + private readonly userPreferencesService: UserPreferencesService = inject(UserPreferencesService); + private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + + private userLocale: string = 'en'; readonly defaultDateConfig: DateConfig = { format: 'medium', @@ -45,8 +53,26 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit }; ngOnInit(): void { - super.ngOnInit(); + // 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(); + }); + this.setConfig(); + super.ngOnInit(); + } + + protected override computeTitle(value: any): string { + if (value) { + return this.localizedDatePipe.transform(value, this.config.tooltipFormat, this.config.locale) || ''; + } + return ''; } private setConfig(): void { @@ -60,13 +86,25 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit private setCustomConfig(): void { this.config.format = this.dateConfig?.format || this.getDefaultFormat(); this.config.tooltipFormat = this.dateConfig?.tooltipFormat || this.getDefaultTooltipFormat(); - this.config.locale = this.dateConfig?.locale || this.getDefaultLocale(); + this.config.locale = this.normalizeLocale(this.dateConfig?.locale || this.getDefaultLocale()); } private setDefaultConfig(): void { this.config.format = this.getDefaultFormat(); this.config.tooltipFormat = this.getDefaultTooltipFormat(); - this.config.locale = this.getDefaultLocale(); + this.config.locale = this.normalizeLocale(this.getDefaultLocale()); + } + + private normalizeLocale(locale: string): string { + if (!locale) { + return locale; + } + // Extract language code from locale like 'fr-FR' -> 'fr', 'en-US' -> 'en' + // but keep special cases like 'pt-BR' and 'zh-CN' intact + if (locale === 'pt-BR' || locale === 'zh-CN') { + return locale; + } + return locale.split('-')[0]; } private getDefaultFormat(): string { @@ -74,7 +112,9 @@ export class DateCellComponent extends DataTableCellComponent implements OnInit } private getDefaultLocale(): string { - return this.getAppConfigPropertyValue('dateValues.defaultLocale', this.defaultDateConfig.locale); + // Always use the user locale from UserPreferencesService + // This is kept in sync via subscription and reflects the user's current locale choice + return this.userLocale; } private getDefaultTooltipFormat(): string { diff --git a/lib/core/src/lib/datatable/components/filesize-cell/filesize-cell.component.ts b/lib/core/src/lib/datatable/components/filesize-cell/filesize-cell.component.ts index 482a4f916a..1fa35219b7 100644 --- a/lib/core/src/lib/datatable/components/filesize-cell/filesize-cell.component.ts +++ b/lib/core/src/lib/datatable/components/filesize-cell/filesize-cell.component.ts @@ -15,24 +15,33 @@ * limitations under the License. */ -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnInit, ViewEncapsulation, inject } from '@angular/core'; import { DataTableCellComponent } from '../datatable-cell/datatable-cell.component'; -import { CommonModule } from '@angular/common'; import { FileSizePipe } from '../../../pipes'; +import { AsyncPipe } from '@angular/common'; @Component({ selector: 'adf-filesize-cell', - imports: [CommonModule, FileSizePipe], + imports: [FileSizePipe, AsyncPipe], template: ` - - {{ fileSize }} - + @let value = value$ | async; + {{ value | adfFileSize }} `, encapsulation: ViewEncapsulation.None, - host: { class: 'adf-filesize-cell' } + host: { class: 'adf-filesize-cell' }, + providers: [FileSizePipe] }) export class FileSizeCellComponent extends DataTableCellComponent implements OnInit { + private readonly fileSizePipe = inject(FileSizePipe); + ngOnInit(): void { super.ngOnInit(); } + + protected override computeTitle(value: any): string { + if (value != null) { + return this.fileSizePipe.transform(value); + } + return ''; + } } diff --git a/lib/core/src/lib/pipes/time-ago.pipe.ts b/lib/core/src/lib/pipes/time-ago.pipe.ts index e69eb6e810..094a004ab4 100644 --- a/lib/core/src/lib/pipes/time-ago.pipe.ts +++ b/lib/core/src/lib/pipes/time-ago.pipe.ts @@ -20,12 +20,13 @@ import { AppConfigService } from '../app-config/app-config.service'; import { UserPreferencesService, UserPreferenceValues } from '../common/services/user-preferences.service'; import { DatePipe } from '@angular/common'; import { differenceInDays, formatDistance } from 'date-fns'; -import * as Locales from 'date-fns/locale'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DateFnsUtils } from '../common/utils/date-fns-utils'; @Pipe({ standalone: true, - name: 'adfTimeAgo' + name: 'adfTimeAgo', + pure: false }) export class TimeAgoPipe implements PipeTransform { static DEFAULT_LOCALE = 'en-US'; @@ -34,7 +35,10 @@ export class TimeAgoPipe implements PipeTransform { defaultLocale: string; defaultDateTimeFormat: string; - constructor(public userPreferenceService: UserPreferencesService, public appConfig: AppConfigService) { + constructor( + public userPreferenceService: UserPreferencesService, + public appConfig: AppConfigService + ) { this.userPreferenceService .select(UserPreferenceValues.Locale) .pipe(takeUntilDestroyed()) @@ -52,7 +56,8 @@ export class TimeAgoPipe implements PipeTransform { const datePipe: DatePipe = new DatePipe(actualLocale); return datePipe.transform(value, this.defaultDateTimeFormat); } else { - return formatDistance(new Date(value), new Date(), { addSuffix: true, locale: Locales[actualLocale] }); + const dateFnsLocale = DateFnsUtils.getLocaleFromString(actualLocale); + return formatDistance(new Date(value), new Date(), { addSuffix: true, locale: dateFnsLocale }); } } return '';