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..bea314f8629f --- /dev/null +++ b/packages/devextreme/js/__internal/core/localization/date.global_formats.test.ts @@ -0,0 +1,275 @@ +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; +type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number]; + +const saveAndRestore = (): { save: () => void; restore: () => void } => { + let savedValues: Partial> = {}; + + 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] as never; + } + }); + }, + }; +}; + +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 = (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'); + }); + + 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/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; 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/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 74de307bb315..40bf2d115c0f 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, licenseKey: '/* ___$$$$$___devextreme___lcp___placeholder____$$$$$ */', floatingActionButtonConfig: { 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..1f8d66de4482 --- /dev/null +++ b/packages/devextreme/js/__internal/core/m_global_format_config.js @@ -0,0 +1,85 @@ +import config from '@js/core/config'; +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'; + +const hasOwn = Object.prototype.hasOwnProperty; + +const resolveByLocaleMap = (localeMap) => { + let currentLocale = coreLocalization.locale(); + + 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 resolveConfigValue = (value) => { + if(value === undefined) { + return undefined; + } + + if(isString(value) || isFunction(value)) { + return value; + } + + 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': + return resolveGlobalFormat('dateFormat'); + case 'datetime': + return resolveGlobalFormat('dateTimeFormat'); + case 'time': + return resolveGlobalFormat('timeFormat'); + case 'number': + return resolveGlobalFormat('numberFormat'); + default: + return undefined; + } +}; + +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..a07da479e517 --- /dev/null +++ b/packages/devextreme/js/__internal/core/m_global_format_config.test.ts @@ -0,0 +1,207 @@ +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; +type GlobalFormatKey = typeof GLOBAL_FORMAT_KEYS[number]; + +describe('m_global_format_config', () => { + let savedValues: Partial>; + + 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] as never; + } + }); + }); + + 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/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/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/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index e61cbd284ab3..4ca57b332f3f 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'; @@ -19,6 +20,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'; @@ -83,6 +85,21 @@ function isDateType(dataType) { return dataType === 'date' || dataType === 'datetime'; } +const getGlobalFormat = (dataType) => { + const globalFormat = getGlobalFormatByDataType(dataType); + + if (!globalFormat) { + return undefined; + } + + return isString(globalFormat) + ? (value) => { + const dateValue = value instanceof Date ? value : new Date(value); + return isNaN(dateValue.getTime()) ? '' : dateLocalization.format(dateValue, globalFormat); + } + : globalFormat; +}; + const setEmptyText = function ($container) { $container.get(0).textContent = '\u00A0'; }; @@ -389,9 +406,9 @@ export default { getFormatByDataType(dataType) { switch (dataType) { case 'date': - return 'shortDate'; + return getGlobalFormat('date') || 'shortDate'; case 'datetime': - return 'shortDateShortTime'; + return getGlobalFormat('datetime') || 'shortDateShortTime'; default: return undefined; } @@ -419,6 +436,7 @@ export default { } else { result = function (data) { let result = column.calculateCellValue(data); + if (result === undefined || result === '') { result = null; } 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 32a1ecfc4fa8..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 @@ -1,10 +1,12 @@ 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 { 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'; @@ -18,6 +20,40 @@ 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; +}; + +const getGlobalColumnFormat = ( + dataType: DataType | undefined, +): Format | undefined => { + if (dataType === 'date' || dataType === 'datetime') { + return getGlobalFormat(dataType); + } + + return undefined; +}; + export function normalizeColumn( column: PreNormalizedColumn, templateNormalizationFunc?: TemplateNormalizationFunc, @@ -27,9 +63,12 @@ export function normalizeColumn( ?? columnFromDataOptions?.dataType ?? defaultColumnProperties.dataType; const columnDataTypeDefaultOptions = defaultColumnPropertiesByDataType[dataType]; + const shouldUseInferredFormat = column.dataType === undefined + || columnFromDataOptions?.dataType === dataType; const columnFormat = column.format - ?? columnDataTypeDefaultOptions?.format - ?? columnFromDataOptions?.format; + ?? (shouldUseInferredFormat ? columnFromDataOptions?.format : undefined) + ?? getGlobalColumnFormat(dataType) + ?? columnDataTypeDefaultOptions?.format; const caption = captionize(column.name); const colWithDefaults = { @@ -229,8 +268,12 @@ export const getColumnFormat = ( return column.format; } - if (column.dataType === 'date' || column.dataType === 'datetime') { - return 'shortDate'; + if (column.dataType === 'date') { + return getGlobalFormat('date') || 'shortDate'; + } + + if (column.dataType === 'datetime') { + return getGlobalFormat('datetime') || 'shortDateShortTime'; } return undefined; 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..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 @@ -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'; @@ -283,6 +284,9 @@ function getFieldsDataType(fields) { } const DATE_INTERVAL_FORMATS = { + year(value) { + return String(value); + }, month(value) { return localizationDate.getMonthNames()[value - 1]; }, @@ -297,7 +301,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', field.groupInterval ? dateIntervalFormat : getGlobalFormatByDataType('date')); } } else if (field.dataType === 'number') { const groupInterval = isNumeric(field.groupInterval) 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/appointments_new/utils/get_date_text.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts index 4e0d893da903..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 @@ -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,21 @@ 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), - ' ', - dateLocalization.format(startDate, timeFormat), - ' - ', - isSameDate ? '' : `${dateLocalization.format(endDate, dateFormat)} `, - dateLocalization.format(endDate, timeFormat), - ].join(''); + formatTooltipDatePart(startDate), + formatTooltipTimePart(startDate), + '-', + !isSameDate && formatTooltipDatePart(endDate), + formatTooltipTimePart(endDate), + ].filter(Boolean).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/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/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; +}; 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/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index c9c53c988640..f94ffa754d43 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.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.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/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/__internal/ui/date_box/time_view.ts b/packages/devextreme/js/__internal/ui/date_box/time_view.ts index da849e333705..b3dea1fd2a65 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,8 @@ class TimeView extends Editor { const { stylingMode } = this.option(); return { + format: 'decimal', + useMaskBehavior: false, showSpinButtons: true, displayValueFormatter(value): string { return (value < 10 ? '0' : '') + value; 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..1cf95283b5e5 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 = String(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); 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/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/js/common.d.ts b/packages/devextreme/js/common.d.ts index 2ac75ae9b8a8..4764813ebcdc 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,36 @@ 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 * @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() : ''; } 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..b78eaa959015 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,181 @@ 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, + 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; + } + }); + }; + + QUnit.test('global dateFormat supports formatter function values', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...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 { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('global dateTimeFormat supports formatter function values', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...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 { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('global numberFormat supports formatter function values', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + numberFormat: (value) => `#${value.toFixed(2)}`, + }); + + assert.strictEqual(numberLocalization.format(12.3), '#12.30'); + } finally { + 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', () => { 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 2dd99f420fdd..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 @@ -5522,3 +5522,154 @@ QUnit.module('Formatting', baseModuleConfig, () => { }), '216 rub'); }); }); + +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, + 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; + } + }); + }; + + QUnit.test('implicit date format uses global dateFormat', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + + const format = gridCore.getFormatByDataType('date'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2), { format }); + + assert.strictEqual(dateText, '02/01/2020', 'global date format is applied for implicit DataGrid date format'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('implicit datetime format uses global dateTimeFormat', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const format = gridCore.getFormatByDataType('datetime'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2, 14, 5), { format }); + + assert.strictEqual(dateText, '02/01/2020, 14:05', 'global datetime format is applied for implicit DataGrid datetime format'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('explicit column.format keeps priority over global format', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateFormat: '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, '1/2/2020', 'explicit preset format is not replaced by global dateFormat'); + } finally { + 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.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index 545a781123a6..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 @@ -17,10 +17,34 @@ 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'; +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'); @@ -803,6 +827,47 @@ 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 = saveGlobalFormats(); + + 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(); + + 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'); + + 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'); + + 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 { + restoreGlobalFormats(savedConfig); + } + }); + // T274290 QUnit.test('Header filter with items when column lookup with simple types', function(assert) { // arrange @@ -2751,6 +2816,34 @@ 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 = saveGlobalFormats(); + + 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 { + restoreGlobalFormats(savedConfig); + } + }); + // T372825 QUnit.test('Filtering by empty string', function(assert) { // arrange 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..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 @@ -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!'; @@ -3373,3 +3373,190 @@ 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() { + const globalConfig = config(); + this.defaultOptions = DateBox.defaultOptions; + this.savedGlobalFormats = { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; + }, + afterEach: function() { + const globalConfig = config(); + Object.keys(this.savedGlobalFormats).forEach((key) => { + const value = this.savedGlobalFormats[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + DateBox.defaultOptions(this.defaultOptions || {}); + }, +}, () => { + QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + + 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(), '02/01/2020'); + }); + + QUnit.test('implicit datetime displayFormat uses global dateTimeFormat', function(assert) { + config({ + ...config(), + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'datetime', + value: new Date(2020, 0, 2, 14, 5), + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + 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('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(), + dateFormat: 'dd/MM/yyyy', + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + displayFormat: 'shortDate', + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '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 $element = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 3), + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.val(), '03/01/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('dateFormat takes priority over dateTimeFormatPresets.shortDate for implicit DateBox', function(assert) { + config({ + ...config(), + dateFormat: 'yyyy-MM-dd', + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + // dateFormat wins over dateTimeFormatPresets for implicit case + assert.strictEqual($input.val(), '2020-01-02'); + }); + + QUnit.test('explicit displayFormat: "shortDate" uses dateTimeFormatPresets override', function(assert) { + config({ + ...config(), + dateTimeFormatPresets: { + shortDate: 'dd/MM/yyyy', + }, + }); + + const $element = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + displayFormat: 'shortDate', + pickerType: 'calendar', + }); + const $input = $element.find(`.${TEXTEDITOR_INPUT_CLASS}`); + + 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'); + }); +}); 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 f2696f84b3a2..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 @@ -1,4 +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'; @@ -34,6 +36,88 @@ const moduleConfig = { } }; +module('Global formatting config (spec): Scheduler tooltip', { + beforeEach() { + fx.off = true; + const globalConfig = config(); + this.savedGlobalFormats = { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + dateTimeFormatPresets: globalConfig.dateTimeFormatPresets, + }; + }, + afterEach() { + fx.off = false; + hide(); + 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({ + 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({ + ...config(), + 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 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(), '11:00 AM - 12:00 PM'); + }); + + test('implicit Scheduler tooltip date/time use global dateFormat and timeFormat', async function(assert) { + config({ + ...config(), + dateFormat: (date) => `Date${date.getDate()}`, + timeFormat: (date) => `Time${date.getHours()}`, + }); + + 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(), 'Date9 Time23 - Date10 Time1'); + }); +}); + module('Integration: Appointment tooltip', moduleConfig, () => { const createScheduler = (options) => createWrapper($.extend(options, { height: 600 })); const getDeltaTz = (schedulerTz, date) => schedulerTz * 3600000 + date.getTimezoneOffset() * 60000; 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(); 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..7138b4ecfdec 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,83 @@ 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 = saveGlobalFormats(); + }, + afterEach: function() { + restoreGlobalFormats(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: { + default: { type: 'fixedPoint', precision: 2 } + } + }); + + this.testTickLabelFormat(assert, [1500], 100, ['1,500.00']); +}); + +QUnit.test('local axis label.format keeps priority over global numberFormat', function(assert) { + config({ + ...config(), + numberFormat: { + default: { type: 'fixedPoint', precision: 2 } + } + }); + + this.testFormat(assert, { + label: { + visible: true, + format: 'thousands' + } + }, [1500], 100, ['2K']); +}); + +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) { diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 800dc60bc087..38fb84454a95 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -1396,6 +1396,38 @@ 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 + | Record + >; /** * [descr:GlobalConfig.decimalSeparator] * @deprecated [depNote:GlobalConfig.decimalSeparator]