Skip to content

Commit 7de2dcd

Browse files
authored
cache timezone calculation (DevExpress#29640)
Co-authored-by: Vladimir Bushmanov <[email protected]>
1 parent 2988349 commit 7de2dcd

File tree

8 files changed

+108
-97
lines changed

8 files changed

+108
-97
lines changed

packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts

Lines changed: 4 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ import $ from '@js/core/renderer';
1111
import dateUtils from '@js/core/utils/date';
1212
import dateSerialization from '@js/core/utils/date_serialization';
1313
import { extend } from '@js/core/utils/extend';
14-
import type AbstractStore from '@js/data/abstract_store';
1514
import Form from '@js/ui/form';
1615
import { current, isFluent } from '@js/ui/themes';
1716

1817
import { createAppointmentAdapter } from '../m_appointment_adapter';
19-
import type { TimezoneLabel } from '../m_utils_time_zone';
2018
import timeZoneUtils from '../m_utils_time_zone';
2119

2220
const SCREEN_SIZE_OF_SINGLE_COLUMN = 600;
@@ -41,10 +39,11 @@ const E2E_TEST_CLASSES = {
4139
recurrenceSwitch: 'e2e-dx-scheduler-form-recurrence-switch',
4240
};
4341

44-
const DEFAULT_TIMEZONE_EDITOR_DATA_SOURCE_OPTIONS = {
42+
const createTimeZoneDataSource = () => new DataSource({
43+
store: timeZoneUtils.getTimeZonesCache(),
4544
paginate: true,
4645
pageSize: 10,
47-
};
46+
});
4847

4948
const getStylingModeFunc = (): string | undefined => (isFluent(current()) ? 'filled' : undefined);
5049

@@ -193,6 +192,7 @@ export class AppointmentForm {
193192
valueExpr: 'id',
194193
placeholder: noTzTitle,
195194
searchEnabled: true,
195+
dataSource: createTimeZoneDataSource(),
196196
onValueChanged: (args) => {
197197
const { form } = this;
198198
const secondTimezoneEditor = form.getEditor(secondTimeZoneExpr);
@@ -428,59 +428,6 @@ export class AppointmentForm {
428428
editor && this.form.itemOption(editorPath, 'editorOptions', extend({}, editor.editorOptions, options));
429429
}
430430

431-
private scheduleTimezoneEditorDataSourceUpdate(
432-
editorName: string,
433-
dataSource: { store: () => AbstractStore; reload: () => void },
434-
selectedTimezoneLabel: TimezoneLabel | null,
435-
date: Date,
436-
): void {
437-
timeZoneUtils.getTimeZoneLabelsAsyncBatch(date)
438-
.catch(() => [] as TimezoneLabel[])
439-
.then(async (timezones) => {
440-
const store = dataSource.store();
441-
442-
await store.remove(selectedTimezoneLabel?.id);
443-
444-
// NOTE: Unfortunately, our store not support bulk operations
445-
// So, we update it record-by-record
446-
const insertPromises = timezones.reduce<Promise<void>[]>((result, timezone) => {
447-
result.push(store.insert(timezone));
448-
return result;
449-
}, []);
450-
451-
// NOTE: We should wait for all insertions before reload
452-
await Promise.all(insertPromises);
453-
454-
dataSource.reload();
455-
// NOTE: We should re-assign dataSource to the editor
456-
// to repaint this editor after dataSource update
457-
this.setEditorOptions(editorName, 'Main', { dataSource });
458-
}).catch(() => {});
459-
}
460-
461-
private setupTimezoneEditorDataSource(
462-
editorName: string,
463-
selectedTimezoneId: string | null,
464-
date: Date,
465-
): void {
466-
const selectedTimezoneLabel = selectedTimezoneId
467-
? timeZoneUtils.getTimeZoneLabel(selectedTimezoneId, date)
468-
: null;
469-
470-
const dataSource = new DataSource({
471-
...DEFAULT_TIMEZONE_EDITOR_DATA_SOURCE_OPTIONS,
472-
store: selectedTimezoneLabel ? [selectedTimezoneLabel] : [],
473-
});
474-
475-
this.setEditorOptions(editorName, 'Main', { dataSource });
476-
this.scheduleTimezoneEditorDataSourceUpdate(
477-
editorName,
478-
dataSource,
479-
selectedTimezoneLabel,
480-
date,
481-
);
482-
}
483-
484431
updateFormData(formData: Record<string, any>): void {
485432
this.isFormUpdating = true;
486433
this.form.option('formData', formData);
@@ -489,16 +436,9 @@ export class AppointmentForm {
489436
const { expr } = dataAccessors;
490437

491438
const rawStartDate = dataAccessors.get('startDate', formData);
492-
const rawEndDate = dataAccessors.get('endDate', formData);
493-
const startDateTimezone = dataAccessors.get('startDateTimeZone', formData) ?? null;
494-
const endDateTimezone = dataAccessors.get('endDateTimeZone', formData) ?? null;
495439

496440
const allDay = dataAccessors.get('allDay', formData);
497441
const startDate = new Date(rawStartDate);
498-
const endDate = new Date(rawEndDate);
499-
500-
this.setupTimezoneEditorDataSource(expr.startDateTimeZoneExpr, startDateTimezone, startDate);
501-
this.setupTimezoneEditorDataSource(expr.endDateTimeZoneExpr, endDateTimezone, endDate);
502442

503443
this.updateRecurrenceEditorStartDate(startDate, expr.recurrenceRuleExpr);
504444

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
import type { Properties } from '@js/ui/scheduler';
22

3-
import type { ArrayElement } from '../utils/types';
4-
5-
export type RawViewType = ArrayElement<Required<Properties>['views']>;
3+
export type RawViewType = Required<Properties>['views'][number];

packages/devextreme/js/__internal/scheduler/m_scheduler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import { hide as hideLoading, show as showLoading } from './m_loading';
6666
import { getRecurrenceProcessor } from './m_recurrence';
6767
import subscribes from './m_subscribes';
6868
import { utils } from './m_utils';
69-
import timeZoneUtils from './m_utils_time_zone';
69+
import timeZoneUtils, { type TimezoneLabel } from './m_utils_time_zone';
7070
import { SchedulerOptionsValidator, SchedulerOptionsValidatorErrorsHandler } from './options_validator/index';
7171
import {
7272
createExpressions,
@@ -229,6 +229,8 @@ class Scheduler extends Widget<any> {
229229

230230
_editAppointmentData: any;
231231

232+
_timeZonesPromise!: Promise<TimezoneLabel[]>;
233+
232234
private _optionsValidator!: SchedulerOptionsValidator;
233235

234236
private _optionsValidatorErrorHandler!: SchedulerOptionsValidatorErrorsHandler;
@@ -1002,6 +1004,7 @@ class Scheduler extends Widget<any> {
10021004
}
10031005

10041006
_init() {
1007+
this._timeZonesPromise = timeZoneUtils.cacheTimeZones();
10051008
this._initExpressions({
10061009
startDateExpr: this.option('startDateExpr'),
10071010
endDateExpr: this.option('endDateExpr'),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
afterAll, beforeAll, describe, expect, it, jest,
3+
} from '@jest/globals';
4+
import { macroTaskArray } from '@ts/scheduler/utils/index';
5+
6+
import timeZoneUtils from './m_utils_time_zone';
7+
import timeZoneList from './timezones/timezone_list';
8+
9+
const defaultTimeZones = timeZoneList.value;
10+
11+
describe('timezone utils', () => {
12+
describe('cacheTimeZones / getTimeZonesCache', () => {
13+
beforeAll(() => {
14+
timeZoneList.value = [
15+
'Etc/GMT+12',
16+
'Etc/GMT+11',
17+
];
18+
});
19+
afterAll(() => {
20+
timeZoneList.value = defaultTimeZones;
21+
});
22+
23+
it('should cache timezones only once and save into global variable', async () => {
24+
const mock = jest.spyOn(macroTaskArray, 'map');
25+
26+
expect(timeZoneUtils.getTimeZonesCache()).toEqual([]);
27+
await timeZoneUtils.cacheTimeZones();
28+
expect(timeZoneUtils.getTimeZonesCache()).toEqual([
29+
{ id: 'Etc/GMT+12', title: '(GMT -12:00) Etc - GMT+12' },
30+
{ id: 'Etc/GMT+11', title: '(GMT -11:00) Etc - GMT+11' },
31+
]);
32+
await timeZoneUtils.cacheTimeZones();
33+
await timeZoneUtils.cacheTimeZones();
34+
expect(mock).toHaveBeenCalledTimes(1);
35+
});
36+
});
37+
38+
describe('getTimeZones', () => {
39+
it('should return timezones with offsets of default timezones list', () => {
40+
timeZoneList.value = [
41+
'Etc/GMT+12',
42+
'Etc/GMT+11',
43+
];
44+
expect(timeZoneUtils.getTimeZones(
45+
new Date('2025-04-23T10:00:00Z'),
46+
)).toEqual([
47+
{ id: 'Etc/GMT+12', title: '(GMT -12:00) Etc - GMT+12', offset: -12 },
48+
{ id: 'Etc/GMT+11', title: '(GMT -11:00) Etc - GMT+11', offset: -11 },
49+
]);
50+
timeZoneList.value = defaultTimeZones;
51+
});
52+
53+
it('should return timezones with offsets of custom timezones list', () => {
54+
expect(timeZoneUtils.getTimeZones(
55+
new Date('2025-04-23T10:00:00Z'),
56+
['Canada/Pacific'],
57+
)).toEqual([
58+
{ id: 'Canada/Pacific', title: '(GMT -07:00) Canada - Pacific', offset: -7 },
59+
]);
60+
});
61+
});
62+
});

packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export interface TimezoneData extends TimezoneLabel {
2323
const toMs = dateUtils.dateToMilliseconds;
2424
const MINUTES_IN_HOUR = 60;
2525
const MS_IN_MINUTE = 60000;
26-
const GET_TIMEZONES_BATCH_SIZE = 20;
2726
const GMT = 'GMT';
2827
const offsetFormatRegexp = /^GMT(?:[+-]\d{2}:\d{2})?$/;
2928

@@ -333,33 +332,40 @@ const addOffsetsWithoutDST = (date: Date, ...offsets: number[]): Date => {
333332
: newDate;
334333
};
335334

336-
const getTimeZoneLabelsAsyncBatch = (
337-
date = new Date(),
338-
): Promise<TimezoneLabel[]> => macroTaskArray.map(
339-
timeZoneList.value,
340-
(timezoneId) => ({
341-
id: timezoneId,
342-
title: getTimezoneTitle(timezoneId, date),
343-
}),
344-
GET_TIMEZONES_BATCH_SIZE,
345-
);
346-
347-
const getTimeZoneLabel = (
348-
timezoneId: string,
349-
date = new Date(),
350-
): TimezoneLabel => ({
351-
id: timezoneId,
352-
title: getTimezoneTitle(timezoneId, date),
353-
});
354-
355335
const getTimeZones = (
356336
date = new Date(),
357-
): TimezoneData[] => timeZoneList.value.map((timezoneId) => ({
337+
timeZones = timeZoneList.value,
338+
): TimezoneData[] => timeZones.map((timezoneId) => ({
358339
id: timezoneId,
359340
title: getTimezoneTitle(timezoneId, date),
360341
offset: calculateTimezoneByValue(timezoneId, date),
361342
}));
362343

344+
const GET_TIMEZONES_BATCH_SIZE = 10;
345+
let timeZoneDataCache: TimezoneLabel[] = [];
346+
let timeZoneDataCachePromise: Promise<TimezoneLabel[]> | undefined;
347+
const cacheTimeZones = async (
348+
date = new Date(),
349+
): Promise<TimezoneLabel[]> => {
350+
if (timeZoneDataCachePromise) {
351+
return timeZoneDataCachePromise;
352+
}
353+
354+
timeZoneDataCachePromise = macroTaskArray.map(
355+
timeZoneList.value,
356+
(timezoneId) => ({
357+
id: timezoneId,
358+
title: getTimezoneTitle(timezoneId, date),
359+
}),
360+
GET_TIMEZONES_BATCH_SIZE,
361+
);
362+
timeZoneDataCache = await timeZoneDataCachePromise;
363+
364+
return timeZoneDataCache;
365+
};
366+
367+
const getTimeZonesCache = (): TimezoneLabel[] => timeZoneDataCache;
368+
363369
const utils = {
364370
getDaylightOffset,
365371
getDaylightOffsetInMs,
@@ -385,9 +391,9 @@ const utils = {
385391
setOffsetsToDate,
386392
addOffsetsWithoutDST,
387393

388-
getTimeZoneLabelsAsyncBatch,
389-
getTimeZoneLabel,
390394
getTimeZones,
395+
getTimeZonesCache,
396+
cacheTimeZones,
391397
};
392398

393399
export default utils;

packages/devextreme/js/__internal/scheduler/utils/types.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

packages/devextreme/js/common/core/environment.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ export type SchedulerTimeZone = {
9292

9393
/**
9494
* @docid utils.getTimeZones
95-
* @publicName getTimeZones(date)
95+
* @publicName getTimeZones(date, timeZones)
9696
* @param1 date:Date|undefined
97+
* @param2 timeZones:Array<string>|undefined
9798
* @namespace DevExpress.common.core.environment
9899
* @static
99100
* @public
100101
*/
101-
export function getTimeZones(date?: Date): Array<SchedulerTimeZone>;
102+
export function getTimeZones(date?: Date, timeZones?: string[]): Array<SchedulerTimeZone>;

packages/devextreme/ts/dx.all.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,9 +2749,12 @@ declare module DevExpress.common.core.environment {
27492749
*/
27502750
export const devices: DevExpress.core.DevicesObject;
27512751
/**
2752-
* [descr:utils.getTimeZones(date)]
2752+
* [descr:utils.getTimeZones(date, timeZones)]
27532753
*/
2754-
export function getTimeZones(date?: Date): Array<SchedulerTimeZone>;
2754+
export function getTimeZones(
2755+
date?: Date,
2756+
timeZones?: string[]
2757+
): Array<SchedulerTimeZone>;
27552758
/**
27562759
* [descr:hideTopOverlay()]
27572760
*/

0 commit comments

Comments
 (0)