diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index 27ac465ae893..8183c8e231ee 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -11,12 +11,10 @@ import $ from '@js/core/renderer'; import dateUtils from '@js/core/utils/date'; import dateSerialization from '@js/core/utils/date_serialization'; import { extend } from '@js/core/utils/extend'; -import type AbstractStore from '@js/data/abstract_store'; import Form from '@js/ui/form'; import { current, isFluent } from '@js/ui/themes'; import { createAppointmentAdapter } from '../m_appointment_adapter'; -import type { TimezoneLabel } from '../m_utils_time_zone'; import timeZoneUtils from '../m_utils_time_zone'; const SCREEN_SIZE_OF_SINGLE_COLUMN = 600; @@ -41,10 +39,11 @@ const E2E_TEST_CLASSES = { recurrenceSwitch: 'e2e-dx-scheduler-form-recurrence-switch', }; -const DEFAULT_TIMEZONE_EDITOR_DATA_SOURCE_OPTIONS = { +const createTimeZoneDataSource = () => new DataSource({ + store: timeZoneUtils.getTimeZonesCache(), paginate: true, pageSize: 10, -}; +}); const getStylingModeFunc = (): string | undefined => (isFluent(current()) ? 'filled' : undefined); @@ -193,6 +192,7 @@ export class AppointmentForm { valueExpr: 'id', placeholder: noTzTitle, searchEnabled: true, + dataSource: createTimeZoneDataSource(), onValueChanged: (args) => { const { form } = this; const secondTimezoneEditor = form.getEditor(secondTimeZoneExpr); @@ -428,59 +428,6 @@ export class AppointmentForm { editor && this.form.itemOption(editorPath, 'editorOptions', extend({}, editor.editorOptions, options)); } - private scheduleTimezoneEditorDataSourceUpdate( - editorName: string, - dataSource: { store: () => AbstractStore; reload: () => void }, - selectedTimezoneLabel: TimezoneLabel | null, - date: Date, - ): void { - timeZoneUtils.getTimeZoneLabelsAsyncBatch(date) - .catch(() => [] as TimezoneLabel[]) - .then(async (timezones) => { - const store = dataSource.store(); - - await store.remove(selectedTimezoneLabel?.id); - - // NOTE: Unfortunately, our store not support bulk operations - // So, we update it record-by-record - const insertPromises = timezones.reduce[]>((result, timezone) => { - result.push(store.insert(timezone)); - return result; - }, []); - - // NOTE: We should wait for all insertions before reload - await Promise.all(insertPromises); - - dataSource.reload(); - // NOTE: We should re-assign dataSource to the editor - // to repaint this editor after dataSource update - this.setEditorOptions(editorName, 'Main', { dataSource }); - }).catch(() => {}); - } - - private setupTimezoneEditorDataSource( - editorName: string, - selectedTimezoneId: string | null, - date: Date, - ): void { - const selectedTimezoneLabel = selectedTimezoneId - ? timeZoneUtils.getTimeZoneLabel(selectedTimezoneId, date) - : null; - - const dataSource = new DataSource({ - ...DEFAULT_TIMEZONE_EDITOR_DATA_SOURCE_OPTIONS, - store: selectedTimezoneLabel ? [selectedTimezoneLabel] : [], - }); - - this.setEditorOptions(editorName, 'Main', { dataSource }); - this.scheduleTimezoneEditorDataSourceUpdate( - editorName, - dataSource, - selectedTimezoneLabel, - date, - ); - } - updateFormData(formData: Record): void { this.isFormUpdating = true; this.form.option('formData', formData); @@ -489,16 +436,9 @@ export class AppointmentForm { const { expr } = dataAccessors; const rawStartDate = dataAccessors.get('startDate', formData); - const rawEndDate = dataAccessors.get('endDate', formData); - const startDateTimezone = dataAccessors.get('startDateTimeZone', formData) ?? null; - const endDateTimezone = dataAccessors.get('endDateTimeZone', formData) ?? null; const allDay = dataAccessors.get('allDay', formData); const startDate = new Date(rawStartDate); - const endDate = new Date(rawEndDate); - - this.setupTimezoneEditorDataSource(expr.startDateTimeZoneExpr, startDateTimezone, startDate); - this.setupTimezoneEditorDataSource(expr.endDateTimeZoneExpr, endDateTimezone, endDate); this.updateRecurrenceEditorStartDate(startDate, expr.recurrenceRuleExpr); diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 24b7fadec179..e650286bf79c 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -1,5 +1,3 @@ import type { Properties } from '@js/ui/scheduler'; -import type { ArrayElement } from '../utils/types'; - -export type RawViewType = ArrayElement['views']>; +export type RawViewType = Required['views'][number]; diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 03cbd1b01c1d..45f91814a23d 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -66,7 +66,7 @@ import { hide as hideLoading, show as showLoading } from './m_loading'; import { getRecurrenceProcessor } from './m_recurrence'; import subscribes from './m_subscribes'; import { utils } from './m_utils'; -import timeZoneUtils from './m_utils_time_zone'; +import timeZoneUtils, { type TimezoneLabel } from './m_utils_time_zone'; import { SchedulerOptionsValidator, SchedulerOptionsValidatorErrorsHandler } from './options_validator/index'; import { AgendaResourceProcessor } from './resources/m_agenda_resource_processor'; import { @@ -229,6 +229,8 @@ class Scheduler extends Widget { _editAppointmentData: any; + _timeZonesPromise!: Promise; + private _optionsValidator!: SchedulerOptionsValidator; private _optionsValidatorErrorHandler!: SchedulerOptionsValidatorErrorsHandler; @@ -1002,6 +1004,7 @@ class Scheduler extends Widget { } _init() { + this._timeZonesPromise = timeZoneUtils.cacheTimeZones(); this._initExpressions({ startDateExpr: this.option('startDateExpr'), endDateExpr: this.option('endDateExpr'), diff --git a/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.test.ts b/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.test.ts new file mode 100644 index 000000000000..cbca90eea071 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.test.ts @@ -0,0 +1,62 @@ +import { + afterAll, beforeAll, describe, expect, it, jest, +} from '@jest/globals'; +import { macroTaskArray } from '@ts/scheduler/utils/index'; + +import timeZoneUtils from './m_utils_time_zone'; +import timeZoneList from './timezones/timezone_list'; + +const defaultTimeZones = timeZoneList.value; + +describe('timezone utils', () => { + describe('cacheTimeZones / getTimeZonesCache', () => { + beforeAll(() => { + timeZoneList.value = [ + 'Etc/GMT+12', + 'Etc/GMT+11', + ]; + }); + afterAll(() => { + timeZoneList.value = defaultTimeZones; + }); + + it('should cache timezones only once and save into global variable', async () => { + const mock = jest.spyOn(macroTaskArray, 'map'); + + expect(timeZoneUtils.getTimeZonesCache()).toEqual([]); + await timeZoneUtils.cacheTimeZones(); + expect(timeZoneUtils.getTimeZonesCache()).toEqual([ + { id: 'Etc/GMT+12', title: '(GMT -12:00) Etc - GMT+12' }, + { id: 'Etc/GMT+11', title: '(GMT -11:00) Etc - GMT+11' }, + ]); + await timeZoneUtils.cacheTimeZones(); + await timeZoneUtils.cacheTimeZones(); + expect(mock).toHaveBeenCalledTimes(1); + }); + }); + + describe('getTimeZones', () => { + it('should return timezones with offsets of default timezones list', () => { + timeZoneList.value = [ + 'Etc/GMT+12', + 'Etc/GMT+11', + ]; + expect(timeZoneUtils.getTimeZones( + new Date('2025-04-23T10:00:00Z'), + )).toEqual([ + { id: 'Etc/GMT+12', title: '(GMT -12:00) Etc - GMT+12', offset: -12 }, + { id: 'Etc/GMT+11', title: '(GMT -11:00) Etc - GMT+11', offset: -11 }, + ]); + timeZoneList.value = defaultTimeZones; + }); + + it('should return timezones with offsets of custom timezones list', () => { + expect(timeZoneUtils.getTimeZones( + new Date('2025-04-23T10:00:00Z'), + ['Canada/Pacific'], + )).toEqual([ + { id: 'Canada/Pacific', title: '(GMT -07:00) Canada - Pacific', offset: -7 }, + ]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts b/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts index af8d6cf98f8d..a61c87795d2c 100644 --- a/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts +++ b/packages/devextreme/js/__internal/scheduler/m_utils_time_zone.ts @@ -23,7 +23,6 @@ export interface TimezoneData extends TimezoneLabel { const toMs = dateUtils.dateToMilliseconds; const MINUTES_IN_HOUR = 60; const MS_IN_MINUTE = 60000; -const GET_TIMEZONES_BATCH_SIZE = 20; const GMT = 'GMT'; const offsetFormatRegexp = /^GMT(?:[+-]\d{2}:\d{2})?$/; @@ -333,33 +332,40 @@ const addOffsetsWithoutDST = (date: Date, ...offsets: number[]): Date => { : newDate; }; -const getTimeZoneLabelsAsyncBatch = ( - date = new Date(), -): Promise => macroTaskArray.map( - timeZoneList.value, - (timezoneId) => ({ - id: timezoneId, - title: getTimezoneTitle(timezoneId, date), - }), - GET_TIMEZONES_BATCH_SIZE, -); - -const getTimeZoneLabel = ( - timezoneId: string, - date = new Date(), -): TimezoneLabel => ({ - id: timezoneId, - title: getTimezoneTitle(timezoneId, date), -}); - const getTimeZones = ( date = new Date(), -): TimezoneData[] => timeZoneList.value.map((timezoneId) => ({ + timeZones = timeZoneList.value, +): TimezoneData[] => timeZones.map((timezoneId) => ({ id: timezoneId, title: getTimezoneTitle(timezoneId, date), offset: calculateTimezoneByValue(timezoneId, date), })); +const GET_TIMEZONES_BATCH_SIZE = 10; +let timeZoneDataCache: TimezoneLabel[] = []; +let timeZoneDataCachePromise: Promise | undefined; +const cacheTimeZones = async ( + date = new Date(), +): Promise => { + if (timeZoneDataCachePromise) { + return timeZoneDataCachePromise; + } + + timeZoneDataCachePromise = macroTaskArray.map( + timeZoneList.value, + (timezoneId) => ({ + id: timezoneId, + title: getTimezoneTitle(timezoneId, date), + }), + GET_TIMEZONES_BATCH_SIZE, + ); + timeZoneDataCache = await timeZoneDataCachePromise; + + return timeZoneDataCache; +}; + +const getTimeZonesCache = (): TimezoneLabel[] => timeZoneDataCache; + const utils = { getDaylightOffset, getDaylightOffsetInMs, @@ -385,9 +391,9 @@ const utils = { setOffsetsToDate, addOffsetsWithoutDST, - getTimeZoneLabelsAsyncBatch, - getTimeZoneLabel, getTimeZones, + getTimeZonesCache, + cacheTimeZones, }; export default utils; diff --git a/packages/devextreme/js/__internal/scheduler/utils/types.ts b/packages/devextreme/js/__internal/scheduler/utils/types.ts deleted file mode 100644 index d3c82340d973..000000000000 --- a/packages/devextreme/js/__internal/scheduler/utils/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type ArrayElement = - ArrayType extends readonly (infer ElementType)[] ? ElementType : never; diff --git a/packages/devextreme/js/common/core/environment.d.ts b/packages/devextreme/js/common/core/environment.d.ts index f07c872dbcb4..e5307777e4a3 100644 --- a/packages/devextreme/js/common/core/environment.d.ts +++ b/packages/devextreme/js/common/core/environment.d.ts @@ -92,10 +92,11 @@ export type SchedulerTimeZone = { /** * @docid utils.getTimeZones -* @publicName getTimeZones(date) +* @publicName getTimeZones(date, timeZones) * @param1 date:Date|undefined +* @param2 timeZones:Array|undefined * @namespace DevExpress.common.core.environment * @static * @public */ -export function getTimeZones(date?: Date): Array; +export function getTimeZones(date?: Date, timeZones?: string[]): Array; diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index eb7c1998e651..734ea5beadde 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -2652,9 +2652,12 @@ declare module DevExpress.common.core.environment { */ export const devices: DevExpress.core.DevicesObject; /** - * [descr:utils.getTimeZones(date)] + * [descr:utils.getTimeZones(date, timeZones)] */ - export function getTimeZones(date?: Date): Array; + export function getTimeZones( + date?: Date, + timeZones?: string[] + ): Array; /** * [descr:hideTopOverlay()] */