Skip to content

Commit 7d3e5ce

Browse files
authored
Merge pull request #22619 from abpframework/feature/#22507
Multi-timezone support in Angular
2 parents cff2545 + 944836a commit 7d3e5ce

File tree

13 files changed

+290
-22
lines changed

13 files changed

+290
-22
lines changed

npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,33 @@
4949
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
5050
<ng-container *abpVisible="row['_' + prop.name]?.visible">
5151
@if (!row['_' + prop.name].component) {
52-
<div
53-
[innerHTML]="
52+
@if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') {
53+
<div
54+
[innerHTML]="
55+
!prop.isExtra
56+
? (row['_' + prop.name]?.value | async | abpUtcToLocal:prop.type)
57+
: ('::' + (row['_' + prop.name]?.value | async | abpUtcToLocal:prop.type) | abpLocalization)
58+
"
59+
(click)="
60+
prop.action && prop.action({ getInjected: getInjected, record: row, index: i })
61+
"
62+
[ngClass]="entityPropTypeClasses[prop.type]"
63+
[class.pointer]="prop.action"
64+
></div>
65+
} @else {
66+
<div
67+
[innerHTML]="
5468
!prop.isExtra
5569
? (row['_' + prop.name]?.value | async)
5670
: ('::' + (row['_' + prop.name]?.value | async) | abpLocalization)
5771
"
58-
(click)="
72+
(click)="
5973
prop.action && prop.action({ getInjected: getInjected, record: row, index: i })
6074
"
61-
[ngClass]="entityPropTypeClasses[prop.type]"
62-
[class.pointer]="prop.action"
63-
></div>
75+
[ngClass]="entityPropTypeClasses[prop.type]"
76+
[class.pointer]="prop.action"
77+
></div>
78+
}
6479
} @else {
6580
<ng-container
6681
*ngComponentOutlet="

npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
TemplateRef,
1515
TrackByFunction,
1616
} from '@angular/core';
17-
import { AsyncPipe, formatDate, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
17+
import { AsyncPipe, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
1818

1919
import { Observable, filter, map } from 'rxjs';
2020

@@ -24,13 +24,12 @@ import { NgxDatatableModule } from '@swimlane/ngx-datatable';
2424
import {
2525
ABP,
2626
ConfigStateService,
27-
getShortDateFormat,
28-
getShortDateShortTimeFormat,
29-
getShortTimeFormat,
3027
ListService,
3128
LocalizationModule,
3229
PermissionDirective,
3330
PermissionService,
31+
TimezoneService,
32+
UtcToLocalPipe,
3433
} from '@abp/ng.core';
3534
import {
3635
AbpVisibleDirective,
@@ -64,6 +63,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
6463
NgxDatatableListDirective,
6564
PermissionDirective,
6665
LocalizationModule,
66+
UtcToLocalPipe,
6767
AsyncPipe,
6868
NgTemplateOutlet,
6969
NgComponentOutlet,
@@ -77,6 +77,7 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
7777
protected readonly cdr = inject(ChangeDetectorRef);
7878
protected readonly locale = inject(LOCALE_ID);
7979
protected readonly config = inject(ConfigStateService);
80+
protected readonly timeZoneService = inject(TimezoneService);
8081
protected readonly entityPropTypeClasses = inject(ENTITY_PROP_TYPE_CLASSES);
8182
protected readonly permissionService = inject(PermissionService);
8283

@@ -134,10 +135,6 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
134135
(this.columnWidths as any) = widths;
135136
}
136137

137-
private getDate(value: Date | undefined, format: string | undefined) {
138-
return value && format ? formatDate(value, format, this.locale) : '';
139-
}
140-
141138
private getIcon(value: boolean) {
142139
return value
143140
? '<div class="text-success"><i class="fa fa-check" aria-hidden="true"></i></div>'
@@ -156,12 +153,6 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
156153
switch (prop.type) {
157154
case ePropType.Boolean:
158155
return this.getIcon(value);
159-
case ePropType.Date:
160-
return this.getDate(value, getShortDateFormat(this.config));
161-
case ePropType.Time:
162-
return this.getDate(value, getShortTimeFormat(this.config));
163-
case ePropType.DateTime:
164-
return this.getDate(value, getShortDateShortTimeFormat(this.config));
165156
case ePropType.Enum:
166157
return this.getEnum(value, prop.enumList || []);
167158
default:

npm/ng-packs/packages/core/ng-package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"angular-oauth2-oidc",
1010
"just-compare",
1111
"just-clone",
12-
"ts-toolbelt"
12+
"ts-toolbelt",
13+
"luxon"
1314
]
1415
}

npm/ng-packs/packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"just-clone": "^6.0.0",
1212
"just-compare": "^2.0.0",
1313
"ts-toolbelt": "^9.0.0",
14-
"tslib": "^2.0.0"
14+
"tslib": "^2.0.0",
15+
"luxon": "^3.0.0"
1516
},
1617
"publishConfig": {
1718
"access": "public"

npm/ng-packs/packages/core/src/lib/core.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
provideHttpClient,
44
withInterceptorsFromDi,
55
withXsrfConfiguration,
6+
HTTP_INTERCEPTORS,
67
} from '@angular/common/http';
78
import { ModuleWithProviders, NgModule } from '@angular/core';
89
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -30,6 +31,7 @@ import { ShortTimePipe } from './pipes/short-time.pipe';
3031
import { ShortDatePipe } from './pipes/short-date.pipe';
3132
import { SafeHtmlPipe } from './pipes/safe-html.pipe';
3233
import { provideAbpCoreChild, provideAbpCore, withOptions } from './providers';
34+
import { UtcToLocalPipe } from './pipes';
3335

3436
const standaloneDirectives = [
3537
AutofocusDirective,
@@ -72,6 +74,7 @@ const standaloneDirectives = [
7274
ReactiveFormsModule,
7375
RouterModule,
7476
LocalizationModule,
77+
UtcToLocalPipe,
7578
...standaloneDirectives,
7679
],
7780
declarations: [
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './api.interceptor';
2+
export * from './timezone.interceptor';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { inject, Injectable } from '@angular/core';
2+
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
3+
import { TimezoneService } from '../services';
4+
import { Observable } from 'rxjs';
5+
6+
@Injectable({
7+
providedIn: 'root',
8+
})
9+
export class TimezoneInterceptor implements HttpInterceptor {
10+
protected readonly timezoneService = inject(TimezoneService);
11+
12+
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
13+
if (!this.timezoneService.isUtcClockEnabled) {
14+
return next.handle(req);
15+
}
16+
const timezone = this.timezoneService.timezone;
17+
if (timezone) {
18+
req = req.clone({
19+
setHeaders: {
20+
__timezone: timezone,
21+
},
22+
});
23+
}
24+
return next.handle(req);
25+
}
26+
}

npm/ng-packs/packages/core/src/lib/pipes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './to-injector.pipe';
55
export * from './short-date.pipe';
66
export * from './short-time.pipe';
77
export * from './short-date-time.pipe';
8+
export * from './utc-to-local.pipe';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Pipe, PipeTransform, Injectable, inject, LOCALE_ID } from '@angular/core';
2+
import { ConfigStateService, LocalizationService, TimeService, TimezoneService } from '../services';
3+
import { getShortDateFormat, getShortDateShortTimeFormat, getShortTimeFormat } from '../utils';
4+
5+
@Injectable()
6+
@Pipe({
7+
name: 'abpUtcToLocal',
8+
})
9+
export class UtcToLocalPipe implements PipeTransform {
10+
protected readonly timezoneService = inject(TimezoneService);
11+
protected readonly timeService = inject(TimeService);
12+
protected readonly configState = inject(ConfigStateService);
13+
protected readonly localizationService = inject(LocalizationService);
14+
protected readonly locale = inject(LOCALE_ID);
15+
16+
transform(
17+
value: string | Date | null | undefined,
18+
type: 'date' | 'datetime' | 'time',
19+
): string | Date {
20+
if (!value) return '';
21+
22+
const date = new Date(value);
23+
if (isNaN(date.getTime())) return '';
24+
25+
const format = this.getFormat(type);
26+
27+
try {
28+
if (this.timezoneService.isUtcClockEnabled) {
29+
const timeZone = this.timezoneService.timezone;
30+
return this.timeService.formatDateWithStandardOffset(date, format, timeZone);
31+
} else {
32+
return this.timeService.formatWithoutTimeZone(date, format);
33+
}
34+
} catch (err) {
35+
return value;
36+
}
37+
}
38+
39+
private getFormat(propType: 'date' | 'datetime' | 'time'): string {
40+
switch (propType) {
41+
case 'date':
42+
return getShortDateFormat(this.configState);
43+
case 'time':
44+
return getShortTimeFormat(this.configState);
45+
case 'datetime':
46+
default:
47+
return getShortDateShortTimeFormat(this.configState);
48+
}
49+
}
50+
}

npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { makeEnvironmentProviders, Provider, inject, provideAppInitializer } from '@angular/core';
22
import { TitleStrategy } from '@angular/router';
33
import {
4+
HTTP_INTERCEPTORS,
45
provideHttpClient,
56
withInterceptorsFromDi,
67
withXsrfConfiguration,
@@ -24,6 +25,7 @@ import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants';
2425
import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services';
2526
import { DefaultQueueManager, getInitialData, localeInitializer } from '../utils';
2627
import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './';
28+
import { TimezoneInterceptor } from '../interceptors';
2729

2830
export enum CoreFeatureKind {
2931
Options,
@@ -128,6 +130,11 @@ export function provideAbpCore(...features: CoreFeature<CoreFeatureKind>[]) {
128130
provide: TitleStrategy,
129131
useExisting: AbpTitleStrategy,
130132
},
133+
{
134+
provide: HTTP_INTERCEPTORS,
135+
useClass: TimezoneInterceptor,
136+
multi: true,
137+
},
131138
];
132139

133140
for (const feature of features) {

0 commit comments

Comments
 (0)