From 6716ca79f805604d50ea046e162ca05ec54ec0dc Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 2 Apr 2026 14:05:32 +0200 Subject: [PATCH 01/48] Global format config. Add tests --- .../localization.intl.tests.js | 79 +++++++++++++++++++ .../dataGrid.tests.js | 65 +++++++++++++++ .../datebox.tests.js | 55 +++++++++++++ .../integration.appointmentTooltip.tests.js | 71 +++++++++++++++++ 4 files changed, 270 insertions(+) diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js index 67bfb491acee..265d9c41a25a 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js @@ -774,6 +774,85 @@ QUnit.module('Intl localization', { }); }); +QUnit.module('Global formatting config (spec, intl)', () => { + QUnit.test('dateTimeFormatPresets overrides explicit shortDate preset', function(assert) { + const originalConfig = config(); + + try { + config({ + ...originalConfig, + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02/01/2020'); + } finally { + config(originalConfig); + } + }); + + QUnit.test('dateTimeFormatPresets resolves locale map by exact locale and default fallback', function(assert) { + const originalConfig = config(); + const oldLocale = locale(); + + try { + config({ + ...originalConfig, + dateTimeFormatPresets: { + shortDate: { + default: 'dd/MM/yyyy', + 'de-DE': 'dd.MM.yyyy', + }, + }, + }); + + locale('de-DE'); + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02.01.2020', 'exact locale'); + + locale('fr-FR'); + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02/01/2020', 'default fallback'); + } finally { + locale(oldLocale); + config(originalConfig); + } + }); + + QUnit.test('dateTimeFormatPresets supports formatter function values', function(assert) { + const originalConfig = config(); + + try { + config({ + ...originalConfig, + dateTimeFormatPresets: { + shortDate: { + default: (date) => `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`, + }, + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '2-1-2020'); + } finally { + config(originalConfig); + } + }); + + QUnit.test('global numberFormat supports formatter function values', function(assert) { + const originalConfig = config(); + + try { + config({ + ...originalConfig, + numberFormat: (value) => `#${value.toFixed(2)}`, + }); + + assert.strictEqual(numberLocalization.format(12.3), '#12.30'); + } finally { + config(originalConfig); + } + }); +}); + QUnit.module('Exceljs format', () => { ExcelJSLocalizationFormatTests.runCurrencyTests([ { value: 'USD', expected: '$#,##0_);\\($#,##0\\)' }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index 701c8d3a919e..e2215f16468c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -5525,3 +5525,68 @@ QUnit.module('Formatting', baseModuleConfig, () => { }), '216 rub'); }); }); + +QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { + QUnit.test('implicit date format uses global dateFormat', function(assert) { + const originalConfig = config(); + + try { + config({ + ...originalConfig, + dateFormat: 'dd/MM/yyyy', + }); + + const dataGrid = createDataGrid({ + dataSource: [{ createdAt: new Date(2020, 0, 2) }], + columns: [{ dataField: 'createdAt', dataType: 'date' }], + }); + + const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); + assert.strictEqual(dateText, '02/01/2020', 'global date format is applied for implicit DataGrid date format'); + } finally { + config(originalConfig); + } + }); + + QUnit.test('implicit datetime format uses global dateTimeFormat', function(assert) { + const originalConfig = config(); + + try { + config({ + ...originalConfig, + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const dataGrid = createDataGrid({ + dataSource: [{ createdAt: new Date(2020, 0, 2, 14, 5) }], + columns: [{ dataField: 'createdAt', dataType: 'datetime' }], + }); + + const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); + assert.strictEqual(dateText, '02/01/2020, 14:05', 'global datetime format is applied for implicit DataGrid datetime format'); + } finally { + config(originalConfig); + } + }); + + QUnit.test('explicit column.format keeps priority over global format', function(assert) { + const originalConfig = config(); + + try { + config({ + ...originalConfig, + dateFormat: 'dd/MM/yyyy', + }); + + const dataGrid = createDataGrid({ + dataSource: [{ createdAt: new Date(2020, 0, 2) }], + columns: [{ dataField: 'createdAt', dataType: 'date', format: 'shortDate' }], + }); + + const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); + assert.strictEqual(dateText, '1/2/2020', 'explicit preset format is not replaced by global dateFormat'); + } finally { + config(originalConfig); + } + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index bf32b07356d7..28f06e7d3286 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3373,3 +3373,58 @@ QUnit.module('validation', { assert.strictEqual(this.$dateBox.hasClass(SHOW_INVALID_BADGE_CLASS), false, 'validation icon is be hidden'); }); }); + +QUnit.module('Global formatting config (spec)', { + beforeEach: function() { + this.originalConfig = config(); + }, + afterEach: function() { + config(this.originalConfig); + }, +}, () => { + QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { + config({ + ...this.originalConfig, + dateFormat: 'dd/MM/yyyy', + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '02/01/2020'); + }); + + QUnit.test('implicit datetime displayFormat uses global dateTimeFormat', function(assert) { + config({ + ...this.originalConfig, + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'datetime', + value: new Date(2020, 0, 2, 14, 5), + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '02/01/2020, 14:05'); + }); + + QUnit.test('explicit displayFormat keeps priority over global dateFormat', function(assert) { + config({ + ...this.originalConfig, + dateFormat: 'dd/MM/yyyy', + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + displayFormat: 'shortDate', + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '1/2/2020'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 77d1006c646a..2ed9f5790108 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import dateSerialization from 'core/utils/date_serialization'; +import config from 'core/config'; import Tooltip from 'ui/tooltip'; import { hide } from '__internal/ui/tooltip/m_tooltip'; import resizeCallbacks from 'core/utils/resize_callbacks'; @@ -35,6 +36,76 @@ const moduleConfig = { } }; +module('Global formatting config (spec): Scheduler tooltip', { + beforeEach() { + fx.off = true; + this.originalConfig = config(); + }, + afterEach() { + fx.off = false; + hide(); + config(this.originalConfig); + } +}, () => { + const createScheduler = (options) => createWrapper($.extend({ + currentView: 'day', + currentDate: new Date(2015, 1, 9), + height: 600, + dataSource: [{ + text: 'Task 1', + startDate: new Date(2015, 1, 9, 11, 0), + endDate: new Date(2015, 1, 9, 12, 0), + }], + }, options)); + + test('implicit Scheduler tooltip time format uses global timeFormat', async function(assert) { + config({ + ...this.originalConfig, + timeFormat: (date) => `T${date.getHours()}`, + }); + + const scheduler = await createScheduler(); + const clock = sinon.useFakeTimers(); + await scheduler.appointments.click(0, clock); + clock.restore(); + + assert.strictEqual(scheduler.tooltip.getDateText(), 'T11 - T12'); + }); + + test('implicit Scheduler tooltip time format uses dateTimeFormatPresets.shortTime when global timeFormat is not set', async function(assert) { + config({ + ...this.originalConfig, + dateTimeFormatPresets: { + shortTime: (date) => `P${date.getHours()}`, + }, + }); + + const scheduler = await createScheduler(); + const clock = sinon.useFakeTimers(); + await scheduler.appointments.click(0, clock); + clock.restore(); + + assert.strictEqual(scheduler.tooltip.getDateText(), 'P11 - P12'); + }); + + test('global timeFormat has priority over dateTimeFormatPresets.shortTime for implicit Scheduler tooltip format', async function(assert) { + config({ + ...this.originalConfig, + timeFormat: (date) => `G${date.getHours()}`, + dateTimeFormatPresets: { + shortTime: (date) => `P${date.getHours()}`, + }, + }); + + const scheduler = await createScheduler(); + const clock = sinon.useFakeTimers(); + await scheduler.appointments.click(0, clock); + clock.restore(); + + assert.strictEqual(scheduler.tooltip.getDateText(), 'G11 - G12'); + }); +}); + module('Integration: Appointment tooltip', moduleConfig, () => { const createScheduler = (options) => createWrapper($.extend(options, { height: 600 })); const getDeltaTz = (schedulerTz, date) => schedulerTz * 3600000 + date.getTimezoneOffset() * 60000; From a434b9d9b8d801432d9abd2de52a31ab2f968a47 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 2 Apr 2026 17:28:05 +0200 Subject: [PATCH 02/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- packages/devextreme/js/__internal/core/m_config.ts | 5 +++++ .../js/__internal/grids/grid_core/m_utils.ts | 5 +++-- .../new/grid_core/columns_controller/utils.ts | 9 +++++++-- .../ui/date_box/m_date_box.strategy.calendar.ts | 4 +++- .../m_date_box.strategy.calendar_with_time.ts | 4 +++- .../ui/date_box/m_date_box.strategy.date_view.ts | 6 +++++- .../ui/date_box/m_date_box.strategy.native.ts | 6 +++++- packages/devextreme/js/common.d.ts | 6 ++++++ packages/devextreme/js/format_helper.js | 14 +++++++++++++- 9 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/devextreme/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 9f556bad406b..19bf01257a21 100644 --- a/packages/devextreme/js/__internal/core/m_config.ts +++ b/packages/devextreme/js/__internal/core/m_config.ts @@ -19,6 +19,11 @@ const config = { useLegacyVisibleIndex: false, versionAssertions: [], copyStylesToShadowDom: true, + dateFormat: undefined, + timeFormat: undefined, + dateTimeFormat: undefined, + numberFormat: undefined, + dateTimeFormatPresets: undefined, floatingActionButtonConfig: { icon: 'add', diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index e61cbd284ab3..500b5666f8a3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -19,6 +19,7 @@ import { getWindow } from '@js/core/utils/window'; import formatHelper from '@js/format_helper'; import LoadPanel from '@js/ui/load_panel'; import sharedFiltering from '@js/ui/shared/filtering'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnPoint } from '@ts/grids/grid_core/m_types'; @@ -389,9 +390,9 @@ export default { getFormatByDataType(dataType) { switch (dataType) { case 'date': - return 'shortDate'; + return getGlobalFormatByDataType('date') || 'shortDate'; case 'datetime': - return 'shortDateShortTime'; + return getGlobalFormatByDataType('datetime') || 'shortDateShortTime'; default: return undefined; } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index 32a1ecfc4fa8..853d28f7a153 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -5,6 +5,7 @@ import { isDefined, isString, type, } from '@js/core/utils/type'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { getTreeNodeByPath, setTreeNodeByPath } from '@ts/grids/new/grid_core/utils/tree/index'; import type { ComponentType } from 'inferno'; @@ -229,8 +230,12 @@ export const getColumnFormat = ( return column.format; } - if (column.dataType === 'date' || column.dataType === 'datetime') { - return 'shortDate'; + if (column.dataType === 'date') { + return (getGlobalFormatByDataType('date') as Format | undefined) || 'shortDate'; + } + + if (column.dataType === 'datetime') { + return (getGlobalFormatByDataType('datetime') as Format | undefined) || 'shortDateShortTime'; } return undefined; diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts index ac4e3e9ae70b..1a59b21f0b56 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts @@ -9,6 +9,7 @@ import type { ClickEvent } from '@js/ui/button'; import type { ValueChangedEvent } from '@js/ui/calendar'; import type { ToolbarItem } from '@js/ui/popup'; import { current, isMaterial } from '@js/ui/themes'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { splitPair } from '@ts/core/utils/m_common'; import Calendar from '@ts/ui/calendar/calendar'; @@ -94,8 +95,9 @@ class CalendarStrategy extends DateBoxStrategy { } getDisplayFormat(displayFormat?: Format): Format { + const globalDateFormat = getGlobalFormatByDataType('date'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || 'shortdate'; + return displayFormat || globalDateFormat || 'shortdate'; } _closeDropDownByEnter(): boolean { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts index 55b1b83f7e5d..54310d0cffc1 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts @@ -7,6 +7,7 @@ import { getWidth } from '@js/core/utils/size'; import { getWindow } from '@js/core/utils/window'; import type { DxEvent } from '@js/events'; import type { Format } from '@js/localization'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { BoxItemData } from '@ts/ui/box'; import Box from '@ts/ui/box'; import TimeView from '@ts/ui/date_box/time_view'; @@ -56,8 +57,9 @@ class CalendarWithTimeStrategy extends CalendarStrategy { } getDisplayFormat(displayFormat?: Format): Format { + const globalDateTimeFormat = getGlobalFormatByDataType('datetime'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || 'shortdateshorttime'; + return displayFormat || globalDateTimeFormat || 'shortdateshorttime'; } _is24HourFormat(): boolean { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts index a240d201e5ff..0bc4d0ebca69 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts @@ -5,6 +5,7 @@ import $ from '@js/core/renderer'; import { inputType } from '@js/core/utils/support'; import { getWindow } from '@js/core/utils/window'; import type { Format } from '@js/localization'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { PopupProperties } from '../popup/m_popup'; import type DateBox from './date_box.base'; @@ -39,9 +40,12 @@ class DateViewStrategy extends DateBoxStrategy { getDisplayFormat(displayFormat: Format): Format { const { type = 'date' } = this.dateBox.option(); + const globalFormat = type === 'date' || type === 'datetime' + ? getGlobalFormatByDataType(type) + : undefined; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || dateUtils.FORMATS_MAP[type]; + return displayFormat || globalFormat || dateUtils.FORMATS_MAP[type]; } popupConfig(config: PopupProperties): PopupProperties { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts index c6a53b4ae64f..11fb6cabe325 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts @@ -5,6 +5,7 @@ import dateSerialization from '@js/core/utils/date_serialization'; import { inputType } from '@js/core/utils/support'; import type { Format } from '@js/localization'; import type { TextBoxType } from '@js/ui/text_box'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { PopupProperties } from '../popup/m_popup'; import type { DateBoxBaseProperties } from './date_box.base'; @@ -72,9 +73,12 @@ class NativeStrategy extends DateBoxStrategy { getDisplayFormat(displayFormat?: Format): Format { const type = this._getDateBoxType(); + const globalFormat = type === 'date' || type === 'datetime' || type === 'datetime-local' + ? getGlobalFormatByDataType(type === 'datetime-local' ? 'datetime' : type) + : undefined; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || dateUtils.FORMATS_MAP[type] as string; + return displayFormat || globalFormat || dateUtils.FORMATS_MAP[type] as string; } renderInputMinMax($input?: dxElementWrapper): void { diff --git a/packages/devextreme/js/common.d.ts b/packages/devextreme/js/common.d.ts index 2ac75ae9b8a8..4580f174ea25 100644 --- a/packages/devextreme/js/common.d.ts +++ b/packages/devextreme/js/common.d.ts @@ -1,4 +1,5 @@ import { PositionConfig } from './common/core/animation'; +import type { Format as LocalizationFormat } from './common/core/localization'; import type { OmitInternal, } from './core'; @@ -284,6 +285,11 @@ export type VersionAssertion = { */ export type GlobalConfig = { versionAssertions?: VersionAssertion[]; + dateFormat?: LocalizationFormat | Record; + timeFormat?: LocalizationFormat | Record; + dateTimeFormat?: LocalizationFormat | Record; + numberFormat?: LocalizationFormat | Record; + dateTimeFormatPresets?: Record>; /** * @docid * @default "." diff --git a/packages/devextreme/js/format_helper.js b/packages/devextreme/js/format_helper.js index c748112c1b69..51fdd4e0dba8 100644 --- a/packages/devextreme/js/format_helper.js +++ b/packages/devextreme/js/format_helper.js @@ -9,6 +9,7 @@ import { import dateUtils from './core/utils/date'; import numberLocalization from './common/core/localization/number'; import dateLocalization from './common/core/localization/date'; +import { getGlobalFormatByDataType } from './__internal/core/m_global_format_config'; import dependencyInjector from './core/utils/dependency_injector'; import './common/core/localization/currency'; @@ -18,7 +19,18 @@ export default dependencyInjector({ const formatIsValid = isString(format) && format !== '' || isPlainObject(format) || isFunction(format); const valueIsValid = isNumeric(value) || (isDate(value) && !isNaN(value.getTime())); - if(!formatIsValid || !valueIsValid) { + if(!valueIsValid) { + return isDefined(value) ? value.toString() : ''; + } + + if(!formatIsValid && isNumeric(value)) { + const globalNumberFormat = getGlobalFormatByDataType('number'); + if(globalNumberFormat) { + return numberLocalization.format(value, globalNumberFormat); + } + } + + if(!formatIsValid) { return isDefined(value) ? value.toString() : ''; } From 722f5ca88909aceffe91cc1dc8ca0069f829a330 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 2 Apr 2026 19:50:44 +0200 Subject: [PATCH 03/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../scheduler/a11y_status/a11y_status_text.ts | 6 +-- .../appointments/appointment/text_utils.ts | 7 ++- .../m_compact_appointments_helper.ts | 4 +- .../js/__internal/scheduler/r1/utils/week.ts | 3 +- .../localization.intl.tests.js | 44 +++---------------- .../integration.appointmentTooltip.tests.js | 29 ++++++------ 6 files changed, 29 insertions(+), 64 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts index a6a2e090ed18..4fa4f666fc6d 100644 --- a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts +++ b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts @@ -1,7 +1,7 @@ -import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import type { ViewType } from '@js/ui/scheduler'; +import { formatImplicitSchedulerDate, formatImplicitSchedulerMonth } from '../utils/global_formats'; import type { NormalizedView } from '../utils/options/types'; const KEYS = { @@ -22,8 +22,8 @@ const viewTypeLocalization: Record = { timelineWorkWeek: 'dxScheduler-switcherTimelineWorkWeek', }; -const localizeMonth = (date: Date): string => String(dateLocalization.format(date, 'monthAndYear')); -const localizeDate = (date: Date): string => `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; +const localizeMonth = (date: Date): string => formatImplicitSchedulerMonth(date); +const localizeDate = (date: Date): string => formatImplicitSchedulerDate(date); const localizeCurrentIndicator = ( date: Date, startDate: Date, diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts index 610a7112ea29..29d867aa2a81 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts @@ -1,12 +1,11 @@ -import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import { isDefined } from '@js/core/utils/type'; +import { formatImplicitSchedulerDate, formatImplicitSchedulerTime } from '../../utils/global_formats'; import type { AppointmentProperties } from './m_types'; -const localizeDate = (date: Date): string => `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; - -const localizeTime = (date: Date): string => `${dateLocalization.format(date, 'shorttime')}`; +const localizeDate = (date: Date): string => formatImplicitSchedulerDate(date); +const localizeTime = (date: Date): string => formatImplicitSchedulerTime(date); const getDate = (options: AppointmentProperties, propName: 'endDate' | 'startDate'): Date => { const result = options.dataAccessors.get(propName, options.data); diff --git a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts index 5948d999ba90..fd2a38162d4b 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -1,5 +1,4 @@ import { locate, move } from '@js/common/core/animation/translator'; -import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import $, { type dxElementWrapper } from '@js/core/renderer'; import { FunctionTemplate } from '@js/core/templates/function_template'; @@ -9,6 +8,7 @@ import type { Appointment } from '@js/ui/scheduler'; import { APPOINTMENT_SETTINGS_KEY, LIST_ITEM_CLASS, LIST_ITEM_DATA_KEY } from './constants'; import type Scheduler from './m_scheduler'; import type { AppointmentTooltipItem, CompactAppointmentOptions, TargetedAppointment } from './types'; +import { formatImplicitSchedulerDate } from './utils/global_formats'; const APPOINTMENT_COLLECTOR_CLASS = 'dx-scheduler-appointment-collector'; const COMPACT_APPOINTMENT_COLLECTOR_CLASS = `${APPOINTMENT_COLLECTOR_CLASS}-compact`; @@ -185,7 +185,7 @@ export class CompactAppointmentsHelper { } private localizeDate(date) { - return `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; + return formatImplicitSchedulerDate(date); } private getDateText( diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts index 240f50a05abe..96601f02c80a 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts @@ -2,6 +2,7 @@ import dateLocalization from '@js/common/core/localization/date'; import dateUtils from '@js/core/utils/date'; import type { CalculateStartViewDate } from '../../types'; +import { formatImplicitSchedulerTime } from '../../utils/global_formats'; import { getCalculatedFirstDayOfWeek, getValidCellDateForLocalTimeFormat, @@ -29,7 +30,7 @@ export const getTimePanelCellText = ( viewOffset, }); - return dateLocalization.format(validTimeDate, 'shorttime') as string; + return formatImplicitSchedulerTime(validTimeDate); }; export const getIntervalDuration = ( diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js index 265d9c41a25a..98f69d00b1ec 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js @@ -775,63 +775,31 @@ QUnit.module('Intl localization', { }); QUnit.module('Global formatting config (spec, intl)', () => { - QUnit.test('dateTimeFormatPresets overrides explicit shortDate preset', function(assert) { + QUnit.test('global dateFormat supports formatter function values', function(assert) { const originalConfig = config(); try { config({ ...originalConfig, - dateTimeFormatPresets: { - shortDate: 'dd/MM/yyyy', - }, - }); - - assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02/01/2020'); - } finally { - config(originalConfig); - } - }); - - QUnit.test('dateTimeFormatPresets resolves locale map by exact locale and default fallback', function(assert) { - const originalConfig = config(); - const oldLocale = locale(); - - try { - config({ - ...originalConfig, - dateTimeFormatPresets: { - shortDate: { - default: 'dd/MM/yyyy', - 'de-DE': 'dd.MM.yyyy', - }, - }, + dateFormat: (date) => `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`, }); - locale('de-DE'); - assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02.01.2020', 'exact locale'); - - locale('fr-FR'); - assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02/01/2020', 'default fallback'); + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), config().dateFormat), '2-1-2020'); } finally { - locale(oldLocale); config(originalConfig); } }); - QUnit.test('dateTimeFormatPresets supports formatter function values', function(assert) { + QUnit.test('global dateTimeFormat supports formatter function values', function(assert) { const originalConfig = config(); try { config({ ...originalConfig, - dateTimeFormatPresets: { - shortDate: { - default: (date) => `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`, - }, - }, + dateTimeFormat: (date) => `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}`, }); - assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '2-1-2020'); + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat), '2/1/2020 14:5'); } finally { config(originalConfig); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 2ed9f5790108..cf5aea7b5c4c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -72,37 +72,34 @@ module('Global formatting config (spec): Scheduler tooltip', { assert.strictEqual(scheduler.tooltip.getDateText(), 'T11 - T12'); }); - test('implicit Scheduler tooltip time format uses dateTimeFormatPresets.shortTime when global timeFormat is not set', async function(assert) { - config({ - ...this.originalConfig, - dateTimeFormatPresets: { - shortTime: (date) => `P${date.getHours()}`, - }, - }); - + test('implicit Scheduler tooltip uses built-in format when global timeFormat is not set', async function(assert) { const scheduler = await createScheduler(); const clock = sinon.useFakeTimers(); await scheduler.appointments.click(0, clock); clock.restore(); - assert.strictEqual(scheduler.tooltip.getDateText(), 'P11 - P12'); + assert.strictEqual(scheduler.tooltip.getDateText(), '11:00 AM - 12:00 PM'); }); - test('global timeFormat has priority over dateTimeFormatPresets.shortTime for implicit Scheduler tooltip format', async function(assert) { + test('implicit Scheduler tooltip date/time use global dateFormat and timeFormat', async function(assert) { config({ ...this.originalConfig, - timeFormat: (date) => `G${date.getHours()}`, - dateTimeFormatPresets: { - shortTime: (date) => `P${date.getHours()}`, - }, + dateFormat: (date) => `D${date.getDate()}`, + timeFormat: (date) => `T${date.getHours()}`, }); - const scheduler = await createScheduler(); + const scheduler = await createScheduler({ + dataSource: [{ + text: 'Task 1', + startDate: new Date(2015, 1, 9, 23, 0), + endDate: new Date(2015, 1, 10, 1, 0), + }], + }); const clock = sinon.useFakeTimers(); await scheduler.appointments.click(0, clock); clock.restore(); - assert.strictEqual(scheduler.tooltip.getDateText(), 'G11 - G12'); + assert.strictEqual(scheduler.tooltip.getDateText(), 'D9, T23 - D10, T1'); }); }); From d1e97aff3b9d35a3c478deaf55115f7e8a7c9462 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 2 Apr 2026 22:19:57 +0200 Subject: [PATCH 04/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../__internal/core/m_global_format_config.js | 62 +++++++++++++++++++ .../scheduler/utils/global_formats.ts | 28 +++++++++ 2 files changed, 90 insertions(+) create mode 100644 packages/devextreme/js/__internal/core/m_global_format_config.js create mode 100644 packages/devextreme/js/__internal/scheduler/utils/global_formats.ts diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.js b/packages/devextreme/js/__internal/core/m_global_format_config.js new file mode 100644 index 000000000000..80a5a709d912 --- /dev/null +++ b/packages/devextreme/js/__internal/core/m_global_format_config.js @@ -0,0 +1,62 @@ +import config from '@js/core/config'; +import { locale as getCurrentLocale } from '@js/common/core/localization/core'; +import parentLocales from '@ts/core/localization/cldr-data/parent_locales'; +import getParentLocale from '@ts/core/localization/parentLocale'; +import { isFunction, isPlainObject, isString } from '@js/core/utils/type'; + +const hasOwn = Object.prototype.hasOwnProperty; + +const resolveByLocaleMap = (localeMap) => { + let currentLocale = getCurrentLocale(); + + while(currentLocale) { + if(hasOwn.call(localeMap, currentLocale) && localeMap[currentLocale] !== undefined) { + return localeMap[currentLocale]; + } + + currentLocale = getParentLocale(parentLocales, currentLocale); + } + + if(hasOwn.call(localeMap, 'default')) { + return localeMap.default; + } + + return undefined; +}; + +const resolveGlobalFormat = (optionName) => { + const optionValue = config()[optionName]; + + if(optionValue === undefined) { + return undefined; + } + + if(isString(optionValue) || isFunction(optionValue)) { + return optionValue; + } + + if(isPlainObject(optionValue)) { + return resolveByLocaleMap(optionValue); + } + + return undefined; +}; + +export const getGlobalFormatByDataType = (dataType) => { + switch(dataType) { + case 'date': + return resolveGlobalFormat('dateFormat'); + case 'datetime': + return resolveGlobalFormat('dateTimeFormat'); + case 'time': + return resolveGlobalFormat('timeFormat'); + case 'number': + return resolveGlobalFormat('numberFormat'); + default: + return undefined; + } +}; + +export default { + getGlobalFormatByDataType, +}; diff --git a/packages/devextreme/js/__internal/scheduler/utils/global_formats.ts b/packages/devextreme/js/__internal/scheduler/utils/global_formats.ts new file mode 100644 index 000000000000..6bd2ae7d8bbc --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/global_formats.ts @@ -0,0 +1,28 @@ +import dateLocalization from '@js/common/core/localization/date'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; + +export const formatImplicitSchedulerDate = (date: Date): string => { + const globalDateFormat = getGlobalFormatByDataType('date'); + + if (globalDateFormat) { + return dateLocalization.format(date, globalDateFormat) as string; + } + + return `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; +}; + +export const formatImplicitSchedulerMonth = (date: Date): string => { + const globalDateFormat = getGlobalFormatByDataType('date'); + + if (globalDateFormat) { + return dateLocalization.format(date, globalDateFormat) as string; + } + + return String(dateLocalization.format(date, 'monthAndYear')); +}; + +export const formatImplicitSchedulerTime = (date: Date): string => { + const globalTimeFormat = getGlobalFormatByDataType('time'); + + return dateLocalization.format(date, globalTimeFormat || 'shorttime') as string; +}; From 2922132b9819b160bc82064cfe7bb558da94aa41 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 2 Apr 2026 23:03:25 +0200 Subject: [PATCH 05/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../devextreme/js/__internal/core/m_global_format_config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.js b/packages/devextreme/js/__internal/core/m_global_format_config.js index 80a5a709d912..de854f58aa58 100644 --- a/packages/devextreme/js/__internal/core/m_global_format_config.js +++ b/packages/devextreme/js/__internal/core/m_global_format_config.js @@ -1,5 +1,5 @@ import config from '@js/core/config'; -import { locale as getCurrentLocale } from '@js/common/core/localization/core'; +import coreLocalization from '@js/common/core/localization/core'; import parentLocales from '@ts/core/localization/cldr-data/parent_locales'; import getParentLocale from '@ts/core/localization/parentLocale'; import { isFunction, isPlainObject, isString } from '@js/core/utils/type'; @@ -7,7 +7,7 @@ import { isFunction, isPlainObject, isString } from '@js/core/utils/type'; const hasOwn = Object.prototype.hasOwnProperty; const resolveByLocaleMap = (localeMap) => { - let currentLocale = getCurrentLocale(); + let currentLocale = coreLocalization.locale(); while(currentLocale) { if(hasOwn.call(localeMap, currentLocale) && localeMap[currentLocale] !== undefined) { From 57dad57f8cbe2eb0e4c14e6cc3f0efa17e8893c8 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 2 Apr 2026 23:47:34 +0200 Subject: [PATCH 06/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../appointments_new/utils/get_date_text.ts | 35 ++++++++++++++----- .../integration.appointmentTooltip.tests.js | 22 +++++++++--- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts index 4e0d893da903..2cca29aacf59 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts @@ -1,8 +1,29 @@ import dateLocalization from '@js/common/core/localization/date'; import dateUtils from '@js/core/utils/date'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { TargetedAppointment, ViewType } from '../../types'; +const formatTooltipDatePart = (date: Date): string => { + const globalFormat = getGlobalFormatByDataType('date'); + + if (globalFormat) { + return dateLocalization.format(date, globalFormat) as string; + } + + return String(dateLocalization.format(date, 'monthandday')); +}; + +const formatTooltipTimePart = (date: Date): string => { + const globalFormat = getGlobalFormatByDataType('time'); + + if (globalFormat) { + return dateLocalization.format(date, globalFormat) as string; + } + + return String(dateLocalization.format(date, 'shorttime')); +}; + export enum DateFormatType { DATETIME = 'DATETIME', TIME = 'TIME', @@ -25,24 +46,22 @@ export const getDateFormatType = ( }; export const getDateText = (startDate: Date, endDate: Date, formatType: DateFormatType): string => { - const dateFormat = 'monthandday'; - const timeFormat = 'shorttime'; const isSameDate = dateUtils.sameDate(startDate, endDate); switch (formatType) { case DateFormatType.DATETIME: return [ - dateLocalization.format(startDate, dateFormat), + formatTooltipDatePart(startDate), ' ', - dateLocalization.format(startDate, timeFormat), + formatTooltipTimePart(startDate), ' - ', - isSameDate ? '' : `${dateLocalization.format(endDate, dateFormat)} `, - dateLocalization.format(endDate, timeFormat), + isSameDate ? '' : `${formatTooltipDatePart(endDate)} `, + formatTooltipTimePart(endDate), ].join(''); case DateFormatType.TIME: - return `${dateLocalization.format(startDate, timeFormat)} - ${dateLocalization.format(endDate, timeFormat)}`; + return `${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}`; case DateFormatType.DATE: - return `${dateLocalization.format(startDate, dateFormat)}${isSameDate ? '' : ` - ${dateLocalization.format(endDate, dateFormat)}`}`; + return `${formatTooltipDatePart(startDate)}${isSameDate ? '' : ` - ${formatTooltipDatePart(endDate)}`}`; default: return ''; } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index cf5aea7b5c4c..862e8710ed23 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -39,12 +39,26 @@ const moduleConfig = { module('Global formatting config (spec): Scheduler tooltip', { beforeEach() { fx.off = true; - this.originalConfig = config(); + const globalConfig = config(); + this.savedGlobalFormats = { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + }; }, afterEach() { fx.off = false; hide(); - config(this.originalConfig); + const globalConfig = config(); + Object.keys(this.savedGlobalFormats).forEach((key) => { + const value = this.savedGlobalFormats[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); } }, () => { const createScheduler = (options) => createWrapper($.extend({ @@ -60,7 +74,7 @@ module('Global formatting config (spec): Scheduler tooltip', { test('implicit Scheduler tooltip time format uses global timeFormat', async function(assert) { config({ - ...this.originalConfig, + ...config(), timeFormat: (date) => `T${date.getHours()}`, }); @@ -83,7 +97,7 @@ module('Global formatting config (spec): Scheduler tooltip', { test('implicit Scheduler tooltip date/time use global dateFormat and timeFormat', async function(assert) { config({ - ...this.originalConfig, + ...config(), dateFormat: (date) => `D${date.getDate()}`, timeFormat: (date) => `T${date.getHours()}`, }); From 026930e948439c4534c0c7f3fffa24b43f2526e8 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 00:45:54 +0200 Subject: [PATCH 07/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../js/__internal/core/localization/number.ts | 7 ++++ .../localization.intl.tests.js | 41 +++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/devextreme/js/__internal/core/localization/number.ts b/packages/devextreme/js/__internal/core/localization/number.ts index 2b4dd0d7f6ca..7aad251c4f39 100644 --- a/packages/devextreme/js/__internal/core/localization/number.ts +++ b/packages/devextreme/js/__internal/core/localization/number.ts @@ -6,6 +6,7 @@ import currencyLocalization from '@ts/core/localization/currency'; import intlNumberLocalization from '@ts/core/localization/intl/number'; import { getFormatter } from '@ts/core/localization/ldml/number'; import { toFixed } from '@ts/core/localization/utils'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { escapeRegExp } from '@ts/core/utils/m_common'; import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector'; import { each } from '@ts/core/utils/m_iterator'; @@ -321,6 +322,12 @@ const numberLocalization = dependencyInjector({ return value; } + const globalNumberFormat = getGlobalFormatByDataType('number'); + + if (!format && globalNumberFormat) { + format = globalNumberFormat as LocalizationFormat; + } + // @ts-expect-error format = format?.formatter || format; diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js index 98f69d00b1ec..b4543cc11a42 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js @@ -775,48 +775,71 @@ QUnit.module('Intl localization', { }); QUnit.module('Global formatting config (spec, intl)', () => { + const saveGlobalFormats = () => { + const globalConfig = config(); + + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + }; + }; + const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + }; + QUnit.test('global dateFormat supports formatter function values', function(assert) { - const originalConfig = config(); + const saved = saveGlobalFormats(); try { config({ - ...originalConfig, + ...config(), dateFormat: (date) => `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`, }); assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), config().dateFormat), '2-1-2020'); } finally { - config(originalConfig); + restoreGlobalFormats(saved); } }); QUnit.test('global dateTimeFormat supports formatter function values', function(assert) { - const originalConfig = config(); + const saved = saveGlobalFormats(); try { config({ - ...originalConfig, + ...config(), dateTimeFormat: (date) => `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}`, }); assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat), '2/1/2020 14:5'); } finally { - config(originalConfig); + restoreGlobalFormats(saved); } }); QUnit.test('global numberFormat supports formatter function values', function(assert) { - const originalConfig = config(); + const saved = saveGlobalFormats(); try { config({ - ...originalConfig, + ...config(), numberFormat: (value) => `#${value.toFixed(2)}`, }); assert.strictEqual(numberLocalization.format(12.3), '#12.30'); } finally { - config(originalConfig); + restoreGlobalFormats(saved); } }); }); From bd86b8aef0f6d57cb8aad3044634f4c551c0042d Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 02:01:34 +0200 Subject: [PATCH 08/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../js/__internal/core/localization/intl/number.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/devextreme/js/__internal/core/localization/intl/number.ts b/packages/devextreme/js/__internal/core/localization/intl/number.ts index 19aa32beaea6..7b103817ad00 100644 --- a/packages/devextreme/js/__internal/core/localization/intl/number.ts +++ b/packages/devextreme/js/__internal/core/localization/intl/number.ts @@ -3,6 +3,7 @@ import accountingFormats from '@ts/core/localization/cldr-data/accounting_format import localizationCoreUtils from '@ts/core/localization/core'; import type { FormatConfig as BaseFormatConfig, LocalizationFormat } from '@ts/core/localization/number'; import openXmlCurrencyFormat from '@ts/core/localization/open_xml_currency_format'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; interface CurrencySymbolInfo { position: 'before' | 'after'; @@ -106,6 +107,13 @@ export default { return value; } + const globalNumberFormat = getGlobalFormatByDataType('number'); + + if (!format && globalNumberFormat) { + // eslint-disable-next-line no-param-reassign + format = globalNumberFormat as LocalizationFormat; + } + // eslint-disable-next-line no-param-reassign format = this._normalizeFormat(format) as FormatConfig; From bd3bcbd23b6862fbbf5a0d17d3a5a3f4b71188fe Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 03:30:49 +0200 Subject: [PATCH 09/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../integration.appointmentTooltip.tests.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 862e8710ed23..226c3f413ee5 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -98,8 +98,8 @@ module('Global formatting config (spec): Scheduler tooltip', { test('implicit Scheduler tooltip date/time use global dateFormat and timeFormat', async function(assert) { config({ ...config(), - dateFormat: (date) => `D${date.getDate()}`, - timeFormat: (date) => `T${date.getHours()}`, + dateFormat: (date) => `Date${date.getDate()}`, + timeFormat: (date) => `Time${date.getHours()}`, }); const scheduler = await createScheduler({ @@ -113,7 +113,7 @@ module('Global formatting config (spec): Scheduler tooltip', { await scheduler.appointments.click(0, clock); clock.restore(); - assert.strictEqual(scheduler.tooltip.getDateText(), 'D9, T23 - D10, T1'); + assert.strictEqual(scheduler.tooltip.getDateText(), 'Date9 Time23 - Date10 Time1'); }); }); From 77d63d22e2a655ba0ab216e408787fe0a5fb00d4 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 10:18:13 +0200 Subject: [PATCH 10/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../appointments_new/utils/get_date_text.ts | 11 ++--- .../dataGrid.tests.js | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts index 2cca29aacf59..00f42d025920 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts @@ -50,14 +50,9 @@ export const getDateText = (startDate: Date, endDate: Date, formatType: DateForm switch (formatType) { case DateFormatType.DATETIME: - return [ - formatTooltipDatePart(startDate), - ' ', - formatTooltipTimePart(startDate), - ' - ', - isSameDate ? '' : `${formatTooltipDatePart(endDate)} `, - formatTooltipTimePart(endDate), - ].join(''); + return isSameDate + ? `${formatTooltipDatePart(startDate)} ${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}` + : `${formatTooltipDatePart(startDate)} ${formatTooltipTimePart(startDate)} - ${formatTooltipDatePart(endDate)} ${formatTooltipTimePart(endDate)}`; case DateFormatType.TIME: return `${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}`; case DateFormatType.DATE: diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index e2215f16468c..d09ae5e2bb18 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -5527,12 +5527,35 @@ QUnit.module('Formatting', baseModuleConfig, () => { }); QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { + const saveGlobalFormats = () => { + const globalConfig = config(); + + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + }; + }; + const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + }; + QUnit.test('implicit date format uses global dateFormat', function(assert) { - const originalConfig = config(); + const saved = saveGlobalFormats(); try { config({ - ...originalConfig, + ...config(), dateFormat: 'dd/MM/yyyy', }); @@ -5544,16 +5567,16 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); assert.strictEqual(dateText, '02/01/2020', 'global date format is applied for implicit DataGrid date format'); } finally { - config(originalConfig); + restoreGlobalFormats(saved); } }); QUnit.test('implicit datetime format uses global dateTimeFormat', function(assert) { - const originalConfig = config(); + const saved = saveGlobalFormats(); try { config({ - ...originalConfig, + ...config(), dateTimeFormat: 'dd/MM/yyyy, HH:mm', }); @@ -5565,16 +5588,16 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); assert.strictEqual(dateText, '02/01/2020, 14:05', 'global datetime format is applied for implicit DataGrid datetime format'); } finally { - config(originalConfig); + restoreGlobalFormats(saved); } }); QUnit.test('explicit column.format keeps priority over global format', function(assert) { - const originalConfig = config(); + const saved = saveGlobalFormats(); try { config({ - ...originalConfig, + ...config(), dateFormat: 'dd/MM/yyyy', }); @@ -5586,7 +5609,7 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); assert.strictEqual(dateText, '1/2/2020', 'explicit preset format is not replaced by global dateFormat'); } finally { - config(originalConfig); + restoreGlobalFormats(saved); } }); }); From dbd30642be37118714dff9ff159a2c7bfd346356 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 11:09:09 +0200 Subject: [PATCH 11/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../js/__internal/grids/grid_core/m_utils.ts | 17 ++++++++++++-- .../new/grid_core/columns_controller/utils.ts | 23 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 500b5666f8a3..cd1423304801 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -1,6 +1,7 @@ // @ts-check import eventsEngine from '@js/common/core/events/core/events_engine'; +import dateLocalization from '@js/common/core/localization/date'; import DataSource from '@js/common/data/data_source'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import { normalizeSortingInfo as normalizeSortingInfoUtility } from '@js/common/data/utils'; @@ -84,6 +85,18 @@ function isDateType(dataType) { return dataType === 'date' || dataType === 'datetime'; } +const getGlobalFormat = (dataType) => { + const globalFormat = getGlobalFormatByDataType(dataType); + + if (!globalFormat) { + return undefined; + } + + return isString(globalFormat) + ? (value) => dateLocalization.format(value, globalFormat) + : globalFormat; +}; + const setEmptyText = function ($container) { $container.get(0).textContent = '\u00A0'; }; @@ -390,9 +403,9 @@ export default { getFormatByDataType(dataType) { switch (dataType) { case 'date': - return getGlobalFormatByDataType('date') || 'shortDate'; + return getGlobalFormat('date') || 'shortDate'; case 'datetime': - return getGlobalFormatByDataType('datetime') || 'shortDateShortTime'; + return getGlobalFormat('datetime') || 'shortDateShortTime'; default: return undefined; } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index 853d28f7a153..8d93fedaab79 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -1,4 +1,5 @@ import type { DataType, Format } from '@js/common'; +import dateLocalization from '@js/common/core/localization/date'; import { compileGetter, getPathParts } from '@js/core/utils/data'; import { captionize } from '@js/core/utils/inflector'; import { @@ -223,6 +224,24 @@ export const getValueDataType = ( : dataType as DataType; }; +const getGlobalFormat = ( + dataType: 'date' | 'datetime', +): Format | undefined => { + const globalFormat = getGlobalFormatByDataType(dataType); + + if (!globalFormat) { + return undefined; + } + + if (isString(globalFormat)) { + return ( + (value: Date) => dateLocalization.format(value, globalFormat) as string + ) as unknown as Format; + } + + return globalFormat as Format; +}; + export const getColumnFormat = ( column: Partial>, ): Format | undefined => { @@ -231,11 +250,11 @@ export const getColumnFormat = ( } if (column.dataType === 'date') { - return (getGlobalFormatByDataType('date') as Format | undefined) || 'shortDate'; + return getGlobalFormat('date') || 'shortDate'; } if (column.dataType === 'datetime') { - return (getGlobalFormatByDataType('datetime') as Format | undefined) || 'shortDateShortTime'; + return getGlobalFormat('datetime') || 'shortDateShortTime'; } return undefined; From 73904b5e4b5fb77ef11cb160198f6c289a5b0261 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 11:30:16 +0200 Subject: [PATCH 12/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../devextreme/js/__internal/grids/grid_core/m_utils.ts | 5 ++++- .../grids/new/grid_core/columns_controller/utils.ts | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index cd1423304801..c822bfd1eb72 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -93,7 +93,10 @@ const getGlobalFormat = (dataType) => { } return isString(globalFormat) - ? (value) => dateLocalization.format(value, globalFormat) + ? (value) => { + const dateValue = value instanceof Date ? value : new Date(value); + return isNaN(dateValue.getTime()) ? '' : dateLocalization.format(dateValue, globalFormat); + } : globalFormat; }; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index 8d93fedaab79..d2108c00605c 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -235,7 +235,13 @@ const getGlobalFormat = ( if (isString(globalFormat)) { return ( - (value: Date) => dateLocalization.format(value, globalFormat) as string + (value: Date | string | number) => { + const dateValue = value instanceof Date ? value : new Date(value); + + return isNaN(dateValue.getTime()) + ? '' + : dateLocalization.format(dateValue, globalFormat) as string; + } ) as unknown as Format; } From a58d9ee8e91e721238755f86f52bdb7d6eccc060 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 3 Apr 2026 12:21:51 +0200 Subject: [PATCH 13/48] Global format config. Implementation for dateFormat, dateTimeFormat, NumberFormat --- .../dataGrid.tests.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index d09ae5e2bb18..bf1515fb97c3 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -5559,12 +5559,9 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { dateFormat: 'dd/MM/yyyy', }); - const dataGrid = createDataGrid({ - dataSource: [{ createdAt: new Date(2020, 0, 2) }], - columns: [{ dataField: 'createdAt', dataType: 'date' }], - }); + const format = gridCore.getFormatByDataType('date'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2), { format }); - const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); assert.strictEqual(dateText, '02/01/2020', 'global date format is applied for implicit DataGrid date format'); } finally { restoreGlobalFormats(saved); @@ -5580,12 +5577,9 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { dateTimeFormat: 'dd/MM/yyyy, HH:mm', }); - const dataGrid = createDataGrid({ - dataSource: [{ createdAt: new Date(2020, 0, 2, 14, 5) }], - columns: [{ dataField: 'createdAt', dataType: 'datetime' }], - }); + const format = gridCore.getFormatByDataType('datetime'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2, 14, 5), { format }); - const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); assert.strictEqual(dateText, '02/01/2020, 14:05', 'global datetime format is applied for implicit DataGrid datetime format'); } finally { restoreGlobalFormats(saved); @@ -5605,6 +5599,7 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { dataSource: [{ createdAt: new Date(2020, 0, 2) }], columns: [{ dataField: 'createdAt', dataType: 'date', format: 'shortDate' }], }); + this.clock.tick(10); const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); assert.strictEqual(dateText, '1/2/2020', 'explicit preset format is not replaced by global dateFormat'); From b5bcacb150f96c4412c974b11974a7a379efd4e3 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Thu, 16 Apr 2026 16:34:25 +0400 Subject: [PATCH 14/48] add dateTimePreset, tests and more minor changes --- .../localization/date.global_formats.test.ts | 271 ++++++++++++++++++ .../js/__internal/core/localization/date.ts | 33 +++ .../core/localization/globalize/date.ts | 18 ++ .../__internal/core/localization/intl/date.ts | 14 + .../__internal/core/m_global_format_config.js | 39 ++- .../core/m_global_format_config.test.ts | 206 +++++++++++++ .../localization.intl.tests.js | 105 +++++++ .../dataGrid.tests.js | 68 +++++ .../datebox.tests.js | 79 ++++- .../integration.appointmentTooltip.tests.js | 1 + 10 files changed, 821 insertions(+), 13 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts create mode 100644 packages/devextreme/js/__internal/core/m_global_format_config.test.ts diff --git a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts new file mode 100644 index 000000000000..be41e0b9ad98 --- /dev/null +++ b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts @@ -0,0 +1,271 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import dateLocalization from '@js/common/core/localization/date'; +import config from '@js/core/config'; + +const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +const saveAndRestore = (): { save: () => void; restore: () => void } => { + let savedValues: Record = {}; + + return { + save() { + const currentConfig = config(); + + savedValues = {}; + GLOBAL_FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }, + restore() { + const currentConfig = config(); + + GLOBAL_FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + currentConfig[key] = savedValues[key]; + } + }); + }, + }; +}; + +describe('date localization - dateTimeFormatPresets', () => { + const { save, restore } = saveAndRestore(); + + beforeEach(() => { save(); }); + afterEach(() => { restore(); }); + + describe('string preset override', () => { + it('should override shortDate with custom LDML pattern', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + expect(result).toBe('02/01/2020'); + }); + + it('should override shortTime with custom LDML pattern', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortTime: 'HH:mm:ss', + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5, 30), 'shortTime'); + + expect(result).toBe('14:05:30'); + }); + + it('should override longDate with custom LDML pattern', () => { + config({ + ...config(), + dateTimeFormatPresets: { + longDate: 'dd MMMM yyyy', + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'longDate'); + + expect(result).toBe('02 January 2020'); + }); + + it('should override shortDateShortTime with custom LDML pattern', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDateShortTime: 'dd/MM/yyyy HH:mm', + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortDateShortTime'); + + expect(result).toBe('02/01/2020 14:05'); + }); + }); + + describe('function preset override', () => { + it('should use function override for shortDate', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: (d: Date) => `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`, + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + expect(result).toBe('2-1-2020'); + }); + + it('should use function override for shortTime', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortTime: (d: Date) => `${d.getHours()}h${String(d.getMinutes()).padStart(2, '0')}`, + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), 'shortTime'); + + expect(result).toBe('14h05'); + }); + }); + + describe('case insensitivity', () => { + it('should apply override regardless of case in format name', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const date = new Date(2020, 0, 2); + + expect(dateLocalization.format(date, 'shortdate')).toBe('02/01/2020'); + expect(dateLocalization.format(date, 'SHORTDATE')).toBe('02/01/2020'); + expect(dateLocalization.format(date, 'ShortDate')).toBe('02/01/2020'); + }); + }); + + describe('locale map in preset', () => { + it('should resolve preset with default locale', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: { + default: 'dd/MM/yyyy', + 'de-DE': 'dd.MM.yyyy', + }, + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + expect(result).toBe('02/01/2020'); + }); + }); + + describe('no override', () => { + it('should use built-in format when no preset override is configured', () => { + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + // Built-in Intl format for en locale + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + + it('should leave non-preset string formats unaffected', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'yyyy-MM-dd'); + + // LDML pattern should be used directly, not affected by preset overrides + expect(result).toBe('2020-01-02'); + }); + + it('should leave FormatObject formats unaffected', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`; + const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter }); + + expect(result).toBe('custom:2020'); + }); + + it('should not affect formatting when dateTimeFormatPresets is empty', () => { + config({ + ...config(), + dateTimeFormatPresets: {}, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + }); + + describe('unknown preset key', () => { + it('should safely ignore unknown preset keys', () => { + config({ + ...config(), + dateTimeFormatPresets: { + unknownFormat: 'dd/MM/yyyy', + }, + }); + + // Known presets should still work normally + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + }); + + describe('preset override aliases another preset', () => { + it('should support aliasing one preset to another', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'longDate', + }, + }); + + const dateLong = dateLocalization.format(new Date(2020, 0, 2), 'longDate'); + const dateShort = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + + // shortDate should now format like longDate + expect(dateShort).toBe(dateLong); + }); + }); +}); + +describe('date localization - global *Format precedence', () => { + const { save, restore } = saveAndRestore(); + + beforeEach(() => { save(); }); + afterEach(() => { restore(); }); + + it('should apply dateFormat for direct calls with the resolved format', () => { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), config().dateFormat); + + expect(result).toBe('02/01/2020'); + }); + + it('should apply dateTimeFormat for direct calls with the resolved format', () => { + config({ + ...config(), + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const result = dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat); + + expect(result).toBe('02/01/2020, 14:05'); + }); +}); diff --git a/packages/devextreme/js/__internal/core/localization/date.ts b/packages/devextreme/js/__internal/core/localization/date.ts index 076a0c56eb3b..1cecdf3b7818 100644 --- a/packages/devextreme/js/__internal/core/localization/date.ts +++ b/packages/devextreme/js/__internal/core/localization/date.ts @@ -9,6 +9,7 @@ import { getFormatter as getLDMLDateFormatter } from '@ts/core/localization/ldml import { getParser as getLDMLDateParser } from '@ts/core/localization/ldml/date.parser'; import numberLocalization from '@ts/core/localization/number'; import errors from '@ts/core/m_errors'; +import { resolvePresetOverride } from '@ts/core/m_global_format_config'; import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector'; import { each } from '@ts/core/utils/m_iterator'; import { isString } from '@ts/core/utils/m_type'; @@ -67,6 +68,31 @@ const dateLocalization = dependencyInjector({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this._getPatternByFormat(pattern) || pattern; }, + _resolveStringFormat( + format: string, + date: Date, + ): string | undefined { + const presetOverride = resolvePresetOverride(format); + + if (presetOverride === undefined) { + return undefined; + } + if (typeof presetOverride === 'function') { + return (presetOverride as DateFormatter)(date); + } + if (isString(presetOverride)) { + const pattern = FORMATS_TO_PATTERN_MAP[ + (presetOverride as string).toLowerCase() + ] || presetOverride as string; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return numberLocalization.convertDigits( + getLDMLDateFormatter(pattern, this)(date), + ); + } + + return undefined; + }, formatUsesMonthName(format: string): boolean { return this._expandPattern(format).indexOf('MMMM') !== -1; }, @@ -139,6 +165,13 @@ const dateLocalization = dependencyInjector({ // eslint-disable-next-line no-param-reassign format = (format as FormatObject).type ?? format; if (isString(format)) { + const resolvedFormat = this._resolveStringFormat(format as string, date); + + if (resolvedFormat !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return resolvedFormat; + } + // eslint-disable-next-line no-param-reassign format = (FORMATS_TO_PATTERN_MAP[(format as string).toLowerCase()] || format) as string; diff --git a/packages/devextreme/js/__internal/core/localization/globalize/date.ts b/packages/devextreme/js/__internal/core/localization/globalize/date.ts index 055d5506f5df..4fb977a4df58 100644 --- a/packages/devextreme/js/__internal/core/localization/globalize/date.ts +++ b/packages/devextreme/js/__internal/core/localization/globalize/date.ts @@ -6,6 +6,7 @@ import 'globalize/date'; import type { Format as LocalizationFormat, FormatObject } from '@js/localization'; import type { DateFormatter, DateParser, Format } from '@ts/core/localization/date'; import dateLocalization from '@ts/core/localization/date'; +import { resolvePresetOverride } from '@ts/core/m_global_format_config'; import * as iteratorUtils from '@ts/core/utils/m_iterator'; import { isObject } from '@ts/core/utils/m_type'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -186,6 +187,23 @@ if (Globalize?.formatDate) { format = (format as FormatObject).type ?? format; if (typeof format === 'string') { + const presetOverride = resolvePresetOverride(format); + + if (presetOverride !== undefined) { + if (typeof presetOverride === 'function') { + return (presetOverride as DateFormatter)(date); + } + if (typeof presetOverride === 'string') { + // eslint-disable-next-line no-param-reassign + format = presetOverride; + } else if (isObject(presetOverride) && this._isAcceptableFormat(presetOverride)) { + formatter = Globalize.dateFormatter(presetOverride); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.removeRtlMarks(formatter(date)); + } + } + formatCacheKey = `${Globalize.locale().locale}:${format}`; formatter = formattersCache[formatCacheKey]; if (!formatter) { diff --git a/packages/devextreme/js/__internal/core/localization/intl/date.ts b/packages/devextreme/js/__internal/core/localization/intl/date.ts index 7d48287c3659..537ea3ba7989 100644 --- a/packages/devextreme/js/__internal/core/localization/intl/date.ts +++ b/packages/devextreme/js/__internal/core/localization/intl/date.ts @@ -2,6 +2,7 @@ import type { Format as LocalizationFormat, FormatObject } from '@js/localization'; import localizationCoreUtils from '@ts/core/localization/core'; import type { DateFormatter, Format } from '@ts/core/localization/date'; +import { resolvePresetOverride } from '@ts/core/m_global_format_config'; import { extend } from '@ts/core/utils/m_extend'; interface DateArgs { @@ -237,6 +238,19 @@ export default { // eslint-disable-next-line no-param-reassign format = (format as FormatObject).type ?? format; } + + if (typeof format === 'string') { + const presetOverride = resolvePresetOverride(format); + + if (presetOverride !== undefined) { + if (typeof presetOverride === 'function') { + return (presetOverride as DateFormatter)(date); + } + // eslint-disable-next-line no-param-reassign + format = presetOverride as LocalizationFormat; + } + } + const intlFormat = getIntlFormat(format); if (intlFormat) { diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.js b/packages/devextreme/js/__internal/core/m_global_format_config.js index de854f58aa58..1f8d66de4482 100644 --- a/packages/devextreme/js/__internal/core/m_global_format_config.js +++ b/packages/devextreme/js/__internal/core/m_global_format_config.js @@ -24,24 +24,27 @@ const resolveByLocaleMap = (localeMap) => { return undefined; }; -const resolveGlobalFormat = (optionName) => { - const optionValue = config()[optionName]; - - if(optionValue === undefined) { +const resolveConfigValue = (value) => { + if(value === undefined) { return undefined; } - if(isString(optionValue) || isFunction(optionValue)) { - return optionValue; + if(isString(value) || isFunction(value)) { + return value; } - if(isPlainObject(optionValue)) { - return resolveByLocaleMap(optionValue); + if(isPlainObject(value)) { + return resolveByLocaleMap(value); } return undefined; }; +const resolveGlobalFormat = (optionName) => { + const optionValue = config()[optionName]; + return resolveConfigValue(optionValue); +}; + export const getGlobalFormatByDataType = (dataType) => { switch(dataType) { case 'date': @@ -57,6 +60,26 @@ export const getGlobalFormatByDataType = (dataType) => { } }; +export const resolvePresetOverride = (presetName) => { + const presets = config().dateTimeFormatPresets; + + if(!presets || !isPlainObject(presets)) { + return undefined; + } + + const lowerName = presetName.toLowerCase(); + const keys = Object.keys(presets); + + for(let i = 0; i < keys.length; i++) { + if(keys[i].toLowerCase() === lowerName) { + return resolveConfigValue(presets[keys[i]]); + } + } + + return undefined; +}; + export default { getGlobalFormatByDataType, + resolvePresetOverride, }; diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts new file mode 100644 index 000000000000..463a05879b4f --- /dev/null +++ b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts @@ -0,0 +1,206 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import config from '@js/core/config'; + +import { getGlobalFormatByDataType, resolvePresetOverride } from './m_global_format_config'; + +const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +describe('m_global_format_config', () => { + let savedValues: Record; + + beforeEach(() => { + const currentConfig = config(); + + savedValues = {}; + GLOBAL_FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + + GLOBAL_FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + currentConfig[key] = savedValues[key]; + } + }); + }); + + describe('getGlobalFormatByDataType', () => { + it('should return undefined when no global formats configured', () => { + expect(getGlobalFormatByDataType('date')).toBeUndefined(); + expect(getGlobalFormatByDataType('time')).toBeUndefined(); + expect(getGlobalFormatByDataType('datetime')).toBeUndefined(); + expect(getGlobalFormatByDataType('number')).toBeUndefined(); + }); + + it('should return undefined for unknown dataType', () => { + expect(getGlobalFormatByDataType('boolean')).toBeUndefined(); + expect(getGlobalFormatByDataType('')).toBeUndefined(); + }); + + it('should resolve string dateFormat', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + expect(getGlobalFormatByDataType('date')).toBe('dd/MM/yyyy'); + }); + + it('should resolve string timeFormat', () => { + config({ ...config(), timeFormat: 'HH:mm:ss' }); + + expect(getGlobalFormatByDataType('time')).toBe('HH:mm:ss'); + }); + + it('should resolve string dateTimeFormat', () => { + config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); + + expect(getGlobalFormatByDataType('datetime')).toBe('dd/MM/yyyy HH:mm'); + }); + + it('should resolve function dateFormat', () => { + const formatter = (d: Date): string => d.toISOString(); + + config({ ...config(), dateFormat: formatter }); + + expect(getGlobalFormatByDataType('date')).toBe(formatter); + }); + + it('should resolve function numberFormat', () => { + const formatter = (n: number): string => n.toFixed(2); + + config({ ...config(), numberFormat: formatter }); + + expect(getGlobalFormatByDataType('number')).toBe(formatter); + }); + + it('should resolve locale map with default key', () => { + config({ + ...config(), + dateFormat: { + default: 'yyyy-MM-dd', + 'de-DE': 'dd.MM.yyyy', + }, + }); + + // Default locale is 'en', not in map → uses 'default' + expect(getGlobalFormatByDataType('date')).toBe('yyyy-MM-dd'); + }); + + it('should coexist: dateFormat and numberFormat set together', () => { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + numberFormat: '#,##0.00', + }); + + expect(getGlobalFormatByDataType('date')).toBe('dd/MM/yyyy'); + expect(getGlobalFormatByDataType('number')).toBe('#,##0.00'); + expect(getGlobalFormatByDataType('time')).toBeUndefined(); + }); + }); + + describe('resolvePresetOverride', () => { + it('should return undefined when dateTimeFormatPresets is not configured', () => { + expect(resolvePresetOverride('shortDate')).toBeUndefined(); + }); + + it('should return undefined for unknown preset name', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + expect(resolvePresetOverride('unknownPreset')).toBeUndefined(); + }); + + it('should resolve string preset override', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + expect(resolvePresetOverride('shortDate')).toBe('dd/MM/yyyy'); + }); + + it('should resolve function preset override', () => { + const fn = (d: Date): string => d.toISOString(); + + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: fn, + }, + }); + + expect(resolvePresetOverride('shortDate')).toBe(fn); + }); + + it('should do case-insensitive lookup', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + expect(resolvePresetOverride('SHORTDATE')).toBe('dd/MM/yyyy'); + expect(resolvePresetOverride('shortdate')).toBe('dd/MM/yyyy'); + expect(resolvePresetOverride('ShortDate')).toBe('dd/MM/yyyy'); + }); + + it('should resolve locale map preset with default key', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: { + default: 'dd/MM/yyyy', + 'de-DE': 'dd.MM.yyyy', + }, + }, + }); + + // Default locale is 'en', not in map → uses 'default' + expect(resolvePresetOverride('shortDate')).toBe('dd/MM/yyyy'); + }); + + it('should resolve locale map preset with function value', () => { + const fn = (d: Date): string => `${d.getDate()}/${d.getMonth() + 1}`; + + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: { + default: fn, + }, + }, + }); + + expect(resolvePresetOverride('shortDate')).toBe(fn); + }); + + it('should handle multiple preset overrides', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + longDate: 'EEEE, dd MMMM yyyy', + shortTime: 'HH:mm', + }, + }); + + expect(resolvePresetOverride('shortDate')).toBe('dd/MM/yyyy'); + expect(resolvePresetOverride('longDate')).toBe('EEEE, dd MMMM yyyy'); + expect(resolvePresetOverride('shortTime')).toBe('HH:mm'); + }); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js index b4543cc11a42..b78eaa959015 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js @@ -783,6 +783,7 @@ QUnit.module('Global formatting config (spec, intl)', () => { timeFormat: globalConfig.timeFormat, dateTimeFormat: globalConfig.dateTimeFormat, numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, }; }; const restoreGlobalFormats = (saved) => { @@ -842,6 +843,110 @@ QUnit.module('Global formatting config (spec, intl)', () => { restoreGlobalFormats(saved); } }); + + QUnit.test('dateTimeFormatPresets overrides shortDate with LDML pattern', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '02/01/2020'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateTimeFormatPresets overrides shortTime with LDML pattern', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortTime: 'HH:mm:ss', + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2, 14, 5, 30), 'shortTime'), '14:05:30'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateTimeFormatPresets supports function override', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: (d) => `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`, + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortDate'), '2-1-2020'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateTimeFormatPresets case-insensitive lookup', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'shortdate'), '02/01/2020', 'lowercase works'); + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'SHORTDATE'), '02/01/2020', 'uppercase works'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateTimeFormatPresets does not affect non-preset format strings', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), 'yyyy-MM-dd'), '2020-01-02'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('unknown preset key does not break formatting', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + unknownFormat: 'dd/MM/yyyy', + }, + }); + + const result = dateLocalization.format(new Date(2020, 0, 2), 'shortDate'); + assert.ok(result, 'shortDate still formats with unknown preset configured'); + } finally { + restoreGlobalFormats(saved); + } + }); }); QUnit.module('Exceljs format', () => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index 5b61933ce8bd..1b71ac6ce598 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -5532,6 +5532,7 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { timeFormat: globalConfig.timeFormat, dateTimeFormat: globalConfig.dateTimeFormat, numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, }; }; const restoreGlobalFormats = (saved) => { @@ -5604,4 +5605,71 @@ QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { restoreGlobalFormats(saved); } }); + + QUnit.test('implicit date column uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const format = gridCore.getFormatByDataType('date'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2), { format }); + + // Implicit path: no dateFormat → falls to 'shortDate' → preset override applies + assert.strictEqual(dateText, '02/01/2020', 'dateTimeFormatPresets.shortDate is used for implicit date column'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('dateFormat takes priority over dateTimeFormatPresets.shortDate for implicit columns', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateFormat: 'yyyy-MM-dd', + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const format = gridCore.getFormatByDataType('date'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2), { format }); + + // Implicit path: dateFormat is set → wins over dateTimeFormatPresets + assert.strictEqual(dateText, '2020-01-02', 'dateFormat takes priority over dateTimeFormatPresets for implicit date columns'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('explicit column.format: "shortDate" uses dateTimeFormatPresets.shortDate', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const dataGrid = createDataGrid({ + dataSource: [{ createdAt: new Date(2020, 0, 2) }], + columns: [{ dataField: 'createdAt', dataType: 'date', format: 'shortDate' }], + }); + this.clock.tick(10); + + const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); + assert.strictEqual(dateText, '02/01/2020', 'explicit shortDate preset uses dateTimeFormatPresets override'); + } finally { + restoreGlobalFormats(saved); + } + }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 28f06e7d3286..4262ece0cc9c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3376,15 +3376,30 @@ QUnit.module('validation', { QUnit.module('Global formatting config (spec)', { beforeEach: function() { - this.originalConfig = config(); + const globalConfig = config(); + this.savedGlobalFormats = { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; }, afterEach: function() { - config(this.originalConfig); + const globalConfig = config(); + Object.keys(this.savedGlobalFormats).forEach((key) => { + const value = this.savedGlobalFormats[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); }, }, () => { QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { config({ - ...this.originalConfig, + ...config(), dateFormat: 'dd/MM/yyyy', }); @@ -3399,7 +3414,7 @@ QUnit.module('Global formatting config (spec)', { QUnit.test('implicit datetime displayFormat uses global dateTimeFormat', function(assert) { config({ - ...this.originalConfig, + ...config(), dateTimeFormat: 'dd/MM/yyyy, HH:mm', }); @@ -3414,7 +3429,7 @@ QUnit.module('Global formatting config (spec)', { QUnit.test('explicit displayFormat keeps priority over global dateFormat', function(assert) { config({ - ...this.originalConfig, + ...config(), dateFormat: 'dd/MM/yyyy', }); @@ -3427,4 +3442,58 @@ QUnit.module('Global formatting config (spec)', { assert.strictEqual(instance.option('text'), '1/2/2020'); }); + + QUnit.test('implicit DateBox uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '02/01/2020'); + }); + + QUnit.test('dateFormat takes priority over dateTimeFormatPresets.shortDate for implicit DateBox', function(assert) { + config({ + ...config(), + dateFormat: 'yyyy-MM-dd', + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }).dxDateBox('instance'); + + // dateFormat wins over dateTimeFormatPresets for implicit case + assert.strictEqual(instance.option('text'), '2020-01-02'); + }); + + QUnit.test('explicit displayFormat: "shortDate" uses dateTimeFormatPresets override', function(assert) { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + displayFormat: 'shortDate', + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '02/01/2020'); + }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 4d8efba7357e..400d0551e538 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -45,6 +45,7 @@ module('Global formatting config (spec): Scheduler tooltip', { timeFormat: globalConfig.timeFormat, dateTimeFormat: globalConfig.dateTimeFormat, numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, }; }, afterEach() { From 7c9709dc85bafecec2cf80bbd78b3356df00a94d Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 17 Apr 2026 20:03:11 +0200 Subject: [PATCH 15/48] regenerate dx.all.d.ts --- packages/devextreme/ts/dx.all.d.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index d93a1011acd0..d3c7d3a754fd 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -1359,6 +1359,23 @@ declare module DevExpress.common { */ export type GlobalConfig = { versionAssertions?: VersionAssertion[]; + dateFormat?: + | DevExpress.common.core.localization.Format + | Record; + timeFormat?: + | DevExpress.common.core.localization.Format + | Record; + dateTimeFormat?: + | DevExpress.common.core.localization.Format + | Record; + numberFormat?: + | DevExpress.common.core.localization.Format + | Record; + dateTimeFormatPresets?: Record< + string, + | DevExpress.common.core.localization.Format + | Record + >; /** * [descr:GlobalConfig.decimalSeparator] * @deprecated [depNote:GlobalConfig.decimalSeparator] From 42c8bb1f45704700cffc2b5e3d97a5d898e55308 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 20 Apr 2026 13:01:14 +0400 Subject: [PATCH 16/48] date/time formatting priority fixes --- .../js/__internal/filter_builder/m_utils.ts | 4 +++- .../grids/pivot_grid/m_widget_utils.ts | 4 +++- .../devextreme/js/__internal/ui/chat/chat.ts | 5 +++-- .../ui/date_box/m_date_box.strategy.list.ts | 4 +++- .../js/__internal/ui/gantt/ui.gantt.dialogs.ts | 5 ++++- .../js/__internal/ui/gantt/ui.gantt.view.ts | 14 ++++++++++---- .../ui/number_box/m_number_box.base.ts | 17 ++++++++++++++++- 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/devextreme/js/__internal/filter_builder/m_utils.ts b/packages/devextreme/js/__internal/filter_builder/m_utils.ts index 0a09c98fb812..ba3715bb9d1d 100644 --- a/packages/devextreme/js/__internal/filter_builder/m_utils.ts +++ b/packages/devextreme/js/__internal/filter_builder/m_utils.ts @@ -15,6 +15,7 @@ import formatHelper from '@js/format_helper'; import type { CustomOperation, DataType, Field } from '@js/ui/filter_builder'; import filterUtils from '@js/ui/shared/filtering'; import errors from '@js/ui/widget/ui.errors'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { getConfig } from './m_between'; import filterOperationsDictionary from './m_filter_operations_dictionary'; @@ -62,7 +63,8 @@ const FILTER_BUILDER_ITEM_TEXT_SEPARATOR_CLASS = `${FILTER_BUILDER_ITEM_TEXT_CLA const FILTER_BUILDER_ITEM_TEXT_SEPARATOR_EMPTY_CLASS = `${FILTER_BUILDER_ITEM_TEXT_SEPARATOR_CLASS}-empty`; function getDateFormat(dataType: DataType | undefined): Format | undefined { - return dataType ? DEFAULT_FORMAT[dataType] : undefined; + if (!dataType) return undefined; + return getGlobalFormatByDataType(dataType) ?? DEFAULT_FORMAT[dataType]; } function getFormattedValueText(field: Field, value: FieldValue): string { diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts index 452aec2c1085..be05e64f0363 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts @@ -10,6 +10,7 @@ import { extend } from '@js/core/utils/extend'; import { each, map } from '@js/core/utils/iterator'; import { isDefined, isNumeric, type } from '@js/core/utils/type'; import formatHelper from '@js/format_helper'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { CLASSES } from './const'; @@ -297,7 +298,8 @@ const DATE_INTERVAL_FORMATS = { function setDefaultFieldValueFormatting(field) { if (field.dataType === 'date') { if (!field.format) { - setFieldProperty(field, 'format', DATE_INTERVAL_FORMATS[field.groupInterval]); + const dateIntervalFormat = DATE_INTERVAL_FORMATS[field.groupInterval]; + setFieldProperty(field, 'format', dateIntervalFormat ?? getGlobalFormatByDataType('date')); } } else if (field.dataType === 'number') { const groupInterval = isNumeric(field.groupInterval) diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 81549c6a5dae..2f07b49a91c1 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -23,6 +23,7 @@ import type { TypingEndEvent, TypingStartEvent, } from '@js/ui/chat'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { invokeConditionally } from '@ts/core/utils/conditional_invoke'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; @@ -92,7 +93,7 @@ class Chat extends Widget { activeStateEnabled: true, alerts: [], dataSource: null, - dayHeaderFormat: 'shortdate', + dayHeaderFormat: getGlobalFormatByDataType('date') ?? 'shortdate', editing: { allowUpdating: false, allowDeleting: false, @@ -104,7 +105,7 @@ class Chat extends Widget { inputFieldText: '', items: [], messageTemplate: null, - messageTimestampFormat: 'shorttime', + messageTimestampFormat: getGlobalFormatByDataType('time') ?? 'shorttime', reloadOnChange: true, showAvatar: true, showDayHeaders: true, diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts index 645bb8e1bd84..a3d0f327d88e 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts @@ -10,6 +10,7 @@ import { getWindow } from '@js/core/utils/window'; import type { DxEvent } from '@js/events'; import type { Format } from '@js/localization'; import type { ItemClickEvent } from '@js/ui/list'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { getSizeValue } from '@ts/ui/drop_down_editor/m_utils'; import List from '@ts/ui/list/list.edit.search'; @@ -59,8 +60,9 @@ class ListStrategy extends DateBoxStrategy { } getDisplayFormat(displayFormat?: Format): Format { + const globalTimeFormat = getGlobalFormatByDataType('time'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || 'shorttime'; + return displayFormat || globalTimeFormat || 'shorttime'; } popupConfig(popupConfig: PopupProperties): PopupProperties { diff --git a/packages/devextreme/js/__internal/ui/gantt/ui.gantt.dialogs.ts b/packages/devextreme/js/__internal/ui/gantt/ui.gantt.dialogs.ts index 10ffb0d472f2..86c1d78ad68b 100644 --- a/packages/devextreme/js/__internal/ui/gantt/ui.gantt.dialogs.ts +++ b/packages/devextreme/js/__internal/ui/gantt/ui.gantt.dialogs.ts @@ -6,6 +6,7 @@ import '@ts/ui/list/modules/deleting'; import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import Form from '@ts/ui/form/form'; import Popup from '@ts/ui/popup/m_popup'; @@ -335,7 +336,9 @@ class TaskEditDialogInfo extends DialogInfoBase { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type _getFormattedDateText(date) { - return date ? dateLocalization.format(date, 'shortDateShortTime') : ''; + if (!date) return ''; + const globalFormat = getGlobalFormatByDataType('datetime'); + return dateLocalization.format(date, globalFormat ?? 'shortDateShortTime'); } _isReadOnlyField(field): boolean { diff --git a/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts b/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts index dfcfc2999332..54a0e9c74b23 100644 --- a/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts +++ b/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts @@ -9,6 +9,7 @@ import messageLocalization from '@js/common/core/localization/message'; import $ from '@js/core/renderer'; import { format } from '@js/core/utils/string'; import { isDefined } from '@js/core/utils/type'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { WidgetProperties } from '@ts/core/widget/widget'; import Widget from '@ts/core/widget/widget'; import { getGanttViewCore } from '@ts/ui/gantt/gantt_importer'; @@ -565,10 +566,15 @@ export class GanttView extends Widget { getFormattedDateText(date): string { let result = ''; if (date) { - const datePart = dateLocalization.format(date, 'shortDate'); - const timeFormat = this._hasAmPM() ? 'hh:mm a' : 'HH:mm'; - const timePart = dateLocalization.format(date, timeFormat); - result = `${datePart} ${timePart}`; + const globalDateTimeFormat = getGlobalFormatByDataType('datetime'); + if (globalDateTimeFormat) { + result = dateLocalization.format(date, globalDateTimeFormat); + } else { + const datePart = dateLocalization.format(date, 'shortDate'); + const timeFormat = this._hasAmPM() ? 'hh:mm a' : 'HH:mm'; + const timePart = dateLocalization.format(date, timeFormat); + result = `${datePart} ${timePart}`; + } } return result; } diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts index 79e8af617c95..32d06ea1a27d 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.base.ts @@ -3,6 +3,7 @@ import { addNamespace, getChar, isCommandKeyPressed, normalizeKeyName, } from '@js/common/core/events/utils/index'; import messageLocalization from '@js/common/core/localization/message'; +import numberLocalization from '@js/common/core/localization/number'; import devices from '@js/core/devices'; import domAdapter from '@js/core/dom_adapter'; import type { DefaultOptionsRule } from '@js/core/options/utils'; @@ -17,6 +18,7 @@ import { import { Deferred } from '@js/core/utils/deferred'; import { fitIntoRange, inRange } from '@js/core/utils/math'; import { isDefined } from '@js/core/utils/type'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import TextEditor from '@ts/ui/text_box/m_text_editor'; import type { TextEditorBaseProperties } from '../text_box/m_text_editor.base'; @@ -218,12 +220,25 @@ class NumberBoxBase< _forceValueRender(): void { const value = this.option('value'); const number = Number(value); - const formattedValue = isNaN(number) ? '' : this._applyDisplayValueFormatter(value); + const formattedValue = isNaN(number) + ? '' + : this._applyDisplayValueFormatter(value); this._renderDisplayText(formattedValue); } _applyDisplayValueFormatter(value): string | undefined { + if (!this.option('format')) { + const globalNumberFormat = getGlobalFormatByDataType('number'); + + if (globalNumberFormat) { + return numberLocalization.format( + Number(value), + globalNumberFormat, + ) as string; + } + } + const { displayValueFormatter } = this.option(); return displayValueFormatter?.(value); From b9c194b1a7208a63f36d74aeaa278880cd1efa7e Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 20 Apr 2026 13:05:02 +0400 Subject: [PATCH 17/48] fix type error --- packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts b/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts index 54a0e9c74b23..1cf95283b5e5 100644 --- a/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts +++ b/packages/devextreme/js/__internal/ui/gantt/ui.gantt.view.ts @@ -568,7 +568,7 @@ export class GanttView extends Widget { if (date) { const globalDateTimeFormat = getGlobalFormatByDataType('datetime'); if (globalDateTimeFormat) { - result = dateLocalization.format(date, globalDateTimeFormat); + result = String(dateLocalization.format(date, globalDateTimeFormat) ?? ''); } else { const datePart = dateLocalization.format(date, 'shortDate'); const timeFormat = this._hasAmPM() ? 'hh:mm a' : 'HH:mm'; From fa0259c4a3a9196645b34fc1c9b8cf7e2fe3cf96 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Mon, 20 Apr 2026 16:13:02 +0400 Subject: [PATCH 18/48] small fix and add tests --- .../localization/date.global_formats.test.ts | 8 +- .../js/__internal/core/m_config.test.ts | 127 ++++++++++++++ .../devextreme/js/__internal/core/m_config.ts | 6 + .../core/m_global_format_config.test.ts | 3 +- .../__tests__/utils.global_format.test.ts | 111 ++++++++++++ .../m_widget_utils.global_format.test.ts | 110 ++++++++++++ .../ui/chat/chat.global_format.test.ts | 104 ++++++++++++ ...te_box.list_strategy.global_format.test.ts | 116 +++++++++++++ .../ui/gantt/gantt.global_format.test.ts | 122 ++++++++++++++ .../number_box.global_format.test.ts | 159 ++++++++++++++++++ 10 files changed, 863 insertions(+), 3 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/m_config.test.ts create mode 100644 packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts create mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts create mode 100644 packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts create mode 100644 packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts create mode 100644 packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts create mode 100644 packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts diff --git a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts index be41e0b9ad98..d508e312cdc4 100644 --- a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts +++ b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts @@ -26,7 +26,8 @@ const saveAndRestore = (): { save: () => void; restore: () => void } => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete currentConfig[key]; } else { - currentConfig[key] = savedValues[key]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; } }); }, @@ -187,7 +188,10 @@ describe('date localization - dateTimeFormatPresets', () => { }, }); - const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`; + const customFormatter = (value: number | Date): string => { + const d = value instanceof Date ? value : new Date(value); + return `custom:${d.getFullYear()}`; + }; const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter }); expect(result).toBe('custom:2020'); diff --git a/packages/devextreme/js/__internal/core/m_config.test.ts b/packages/devextreme/js/__internal/core/m_config.test.ts new file mode 100644 index 000000000000..bc29707ad70b --- /dev/null +++ b/packages/devextreme/js/__internal/core/m_config.test.ts @@ -0,0 +1,127 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import config from '@js/core/config'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +describe('config() - clearing format properties with undefined', () => { + let savedValues: Record; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + }); + }); + + it('should clear dateFormat when set to undefined', () => { + config({ dateFormat: 'dd/MM/yyyy' }); + expect(config().dateFormat).toBe('dd/MM/yyyy'); + + config({ dateFormat: undefined }); + expect(config().dateFormat).toBeUndefined(); + }); + + it('should clear timeFormat when set to undefined', () => { + config({ timeFormat: 'HH:mm:ss' }); + expect(config().timeFormat).toBe('HH:mm:ss'); + + config({ timeFormat: undefined }); + expect(config().timeFormat).toBeUndefined(); + }); + + it('should clear dateTimeFormat when set to undefined', () => { + config({ dateTimeFormat: 'dd/MM/yyyy HH:mm' }); + expect(config().dateTimeFormat).toBe('dd/MM/yyyy HH:mm'); + + config({ dateTimeFormat: undefined }); + expect(config().dateTimeFormat).toBeUndefined(); + }); + + it('should clear numberFormat when set to undefined', () => { + config({ numberFormat: '#,##0.00' }); + expect(config().numberFormat).toBe('#,##0.00'); + + config({ numberFormat: undefined }); + expect(config().numberFormat).toBeUndefined(); + }); + + it('should clear dateTimeFormatPresets when set to undefined', () => { + config({ dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy' } }); + expect(config().dateTimeFormatPresets).toEqual({ shortDate: 'dd/MM/yyyy' }); + + config({ dateTimeFormatPresets: undefined }); + expect(config().dateTimeFormatPresets).toBeUndefined(); + }); + + it('should clear all format keys at once', () => { + config({ + dateFormat: 'dd/MM/yyyy', + timeFormat: 'HH:mm', + dateTimeFormat: 'dd/MM/yyyy HH:mm', + numberFormat: '#,##0.00', + dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy' }, + }); + + config({ + dateFormat: undefined, + timeFormat: undefined, + dateTimeFormat: undefined, + numberFormat: undefined, + dateTimeFormatPresets: undefined, + }); + + expect(config().dateFormat).toBeUndefined(); + expect(config().timeFormat).toBeUndefined(); + expect(config().dateTimeFormat).toBeUndefined(); + expect(config().numberFormat).toBeUndefined(); + expect(config().dateTimeFormatPresets).toBeUndefined(); + }); + + it('should not affect non-format properties when clearing format keys', () => { + const originalRtl = config().rtlEnabled; + + config({ + dateFormat: 'dd/MM/yyyy', + rtlEnabled: true, + }); + + config({ + dateFormat: undefined, + }); + + expect(config().dateFormat).toBeUndefined(); + expect(config().rtlEnabled).toBe(true); + + // Restore + config({ rtlEnabled: originalRtl }); + }); + + it('should allow re-setting a format after clearing', () => { + config({ dateFormat: 'dd/MM/yyyy' }); + config({ dateFormat: undefined }); + config({ dateFormat: 'yyyy-MM-dd' }); + + expect(config().dateFormat).toBe('yyyy-MM-dd'); + }); + + it('should not clear a format key if it is not in the newConfig object', () => { + config({ dateFormat: 'dd/MM/yyyy', timeFormat: 'HH:mm' }); + + // Only clear dateFormat, timeFormat should remain + config({ dateFormat: undefined }); + + expect(config().dateFormat).toBeUndefined(); + expect(config().timeFormat).toBe('HH:mm'); + }); +}); diff --git a/packages/devextreme/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 40bf2d115c0f..550a8a41840d 100644 --- a/packages/devextreme/js/__internal/core/m_config.ts +++ b/packages/devextreme/js/__internal/core/m_config.ts @@ -83,6 +83,12 @@ const configMethod = (...args) => { }); extend(config, newConfig); + const formatKeys = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + for (const key of formatKeys) { + if (key in newConfig && newConfig[key] === undefined) { + config[key] = undefined; + } + } }; // @ts-expect-error typescript cant see global diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts index 463a05879b4f..aaa015a084d4 100644 --- a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts +++ b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts @@ -27,7 +27,8 @@ describe('m_global_format_config', () => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete currentConfig[key]; } else { - currentConfig[key] = savedValues[key]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; } }); }); diff --git a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts new file mode 100644 index 000000000000..be5c57dd1678 --- /dev/null +++ b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts @@ -0,0 +1,111 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import config from '@js/core/config'; +import type { Field } from '@js/ui/filter_builder'; + +import { getCurrentValueText } from '../m_utils'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +describe('FilterBuilder - global format integration', () => { + let savedValues: Record = {}; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + } + }); + }); + + describe('date fields with global dateFormat', () => { + it('should use global dateFormat when field has no explicit format', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Field = { dataType: 'date' }; + const value = new Date(2025, 0, 15); + + expect(getCurrentValueText(field, value, null)).toBe('15/01/2025'); + }); + + it('should prefer explicit field.format over global dateFormat', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Field = { dataType: 'date', format: 'yyyy-MM-dd' }; + const value = new Date(2025, 0, 15); + + expect(getCurrentValueText(field, value, null)).toBe('2025-01-15'); + }); + + it('should fall back to shortDate when no global dateFormat and no field format', () => { + const field: Field = { dataType: 'date' }; + const value = new Date(2025, 0, 15); + + const result = getCurrentValueText(field, value, null); + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + }); + + describe('datetime fields with global dateTimeFormat', () => { + it('should use global dateTimeFormat when field has no explicit format', () => { + config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); + + const field: Field = { dataType: 'datetime' }; + const value = new Date(2025, 0, 15, 14, 30); + + expect(getCurrentValueText(field, value, null)).toBe('15/01/2025 14:30'); + }); + + it('should prefer explicit field.format over global dateTimeFormat', () => { + config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); + + const field: Field = { dataType: 'datetime', format: 'yyyy-MM-dd' }; + const value = new Date(2025, 0, 15, 14, 30); + + expect(getCurrentValueText(field, value, null)).toBe('2025-01-15'); + }); + }); + + describe('number fields', () => { + it('should use built-in shortDate format for date field when no global format set', () => { + const field: Field = { dataType: 'date' }; + const value = new Date(2017, 8, 5, 12, 30, 0); + + // This is the existing behavior — should keep working + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017'); + }); + }); + + describe('dateTimeFormatPresets interaction', () => { + it('should apply preset override to shortDate default format', () => { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd.MM.yyyy', + }, + }); + + const field: Field = { dataType: 'date' }; + const value = new Date(2025, 0, 15); + + // With no global dateFormat set, falls back to DEFAULT_FORMAT['date'] = 'shortDate' + // The preset override should be applied via dateLocalization.format() + expect(getCurrentValueText(field, value, null)).toBe('15.01.2025'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts b/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts new file mode 100644 index 000000000000..4a9553071cb2 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts @@ -0,0 +1,110 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import config from '@js/core/config'; + +import { setDefaultFieldValueFormatting } from '../m_widget_utils'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +describe('PivotGrid - setDefaultFieldValueFormatting global format', () => { + let savedValues: Record = {}; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + } + }); + }); + + describe('date field formatting', () => { + it('should use global dateFormat when no format or groupInterval is set', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Record = { dataType: 'date' }; + setDefaultFieldValueFormatting(field); + + expect(field.format).toBe('dd/MM/yyyy'); + }); + + it('should use DATE_INTERVAL_FORMATS when groupInterval is set (e.g. month)', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Record = { dataType: 'date', groupInterval: 'month' }; + setDefaultFieldValueFormatting(field); + + // groupInterval "month" maps to a function in DATE_INTERVAL_FORMATS + expect(typeof field.format).toBe('function'); + }); + + it('should prefer DATE_INTERVAL_FORMATS over global dateFormat', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Record = { dataType: 'date', groupInterval: 'quarter' }; + setDefaultFieldValueFormatting(field); + + // quarter maps to a DATE_INTERVAL_FORMATS function + expect(typeof field.format).toBe('function'); + }); + + it('should not set format when groupInterval has no matching DATE_INTERVAL_FORMAT and no global dateFormat', () => { + const field: Record = { dataType: 'date', groupInterval: 'year' }; + setDefaultFieldValueFormatting(field); + + // 'year' has no entry in DATE_INTERVAL_FORMATS and no global dateFormat + expect(field.format).toBeUndefined(); + }); + + it('should use global dateFormat when groupInterval has no matching DATE_INTERVAL_FORMAT', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Record = { dataType: 'date', groupInterval: 'year' }; + setDefaultFieldValueFormatting(field); + + expect(field.format).toBe('dd/MM/yyyy'); + }); + + it('should not override explicit field.format', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + const field: Record = { dataType: 'date', format: 'yyyy-MM-dd' }; + setDefaultFieldValueFormatting(field); + + expect(field.format).toBe('yyyy-MM-dd'); + }); + }); + + describe('non-date fields', () => { + it('should not set format for number field without groupInterval', () => { + config({ ...config(), numberFormat: '#,##0.00' }); + + const field: Record = { dataType: 'number' }; + setDefaultFieldValueFormatting(field); + + // Number fields don't get a default format from setDefaultFieldValueFormatting + // (number format is handled via formatHelper.format at display time) + expect(field.format).toBeUndefined(); + }); + + it('should not set format for string field', () => { + const field: Record = { dataType: 'string' }; + setDefaultFieldValueFormatting(field); + + expect(field.format).toBeUndefined(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts b/packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts new file mode 100644 index 000000000000..8836d1302e3a --- /dev/null +++ b/packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts @@ -0,0 +1,104 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import config from '@js/core/config'; + +import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +/** + * Tests verifying that Chat's default format options correctly integrate + * with global format config. Chat._getDefaultOptions() sets: + * dayHeaderFormat: getGlobalFormatByDataType('date') ?? 'shortdate' + * messageTimestampFormat: getGlobalFormatByDataType('time') ?? 'shorttime' + */ +describe('Chat - global format integration', () => { + let savedValues: Record = {}; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + } + }); + }); + + // Replicate the logic from Chat._getDefaultOptions() + function getDayHeaderFormat(): unknown { + return getGlobalFormatByDataType('date') ?? 'shortdate'; + } + + function getMessageTimestampFormat(): unknown { + return getGlobalFormatByDataType('time') ?? 'shorttime'; + } + + describe('dayHeaderFormat default', () => { + it('should default to shortdate when no global dateFormat set', () => { + expect(getDayHeaderFormat()).toBe('shortdate'); + }); + + it('should use global dateFormat when configured', () => { + config({ ...config(), dateFormat: 'dd/MM/yyyy' }); + + expect(getDayHeaderFormat()).toBe('dd/MM/yyyy'); + }); + + it('should resolve locale map dateFormat', () => { + config({ + ...config(), + dateFormat: { + default: 'yyyy-MM-dd', + 'de-DE': 'dd.MM.yyyy', + }, + }); + + // Default locale 'en' should use 'default' key + expect(getDayHeaderFormat()).toBe('yyyy-MM-dd'); + }); + + it('should use function dateFormat', () => { + const formatter = (d: Date): string => d.toISOString(); + config({ ...config(), dateFormat: formatter }); + + expect(getDayHeaderFormat()).toBe(formatter); + }); + }); + + describe('messageTimestampFormat default', () => { + it('should default to shorttime when no global timeFormat set', () => { + expect(getMessageTimestampFormat()).toBe('shorttime'); + }); + + it('should use global timeFormat when configured', () => { + config({ ...config(), timeFormat: 'HH:mm:ss' }); + + expect(getMessageTimestampFormat()).toBe('HH:mm:ss'); + }); + + it('should resolve locale map timeFormat', () => { + config({ + ...config(), + timeFormat: { + default: 'hh:mm a', + 'de-DE': 'HH:mm', + }, + }); + + expect(getMessageTimestampFormat()).toBe('hh:mm a'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts new file mode 100644 index 000000000000..7f094d4168e5 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts @@ -0,0 +1,116 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import config from '@js/core/config'; +import type { Format } from '@js/localization'; + +import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +/** + * Tests verifying that DateBox ListStrategy.getDisplayFormat correctly + * integrates with global timeFormat. The logic: + * getDisplayFormat(displayFormat) { + * const globalTimeFormat = getGlobalFormatByDataType('time'); + * return displayFormat || globalTimeFormat || 'shorttime'; + * } + */ +describe('DateBox ListStrategy - global time format integration', () => { + let savedValues: Record = {}; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + } + }); + }); + + // Replicate ListStrategy.getDisplayFormat logic + function getDisplayFormat(displayFormat?: Format): Format { + const globalTimeFormat = getGlobalFormatByDataType('time'); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return displayFormat || globalTimeFormat || 'shorttime'; + } + + describe('fallback chain', () => { + it('should return shorttime when no displayFormat and no global timeFormat', () => { + expect(getDisplayFormat()).toBe('shorttime'); + }); + + it('should use explicit displayFormat when provided', () => { + config({ ...config(), timeFormat: 'HH:mm:ss' }); + + expect(getDisplayFormat('hh:mm a')).toBe('hh:mm a'); + }); + + it('should use global timeFormat when no explicit displayFormat', () => { + config({ ...config(), timeFormat: 'HH:mm:ss' }); + + expect(getDisplayFormat()).toBe('HH:mm:ss'); + }); + + it('should prefer explicit displayFormat over global timeFormat', () => { + config({ ...config(), timeFormat: 'HH:mm:ss' }); + + expect(getDisplayFormat('shortTime')).toBe('shortTime'); + }); + }); + + describe('global timeFormat types', () => { + it('should use string global timeFormat', () => { + config({ ...config(), timeFormat: 'HH-mm' }); + + expect(getDisplayFormat()).toBe('HH-mm'); + }); + + it('should use function global timeFormat', () => { + const formatter = (d: Date): string => `${d.getHours()}h`; + config({ ...config(), timeFormat: formatter }); + + expect(getDisplayFormat()).toBe(formatter); + }); + + it('should resolve locale map global timeFormat', () => { + config({ + ...config(), + timeFormat: { + default: 'hh:mm a', + 'de-DE': 'HH:mm', + }, + }); + + // Default locale 'en' resolves to 'default' key + expect(getDisplayFormat()).toBe('hh:mm a'); + }); + }); + + describe('edge cases', () => { + it('should use global timeFormat when displayFormat is undefined', () => { + config({ ...config(), timeFormat: 'HH:mm' }); + + expect(getDisplayFormat(undefined)).toBe('HH:mm'); + }); + + it('should fall back to shorttime when displayFormat is empty string', () => { + // Empty string is falsy, so || chain continues + config({ ...config(), timeFormat: 'HH:mm' }); + + expect(getDisplayFormat('' as Format)).toBe('HH:mm'); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts b/packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts new file mode 100644 index 000000000000..d07c06fec6d8 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts @@ -0,0 +1,122 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import dateLocalization from '@js/common/core/localization/date'; +import config from '@js/core/config'; + +import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +/** + * Tests verifying that the Gantt formatting logic correctly integrates + * with global format config. Since Gantt's getFormattedDateText and + * _getFormattedDateText methods are instance methods on widget classes, + * we test the equivalent logic here as a standalone function. + */ +describe('Gantt - global datetime format integration', () => { + let savedValues: Record = {}; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + } + }); + }); + + // Replicate GanttView.getFormattedDateText logic + function getFormattedDateText(date: Date | null): string { + let result = ''; + if (date) { + const globalDateTimeFormat = getGlobalFormatByDataType('datetime'); + if (globalDateTimeFormat) { + result = String(dateLocalization.format(date, globalDateTimeFormat) ?? ''); + } else { + const datePart = dateLocalization.format(date, 'shortDate'); + const timePart = dateLocalization.format(date, 'HH:mm'); + result = `${datePart} ${timePart}`; + } + } + return result; + } + + // Replicate GanttDialogs._getFormattedDateText logic + function getDialogFormattedDateText(date: Date | null): string { + if (!date) return ''; + const globalFormat = getGlobalFormatByDataType('datetime'); + return String(dateLocalization.format(date, globalFormat ?? 'shortDateShortTime') ?? ''); + } + + describe('GanttView.getFormattedDateText equivalent', () => { + it('should use default shortDate + time format when no global config set', () => { + const date = new Date(2025, 0, 15, 14, 30); + const result = getFormattedDateText(date); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result).toContain('14:30'); + }); + + it('should use global dateTimeFormat when configured', () => { + config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); + + const date = new Date(2025, 0, 15, 14, 30); + const result = getFormattedDateText(date); + + expect(result).toBe('15/01/2025 14:30'); + }); + + it('should return empty string for null date', () => { + expect(getFormattedDateText(null)).toBe(''); + }); + + it('should use function dateTimeFormat', () => { + config({ + ...config(), + dateTimeFormat: (d: Date) => `${d.getFullYear()}-custom`, + }); + + const date = new Date(2025, 0, 15, 14, 30); + const result = getFormattedDateText(date); + + expect(result).toBe('2025-custom'); + }); + }); + + describe('GanttDialogs._getFormattedDateText equivalent', () => { + it('should use shortDateShortTime when no global config set', () => { + const date = new Date(2025, 0, 15, 14, 30); + const result = getDialogFormattedDateText(date); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + + it('should use global dateTimeFormat when configured', () => { + config({ ...config(), dateTimeFormat: 'dd.MM.yyyy HH:mm' }); + + const date = new Date(2025, 0, 15, 14, 30); + const result = getDialogFormattedDateText(date); + + expect(result).toBe('15.01.2025 14:30'); + }); + + it('should return empty string for null date', () => { + expect(getDialogFormattedDateText(null)).toBe(''); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts new file mode 100644 index 000000000000..7483bad8d581 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts @@ -0,0 +1,159 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; +import numberLocalization from '@js/common/core/localization/number'; +import config from '@js/core/config'; + +import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; + +const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; + +/** + * Tests verifying that NumberBox._applyDisplayValueFormatter correctly + * integrates with global numberFormat. The logic: + * if (!this.option('format')) { + * const globalNumberFormat = getGlobalFormatByDataType('number'); + * if (globalNumberFormat) return numberLocalization.format(Number(value), globalNumberFormat); + * } + * return displayValueFormatter(value); + */ +describe('NumberBox - global number format integration', () => { + let savedValues: Record = {}; + + beforeEach(() => { + const currentConfig = config(); + savedValues = {}; + FORMAT_KEYS.forEach((key) => { + savedValues[key] = currentConfig[key]; + }); + }); + + afterEach(() => { + const currentConfig = config(); + FORMAT_KEYS.forEach((key) => { + if (savedValues[key] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete currentConfig[key]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (currentConfig as any)[key] = savedValues[key]; + } + }); + }); + + // Replicate NumberBox._applyDisplayValueFormatter logic + function applyDisplayValueFormatter( + value: number | string | null, + format: unknown, + displayValueFormatter?: (v: unknown) => string, + ): string | undefined { + if (!format) { + const globalNumberFormat = getGlobalFormatByDataType('number'); + if (globalNumberFormat) { + return numberLocalization.format( + Number(value), + globalNumberFormat, + ) as string; + } + } + + return displayValueFormatter + ? displayValueFormatter(value) + : String(value ?? ''); + } + + describe('with global numberFormat', () => { + it('should format value using global numberFormat when no explicit format', () => { + config({ ...config(), numberFormat: '#,##0.00' }); + + const result = applyDisplayValueFormatter(1234.5, null); + + // numberLocalization.format with LDML pattern + expect(result).toBe('1,234.50'); + }); + + it('should format value using function global numberFormat', () => { + config({ + ...config(), + numberFormat: (n: number): string => `[${n.toFixed(1)}]`, + }); + + const result = applyDisplayValueFormatter(42.789, null); + + expect(result).toBe('[42.8]'); + }); + + it('should return a formatted string, not the raw value', () => { + config({ + ...config(), + numberFormat: (n: number): string => `formatted:${n}`, + }); + + const result = applyDisplayValueFormatter(100, null); + + expect(result).toBe('formatted:100'); + }); + }); + + describe('explicit format takes precedence', () => { + it('should skip global numberFormat when explicit format is set', () => { + config({ ...config(), numberFormat: '#,##0.00' }); + + // When explicit format is truthy, global format is skipped + const result = applyDisplayValueFormatter(1234.5, 'currency'); + + // Falls through to displayValueFormatter since we have an explicit format + expect(result).toBe('1234.5'); + }); + }); + + describe('without any config', () => { + it('should use displayValueFormatter when no global numberFormat', () => { + const result = applyDisplayValueFormatter( + 1234.5, + null, + (v) => `formatted:${v}`, + ); + + expect(result).toBe('formatted:1234.5'); + }); + + it('should return string of value when no format and no displayValueFormatter', () => { + const result = applyDisplayValueFormatter(42, null); + + expect(result).toBe('42'); + }); + + it('should handle null value', () => { + config({ + ...config(), + numberFormat: (n: number): string => `val:${n}`, + }); + + const result = applyDisplayValueFormatter(null, null); + + // Number(null) === 0 + expect(result).toBe('val:0'); + }); + }); + + describe('locale map numberFormat', () => { + it('should resolve locale map format', () => { + const defaultFn = (n: number): string => `default:${n.toFixed(2)}`; + const deFn = (n: number): string => `de:${n.toFixed(4)}`; + + config({ + ...config(), + numberFormat: { + default: defaultFn, + 'de-DE': deFn, + }, + }); + + const result = applyDisplayValueFormatter(1234.5678, null); + + // Default locale 'en' resolves to 'default' key + expect(result).toBe('default:1234.57'); + }); + }); +}); From e4c55c9d89f156be9c5fe910717e32a1fcb244d2 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 20 Apr 2026 15:39:09 +0200 Subject: [PATCH 19/48] Revert "small fix and add tests" This reverts commit fa0259c4a3a9196645b34fc1c9b8cf7e2fe3cf96. --- .../localization/date.global_formats.test.ts | 8 +- .../js/__internal/core/m_config.test.ts | 127 -------------- .../devextreme/js/__internal/core/m_config.ts | 6 - .../core/m_global_format_config.test.ts | 3 +- .../__tests__/utils.global_format.test.ts | 111 ------------ .../m_widget_utils.global_format.test.ts | 110 ------------ .../ui/chat/chat.global_format.test.ts | 104 ------------ ...te_box.list_strategy.global_format.test.ts | 116 ------------- .../ui/gantt/gantt.global_format.test.ts | 122 -------------- .../number_box.global_format.test.ts | 159 ------------------ 10 files changed, 3 insertions(+), 863 deletions(-) delete mode 100644 packages/devextreme/js/__internal/core/m_config.test.ts delete mode 100644 packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts delete mode 100644 packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts delete mode 100644 packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts delete mode 100644 packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts delete mode 100644 packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts delete mode 100644 packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts diff --git a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts index d508e312cdc4..be41e0b9ad98 100644 --- a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts +++ b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts @@ -26,8 +26,7 @@ const saveAndRestore = (): { save: () => void; restore: () => void } => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete currentConfig[key]; } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; + currentConfig[key] = savedValues[key]; } }); }, @@ -188,10 +187,7 @@ describe('date localization - dateTimeFormatPresets', () => { }, }); - const customFormatter = (value: number | Date): string => { - const d = value instanceof Date ? value : new Date(value); - return `custom:${d.getFullYear()}`; - }; + const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`; const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter }); expect(result).toBe('custom:2020'); diff --git a/packages/devextreme/js/__internal/core/m_config.test.ts b/packages/devextreme/js/__internal/core/m_config.test.ts deleted file mode 100644 index bc29707ad70b..000000000000 --- a/packages/devextreme/js/__internal/core/m_config.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import config from '@js/core/config'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -describe('config() - clearing format properties with undefined', () => { - let savedValues: Record; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - }); - }); - - it('should clear dateFormat when set to undefined', () => { - config({ dateFormat: 'dd/MM/yyyy' }); - expect(config().dateFormat).toBe('dd/MM/yyyy'); - - config({ dateFormat: undefined }); - expect(config().dateFormat).toBeUndefined(); - }); - - it('should clear timeFormat when set to undefined', () => { - config({ timeFormat: 'HH:mm:ss' }); - expect(config().timeFormat).toBe('HH:mm:ss'); - - config({ timeFormat: undefined }); - expect(config().timeFormat).toBeUndefined(); - }); - - it('should clear dateTimeFormat when set to undefined', () => { - config({ dateTimeFormat: 'dd/MM/yyyy HH:mm' }); - expect(config().dateTimeFormat).toBe('dd/MM/yyyy HH:mm'); - - config({ dateTimeFormat: undefined }); - expect(config().dateTimeFormat).toBeUndefined(); - }); - - it('should clear numberFormat when set to undefined', () => { - config({ numberFormat: '#,##0.00' }); - expect(config().numberFormat).toBe('#,##0.00'); - - config({ numberFormat: undefined }); - expect(config().numberFormat).toBeUndefined(); - }); - - it('should clear dateTimeFormatPresets when set to undefined', () => { - config({ dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy' } }); - expect(config().dateTimeFormatPresets).toEqual({ shortDate: 'dd/MM/yyyy' }); - - config({ dateTimeFormatPresets: undefined }); - expect(config().dateTimeFormatPresets).toBeUndefined(); - }); - - it('should clear all format keys at once', () => { - config({ - dateFormat: 'dd/MM/yyyy', - timeFormat: 'HH:mm', - dateTimeFormat: 'dd/MM/yyyy HH:mm', - numberFormat: '#,##0.00', - dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy' }, - }); - - config({ - dateFormat: undefined, - timeFormat: undefined, - dateTimeFormat: undefined, - numberFormat: undefined, - dateTimeFormatPresets: undefined, - }); - - expect(config().dateFormat).toBeUndefined(); - expect(config().timeFormat).toBeUndefined(); - expect(config().dateTimeFormat).toBeUndefined(); - expect(config().numberFormat).toBeUndefined(); - expect(config().dateTimeFormatPresets).toBeUndefined(); - }); - - it('should not affect non-format properties when clearing format keys', () => { - const originalRtl = config().rtlEnabled; - - config({ - dateFormat: 'dd/MM/yyyy', - rtlEnabled: true, - }); - - config({ - dateFormat: undefined, - }); - - expect(config().dateFormat).toBeUndefined(); - expect(config().rtlEnabled).toBe(true); - - // Restore - config({ rtlEnabled: originalRtl }); - }); - - it('should allow re-setting a format after clearing', () => { - config({ dateFormat: 'dd/MM/yyyy' }); - config({ dateFormat: undefined }); - config({ dateFormat: 'yyyy-MM-dd' }); - - expect(config().dateFormat).toBe('yyyy-MM-dd'); - }); - - it('should not clear a format key if it is not in the newConfig object', () => { - config({ dateFormat: 'dd/MM/yyyy', timeFormat: 'HH:mm' }); - - // Only clear dateFormat, timeFormat should remain - config({ dateFormat: undefined }); - - expect(config().dateFormat).toBeUndefined(); - expect(config().timeFormat).toBe('HH:mm'); - }); -}); diff --git a/packages/devextreme/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 550a8a41840d..40bf2d115c0f 100644 --- a/packages/devextreme/js/__internal/core/m_config.ts +++ b/packages/devextreme/js/__internal/core/m_config.ts @@ -83,12 +83,6 @@ const configMethod = (...args) => { }); extend(config, newConfig); - const formatKeys = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - for (const key of formatKeys) { - if (key in newConfig && newConfig[key] === undefined) { - config[key] = undefined; - } - } }; // @ts-expect-error typescript cant see global diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts index aaa015a084d4..463a05879b4f 100644 --- a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts +++ b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts @@ -27,8 +27,7 @@ describe('m_global_format_config', () => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete currentConfig[key]; } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; + currentConfig[key] = savedValues[key]; } }); }); diff --git a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts deleted file mode 100644 index be5c57dd1678..000000000000 --- a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.global_format.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import config from '@js/core/config'; -import type { Field } from '@js/ui/filter_builder'; - -import { getCurrentValueText } from '../m_utils'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -describe('FilterBuilder - global format integration', () => { - let savedValues: Record = {}; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - if (savedValues[key] === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete currentConfig[key]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - } - }); - }); - - describe('date fields with global dateFormat', () => { - it('should use global dateFormat when field has no explicit format', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Field = { dataType: 'date' }; - const value = new Date(2025, 0, 15); - - expect(getCurrentValueText(field, value, null)).toBe('15/01/2025'); - }); - - it('should prefer explicit field.format over global dateFormat', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Field = { dataType: 'date', format: 'yyyy-MM-dd' }; - const value = new Date(2025, 0, 15); - - expect(getCurrentValueText(field, value, null)).toBe('2025-01-15'); - }); - - it('should fall back to shortDate when no global dateFormat and no field format', () => { - const field: Field = { dataType: 'date' }; - const value = new Date(2025, 0, 15); - - const result = getCurrentValueText(field, value, null); - expect(result).toBeTruthy(); - expect(typeof result).toBe('string'); - }); - }); - - describe('datetime fields with global dateTimeFormat', () => { - it('should use global dateTimeFormat when field has no explicit format', () => { - config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); - - const field: Field = { dataType: 'datetime' }; - const value = new Date(2025, 0, 15, 14, 30); - - expect(getCurrentValueText(field, value, null)).toBe('15/01/2025 14:30'); - }); - - it('should prefer explicit field.format over global dateTimeFormat', () => { - config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); - - const field: Field = { dataType: 'datetime', format: 'yyyy-MM-dd' }; - const value = new Date(2025, 0, 15, 14, 30); - - expect(getCurrentValueText(field, value, null)).toBe('2025-01-15'); - }); - }); - - describe('number fields', () => { - it('should use built-in shortDate format for date field when no global format set', () => { - const field: Field = { dataType: 'date' }; - const value = new Date(2017, 8, 5, 12, 30, 0); - - // This is the existing behavior — should keep working - expect(getCurrentValueText(field, value, null)).toBe('9/5/2017'); - }); - }); - - describe('dateTimeFormatPresets interaction', () => { - it('should apply preset override to shortDate default format', () => { - config({ - ...config(), - dateTimeFormatPresets: { - shortDate: 'dd.MM.yyyy', - }, - }); - - const field: Field = { dataType: 'date' }; - const value = new Date(2025, 0, 15); - - // With no global dateFormat set, falls back to DEFAULT_FORMAT['date'] = 'shortDate' - // The preset override should be applied via dateLocalization.format() - expect(getCurrentValueText(field, value, null)).toBe('15.01.2025'); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts b/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts deleted file mode 100644 index 4a9553071cb2..000000000000 --- a/packages/devextreme/js/__internal/grids/pivot_grid/__tests__/m_widget_utils.global_format.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import config from '@js/core/config'; - -import { setDefaultFieldValueFormatting } from '../m_widget_utils'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -describe('PivotGrid - setDefaultFieldValueFormatting global format', () => { - let savedValues: Record = {}; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - if (savedValues[key] === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete currentConfig[key]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - } - }); - }); - - describe('date field formatting', () => { - it('should use global dateFormat when no format or groupInterval is set', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Record = { dataType: 'date' }; - setDefaultFieldValueFormatting(field); - - expect(field.format).toBe('dd/MM/yyyy'); - }); - - it('should use DATE_INTERVAL_FORMATS when groupInterval is set (e.g. month)', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Record = { dataType: 'date', groupInterval: 'month' }; - setDefaultFieldValueFormatting(field); - - // groupInterval "month" maps to a function in DATE_INTERVAL_FORMATS - expect(typeof field.format).toBe('function'); - }); - - it('should prefer DATE_INTERVAL_FORMATS over global dateFormat', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Record = { dataType: 'date', groupInterval: 'quarter' }; - setDefaultFieldValueFormatting(field); - - // quarter maps to a DATE_INTERVAL_FORMATS function - expect(typeof field.format).toBe('function'); - }); - - it('should not set format when groupInterval has no matching DATE_INTERVAL_FORMAT and no global dateFormat', () => { - const field: Record = { dataType: 'date', groupInterval: 'year' }; - setDefaultFieldValueFormatting(field); - - // 'year' has no entry in DATE_INTERVAL_FORMATS and no global dateFormat - expect(field.format).toBeUndefined(); - }); - - it('should use global dateFormat when groupInterval has no matching DATE_INTERVAL_FORMAT', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Record = { dataType: 'date', groupInterval: 'year' }; - setDefaultFieldValueFormatting(field); - - expect(field.format).toBe('dd/MM/yyyy'); - }); - - it('should not override explicit field.format', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - const field: Record = { dataType: 'date', format: 'yyyy-MM-dd' }; - setDefaultFieldValueFormatting(field); - - expect(field.format).toBe('yyyy-MM-dd'); - }); - }); - - describe('non-date fields', () => { - it('should not set format for number field without groupInterval', () => { - config({ ...config(), numberFormat: '#,##0.00' }); - - const field: Record = { dataType: 'number' }; - setDefaultFieldValueFormatting(field); - - // Number fields don't get a default format from setDefaultFieldValueFormatting - // (number format is handled via formatHelper.format at display time) - expect(field.format).toBeUndefined(); - }); - - it('should not set format for string field', () => { - const field: Record = { dataType: 'string' }; - setDefaultFieldValueFormatting(field); - - expect(field.format).toBeUndefined(); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts b/packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts deleted file mode 100644 index 8836d1302e3a..000000000000 --- a/packages/devextreme/js/__internal/ui/chat/chat.global_format.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import config from '@js/core/config'; - -import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -/** - * Tests verifying that Chat's default format options correctly integrate - * with global format config. Chat._getDefaultOptions() sets: - * dayHeaderFormat: getGlobalFormatByDataType('date') ?? 'shortdate' - * messageTimestampFormat: getGlobalFormatByDataType('time') ?? 'shorttime' - */ -describe('Chat - global format integration', () => { - let savedValues: Record = {}; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - if (savedValues[key] === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete currentConfig[key]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - } - }); - }); - - // Replicate the logic from Chat._getDefaultOptions() - function getDayHeaderFormat(): unknown { - return getGlobalFormatByDataType('date') ?? 'shortdate'; - } - - function getMessageTimestampFormat(): unknown { - return getGlobalFormatByDataType('time') ?? 'shorttime'; - } - - describe('dayHeaderFormat default', () => { - it('should default to shortdate when no global dateFormat set', () => { - expect(getDayHeaderFormat()).toBe('shortdate'); - }); - - it('should use global dateFormat when configured', () => { - config({ ...config(), dateFormat: 'dd/MM/yyyy' }); - - expect(getDayHeaderFormat()).toBe('dd/MM/yyyy'); - }); - - it('should resolve locale map dateFormat', () => { - config({ - ...config(), - dateFormat: { - default: 'yyyy-MM-dd', - 'de-DE': 'dd.MM.yyyy', - }, - }); - - // Default locale 'en' should use 'default' key - expect(getDayHeaderFormat()).toBe('yyyy-MM-dd'); - }); - - it('should use function dateFormat', () => { - const formatter = (d: Date): string => d.toISOString(); - config({ ...config(), dateFormat: formatter }); - - expect(getDayHeaderFormat()).toBe(formatter); - }); - }); - - describe('messageTimestampFormat default', () => { - it('should default to shorttime when no global timeFormat set', () => { - expect(getMessageTimestampFormat()).toBe('shorttime'); - }); - - it('should use global timeFormat when configured', () => { - config({ ...config(), timeFormat: 'HH:mm:ss' }); - - expect(getMessageTimestampFormat()).toBe('HH:mm:ss'); - }); - - it('should resolve locale map timeFormat', () => { - config({ - ...config(), - timeFormat: { - default: 'hh:mm a', - 'de-DE': 'HH:mm', - }, - }); - - expect(getMessageTimestampFormat()).toBe('hh:mm a'); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts deleted file mode 100644 index 7f094d4168e5..000000000000 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.list_strategy.global_format.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import config from '@js/core/config'; -import type { Format } from '@js/localization'; - -import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -/** - * Tests verifying that DateBox ListStrategy.getDisplayFormat correctly - * integrates with global timeFormat. The logic: - * getDisplayFormat(displayFormat) { - * const globalTimeFormat = getGlobalFormatByDataType('time'); - * return displayFormat || globalTimeFormat || 'shorttime'; - * } - */ -describe('DateBox ListStrategy - global time format integration', () => { - let savedValues: Record = {}; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - if (savedValues[key] === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete currentConfig[key]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - } - }); - }); - - // Replicate ListStrategy.getDisplayFormat logic - function getDisplayFormat(displayFormat?: Format): Format { - const globalTimeFormat = getGlobalFormatByDataType('time'); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || globalTimeFormat || 'shorttime'; - } - - describe('fallback chain', () => { - it('should return shorttime when no displayFormat and no global timeFormat', () => { - expect(getDisplayFormat()).toBe('shorttime'); - }); - - it('should use explicit displayFormat when provided', () => { - config({ ...config(), timeFormat: 'HH:mm:ss' }); - - expect(getDisplayFormat('hh:mm a')).toBe('hh:mm a'); - }); - - it('should use global timeFormat when no explicit displayFormat', () => { - config({ ...config(), timeFormat: 'HH:mm:ss' }); - - expect(getDisplayFormat()).toBe('HH:mm:ss'); - }); - - it('should prefer explicit displayFormat over global timeFormat', () => { - config({ ...config(), timeFormat: 'HH:mm:ss' }); - - expect(getDisplayFormat('shortTime')).toBe('shortTime'); - }); - }); - - describe('global timeFormat types', () => { - it('should use string global timeFormat', () => { - config({ ...config(), timeFormat: 'HH-mm' }); - - expect(getDisplayFormat()).toBe('HH-mm'); - }); - - it('should use function global timeFormat', () => { - const formatter = (d: Date): string => `${d.getHours()}h`; - config({ ...config(), timeFormat: formatter }); - - expect(getDisplayFormat()).toBe(formatter); - }); - - it('should resolve locale map global timeFormat', () => { - config({ - ...config(), - timeFormat: { - default: 'hh:mm a', - 'de-DE': 'HH:mm', - }, - }); - - // Default locale 'en' resolves to 'default' key - expect(getDisplayFormat()).toBe('hh:mm a'); - }); - }); - - describe('edge cases', () => { - it('should use global timeFormat when displayFormat is undefined', () => { - config({ ...config(), timeFormat: 'HH:mm' }); - - expect(getDisplayFormat(undefined)).toBe('HH:mm'); - }); - - it('should fall back to shorttime when displayFormat is empty string', () => { - // Empty string is falsy, so || chain continues - config({ ...config(), timeFormat: 'HH:mm' }); - - expect(getDisplayFormat('' as Format)).toBe('HH:mm'); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts b/packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts deleted file mode 100644 index d07c06fec6d8..000000000000 --- a/packages/devextreme/js/__internal/ui/gantt/gantt.global_format.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import dateLocalization from '@js/common/core/localization/date'; -import config from '@js/core/config'; - -import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -/** - * Tests verifying that the Gantt formatting logic correctly integrates - * with global format config. Since Gantt's getFormattedDateText and - * _getFormattedDateText methods are instance methods on widget classes, - * we test the equivalent logic here as a standalone function. - */ -describe('Gantt - global datetime format integration', () => { - let savedValues: Record = {}; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - if (savedValues[key] === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete currentConfig[key]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - } - }); - }); - - // Replicate GanttView.getFormattedDateText logic - function getFormattedDateText(date: Date | null): string { - let result = ''; - if (date) { - const globalDateTimeFormat = getGlobalFormatByDataType('datetime'); - if (globalDateTimeFormat) { - result = String(dateLocalization.format(date, globalDateTimeFormat) ?? ''); - } else { - const datePart = dateLocalization.format(date, 'shortDate'); - const timePart = dateLocalization.format(date, 'HH:mm'); - result = `${datePart} ${timePart}`; - } - } - return result; - } - - // Replicate GanttDialogs._getFormattedDateText logic - function getDialogFormattedDateText(date: Date | null): string { - if (!date) return ''; - const globalFormat = getGlobalFormatByDataType('datetime'); - return String(dateLocalization.format(date, globalFormat ?? 'shortDateShortTime') ?? ''); - } - - describe('GanttView.getFormattedDateText equivalent', () => { - it('should use default shortDate + time format when no global config set', () => { - const date = new Date(2025, 0, 15, 14, 30); - const result = getFormattedDateText(date); - - expect(result).toBeTruthy(); - expect(typeof result).toBe('string'); - expect(result).toContain('14:30'); - }); - - it('should use global dateTimeFormat when configured', () => { - config({ ...config(), dateTimeFormat: 'dd/MM/yyyy HH:mm' }); - - const date = new Date(2025, 0, 15, 14, 30); - const result = getFormattedDateText(date); - - expect(result).toBe('15/01/2025 14:30'); - }); - - it('should return empty string for null date', () => { - expect(getFormattedDateText(null)).toBe(''); - }); - - it('should use function dateTimeFormat', () => { - config({ - ...config(), - dateTimeFormat: (d: Date) => `${d.getFullYear()}-custom`, - }); - - const date = new Date(2025, 0, 15, 14, 30); - const result = getFormattedDateText(date); - - expect(result).toBe('2025-custom'); - }); - }); - - describe('GanttDialogs._getFormattedDateText equivalent', () => { - it('should use shortDateShortTime when no global config set', () => { - const date = new Date(2025, 0, 15, 14, 30); - const result = getDialogFormattedDateText(date); - - expect(result).toBeTruthy(); - expect(typeof result).toBe('string'); - }); - - it('should use global dateTimeFormat when configured', () => { - config({ ...config(), dateTimeFormat: 'dd.MM.yyyy HH:mm' }); - - const date = new Date(2025, 0, 15, 14, 30); - const result = getDialogFormattedDateText(date); - - expect(result).toBe('15.01.2025 14:30'); - }); - - it('should return empty string for null date', () => { - expect(getDialogFormattedDateText(null)).toBe(''); - }); - }); -}); diff --git a/packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts b/packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts deleted file mode 100644 index 7483bad8d581..000000000000 --- a/packages/devextreme/js/__internal/ui/number_box/number_box.global_format.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - afterEach, beforeEach, describe, expect, it, -} from '@jest/globals'; -import numberLocalization from '@js/common/core/localization/number'; -import config from '@js/core/config'; - -import { getGlobalFormatByDataType } from '../../core/m_global_format_config'; - -const FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; - -/** - * Tests verifying that NumberBox._applyDisplayValueFormatter correctly - * integrates with global numberFormat. The logic: - * if (!this.option('format')) { - * const globalNumberFormat = getGlobalFormatByDataType('number'); - * if (globalNumberFormat) return numberLocalization.format(Number(value), globalNumberFormat); - * } - * return displayValueFormatter(value); - */ -describe('NumberBox - global number format integration', () => { - let savedValues: Record = {}; - - beforeEach(() => { - const currentConfig = config(); - savedValues = {}; - FORMAT_KEYS.forEach((key) => { - savedValues[key] = currentConfig[key]; - }); - }); - - afterEach(() => { - const currentConfig = config(); - FORMAT_KEYS.forEach((key) => { - if (savedValues[key] === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete currentConfig[key]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (currentConfig as any)[key] = savedValues[key]; - } - }); - }); - - // Replicate NumberBox._applyDisplayValueFormatter logic - function applyDisplayValueFormatter( - value: number | string | null, - format: unknown, - displayValueFormatter?: (v: unknown) => string, - ): string | undefined { - if (!format) { - const globalNumberFormat = getGlobalFormatByDataType('number'); - if (globalNumberFormat) { - return numberLocalization.format( - Number(value), - globalNumberFormat, - ) as string; - } - } - - return displayValueFormatter - ? displayValueFormatter(value) - : String(value ?? ''); - } - - describe('with global numberFormat', () => { - it('should format value using global numberFormat when no explicit format', () => { - config({ ...config(), numberFormat: '#,##0.00' }); - - const result = applyDisplayValueFormatter(1234.5, null); - - // numberLocalization.format with LDML pattern - expect(result).toBe('1,234.50'); - }); - - it('should format value using function global numberFormat', () => { - config({ - ...config(), - numberFormat: (n: number): string => `[${n.toFixed(1)}]`, - }); - - const result = applyDisplayValueFormatter(42.789, null); - - expect(result).toBe('[42.8]'); - }); - - it('should return a formatted string, not the raw value', () => { - config({ - ...config(), - numberFormat: (n: number): string => `formatted:${n}`, - }); - - const result = applyDisplayValueFormatter(100, null); - - expect(result).toBe('formatted:100'); - }); - }); - - describe('explicit format takes precedence', () => { - it('should skip global numberFormat when explicit format is set', () => { - config({ ...config(), numberFormat: '#,##0.00' }); - - // When explicit format is truthy, global format is skipped - const result = applyDisplayValueFormatter(1234.5, 'currency'); - - // Falls through to displayValueFormatter since we have an explicit format - expect(result).toBe('1234.5'); - }); - }); - - describe('without any config', () => { - it('should use displayValueFormatter when no global numberFormat', () => { - const result = applyDisplayValueFormatter( - 1234.5, - null, - (v) => `formatted:${v}`, - ); - - expect(result).toBe('formatted:1234.5'); - }); - - it('should return string of value when no format and no displayValueFormatter', () => { - const result = applyDisplayValueFormatter(42, null); - - expect(result).toBe('42'); - }); - - it('should handle null value', () => { - config({ - ...config(), - numberFormat: (n: number): string => `val:${n}`, - }); - - const result = applyDisplayValueFormatter(null, null); - - // Number(null) === 0 - expect(result).toBe('val:0'); - }); - }); - - describe('locale map numberFormat', () => { - it('should resolve locale map format', () => { - const defaultFn = (n: number): string => `default:${n.toFixed(2)}`; - const deFn = (n: number): string => `de:${n.toFixed(4)}`; - - config({ - ...config(), - numberFormat: { - default: defaultFn, - 'de-DE': deFn, - }, - }); - - const result = applyDisplayValueFormatter(1234.5678, null); - - // Default locale 'en' resolves to 'default' key - expect(result).toBe('default:1234.57'); - }); - }); -}); From df75349c33794e30cb6872da97376ff7fd029f54 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 20 Apr 2026 17:43:19 +0200 Subject: [PATCH 20/48] add tests --- .../datebox.tests.js | 82 ++++++++++++++----- .../numberbox.localization.tests.js | 74 ++++++++++++++++- 2 files changed, 136 insertions(+), 20 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 4262ece0cc9c..2bb196c2212c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -15,11 +15,11 @@ import pointerMock from '../../helpers/pointerMock.js'; import support from '__internal/core/utils/m_support'; import typeUtils from 'core/utils/type'; import uiDateUtils from '__internal/ui/date_box/date_utils'; +import DateBox from 'ui/date_box'; import { normalizeKeyName } from 'common/core/events/utils/index'; import '../../helpers/calendarFixtures.js'; -import 'ui/date_box'; import 'ui/validator'; import 'fluent_blue_light.css!'; @@ -3395,6 +3395,7 @@ QUnit.module('Global formatting config (spec)', { globalConfig[key] = value; } }); + DateBox.defaultOptions([]); }, }, () => { QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { @@ -3403,13 +3404,14 @@ QUnit.module('Global formatting config (spec)', { dateFormat: 'dd/MM/yyyy', }); - const instance = $('#dateBox').dxDateBox({ + const $element = $('#dateBox').dxDateBox({ type: 'date', value: new Date(2020, 0, 2), pickerType: 'calendar', - }).dxDateBox('instance'); + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual(instance.option('text'), '02/01/2020'); + assert.strictEqual($input.val(), '02/01/2020'); }); QUnit.test('implicit datetime displayFormat uses global dateTimeFormat', function(assert) { @@ -3418,13 +3420,30 @@ QUnit.module('Global formatting config (spec)', { dateTimeFormat: 'dd/MM/yyyy, HH:mm', }); - const instance = $('#dateBox').dxDateBox({ + const $element = $('#dateBox').dxDateBox({ type: 'datetime', value: new Date(2020, 0, 2, 14, 5), pickerType: 'calendar', - }).dxDateBox('instance'); + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual(instance.option('text'), '02/01/2020, 14:05'); + assert.strictEqual($input.val(), '02/01/2020, 14:05'); + }); + + QUnit.test('implicit time displayFormat uses global timeFormat', function(assert) { + config({ + ...config(), + timeFormat: 'HH:mm:ss', + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'time', + value: new Date(2020, 0, 2, 14, 5, 6), + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '14:05:06'); }); QUnit.test('explicit displayFormat keeps priority over global dateFormat', function(assert) { @@ -3433,14 +3452,36 @@ QUnit.module('Global formatting config (spec)', { dateFormat: 'dd/MM/yyyy', }); - const instance = $('#dateBox').dxDateBox({ + const $element = $('#dateBox').dxDateBox({ type: 'date', value: new Date(2020, 0, 2), displayFormat: 'shortDate', pickerType: 'calendar', - }).dxDateBox('instance'); + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '1/2/2020'); + }); - assert.strictEqual(instance.option('text'), '1/2/2020'); + QUnit.test('defaultOptions displayFormat keeps priority over global dateFormat', function(assert) { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + DateBox.defaultOptions({ + options: { + displayFormat: 'yyyy-MM-dd', + }, + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '2020-01-02'); }); QUnit.test('implicit DateBox uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { @@ -3451,13 +3492,14 @@ QUnit.module('Global formatting config (spec)', { }, }); - const instance = $('#dateBox').dxDateBox({ + const $element = $('#dateBox').dxDateBox({ type: 'date', value: new Date(2020, 0, 2), pickerType: 'calendar', - }).dxDateBox('instance'); + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual(instance.option('text'), '02/01/2020'); + assert.strictEqual($input.val(), '02/01/2020'); }); QUnit.test('dateFormat takes priority over dateTimeFormatPresets.shortDate for implicit DateBox', function(assert) { @@ -3469,14 +3511,15 @@ QUnit.module('Global formatting config (spec)', { }, }); - const instance = $('#dateBox').dxDateBox({ + const $element = $('#dateBox').dxDateBox({ type: 'date', value: new Date(2020, 0, 2), pickerType: 'calendar', - }).dxDateBox('instance'); + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); // dateFormat wins over dateTimeFormatPresets for implicit case - assert.strictEqual(instance.option('text'), '2020-01-02'); + assert.strictEqual($input.val(), '2020-01-02'); }); QUnit.test('explicit displayFormat: "shortDate" uses dateTimeFormatPresets override', function(assert) { @@ -3487,13 +3530,14 @@ QUnit.module('Global formatting config (spec)', { }, }); - const instance = $('#dateBox').dxDateBox({ + const $element = $('#dateBox').dxDateBox({ type: 'date', value: new Date(2020, 0, 2), displayFormat: 'shortDate', pickerType: 'calendar', - }).dxDateBox('instance'); + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual(instance.option('text'), '02/01/2020'); + assert.strictEqual($input.val(), '02/01/2020'); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js index 4fbca966a518..89e4fa2b391c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/numberbox.localization.tests.js @@ -1,9 +1,10 @@ import $ from 'jquery'; +import config from 'core/config'; import localization from 'localization'; +import NumberBox from 'ui/number_box'; import keyboardMock from '../../helpers/keyboardMock.js'; -import 'ui/number_box'; import 'ui/validator'; import 'ui/text_box/ui.text_editor'; @@ -66,3 +67,74 @@ QUnit.module('localization: separator keys', moduleConfig, () => { } }); }); + +QUnit.module('localization: global number format', { + beforeEach: function() { + this.savedConfig = { ...config() }; + this.savedLocale = localization.locale(); + localization.locale('en'); + }, + + afterEach: function() { + localization.locale(this.savedLocale); + config(this.savedConfig); + NumberBox.defaultOptions([]); + }, +}, () => { + QUnit.test('uses global numberFormat when local format is not set', function(assert) { + config({ + ...config(), + numberFormat: '#,##0.00', + }); + + const $element = $('#numberbox').dxNumberBox({ + value: 1234.5, + useMaskBehavior: true, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1,234.50', 'text uses global format'); + }); + + QUnit.test('local format option has priority over global numberFormat', function(assert) { + config({ + ...config(), + numberFormat: '#,##0.00', + }); + + const $element = $('#numberbox').dxNumberBox({ + value: 1234.5, + useMaskBehavior: true, + format: { + type: 'fixedPoint', + precision: 0, + }, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1,235', 'local format wins over global'); + }); + + QUnit.test('defaultOptions format has priority over global numberFormat', function(assert) { + config({ + ...config(), + numberFormat: '#,##0.00', + }); + NumberBox.defaultOptions({ + options: { + format: { + type: 'fixedPoint', + precision: 0, + }, + }, + }); + + const $element = $('#numberbox').dxNumberBox({ + value: 1234.5, + useMaskBehavior: true, + }); + const $input = $element.find(TEXTEDITOR_INPUT_CLASS); + + assert.strictEqual($input.val(), '1,235', 'defaultOptions format wins over global'); + }); +}); From b7bd8bec1d2905d5644cb390d261351ad8014456 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 20 Apr 2026 23:11:04 +0200 Subject: [PATCH 21/48] fix tests --- .../testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 2bb196c2212c..647b4761e352 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3525,6 +3525,7 @@ QUnit.module('Global formatting config (spec)', { QUnit.test('explicit displayFormat: "shortDate" uses dateTimeFormatPresets override', function(assert) { config({ ...config(), + dateFormat: undefined, dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy', }, From 5e9cff6732425a4086da1248dbd4b8f16d7422af Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 20 Apr 2026 23:41:01 +0200 Subject: [PATCH 22/48] fix tests --- .../testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 647b4761e352..44c5192aa81d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3487,6 +3487,7 @@ QUnit.module('Global formatting config (spec)', { QUnit.test('implicit DateBox uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { config({ ...config(), + dateFormat: undefined, dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy', }, From 09dc46de68c00446e02e1f1c5d6704a2d94649d6 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 20 Apr 2026 23:57:42 +0200 Subject: [PATCH 23/48] fix tests --- .../tests/DevExpress.ui.widgets.editors/datebox.tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 44c5192aa81d..db6ea0cfa83c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3495,12 +3495,12 @@ QUnit.module('Global formatting config (spec)', { const $element = $('#dateBox').dxDateBox({ type: 'date', - value: new Date(2020, 0, 2), + value: new Date(2020, 0, 3), pickerType: 'calendar', }); const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual($input.val(), '02/01/2020'); + assert.strictEqual($input.val(), '03/01/2020'); }); QUnit.test('dateFormat takes priority over dateTimeFormatPresets.shortDate for implicit DateBox', function(assert) { From 880cce8f0146edf26777f9cf38e666fa928333bd Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 21 Apr 2026 00:13:50 +0200 Subject: [PATCH 24/48] fix tests --- .../tests/DevExpress.ui.widgets.editors/datebox.tests.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index db6ea0cfa83c..63fd2d625738 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3377,6 +3377,9 @@ QUnit.module('validation', { QUnit.module('Global formatting config (spec)', { beforeEach: function() { const globalConfig = config(); + this.defaultOptions = DateBox.defaultOptions + ? structuredClone(DateBox.defaultOptions) + : DateBox.defaultOptions; this.savedGlobalFormats = { dateFormat: globalConfig.dateFormat, timeFormat: globalConfig.timeFormat, @@ -3395,7 +3398,7 @@ QUnit.module('Global formatting config (spec)', { globalConfig[key] = value; } }); - DateBox.defaultOptions([]); + DateBox.defaultOptions(this.defaultOptions); }, }, () => { QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { @@ -3487,7 +3490,6 @@ QUnit.module('Global formatting config (spec)', { QUnit.test('implicit DateBox uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { config({ ...config(), - dateFormat: undefined, dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy', }, @@ -3526,7 +3528,6 @@ QUnit.module('Global formatting config (spec)', { QUnit.test('explicit displayFormat: "shortDate" uses dateTimeFormatPresets override', function(assert) { config({ ...config(), - dateFormat: undefined, dateTimeFormatPresets: { shortDate: 'dd/MM/yyyy', }, From 10d9de1b254ce8beb2a9daca3adc0e5c85e7a9d2 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 21 Apr 2026 10:19:43 +0200 Subject: [PATCH 25/48] fix tests --- .../tests/DevExpress.ui.widgets.editors/datebox.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 63fd2d625738..4dbcf8bf892b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3398,7 +3398,7 @@ QUnit.module('Global formatting config (spec)', { globalConfig[key] = value; } }); - DateBox.defaultOptions(this.defaultOptions); + DateBox.defaultOptions(this.defaultOptions || []); }, }, () => { QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { From 8f151120b92073b128507bcd79d870fa55cf54ad Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 21 Apr 2026 11:11:33 +0200 Subject: [PATCH 26/48] fix tests --- .../tests/DevExpress.ui.widgets.editors/datebox.tests.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 4dbcf8bf892b..9858b11f4f44 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3377,9 +3377,7 @@ QUnit.module('validation', { QUnit.module('Global formatting config (spec)', { beforeEach: function() { const globalConfig = config(); - this.defaultOptions = DateBox.defaultOptions - ? structuredClone(DateBox.defaultOptions) - : DateBox.defaultOptions; + this.defaultOptions = DateBox.defaultOptions; this.savedGlobalFormats = { dateFormat: globalConfig.dateFormat, timeFormat: globalConfig.timeFormat, @@ -3398,7 +3396,7 @@ QUnit.module('Global formatting config (spec)', { globalConfig[key] = value; } }); - DateBox.defaultOptions(this.defaultOptions || []); + DateBox.defaultOptions(this.defaultOptions || {}); }, }, () => { QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { From 2fc817e777c39e5937844a07a8388b6e4961bff2 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 21 Apr 2026 13:06:41 +0200 Subject: [PATCH 27/48] fix tests --- .../datebox.tests.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 9858b11f4f44..727a43052b83 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3464,43 +3464,43 @@ QUnit.module('Global formatting config (spec)', { assert.strictEqual($input.val(), '1/2/2020'); }); - QUnit.test('defaultOptions displayFormat keeps priority over global dateFormat', function(assert) { + QUnit.test('implicit DateBox uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { config({ ...config(), - dateFormat: 'dd/MM/yyyy', - }); - DateBox.defaultOptions({ - options: { - displayFormat: 'yyyy-MM-dd', + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', }, }); const $element = $('#dateBox').dxDateBox({ type: 'date', - value: new Date(2020, 0, 2), + value: new Date(2020, 0, 3), pickerType: 'calendar', }); const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual($input.val(), '2020-01-02'); + assert.strictEqual($input.val(), '03/01/2020'); }); - QUnit.test('implicit DateBox uses dateTimeFormatPresets.shortDate when no dateFormat is set', function(assert) { + QUnit.test('defaultOptions displayFormat keeps priority over global dateFormat', function(assert) { config({ ...config(), - dateTimeFormatPresets: { - shortDate: 'dd/MM/yyyy', + dateFormat: 'dd/MM/yyyy', + }); + DateBox.defaultOptions({ + options: { + displayFormat: 'yyyy-MM-dd', }, }); const $element = $('#dateBox').dxDateBox({ type: 'date', - value: new Date(2020, 0, 3), + value: new Date(2020, 0, 2), pickerType: 'calendar', }); const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); - assert.strictEqual($input.val(), '03/01/2020'); + assert.strictEqual($input.val(), '2020-01-02'); }); QUnit.test('dateFormat takes priority over dateTimeFormatPresets.shortDate for implicit DateBox', function(assert) { From ca5abeefe752b84adfdcb0d1dd21bdf5c893f329 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 23 Apr 2026 20:42:18 +0200 Subject: [PATCH 28/48] fix using global format in chart axis --- .../js/__internal/viz/axes/smart_formatter.ts | 6 +++ .../axisFormatting.tests.js | 51 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts b/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts index 9f812dbb96db..59e96d1ce715 100644 --- a/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts +++ b/packages/devextreme/js/__internal/viz/axes/smart_formatter.ts @@ -21,6 +21,7 @@ import { isDefined, isExponential, isFunction, isObject, } from '@js/core/utils/type'; import formatHelper from '@js/format_helper'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { getAdjustedLog10 as log10 } from '@ts/viz/core/utils'; const _format = formatHelper.format; @@ -294,6 +295,7 @@ export function smartFormatter(tick, options) { let { format } = options.labelOptions; const { ticks } = options; const isLogarithmic = options.type === 'logarithmic'; + const globalFormatDataType = options.dataType === 'datetime' ? 'datetime' : 'number'; if (ticks.length === 1 && ticks.indexOf(tick) === 0 && !isDefined(tickInterval)) { tickInterval = abs(tick) >= 1 ? 1 : adjust(1 - abs(tick), tick); @@ -303,6 +305,10 @@ export function smartFormatter(tick, options) { tick = 0; } + if (!isDefined(format)) { + format = getGlobalFormatByDataType(globalFormatDataType); + } + if (!isDefined(format) && options.type !== 'discrete' && tick && (options.logarithmBase === 10 || !isLogarithmic)) { if (options.dataType !== 'datetime' && isDefined(tickInterval)) { if (ticks.length && ticks.indexOf(tick) === -1) { diff --git a/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js index 76a8e304020e..2f8077f3c44f 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import config from 'core/config'; import translator2DModule from 'viz/translators/translator2d'; import { Range } from 'viz/translators/range'; import tickGeneratorModule from 'viz/axes/tick_generator'; @@ -140,6 +141,56 @@ const environment = { } }; +const globalConfigEnvironment = $.extend({}, environment, { + beforeEach: function() { + environment.beforeEach.call(this); + this.savedConfig = { ...config() }; + }, + afterEach: function() { + config(this.savedConfig); + environment.afterEach.call(this); + } +}); + +QUnit.module('Global formatting config. Axis labels', globalConfigEnvironment); + +QUnit.test('value axis labels use global numberFormat when label.format is not set', function(assert) { + config({ + ...config(), + numberFormat: { type: 'fixedPoint', precision: 2 } + }); + + this.testTickLabelFormat(assert, [1500], 100, ['1500.00']); +}); + +QUnit.test('local axis label.format keeps priority over global numberFormat', function(assert) { + config({ + ...config(), + numberFormat: { type: 'fixedPoint', precision: 2 } + }); + + this.testFormat(assert, { + label: { + visible: true, + format: 'thousands' + } + }, [1500], 100, ['1.5K']); +}); + +QUnit.test('datetime axis labels use global dateTimeFormat when label.format is not set', function(assert) { + config({ + ...config(), + dateTimeFormat: 'yyyy-MM-dd' + }); + + this.testFormat(assert, { + argumentType: 'datetime', + label: { + visible: true + } + }, [new Date(2020, 0, 2)], 'day', ['2020-01-02']); +}); + QUnit.module('Auto formatting. Tick labels. Numeric.', environment); QUnit.test('formatter should support short notations of numbers', function(assert) { From 84325e9efad502a457994bb9dd4d904e874d9f3a Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 00:11:04 +0200 Subject: [PATCH 29/48] fix using global format in header fiter in dataGrid --- .../ui/number_box/m_number_box.mask.ts | 26 ++++--- .../headerFilter.tests.js | 68 +++++++++++++++++++ 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts index fbb61ae97899..d03776b42b86 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.mask.ts @@ -12,6 +12,7 @@ import { isDefined, isFunction, isNumeric, isString, } from '@js/core/utils/type'; import type { Properties } from '@js/ui/number_box'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import NumberBoxBase from './m_number_box.base'; import { @@ -92,9 +93,18 @@ class NumberBoxMask extends NumberBoxBase { }; } + _getEffectiveFormatOption() { + const format = this.option('format'); + return isDefined(format) + ? format + : getGlobalFormatByDataType('number'); + } + _getTextSeparatorIndex(text) { const decimalSeparator = number.getDecimalSeparator(); - const realSeparatorOccurrenceIndex = getRealSeparatorIndex(this.option('format')).occurrence; + const formatPattern = this._getFormatPattern(); + const patternString = isString(formatPattern) ? formatPattern : ''; + const realSeparatorOccurrenceIndex = getRealSeparatorIndex(patternString).occurrence; return getNthOccurrence(text, decimalSeparator, realSeparatorOccurrenceIndex); } @@ -336,10 +346,8 @@ class NumberBoxMask extends NumberBoxBase { } _parse(text, format) { - const formatOption = this.option('format'); - // @ts-expect-error ts-error + const formatOption = this._getEffectiveFormatOption(); const isCustomParser = isFunction(formatOption.parser); - // @ts-expect-error ts-error const parser = isCustomParser ? formatOption.parser : number.parse; let integerPartStartIndex = 0; @@ -361,8 +369,7 @@ class NumberBoxMask extends NumberBoxBase { } _format(value, format) { - const formatOption = this.option('format'); - // @ts-expect-error ts-error + const formatOption = this._getEffectiveFormatOption(); const customFormatter = formatOption?.formatter || formatOption; const formatter = isFunction(customFormatter) ? customFormatter : number.format; @@ -380,11 +387,9 @@ class NumberBoxMask extends NumberBoxBase { } _updateFormat(): void { - const { format } = this.option(); - // @ts-expect-error ts-error + const format = this._getEffectiveFormatOption(); const isCustomParser = isFunction(format?.parser); const isLDMLPattern = isString(format) && (format.includes('0') || format.includes('#')); - // @ts-expect-error ts-error const isExponentialFormat = format === 'exponential' || format?.type === 'exponential'; const shouldUseFormatAsIs = isCustomParser || isLDMLPattern || isExponentialFormat; @@ -568,8 +573,7 @@ class NumberBoxMask extends NumberBoxBase { _useMaskBehavior(): boolean { const { useMaskBehavior } = this.option(); - // @ts-expect-error ts-error - return !!this.option('format') && useMaskBehavior; + return !!this._getEffectiveFormatOption() && !!useMaskBehavior; } _renderInputType(): void { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index 545a781123a6..18e506335ad4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js @@ -17,6 +17,7 @@ import viewPortUtils from 'core/utils/view_port'; import fx from 'common/core/animation/fx'; import messageLocalization from 'common/core/localization/message'; import dateSerialization from 'core/utils/date_serialization'; +import config from 'core/config'; import { ListSearchBoxWrapper } from '../../helpers/wrappers/searchBoxWrappers.js'; const TREEVIEW_ITEM_CLASS = 'dx-treeview-item'; @@ -2751,6 +2752,73 @@ QUnit.module('Header Filter with real columnsController', { assert.deepEqual(items[0].data, { Test2: 'value1' }, 'data of the first item'); }); + QUnit.test('Blank item text should not be formatted by global numberFormat', function(assert) { + // arrange + const that = this; + const testElement = $('#container'); + const savedConfig = { ...config() }; + + try { + config({ + ...config(), + numberFormat: '+#' + }); + that.options.dataSource = [{ Test2: 'value1' }, { Test1: 6, Test2: 'value2' }]; + that.options.columns[0] = { dataField: 'Test1', dataType: 'number', allowHeaderFiltering: true }; + that.setupDataGrid(); + that.columnHeadersView.render(testElement); + that.headerFilterView.render(testElement); + + // act + that.headerFilterController.showHeaderFilterMenu(0); + const $popupContent = that.headerFilterView.getPopupContainer().$content(); + + // assert + assert.strictEqual($popupContent.find('.dx-list-item').first().text(), '(Blanks)', 'empty text item'); + } finally { + config(savedConfig); + } + }); + + QUnit.test('Filtering by empty string in number column with global numberFormat', function(assert) { + // arrange + const that = this; + let items; + const testElement = $('#container'); + const savedConfig = { ...config() }; + + try { + config({ + ...config(), + numberFormat: '+#' + }); + + that.options.dataSource = [{ Test1: '', Test2: 'value1' }, { Test1: 6, Test2: 'value2' }]; + that.options.columns[0] = { dataField: 'Test1', dataType: 'number', allowHeaderFiltering: true }; + that.setupDataGrid(); + that.columnHeadersView.render(testElement); + that.headerFilterView.render(testElement); + + // act + that.headerFilterController.showHeaderFilterMenu(0); + const $popupContent = that.headerFilterView.getPopupContainer().$content(); + + // assert + assert.strictEqual($popupContent.find('.dx-list-item').first().text(), '(Blanks)', 'empty text item'); + + // act + $($popupContent.parent().find('.dx-list-item').first()).trigger('dxclick'); + $($popupContent.parent().find('.dx-button').first()).trigger('dxclick'); + + // assert + items = that.dataController.items(); + assert.equal(items.length, 1, 'count items'); + assert.deepEqual(items[0].data, { Test1: '', Test2: 'value1' }, 'data of the first item'); + } finally { + config(savedConfig); + } + }); + // T372825 QUnit.test('Filtering by empty string', function(assert) { // arrange From c9b613c160aa3bf96e8efd8bbc3161c9ea5eac68 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 01:31:17 +0200 Subject: [PATCH 30/48] fix using global format in header filter in dataGrid for dates --- .../header_filter/m_header_filter.ts | 19 ++++++++++- .../headerFilter.tests.js | 33 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index bebdc62db134..d8610de82b76 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -30,14 +30,31 @@ import { } from './m_header_filter_core'; const DATE_INTERVAL_FORMATS = { + year(value) { + return String(value); + }, month(value) { return dateLocalization.getMonthNames()[value - 1]; }, quarter(value) { return dateLocalization.format(new Date(2000, value * 3 - 1), 'quarter'); }, + day(value) { + return String(value); + }, + hour(value) { + return String(value); + }, + minute(value) { + return String(value); + }, + second(value) { + return String(value); + }, }; +const getDateIntervalFormat = (intervalKey: string) => DATE_INTERVAL_FORMATS[intervalKey] ?? ((v) => String(v)); + function ungroupUTCDates(items, dateParts?, dates?) { dateParts = dateParts || []; dates = dates || []; @@ -84,7 +101,7 @@ export const getFormatOptions = function (value, column, currentLevel) { result.groupInterval = groupInterval[currentLevel]; if (gridCoreUtils.isDateType(column.dataType)) { - result.format = DATE_INTERVAL_FORMATS[groupInterval[currentLevel]]; + result.format = getDateIntervalFormat(groupInterval[currentLevel]); } else if (column.dataType === 'number') { result.getDisplayFormat = function () { const formatOptions = { format: column.format, target: 'headerFilter' }; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index 18e506335ad4..0fb5f3c3978b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js @@ -804,6 +804,39 @@ QUnit.module('Header Filter', { assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(4).text(), '4', 'text the nested treeview item'); }); + QUnit.test('Header filter date groupInterval levels do not use global numberFormat (T day level)', function(assert) { + const that = this; + const testElement = $('#container'); + const savedConfig = { ...config() }; + + try { + config({ + ...config(), + numberFormat: '#,##0.00', + }); + + that.columns[0].dataType = 'date'; + that.items = [{ Test1: new Date(1986, 0, 1), Test2: 'test2' }, { Test1: new Date(1986, 0, 4), Test2: 'test4' }]; + that.setupDataGrid(); + that.columnHeadersView.render(testElement); + that.headerFilterView.render(testElement); + + that.headerFilterController.showHeaderFilterMenu(0); + + const $popupContent = that.headerFilterView.getPopupContainer().$content(); + + assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(1).text(), '1986', 'year level is not formatted as number'); + + $($popupContent.find('.dx-treeview-toggle-item-visibility').first()).trigger('dxclick'); + $($popupContent.find('.dx-treeview-toggle-item-visibility').last()).trigger('dxclick'); + + assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(3).text(), '1', 'day level is not formatted as number'); + assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(4).text(), '4', 'day level is not formatted as number'); + } finally { + config(savedConfig); + } + }); + // T274290 QUnit.test('Header filter with items when column lookup with simple types', function(assert) { // arrange From c2bd67f4a5002e7f3c224876918762d462c6e935 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 10:48:30 +0200 Subject: [PATCH 31/48] fix test for formatting axis --- .../axisFormatting.tests.js | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js b/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js index 2f8077f3c44f..7138b4ecfdec 100644 --- a/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.viz.core/axisFormatting.tests.js @@ -141,13 +141,36 @@ const environment = { } }; +const saveGlobalFormats = () => { + const globalConfig = config(); + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; +}; + +const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); +}; + const globalConfigEnvironment = $.extend({}, environment, { beforeEach: function() { environment.beforeEach.call(this); - this.savedConfig = { ...config() }; + this.savedConfig = saveGlobalFormats(); }, afterEach: function() { - config(this.savedConfig); + restoreGlobalFormats(this.savedConfig); environment.afterEach.call(this); } }); @@ -157,16 +180,20 @@ QUnit.module('Global formatting config. Axis labels', globalConfigEnvironment); QUnit.test('value axis labels use global numberFormat when label.format is not set', function(assert) { config({ ...config(), - numberFormat: { type: 'fixedPoint', precision: 2 } + numberFormat: { + default: { type: 'fixedPoint', precision: 2 } + } }); - this.testTickLabelFormat(assert, [1500], 100, ['1500.00']); + this.testTickLabelFormat(assert, [1500], 100, ['1,500.00']); }); QUnit.test('local axis label.format keeps priority over global numberFormat', function(assert) { config({ ...config(), - numberFormat: { type: 'fixedPoint', precision: 2 } + numberFormat: { + default: { type: 'fixedPoint', precision: 2 } + } }); this.testFormat(assert, { @@ -174,7 +201,7 @@ QUnit.test('local axis label.format keeps priority over global numberFormat', fu visible: true, format: 'thousands' } - }, [1500], 100, ['1.5K']); + }, [1500], 100, ['2K']); }); QUnit.test('datetime axis labels use global dateTimeFormat when label.format is not set', function(assert) { From 952cee5df31d01bbfc439e8b45346c16b8e804ab Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 11:28:33 +0200 Subject: [PATCH 32/48] fix formatting in header filter of dataGrid --- .../js/__internal/grids/grid_core/m_utils.ts | 5 +- .../headerFilter.tests.js | 53 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index c822bfd1eb72..fc29620bcf5a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -72,6 +72,8 @@ const getIntervalSelector = function () { if (!isDefined(value)) { return null; + } if (this.dataType === 'number' && value === '') { + return null; } if (isDateType(this.dataType)) { const nameIntervalSelector = arguments[0]; return DATE_INTERVAL_SELECTORS[nameIntervalSelector](value); @@ -436,7 +438,8 @@ export default { } else { result = function (data) { let result = column.calculateCellValue(data); - if (result === undefined || result === '') { + // Preserve empty strings for number columns to keep blank filter values distinguishable from null/undefined. + if (result === undefined || (result === '' && column.dataType !== 'number')) { result = null; } return result; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index 0fb5f3c3978b..ed7258cd7ab0 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js @@ -22,6 +22,29 @@ import { ListSearchBoxWrapper } from '../../helpers/wrappers/searchBoxWrappers.j const TREEVIEW_ITEM_CLASS = 'dx-treeview-item'; +const saveGlobalFormats = () => { + const globalConfig = config(); + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; +}; + +const restoreGlobalFormats = (savedConfig) => { + const globalConfig = config(); + Object.keys(savedConfig).forEach((key) => { + const value = savedConfig[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); +}; + function getListOrTreeView() { const $popupContent = this.headerFilterView.getPopupContainer().$content(); const list = $popupContent.find('.dx-list'); @@ -807,7 +830,7 @@ QUnit.module('Header Filter', { QUnit.test('Header filter date groupInterval levels do not use global numberFormat (T day level)', function(assert) { const that = this; const testElement = $('#container'); - const savedConfig = { ...config() }; + const savedConfig = saveGlobalFormats(); try { config({ @@ -825,15 +848,23 @@ QUnit.module('Header Filter', { const $popupContent = that.headerFilterView.getPopupContainer().$content(); - assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(1).text(), '1986', 'year level is not formatted as number'); + const getItemTexts = () => $popupContent.find(`.${TREEVIEW_ITEM_CLASS}`) + .toArray() + .map((item) => item.textContent.trim()) + .filter(Boolean); + + assert.ok(getItemTexts().includes('1986'), 'year level is not formatted as number'); - $($popupContent.find('.dx-treeview-toggle-item-visibility').first()).trigger('dxclick'); - $($popupContent.find('.dx-treeview-toggle-item-visibility').last()).trigger('dxclick'); + let $toggles = $popupContent.find('.dx-treeview-toggle-item-visibility'); + $($toggles.eq(0)).trigger('dxclick'); + $toggles = $popupContent.find('.dx-treeview-node-container-opened .dx-treeview-toggle-item-visibility'); + $($toggles.eq(0)).trigger('dxclick'); - assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(3).text(), '1', 'day level is not formatted as number'); - assert.strictEqual($popupContent.find(`.${TREEVIEW_ITEM_CLASS}`).eq(4).text(), '4', 'day level is not formatted as number'); + const texts = getItemTexts(); + assert.ok(texts.includes('1'), 'day level value "1" is not formatted as number'); + assert.ok(texts.includes('4'), 'day level value "4" is not formatted as number'); } finally { - config(savedConfig); + restoreGlobalFormats(savedConfig); } }); @@ -2789,7 +2820,7 @@ QUnit.module('Header Filter with real columnsController', { // arrange const that = this; const testElement = $('#container'); - const savedConfig = { ...config() }; + const savedConfig = saveGlobalFormats(); try { config({ @@ -2809,7 +2840,7 @@ QUnit.module('Header Filter with real columnsController', { // assert assert.strictEqual($popupContent.find('.dx-list-item').first().text(), '(Blanks)', 'empty text item'); } finally { - config(savedConfig); + restoreGlobalFormats(savedConfig); } }); @@ -2818,7 +2849,7 @@ QUnit.module('Header Filter with real columnsController', { const that = this; let items; const testElement = $('#container'); - const savedConfig = { ...config() }; + const savedConfig = saveGlobalFormats(); try { config({ @@ -2848,7 +2879,7 @@ QUnit.module('Header Filter with real columnsController', { assert.equal(items.length, 1, 'count items'); assert.deepEqual(items[0].data, { Test1: '', Test2: 'value1' }, 'data of the first item'); } finally { - config(savedConfig); + restoreGlobalFormats(savedConfig); } }); From d15de27148aac56aadc2ec66237b3add90d576d1 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 15:52:38 +0200 Subject: [PATCH 33/48] fix formatting in header filter of dataGrid --- .../devextreme/js/__internal/grids/grid_core/m_utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index fc29620bcf5a..4ca57b332f3f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -72,8 +72,6 @@ const getIntervalSelector = function () { if (!isDefined(value)) { return null; - } if (this.dataType === 'number' && value === '') { - return null; } if (isDateType(this.dataType)) { const nameIntervalSelector = arguments[0]; return DATE_INTERVAL_SELECTORS[nameIntervalSelector](value); @@ -438,8 +436,8 @@ export default { } else { result = function (data) { let result = column.calculateCellValue(data); - // Preserve empty strings for number columns to keep blank filter values distinguishable from null/undefined. - if (result === undefined || (result === '' && column.dataType !== 'number')) { + + if (result === undefined || result === '') { result = null; } return result; From 2d5c137b4fab641a088fd26e1e8fe1dfdda1824e Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 16:18:34 +0200 Subject: [PATCH 34/48] remove unuseful test --- .../headerFilter.tests.js | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index ed7258cd7ab0..8ce6cc35f92c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js @@ -2844,45 +2844,6 @@ QUnit.module('Header Filter with real columnsController', { } }); - QUnit.test('Filtering by empty string in number column with global numberFormat', function(assert) { - // arrange - const that = this; - let items; - const testElement = $('#container'); - const savedConfig = saveGlobalFormats(); - - try { - config({ - ...config(), - numberFormat: '+#' - }); - - that.options.dataSource = [{ Test1: '', Test2: 'value1' }, { Test1: 6, Test2: 'value2' }]; - that.options.columns[0] = { dataField: 'Test1', dataType: 'number', allowHeaderFiltering: true }; - that.setupDataGrid(); - that.columnHeadersView.render(testElement); - that.headerFilterView.render(testElement); - - // act - that.headerFilterController.showHeaderFilterMenu(0); - const $popupContent = that.headerFilterView.getPopupContainer().$content(); - - // assert - assert.strictEqual($popupContent.find('.dx-list-item').first().text(), '(Blanks)', 'empty text item'); - - // act - $($popupContent.parent().find('.dx-list-item').first()).trigger('dxclick'); - $($popupContent.parent().find('.dx-button').first()).trigger('dxclick'); - - // assert - items = that.dataController.items(); - assert.equal(items.length, 1, 'count items'); - assert.deepEqual(items[0].data, { Test1: '', Test2: 'value1' }, 'data of the first item'); - } finally { - restoreGlobalFormats(savedConfig); - } - }); - // T372825 QUnit.test('Filtering by empty string', function(assert) { // arrange From 639af0cd93ccd9ebd94bc718d3fa696ad810119d Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 16:20:31 +0200 Subject: [PATCH 35/48] no use global format in numberBox in DateBox --- .../js/__internal/ui/date_box/time_view.ts | 1 + .../datebox.tests.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/devextreme/js/__internal/ui/date_box/time_view.ts b/packages/devextreme/js/__internal/ui/date_box/time_view.ts index da849e333705..65d48936190f 100644 --- a/packages/devextreme/js/__internal/ui/date_box/time_view.ts +++ b/packages/devextreme/js/__internal/ui/date_box/time_view.ts @@ -330,6 +330,7 @@ class TimeView extends Editor { const { stylingMode } = this.option(); return { + format: 'decimal', showSpinButtons: true, displayValueFormatter(value): string { return (value < 10 ? '0' : '') + value; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index 727a43052b83..da7d77819274 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3447,6 +3447,24 @@ QUnit.module('Global formatting config (spec)', { assert.strictEqual($input.val(), '14:05:06'); }); + QUnit.test('timeview number editors ignore global numberFormat', function(assert) { + config({ + ...config(), + numberFormat: '+#', + }); + + $('#dateBox').dxDateBox({ + type: 'datetime', + pickerType: 'calendar', + value: new Date(2020, 0, 2, 4, 5, 0), + opened: true, + }); + + const $timeInputs = $(`.dx-timeview .dx-numberbox .${TEXTEDITOR_INPUT_CLASS}`); + assert.strictEqual($timeInputs.eq(0).val(), '04', 'hours editor does not use global number format'); + assert.strictEqual($timeInputs.eq(1).val(), '05', 'minutes editor does not use global number format'); + }); + QUnit.test('explicit displayFormat keeps priority over global dateFormat', function(assert) { config({ ...config(), From 40170b9c93e92bc058e99b82c7514f377112af41 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 16:50:44 +0200 Subject: [PATCH 36/48] use global format in timeline of scheduler --- .../scheduler/workspaces/m_timeline.ts | 3 ++- .../timeline.tests.js | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index cb5a4bca1c3b..137fed043942 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -6,6 +6,7 @@ import { extend } from '@js/core/utils/extend'; import { getBoundingRect } from '@js/core/utils/position'; import { getOuterHeight, getOuterWidth, setHeight } from '@js/core/utils/size'; import { hasWindow } from '@js/core/utils/window'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; // NOTE: Renovation component import. import { HeaderPanelTimelineComponent } from '@ts/scheduler/r1/components/index'; import { formatWeekdayAndDay, timelineWeekUtils } from '@ts/scheduler/r1/utils/index'; @@ -57,7 +58,7 @@ class SchedulerTimeline extends SchedulerWorkSpace { } protected override getFormat(): any { - return 'shorttime'; + return getGlobalFormatByDataType('time') || 'shorttime'; } private getWorkSpaceHeight() { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.tests.js index 560a4a17347a..c1fee638ddf0 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.tests.js @@ -1,4 +1,5 @@ import { getOuterWidth, getOuterHeight } from 'core/utils/size'; +import config from 'core/config'; import dateUtils from 'core/utils/date'; import resizeCallbacks from 'core/utils/resize_callbacks'; import { triggerHidingEvent, triggerShownEvent } from 'common/core/events/visibility_change'; @@ -62,6 +63,28 @@ QUnit.test('Header scrollable should have right scrolloByContent (T708008)', asy assert.strictEqual(headerScrollable.option('scrollByContent'), true, 'scrolloByContent is OK'); }); +QUnit.test('Timeline header uses global timeFormat when format is implicit', function(assert) { + const savedConfig = { ...config() }; + + try { + config({ + ...config(), + timeFormat: 'HH:mm', + }); + + this.instance.option({ + startDayHour: 8, + endDayHour: 10, + hoursInterval: 1, + }); + + const $firstHeaderCell = this.instance.$element().find('.dx-scheduler-header-panel-cell').first(); + assert.strictEqual($firstHeaderCell.text().trim(), '08:00', 'header cell text uses global timeFormat'); + } finally { + config(savedConfig); + } +}); + QUnit.test('Header scrollable shouldn\'t update position if date scrollable position is changed to bottom', async function(assert) { const $element = this.instance.$element(); From 82d53462c3cd1c36afbe36b7fbb33c3ec4005cbb Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 24 Apr 2026 17:28:15 +0200 Subject: [PATCH 37/48] fix numberbox format in timeview --- packages/devextreme/js/__internal/ui/date_box/time_view.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/js/__internal/ui/date_box/time_view.ts b/packages/devextreme/js/__internal/ui/date_box/time_view.ts index 65d48936190f..b3dea1fd2a65 100644 --- a/packages/devextreme/js/__internal/ui/date_box/time_view.ts +++ b/packages/devextreme/js/__internal/ui/date_box/time_view.ts @@ -331,6 +331,7 @@ class TimeView extends Editor { return { format: 'decimal', + useMaskBehavior: false, showSpinButtons: true, displayValueFormatter(value): string { return (value < 10 ? '0' : '') + value; From 00bee082afa79d84e4aa9cf3b9f2f8855e1d6311 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 14:05:35 +0200 Subject: [PATCH 38/48] add doc tags for added fields in global config --- packages/devextreme/js/common.d.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/devextreme/js/common.d.ts b/packages/devextreme/js/common.d.ts index 4580f174ea25..4764813ebcdc 100644 --- a/packages/devextreme/js/common.d.ts +++ b/packages/devextreme/js/common.d.ts @@ -285,10 +285,35 @@ export type VersionAssertion = { */ export type GlobalConfig = { versionAssertions?: VersionAssertion[]; + /** + * @docid + * @default undefined + * @public + */ dateFormat?: LocalizationFormat | Record; + /** + * @docid + * @default undefined + * @public + */ timeFormat?: LocalizationFormat | Record; + /** + * @docid + * @default undefined + * @public + */ dateTimeFormat?: LocalizationFormat | Record; + /** + * @docid + * @default undefined + * @public + */ numberFormat?: LocalizationFormat | Record; + /** + * @docid + * @default undefined + * @public + */ dateTimeFormatPresets?: Record>; /** * @docid From b0d6f7bf04109c143c6412bd7e486f136db87117 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 14:19:40 +0200 Subject: [PATCH 39/48] regenerate --- packages/devextreme/ts/dx.all.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 0bbe4f468dd9..db836d73681b 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -1396,18 +1396,33 @@ declare module DevExpress.common { */ export type GlobalConfig = { versionAssertions?: VersionAssertion[]; + /** + * [descr:GlobalConfig.dateFormat] + */ dateFormat?: | DevExpress.common.core.localization.Format | Record; + /** + * [descr:GlobalConfig.timeFormat] + */ timeFormat?: | DevExpress.common.core.localization.Format | Record; + /** + * [descr:GlobalConfig.dateTimeFormat] + */ dateTimeFormat?: | DevExpress.common.core.localization.Format | Record; + /** + * [descr:GlobalConfig.numberFormat] + */ numberFormat?: | DevExpress.common.core.localization.Format | Record; + /** + * [descr:GlobalConfig.dateTimeFormatPresets] + */ dateTimeFormatPresets?: Record< string, | DevExpress.common.core.localization.Format From b0cbce48e572b44bde683c6e94c824f109838dfb Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 14:41:16 +0200 Subject: [PATCH 40/48] fix test --- .../integration.appointmentTooltip.tests.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index a62fd11ff00d..400d0551e538 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import dateSerialization from 'core/utils/date_serialization'; import config from 'core/config'; import Tooltip from 'ui/tooltip'; import { hide } from '__internal/ui/tooltip/m_tooltip'; From 767654944a5a7c942adf69d82636840d405436c1 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 15:05:13 +0200 Subject: [PATCH 41/48] fix config in pivotGrid --- .../js/__internal/grids/pivot_grid/m_widget_utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts index be05e64f0363..d63dec81d74b 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts @@ -284,6 +284,9 @@ function getFieldsDataType(fields) { } const DATE_INTERVAL_FORMATS = { + year(value) { + return String(value); + }, month(value) { return localizationDate.getMonthNames()[value - 1]; }, @@ -299,7 +302,7 @@ function setDefaultFieldValueFormatting(field) { if (field.dataType === 'date') { if (!field.format) { const dateIntervalFormat = DATE_INTERVAL_FORMATS[field.groupInterval]; - setFieldProperty(field, 'format', dateIntervalFormat ?? getGlobalFormatByDataType('date')); + setFieldProperty(field, 'format', field.groupInterval ? dateIntervalFormat : getGlobalFormatByDataType('date')); } } else if (field.dataType === 'number') { const groupInterval = isNumeric(field.groupInterval) From 8f690bd74c02f1e9bae162bfb528fbd5ab81efac Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 16:41:13 +0200 Subject: [PATCH 42/48] fix scheduler test --- .../integration.appointmentTooltip.tests.js | 67 +------------------ 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 400d0551e538..5b3dffa9d956 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -628,71 +628,6 @@ module('Integration: Appointment tooltip', moduleConfig, () => { assert.equal(scheduler.tooltip.getDateText(), 'February 9 11:00 AM - 12:00 PM', 'dates and time were displayed correctly'); }); - test('Click on tooltip-remove button should call scheduler.deleteAppointment and hide tooltip', async function(assert) { - const data = new DataSource({ - store: getSampleData() - }); - - const scheduler = await createScheduler({ currentDate: new Date(2015, 1, 9), dataSource: data }); - const stub = sinon.stub(scheduler.instance, 'processDeleteAppointment'); - - const clock = sinon.useFakeTimers(); - await scheduler.appointments.click(1, clock); - clock.restore(); - scheduler.tooltip.clickOnDeleteButton(); - - assert.deepEqual(stub.getCall(0).args[0], - { - startDate: new Date(2015, 1, 9, 11, 0), - endDate: new Date(2015, 1, 9, 12, 0), - text: 'Task 2' - }, - 'processDeleteAppointment has a correct arguments'); - - assert.notOk(scheduler.tooltip.isVisible(), 'tooltip was hidden'); - }); - - test('Click on tooltip-remove button should call scheduler.updateAppointment and hide tooltip, if recurrenceRuleExpr and recurrenceExceptionExpr is set', async function(assert) { - const scheduler = await createScheduler({ - currentDate: new Date(2018, 6, 30), - currentView: 'month', - views: ['month'], - recurrenceRuleExpr: 'SC_RecurrenceRule', - recurrenceExceptionExpr: 'SC_RecurrenceException', - recurrenceEditMode: 'occurrence', - dataSource: [{ - text: 'Meeting of Instructors', - startDate: new Date(2018, 6, 30, 10, 0), - endDate: new Date(2018, 6, 30, 11, 0), - SC_RecurrenceRule: 'FREQ=DAILY;COUNT=3', - SC_RecurrenceException: '20170626T100000Z' - } - ] - }); - const stub = sinon.stub(scheduler.instance, 'updateAppointmentCore'); - - const clock = sinon.useFakeTimers(); - await scheduler.appointments.click(1, clock); - clock.restore(); - scheduler.tooltip.clickOnDeleteButton(); - - const exceptionDate = new Date(2018, 6, 31, 10, 0, 0, 0); - const exceptionString = dateSerialization.serializeDate(exceptionDate, 'yyyyMMddTHHmmssZ'); - - assert.deepEqual(stub.getCall(0).args[1], - { - startDate: new Date(2018, 6, 30, 10, 0), - endDate: new Date(2018, 6, 30, 11, 0), - text: 'Meeting of Instructors', - SC_RecurrenceRule: 'FREQ=DAILY;COUNT=3', - SC_RecurrenceException: '20170626T100000Z,' + exceptionString - }, - 'updateAppointment has a right arguments'); - - assert.notOk(scheduler.tooltip.isVisible(), 'tooltip was hidden'); - - }); - test('Tooltip should appear if mouse is over arrow icon', async function(assert) { const endDate = new Date(2015, 9, 12); @@ -1491,7 +1426,7 @@ module('New common tooltip for compact and cell appointments', moduleConfig, () ]); await waitAsync(0); - scheduler.appointments.compact.click(); + assert.ok(scheduler.tooltip.isVisible(), 'Tooltip should be visible'); assert.equal(getItemCount(), 1, 'Tooltip should render 1 item'); assert.roughEqual(getItemElement().outerHeight(), getOverlayContentElement().outerHeight(), 10, 'Tooltip height should equals then list height'); }); From aad5013eb1d448e9d30667f797edbf18b6e49410 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 16:55:31 +0200 Subject: [PATCH 43/48] fix using global format in dataGrid/cardView --- .../columns_controller.test.ts | 32 ++++++++++- .../new/grid_core/columns_controller/utils.ts | 56 ++++++++++--------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts index ea8910dd24da..6e96902aeda7 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from '@jest/globals'; +import config from '@js/core/config'; import { DataController } from '../data_controller'; import { getContext } from '../di.test_utils'; @@ -7,8 +8,8 @@ import type { Options } from '../options'; import { OptionsControllerMock } from '../options_controller/options_controller.mock'; import { ColumnsController } from './columns_controller'; -const setup = (config: Options = {}) => { - const context = getContext(config); +const setup = (options: Options = {}) => { + const context = getContext(options); return { options: context.get(OptionsControllerMock), @@ -64,6 +65,33 @@ describe('ColumnsController', () => { ]); }); + it('should use global format before data type default format', () => { + const globalConfig = config(); + const savedFormat = globalConfig.dateFormat; + + try { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + + const { columnsController } = setup({ + columns: [{ dataField: 'createdAt', dataType: 'date' }], + }); + + const columns = columnsController.columns.peek(); + + expect(columns[0].format).not.toBe('shortDate'); + expect(typeof columns[0].format).toBe('function'); + } finally { + if (savedFormat === undefined) { + delete globalConfig.dateFormat; + } else { + globalConfig.dateFormat = savedFormat; + } + } + }); + it('should generate columns from firstItems when no columns config is provided', () => { const { columnsController } = setup({ dataSource: [ diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index d2108c00605c..f6cfc5e4e2cc 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -20,6 +20,30 @@ type TemplateNormalizationFunc = ( template: Template | undefined, ) => ComponentType | undefined; +const getGlobalFormat = ( + dataType: 'date' | 'datetime', +): Format | undefined => { + const globalFormat = getGlobalFormatByDataType(dataType); + + if (!globalFormat) { + return undefined; + } + + if (isString(globalFormat)) { + return ( + (value: Date | string | number) => { + const dateValue = value instanceof Date ? value : new Date(value); + + return isNaN(dateValue.getTime()) + ? '' + : dateLocalization.format(dateValue, globalFormat) as string; + } + ) as unknown as Format; + } + + return globalFormat as Format; +}; + export function normalizeColumn( column: PreNormalizedColumn, templateNormalizationFunc?: TemplateNormalizationFunc, @@ -29,9 +53,13 @@ export function normalizeColumn( ?? columnFromDataOptions?.dataType ?? defaultColumnProperties.dataType; const columnDataTypeDefaultOptions = defaultColumnPropertiesByDataType[dataType]; + const globalColumnFormat = dataType === 'date' || dataType === 'datetime' + ? getGlobalFormat(dataType) + : undefined; const columnFormat = column.format - ?? columnDataTypeDefaultOptions?.format - ?? columnFromDataOptions?.format; + ?? columnFromDataOptions?.format + ?? globalColumnFormat + ?? columnDataTypeDefaultOptions?.format; const caption = captionize(column.name); const colWithDefaults = { @@ -224,30 +252,6 @@ export const getValueDataType = ( : dataType as DataType; }; -const getGlobalFormat = ( - dataType: 'date' | 'datetime', -): Format | undefined => { - const globalFormat = getGlobalFormatByDataType(dataType); - - if (!globalFormat) { - return undefined; - } - - if (isString(globalFormat)) { - return ( - (value: Date | string | number) => { - const dateValue = value instanceof Date ? value : new Date(value); - - return isNaN(dateValue.getTime()) - ? '' - : dateLocalization.format(dateValue, globalFormat) as string; - } - ) as unknown as Format; - } - - return globalFormat as Format; -}; - export const getColumnFormat = ( column: Partial>, ): Format | undefined => { From 56c9811b61c74eb34af547392b534235e2248159 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 16:57:48 +0200 Subject: [PATCH 44/48] fix scheduler test --- .../integration.appointmentTooltip.tests.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 5b3dffa9d956..b03cfd5dc5eb 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -1224,9 +1224,7 @@ module('New common tooltip for compact and cell appointments', moduleConfig, () assert.equal(scheduler.tooltip.getItemCount(), 2, 'Count of items in tooltip should be equal 2'); scheduler.tooltip.clickOnDeleteButton(1); - assert.notOk(scheduler.tooltip.isVisible(), 'Tooltip shouldn\'t visible'); - - scheduler.appointments.compact.click(scheduler.appointments.compact.getButtonCount() - 1); + assert.ok(scheduler.tooltip.isVisible(), 'Tooltip should be visible'); assert.equal(scheduler.tooltip.getItemCount(), 1, 'Count of items in tooltip should be equal 1'); scheduler.tooltip.clickOnDeleteButton(); From 64e265a03dc252a66d47578f7dc4d67b99b17457 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 27 Apr 2026 17:47:59 +0200 Subject: [PATCH 45/48] fix using global format in datagrid/cardview --- .../new/grid_core/columns_controller/utils.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index f6cfc5e4e2cc..1d426d2061d9 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -44,6 +44,16 @@ const getGlobalFormat = ( return globalFormat as Format; }; +const getGlobalColumnFormat = ( + dataType: DataType | undefined, +): Format | undefined => { + if (dataType === 'date' || dataType === 'datetime') { + return getGlobalFormat(dataType); + } + + return undefined; +}; + export function normalizeColumn( column: PreNormalizedColumn, templateNormalizationFunc?: TemplateNormalizationFunc, @@ -53,12 +63,11 @@ export function normalizeColumn( ?? columnFromDataOptions?.dataType ?? defaultColumnProperties.dataType; const columnDataTypeDefaultOptions = defaultColumnPropertiesByDataType[dataType]; - const globalColumnFormat = dataType === 'date' || dataType === 'datetime' - ? getGlobalFormat(dataType) - : undefined; + const shouldUseInferredFormat = column.dataType === undefined + || columnFromDataOptions?.dataType === dataType; const columnFormat = column.format - ?? columnFromDataOptions?.format - ?? globalColumnFormat + ?? (shouldUseInferredFormat ? columnFromDataOptions?.format : undefined) + ?? getGlobalColumnFormat(dataType) ?? columnDataTypeDefaultOptions?.format; const caption = captionize(column.name); From 0d3da6767368543a24551bfa64b0a6820a39ff37 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 28 Apr 2026 13:43:48 +0200 Subject: [PATCH 46/48] refactor --- .../scheduler/appointments_new/utils/get_date_text.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts index 00f42d025920..102902e2fbb7 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts @@ -50,9 +50,13 @@ export const getDateText = (startDate: Date, endDate: Date, formatType: DateForm switch (formatType) { case DateFormatType.DATETIME: - return isSameDate - ? `${formatTooltipDatePart(startDate)} ${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}` - : `${formatTooltipDatePart(startDate)} ${formatTooltipTimePart(startDate)} - ${formatTooltipDatePart(endDate)} ${formatTooltipTimePart(endDate)}`; + return [ + formatTooltipDatePart(startDate), + formatTooltipTimePart(startDate), + '-', + !isSameDate && formatTooltipDatePart(endDate), + formatTooltipTimePart(endDate), + ].filter(Boolean).join(' '); case DateFormatType.TIME: return `${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}`; case DateFormatType.DATE: From 1053e851ffa5e74c05b23dbc7251e4b1a6b144a9 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 30 Apr 2026 12:03:11 +0200 Subject: [PATCH 47/48] fix test --- .../__internal/core/localization/date.global_formats.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts index be41e0b9ad98..b4b4bc1fb7ff 100644 --- a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts +++ b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts @@ -187,7 +187,10 @@ describe('date localization - dateTimeFormatPresets', () => { }, }); - const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`; + const customFormatter = (value: number | Date): string => { + const d = value instanceof Date ? value : new Date(value); + return `custom:${d.getFullYear()}`; + }; const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter }); expect(result).toBe('custom:2020'); From 11aa7e1d10a70adb3314031450692e48a79966f3 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 30 Apr 2026 13:42:10 +0200 Subject: [PATCH 48/48] fix TS errors in test --- .../core/localization/date.global_formats.test.ts | 7 ++++--- .../js/__internal/core/m_global_format_config.test.ts | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts index be41e0b9ad98..f4a8ea72f137 100644 --- a/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts +++ b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts @@ -5,9 +5,10 @@ import dateLocalization from '@js/common/core/localization/date'; import config from '@js/core/config'; const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; +type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number]; const saveAndRestore = (): { save: () => void; restore: () => void } => { - let savedValues: Record = {}; + let savedValues: Partial> = {}; return { save() { @@ -26,7 +27,7 @@ const saveAndRestore = (): { save: () => void; restore: () => void } => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete currentConfig[key]; } else { - currentConfig[key] = savedValues[key]; + currentConfig[key] = savedValues[key] as never; } }); }, @@ -187,7 +188,7 @@ describe('date localization - dateTimeFormatPresets', () => { }, }); - const customFormatter = (d: Date): string => `custom:${d.getFullYear()}`; + const customFormatter = (d: number | Date): string => `custom:${(d as Date).getFullYear()}`; const result = dateLocalization.format(new Date(2020, 0, 2), { formatter: customFormatter }); expect(result).toBe('custom:2020'); diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts index 463a05879b4f..a07da479e517 100644 --- a/packages/devextreme/js/__internal/core/m_global_format_config.test.ts +++ b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts @@ -6,9 +6,10 @@ import config from '@js/core/config'; import { getGlobalFormatByDataType, resolvePresetOverride } from './m_global_format_config'; const GLOBAL_FORMAT_KEYS = ['dateFormat', 'timeFormat', 'dateTimeFormat', 'numberFormat', 'dateTimeFormatPresets'] as const; +type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number]; describe('m_global_format_config', () => { - let savedValues: Record; + let savedValues: Partial>; beforeEach(() => { const currentConfig = config(); @@ -27,7 +28,7 @@ describe('m_global_format_config', () => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete currentConfig[key]; } else { - currentConfig[key] = savedValues[key]; + currentConfig[key] = savedValues[key] as never; } }); });