From c3117589b941903e418affcd252ce9c6ee2fb548 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Fri, 23 Jan 2026 11:48:06 +0100 Subject: [PATCH] FilterBuilder: fix invalid date formatting --- .../filter_builder/__tests__/utils.test.ts | 109 ++++++++++++++++++ .../js/__internal/filter_builder/m_utils.ts | 60 +++++++--- packages/devextreme/js/format_helper.js | 3 +- .../filterBuilderParts/utilsTests.js | 95 --------------- .../utils.formatHelper.tests.js | 86 +++++++++++++- 5 files changed, 240 insertions(+), 113 deletions(-) create mode 100644 packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts diff --git a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts new file mode 100644 index 000000000000..6ec3ca1ec562 --- /dev/null +++ b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from '@jest/globals'; +import type { CustomOperation, Field } from '@js/ui/filter_builder'; + +import { getCurrentValueText } from '../m_utils'; + +describe('Formatting', () => { + it('empty string', () => { + const field = {}; + const value = ''; + + expect(getCurrentValueText(field, value, null)).toBe(''); + }); + + it('string', () => { + const field = {}; + const value = 'Text'; + + expect(getCurrentValueText(field, value, null)).toBe('Text'); + }); + + it('shortDate', () => { + const field = { format: 'shortDate' }; + const value = new Date(2017, 8, 5); + + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017'); + }); + + it('invalid date string (T1319193)', () => { + const field = { format: 'shortDate' }; + const dateString = 'Weekend'; + + expect(getCurrentValueText(field, dateString, null)).toBe(dateString); + }); + + it('boolean', () => { + const field: Field = { dataType: 'boolean' }; + let value = true; + + expect(getCurrentValueText(field, value, null)).toBe('true'); + + value = false; + expect(getCurrentValueText(field, value, null)).toBe('false'); + + field.falseText = 'False Text'; + expect(getCurrentValueText(field, value, null)).toBe('False Text'); + }); + + it('field.customizeText', () => { + const field: Field = { + customizeText(conditionInfo) { + return `${conditionInfo.valueText}Test`; + }, + }; + const value = 'MyValue'; + + expect(getCurrentValueText(field, value, null)).toBe('MyValueTest'); + }); + + it('customOperation.customizeText', () => { + const field: Field = { + customizeText(conditionInfo) { + return `${conditionInfo.valueText}Test`; + }, + }; + const value = 'MyValue'; + const customOperation: CustomOperation = { + customizeText(conditionInfo) { + return `${conditionInfo.valueText}CustomOperation`; + }, + }; + + expect(getCurrentValueText(field, value, customOperation)).toBe('MyValueTestCustomOperation'); + }); + + it('customOperation.customizeText for array', async () => { + const field: Field = { dataType: 'string' }; + + const customOperation = { customizeText: (): string => '(Blanks)' }; + let text = await getCurrentValueText(field, '', customOperation); + + expect(text).toBe(''); + + text = await getCurrentValueText(field, [null], customOperation); + expect(text).toEqual(['(Blanks)']); + + const field2: Field = { dataType: 'number' }; + + text = await getCurrentValueText(field2, null, customOperation); + + expect(text).toBe(''); + + text = await getCurrentValueText(field, [null], customOperation); + expect(text).toEqual(['(Blanks)']); + }); + + it('default format for date', () => { + const field: Field = { dataType: 'date' }; + const value = new Date(2017, 8, 5, 12, 30, 0); + + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017'); + }); + + it('default format for datetime', () => { + const field: Field = { dataType: 'datetime' }; + const value = new Date(2017, 8, 5, 12, 30, 0); + + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017, 12:30 PM'); + }); +}); diff --git a/packages/devextreme/js/__internal/filter_builder/m_utils.ts b/packages/devextreme/js/__internal/filter_builder/m_utils.ts index 8fbbbbe909a6..0a09c98fb812 100644 --- a/packages/devextreme/js/__internal/filter_builder/m_utils.ts +++ b/packages/devextreme/js/__internal/filter_builder/m_utils.ts @@ -1,19 +1,26 @@ +import type { Format } from '@js/common/core/localization'; import messageLocalization from '@js/common/core/localization/message'; import { DataSource } from '@js/common/data/data_source/data_source'; import { errors as dataErrors } from '@js/common/data/errors'; import $ from '@js/core/renderer'; import { compileGetter } from '@js/core/utils/data'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { captionize } from '@js/core/utils/inflector'; -import { isBoolean, isDefined, isFunction } from '@js/core/utils/type'; +import { + isBoolean, isDefined, isFunction, isNumeric, isString, +} from '@js/core/utils/type'; 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 { getConfig } from './m_between'; import filterOperationsDictionary from './m_filter_operations_dictionary'; +type FieldValue = string | number | boolean | Date | null | undefined; + const DEFAULT_DATA_TYPE = 'string'; const EMPTY_MENU_ICON = 'icon-none'; const AND_GROUP_OPERATION = 'and'; @@ -27,7 +34,7 @@ const DATATYPE_OPERATIONS = { boolean: ['=', '<>', 'isblank', 'isnotblank'], object: ['isblank', 'isnotblank'], }; -const DEFAULT_FORMAT = { +const DEFAULT_FORMAT: Partial> = { date: 'shortDate', datetime: 'shortDateShortTime', }; @@ -54,19 +61,24 @@ const FILTER_BUILDER_ITEM_TEXT_PART_CLASS = `${FILTER_BUILDER_ITEM_TEXT_CLASS}-p const FILTER_BUILDER_ITEM_TEXT_SEPARATOR_CLASS = `${FILTER_BUILDER_ITEM_TEXT_CLASS}-separator`; const FILTER_BUILDER_ITEM_TEXT_SEPARATOR_EMPTY_CLASS = `${FILTER_BUILDER_ITEM_TEXT_SEPARATOR_CLASS}-empty`; -function getFormattedValueText(field, value): string { - const fieldFormat = field.format || DEFAULT_FORMAT[field.dataType]; +function getDateFormat(dataType: DataType | undefined): Format | undefined { + return dataType ? DEFAULT_FORMAT[dataType] : undefined; +} + +function getFormattedValueText(field: Field, value: FieldValue): string { + const fieldFormat = field.format ?? getDateFormat(field.dataType); if (isBoolean(value)) { - const trueText: string = field.trueText || messageLocalization.format('dxDataGrid-trueText'); - const falseText: string = field.falseText || messageLocalization.format('dxDataGrid-falseText'); + const trueText = field.trueText ?? messageLocalization.format('dxDataGrid-trueText'); + const falseText = field.falseText ?? messageLocalization.format('dxDataGrid-falseText'); return value ? trueText : falseText; } if (field.dataType === 'date' || field.dataType === 'datetime') { - // value can be string or number, we need to convert it to Date object - return formatHelper.format(new Date(value), fieldFormat); + if (isString(value) || isNumeric(value)) { + return formatHelper.format(new Date(value), fieldFormat); + } } return formatHelper.format(value, fieldFormat); @@ -574,11 +586,18 @@ export function getCurrentLookupValueText(field, value, handler) { } } -function getPrimitiveValueText(field, value, customOperation, target, options?) { +function getPrimitiveValueText( + field: Field, + value: FieldValue, + customOperation: CustomOperation | null, + target: string, + options?, +): string { let valueText = getFormattedValueText(field, value); if (field.customizeText) { valueText = field.customizeText.call(field, { + // @ts-expect-error value, valueText, target, @@ -591,32 +610,43 @@ function getPrimitiveValueText(field, value, customOperation, target, options?) valueText, field, target, + // @ts-expect-error }, options); } return valueText; } -function getArrayValueText(field, value, customOperation, target) { +function getArrayValueText( + field: Field, + value: FieldValue[], + customOperation: CustomOperation | null, + target: string, +): string[] { const options = { values: value }; return value.map((v) => getPrimitiveValueText(field, v, customOperation, target, options)); } -function checkDefaultValue(value) { +function checkDefaultValue(value: FieldValue | FieldValue[]): value is '' | null { return value === '' || value === null; } -export function getCurrentValueText(field, value, customOperation, target = 'filterBuilder') { +export function getCurrentValueText( + field: Field, + value: FieldValue | FieldValue[], + customOperation: CustomOperation | null, + target = 'filterBuilder', +): string | DeferredObj { if (checkDefaultValue(value)) { return ''; } if (Array.isArray(value)) { // @ts-expect-error Deferred has badly typed ctor function - const result = new Deferred(); + const result: DeferredObj = new Deferred(); when.apply(this, getArrayValueText(field, value, customOperation, target)).done((...args) => { - const text = args.some((item) => !checkDefaultValue(item)) - ? args.map((item) => (!checkDefaultValue(item) ? item : '?')) + const text: string | string[] = (args as string[]).some((item) => !checkDefaultValue(item)) + ? (args as string[]).map((item) => (!checkDefaultValue(item) ? item : '?')) : ''; result.resolve(text); }); diff --git a/packages/devextreme/js/format_helper.js b/packages/devextreme/js/format_helper.js index de05bc7ec2a0..c748112c1b69 100644 --- a/packages/devextreme/js/format_helper.js +++ b/packages/devextreme/js/format_helper.js @@ -16,8 +16,7 @@ import './common/core/localization/currency'; export default dependencyInjector({ format: function(value, format) { const formatIsValid = isString(format) && format !== '' || isPlainObject(format) || isFunction(format); - const valueIsValid = isNumeric(value) || isDate(value); - + const valueIsValid = isNumeric(value) || (isDate(value) && !isNaN(value.getTime())); if(!formatIsValid || !valueIsValid) { return isDefined(value) ? value.toString() : ''; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js index 94ace09cb88d..135475be34e1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js @@ -1506,101 +1506,6 @@ QUnit.module('Custom filter expressions', { }); }); -QUnit.module('Formatting', function() { - QUnit.test('empty string', function(assert) { - const field = {}; - const value = ''; - - assert.equal(utils.getCurrentValueText(field, value), ''); - }); - - QUnit.test('string', function(assert) { - const field = {}; - const value = 'Text'; - assert.equal(utils.getCurrentValueText(field, value), 'Text'); - }); - - QUnit.test('shortDate', function(assert) { - const field = { format: 'shortDate' }; - const value = new Date(2017, 8, 5); - assert.equal(utils.getCurrentValueText(field, value), '9/5/2017'); - }); - - QUnit.test('boolean', function(assert) { - const field = { dataType: 'boolean' }; - let value = true; - assert.equal(utils.getCurrentValueText(field, value), 'true'); - - value = false; - assert.equal(utils.getCurrentValueText(field, value), 'false'); - - field.falseText = 'False Text'; - assert.equal(utils.getCurrentValueText(field, value), 'False Text'); - }); - - QUnit.test('field.customizeText', function(assert) { - const field = { - customizeText: function(conditionInfo) { - return conditionInfo.valueText + 'Test'; - } - }; - const value = 'MyValue'; - assert.equal(utils.getCurrentValueText(field, value), 'MyValueTest'); - }); - - QUnit.test('customOperation.customizeText', function(assert) { - const field = { - customizeText: function(conditionInfo) { - return conditionInfo.valueText + 'Test'; - } - }; - const value = 'MyValue'; - const customOperation = { - customizeText: function(conditionInfo) { - return conditionInfo.valueText + 'CustomOperation'; - } - }; - assert.equal(utils.getCurrentValueText(field, value, customOperation), 'MyValueTestCustomOperation'); - }); - - QUnit.test('customOperation.customizeText for array', function(assert) { - const field = { dataType: 'string' }; - - const customOperation = { customizeText: (fieldInfo) => '(Blanks)' }; - let text = utils.getCurrentValueText(field, '', customOperation); - - assert.equal(text, ''); - - text = utils.getCurrentValueText(field, [null], customOperation).done(text => { - assert.equal(text, '(Blanks)'); - }); - - const field2 = { dataType: 'number' }; - - text = utils.getCurrentValueText(field2, null, customOperation); - - assert.equal(text, ''); - - text = utils.getCurrentValueText(field, [null], customOperation).done(text => { - assert.equal(text, '(Blanks)'); - }); - }); - - QUnit.test('default format for date', function(assert) { - const field = { dataType: 'date' }; - const value = new Date(2017, 8, 5, 12, 30, 0); - - assert.equal(utils.getCurrentValueText(field, value), '9/5/2017'); - }); - - QUnit.test('default format for datetime', function(assert) { - const field = { dataType: 'datetime' }; - const value = new Date(2017, 8, 5, 12, 30, 0); - - assert.equal(utils.getCurrentValueText(field, value), '9/5/2017, 12:30 PM'); - }); -}); - QUnit.module('Lookup Value', function() { QUnit.test('array of strings & value=empty', function(assert) { const field = { diff --git a/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js b/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js index f7a1e1f7aedf..abaf4dbdfcf5 100644 --- a/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js @@ -1,4 +1,5 @@ -const formatHelper = require('format_helper'); +import formatHelper from 'format_helper'; + const getDateFormatByTickInterval = formatHelper.getDateFormatByTickInterval; function checkDateWithFormat(date, format, expected, assert) { @@ -418,3 +419,86 @@ QUnit.test('Case insensitive', function(assert) { checkDateWithFormat(date1, getDateFormatByTickInterval(date1, date2, 'seCond'), '8:22:30 AM', assert); checkDateWithFormat(date1, getDateFormatByTickInterval(date1, date2, 'miLLisecond'), '333', assert); }); + +QUnit.module('Format method', function() { + QUnit.test('format with invalid format should return value as string', function(assert) { + assert.equal(formatHelper.format(123, ''), '123', 'Empty string format returns value.toString()'); + assert.equal(formatHelper.format(123, null), '123', 'Null format returns value.toString()'); + assert.equal(formatHelper.format(123, undefined), '123', 'Undefined format returns value.toString()'); + }); + + QUnit.test('format with invalid value should return value as string or empty string', function(assert) { + assert.equal(formatHelper.format('text', 'decimal'), 'text', 'String value with numeric format returns value.toString()'); + assert.equal(formatHelper.format(null, 'decimal'), '', 'Null value returns empty string'); + assert.equal(formatHelper.format(undefined, 'decimal'), '', 'Undefined value returns empty string'); + }); + + QUnit.test('format with invalid Date should return value as string', function(assert) { + const invalidDate = new Date('weekend'); + assert.equal(formatHelper.format(invalidDate, 'shortDate'), 'Invalid Date', 'Invalid Date returns "Invalid Date" string'); + }); + + QUnit.test('format numeric value with string format', function(assert) { + assert.equal(formatHelper.format(1234.5, 'decimal'), '1234.5', 'Decimal format'); + assert.equal(formatHelper.format(0.45, 'percent'), '45%', 'Percent format'); + assert.equal(formatHelper.format(1234.5, 'currency'), '$1,235', 'Currency format'); + }); + + QUnit.test('format numeric value with object format', function(assert) { + assert.equal(formatHelper.format(1234.567, { type: 'fixedPoint', precision: 2 }), '1,234.57', 'FixedPoint with precision'); + assert.equal(formatHelper.format(0.123, { type: 'percent', precision: 1 }), '12.3%', 'Percent with precision'); + }); + + QUnit.test('format numeric value with function format', function(assert) { + const customFormat = function(value) { + return 'Custom: ' + value; + }; + assert.equal(formatHelper.format(123, customFormat), 'Custom: 123', 'Function format'); + }); + + QUnit.test('format date value with string format', function(assert) { + const date = new Date(2017, 8, 5, 14, 30, 45); + assert.equal(formatHelper.format(date, 'shortDate'), '9/5/2017', 'Short date format'); + assert.equal(formatHelper.format(date, 'shortTime'), '2:30 PM', 'Short time format'); + }); + + QUnit.test('format date value with object format', function(assert) { + const date = new Date(2017, 8, 5, 14, 30, 45); + assert.equal(formatHelper.format(date, { type: 'shortDate' }), '9/5/2017', 'Short date format object'); + assert.equal(formatHelper.format(date, { type: 'longDate' }), 'Tuesday, September 5, 2017', 'Long date format object'); + }); + + QUnit.test('format date value with function format', function(assert) { + const date = new Date(2017, 8, 5); + const customFormat = function(value) { + return 'Year: ' + value.getFullYear(); + }; + assert.equal(formatHelper.format(date, customFormat), 'Year: 2017', 'Function format for date'); + }); + + QUnit.test('format zero value', function(assert) { + assert.equal(formatHelper.format(0, 'decimal'), '0', 'Zero with decimal format'); + assert.equal(formatHelper.format(0, 'percent'), '0%', 'Zero with percent format'); + }); + + QUnit.test('format negative value', function(assert) { + assert.equal(formatHelper.format(-123.45, 'fixedPoint'), '-123', 'Negative number with fixedPoint'); + assert.equal(formatHelper.format(-0.5, 'percent'), '-50%', 'Negative percent'); + }); + + QUnit.test('format with custom format pattern', function(assert) { + assert.equal(formatHelper.format(1234.5, '#,##0.00'), '1,234.50', 'Custom number pattern'); + const date = new Date(2017, 8, 5); + assert.equal(formatHelper.format(date, 'yyyy-MM-dd'), '2017-09-05', 'Custom date pattern'); + }); + + QUnit.test('format empty string value', function(assert) { + assert.equal(formatHelper.format('', 'decimal'), '', 'Empty string returns empty string'); + }); + + QUnit.test('format boolean value should return string', function(assert) { + assert.equal(formatHelper.format(true, 'decimal'), 'true', 'Boolean true returns "true"'); + assert.equal(formatHelper.format(false, 'decimal'), 'false', 'Boolean false returns "false"'); + }); +}); +