From 4c61dc602072c205f2926e0999ac3024ef79a68e Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 28 May 2026 13:12:09 +0300 Subject: [PATCH 01/12] Fix filter command --- .../commands/executeGridAssistant.test.ts | 24 ++++++++++++++++++ .../commands/executeGridAssistant.ts | 25 ++++++++++++++++--- .../commands/__tests__/filtering.test.ts | 15 +++++++++++ .../ai_assistant/commands/filtering.ts | 8 +++--- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts index 137155d3868f..d90e8a6e9bfd 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts @@ -118,6 +118,30 @@ describe('ExecuteGridAssistantCommand', () => { actions: [{ name: 'sort', args: { columnName: 'name', sortOrder: 'asc' } }], }); }); + + it('should convert AIDate strings to Date objects in string response', () => { + const response = '{"actions":[{"name":"filterValue","args":{"expression":{"field":"SaleDate","operator":"=","value":"AIDate(2024, 5, 10)"}}}]}'; + // @ts-expect-error Access to protected property for a test + const result = command.parseResult(response); + + const { args } = result.actions[0]; + const expression = args.expression as Record; + expect(expression.value).toBeInstanceOf(Date); + expect(expression.value).toStrictEqual(new Date(2024, 4, 10)); + }); + + it('should convert AIDate strings to Date objects in stringified actions', () => { + const response = { + actions: '[{"name":"filterValue","args":{"expression":{"field":"SaleDate","operator":"=","value":"AIDate(2024, 12, 1)"}}}]', + }; + // @ts-expect-error Access to protected property for a test + const result = command.parseResult(response); + + const { args } = result.actions[0]; + const expression = args.expression as Record; + expect(expression.value).toBeInstanceOf(Date); + expect(expression.value).toStrictEqual(new Date(2024, 11, 1)); + }); }); describe('execute', () => { diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts index 4dea8c208a54..d15e5d044ff9 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts @@ -6,6 +6,26 @@ import type { import { BaseCommand } from '@ts/core/ai_integration/commands/base'; import type { PromptData, PromptTemplateName } from '@ts/core/ai_integration/core/prompt_manager'; +/** + * Matches "AIDate(year, month, day)" format used by the filtering command. + * All components are 1-based. + */ +const AI_DATE_REGEX = /^AIDate\((\d+),\s*(\d+),\s*(\d+)\)$/; + +function parseDates(_key: string, value: unknown): unknown { + if (typeof value === 'string') { + const match = AI_DATE_REGEX.exec(value); + if (match) { + return new Date( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + ); + } + } + return value; +} + export class ExecuteGridAssistantCommand extends BaseCommand< ExecuteGridAssistantCommandParams, ExecuteGridAssistantCommandResult @@ -23,7 +43,6 @@ export class ExecuteGridAssistantCommand extends BaseCommand< }; } - // TODO: check response more carefully protected parseResult( response: ExecuteGridAssistantCommandResponse, ): ExecuteGridAssistantCommandResult { @@ -31,11 +50,11 @@ export class ExecuteGridAssistantCommand extends BaseCommand< if (response === '') { return { actions: [] }; } - return JSON.parse(response) as ExecuteGridAssistantCommandResult; + return JSON.parse(response, parseDates) as ExecuteGridAssistantCommandResult; } const actions = typeof response.actions === 'string' - ? JSON.parse(response.actions) + ? JSON.parse(response.actions, parseDates) : response.actions; return { actions }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts index 26980fc12d33..a56a9759ddd1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts @@ -113,6 +113,7 @@ describe('filterValueCommand', () => { [singleBasic('name', '=', 1)], [singleBasic('name', '=', true)], [singleBasic('name', '=', null)], + [singleBasic('name', '=', new Date(2024, 4, 10))], ])('accepts scalar value %p', (expression) => { expect(filterValueCommand.schema.safeParse({ expression }).success).toBe(true); }); @@ -240,6 +241,20 @@ describe('filterValueCommand', () => { expect(result.status).toBe('success'); }); + it('passes Date values through to the filter array', async () => { + const instance = await createGrid(); + const spy = jest.spyOn(instance, 'option'); + const callbacks = createCallbacks(); + const date = new Date(2024, 4, 10); + + const result = await filterValueCommand.execute(instance, callbacks)({ + expression: singleBasic('SaleDate', '=', date), + }); + + expect(spy).toHaveBeenCalledWith('filterValue', ['SaleDate', '=', date]); + expect(result.status).toBe('success'); + }); + it('converts a combined node into the legacy array form', async () => { const instance = await createGrid(); const spy = jest.spyOn(instance, 'option'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts index ad1437533a17..d176f7075998 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts @@ -13,7 +13,7 @@ interface BasicFilterExpr { type: 'basic'; field: string; operator: SearchOperation; - value: string | number | boolean | null; + value: string | number | boolean | Date | null; } interface CombinedFilterExpr { @@ -40,13 +40,13 @@ interface FilterExprTree { nodes: FilterExprNode[]; } -type FilterExprArray = | [string, SearchOperation, string | number | boolean | null] +type FilterExprArray = | [string, SearchOperation, string | number | boolean | Date | null] | [FilterExprArray, 'and' | 'or', FilterExprArray] | ['!', FilterExprArray]; const filterOpSchema = z.enum(FILTER_OPS); -const filterValueScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +const filterValueScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null(), z.date()]); const basicFilterExprSchema = z.object({ type: z.enum(['basic']), @@ -128,7 +128,7 @@ function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray { export const filterValueCommand = defineGridCommand({ name: 'filterValue', - description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":,"expr":}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.', + description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":,"expr":}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date, encode it as "AIDate(year, month, day)" where year is the full year, month is 1-based (1=January, 12=December), day is the day of the month. Example: May 10, 2024 → "AIDate(2024, 5, 10)". Do NOT use ISO strings or any other date format. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.', schema: filterValueCommandSchema, execute: (component, { success, failure }) => (args): Promise => { const defaultMessage = args.expression === null From 2ff245ffc369bad5db4215ede8793c309c4e90dc Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 28 May 2026 16:01:07 +0300 Subject: [PATCH 02/12] Fix test/comments --- .../commands/executeGridAssistant.ts | 21 +++++++++++++------ .../commands/__tests__/filtering.test.ts | 12 +++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts index d15e5d044ff9..241aadf237dc 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts @@ -8,7 +8,7 @@ import type { PromptData, PromptTemplateName } from '@ts/core/ai_integration/cor /** * Matches "AIDate(year, month, day)" format used by the filtering command. - * All components are 1-based. + * The year is the full year; month and day are 1-based. */ const AI_DATE_REGEX = /^AIDate\((\d+),\s*(\d+),\s*(\d+)\)$/; @@ -16,11 +16,20 @@ function parseDates(_key: string, value: unknown): unknown { if (typeof value === 'string') { const match = AI_DATE_REGEX.exec(value); if (match) { - return new Date( - Number(match[1]), - Number(match[2]) - 1, - Number(match[3]), - ); + const year = Number(match[1]); + const month = Number(match[2]) - 1; + const day = Number(match[3]); + const date = new Date(year, month, day); + + const isValid = date.getFullYear() === year + && date.getMonth() === month + && date.getDate() === day; + + if (!isValid) { + return value; + } + + return date; } } return value; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts index a56a9759ddd1..e225ecd2a50f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts @@ -242,10 +242,18 @@ describe('filterValueCommand', () => { }); it('passes Date values through to the filter array', async () => { - const instance = await createGrid(); + const date = new Date(2024, 4, 10); + const instance = await createGrid({ + dataSource: [ + { id: 1, SaleDate: date }, + ], + columns: [ + { dataField: 'id', dataType: 'number' }, + { dataField: 'SaleDate', dataType: 'date' }, + ], + }); const spy = jest.spyOn(instance, 'option'); const callbacks = createCallbacks(); - const date = new Date(2024, 4, 10); const result = await filterValueCommand.execute(instance, callbacks)({ expression: singleBasic('SaleDate', '=', date), From c016dddac121b2fa4f65e4a97a3b49f172ed7008 Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 28 May 2026 16:46:41 +0300 Subject: [PATCH 03/12] Fix qUnit --- packages/devextreme/testing/helpers/stubs/zodStub.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/testing/helpers/stubs/zodStub.js b/packages/devextreme/testing/helpers/stubs/zodStub.js index 41a13cd622da..8bfbb1045240 100644 --- a/packages/devextreme/testing/helpers/stubs/zodStub.js +++ b/packages/devextreme/testing/helpers/stubs/zodStub.js @@ -12,6 +12,7 @@ string: function() { return z; }, boolean: function() { return z; }, number: function() { return z; }, + date: function() { return z; }, null: function() { return z; }, enum: function() { return z; }, union: function() { return z; }, From b4b30086c1370eb79d9e066e07410953e5df3ab5 Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 28 May 2026 17:12:57 +0300 Subject: [PATCH 04/12] Extract utils --- .../commands/executeGridAssistant.ts | 29 +-------- .../ai_integration/commands/utils.test.ts | 65 +++++++++++++++++++ .../core/ai_integration/commands/utils.ts | 28 ++++++++ 3 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts create mode 100644 packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts index 241aadf237dc..a356d9ffc8c8 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts @@ -6,34 +6,7 @@ import type { import { BaseCommand } from '@ts/core/ai_integration/commands/base'; import type { PromptData, PromptTemplateName } from '@ts/core/ai_integration/core/prompt_manager'; -/** - * Matches "AIDate(year, month, day)" format used by the filtering command. - * The year is the full year; month and day are 1-based. - */ -const AI_DATE_REGEX = /^AIDate\((\d+),\s*(\d+),\s*(\d+)\)$/; - -function parseDates(_key: string, value: unknown): unknown { - if (typeof value === 'string') { - const match = AI_DATE_REGEX.exec(value); - if (match) { - const year = Number(match[1]); - const month = Number(match[2]) - 1; - const day = Number(match[3]); - const date = new Date(year, month, day); - - const isValid = date.getFullYear() === year - && date.getMonth() === month - && date.getDate() === day; - - if (!isValid) { - return value; - } - - return date; - } - } - return value; -} +import { parseDates } from './utils'; export class ExecuteGridAssistantCommand extends BaseCommand< ExecuteGridAssistantCommandParams, diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts new file mode 100644 index 000000000000..7734722095d9 --- /dev/null +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts @@ -0,0 +1,65 @@ +import { + describe, + expect, + it, +} from '@jest/globals'; + +import { parseDates } from './utils'; + +describe('parseDates', () => { + it('converts valid AIDate string to Date object', () => { + const result = parseDates('key', 'AIDate(2024, 5, 10)'); + expect(result).toEqual(new Date(2024, 4, 10)); + }); + + it('handles single-digit month and day', () => { + const result = parseDates('key', 'AIDate(2024, 1, 1)'); + expect(result).toEqual(new Date(2024, 0, 1)); + }); + + it('handles December 31', () => { + const result = parseDates('key', 'AIDate(2024, 12, 31)'); + expect(result).toEqual(new Date(2024, 11, 31)); + }); + + it('returns original string for invalid date (month 13)', () => { + const result = parseDates('key', 'AIDate(2024, 13, 1)'); + expect(result).toBe('AIDate(2024, 13, 1)'); + }); + + it('returns original string for invalid date (day 32)', () => { + const result = parseDates('key', 'AIDate(2024, 1, 32)'); + expect(result).toBe('AIDate(2024, 1, 32)'); + }); + + it('returns original string for February 30', () => { + const result = parseDates('key', 'AIDate(2024, 2, 30)'); + expect(result).toBe('AIDate(2024, 2, 30)'); + }); + + it('passes through non-AIDate strings unchanged', () => { + expect(parseDates('key', 'hello')).toBe('hello'); + expect(parseDates('key', '2024-05-10')).toBe('2024-05-10'); + }); + + it('passes through non-string values unchanged', () => { + expect(parseDates('key', 42)).toBe(42); + expect(parseDates('key', null)).toBe(null); + expect(parseDates('key', true)).toBe(true); + }); + + it('works as JSON.parse reviver', () => { + const json = '{"date":"AIDate(2024, 5, 10)","name":"test","count":5}'; + const result = JSON.parse(json, parseDates); + expect(result).toEqual({ + date: new Date(2024, 4, 10), + name: 'test', + count: 5, + }); + }); + + it('handles AIDate without spaces after commas', () => { + const result = parseDates('key', 'AIDate(2024,5,10)'); + expect(result).toEqual(new Date(2024, 4, 10)); + }); +}); diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts new file mode 100644 index 000000000000..21b96a47f0ba --- /dev/null +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts @@ -0,0 +1,28 @@ +/** + * Matches "AIDate(year, month, day)" format used by the filtering command. + * The year is the full year; month and day are 1-based. + */ +const AI_DATE_REGEX = /^AIDate\((\d+),\s*(\d+),\s*(\d+)\)$/; + +export function parseDates(_key: string, value: unknown): unknown { + if (typeof value === 'string') { + const match = AI_DATE_REGEX.exec(value); + if (match) { + const year = Number(match[1]); + const month = Number(match[2]) - 1; + const day = Number(match[3]); + const date = new Date(year, month, day); + + const isValid = date.getFullYear() === year + && date.getMonth() === month + && date.getDate() === day; + + if (!isValid) { + return value; + } + + return date; + } + } + return value; +} From be3a2a448bb78b6809c8e2cd4bbee83b1d0d8627 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 29 May 2026 18:02:08 +0300 Subject: [PATCH 05/12] Fix .d.ts --- packages/devextreme/ts/dx.all.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 0cee04115773..0a793a6a963f 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -4706,7 +4706,7 @@ declare module DevExpress.common.grids { type: 'basic'; field: string; operator: DevExpress.common.data.SearchOperation; - value: string | number | boolean | null; + value: string | number | boolean | null | Date; }; /** * [descr:ColumnAIOptions] From 966ee7e12d4d9147069f3f9f863565e48d36f0f8 Mon Sep 17 00:00:00 2001 From: Raushen Date: Tue, 2 Jun 2026 11:30:00 +0300 Subject: [PATCH 06/12] Fix schema --- .../ai_assistant/commands/__tests__/filtering.test.ts | 1 - .../grids/grid_core/ai_assistant/commands/filtering.ts | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts index e225ecd2a50f..b8b495d0f7f9 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts @@ -113,7 +113,6 @@ describe('filterValueCommand', () => { [singleBasic('name', '=', 1)], [singleBasic('name', '=', true)], [singleBasic('name', '=', null)], - [singleBasic('name', '=', new Date(2024, 4, 10))], ])('accepts scalar value %p', (expression) => { expect(filterValueCommand.schema.safeParse({ expression }).success).toBe(true); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts index aea058f6787b..14261cf83b84 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts @@ -16,7 +16,14 @@ type FilterExprArray = | [string, SearchOperation, string | number | boolean | D const filterOpSchema = z.enum(FILTER_OPS); -const filterValueScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null(), z.date()]); +const filterValueScalarSchema = z.union([ + z.string().describe( + 'A plain string value. If the value represents a date, it must use the AIDate(year, month, day) format where month is 1-based (1=January, 12=December), instead of ISO string or natural language.', + ), + z.number().describe('A numeric filter value.'), + z.boolean().describe('A boolean filter value.'), + z.null().describe('A null filter value.'), +]); const basicFilterExprSchema = z.object({ type: z.enum(['basic']), From de2aa72edc68578a86ccadb120be98aab25b4d00 Mon Sep 17 00:00:00 2001 From: Raushen Date: Tue, 2 Jun 2026 18:20:56 +0300 Subject: [PATCH 07/12] Fix parsing --- .../commands/executeGridAssistant.ts | 6 +- .../ai_integration/commands/utils.test.ts | 65 ------------------- .../core/ai_integration/commands/utils.ts | 28 -------- .../ai_assistant/commands/filtering.ts | 43 +++++++++--- .../js/__internal/grids/grid_core/m_utils.ts | 8 +-- 5 files changed, 41 insertions(+), 109 deletions(-) delete mode 100644 packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts delete mode 100644 packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts index a356d9ffc8c8..6365e4c075ec 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts @@ -6,8 +6,6 @@ import type { import { BaseCommand } from '@ts/core/ai_integration/commands/base'; import type { PromptData, PromptTemplateName } from '@ts/core/ai_integration/core/prompt_manager'; -import { parseDates } from './utils'; - export class ExecuteGridAssistantCommand extends BaseCommand< ExecuteGridAssistantCommandParams, ExecuteGridAssistantCommandResult @@ -32,11 +30,11 @@ export class ExecuteGridAssistantCommand extends BaseCommand< if (response === '') { return { actions: [] }; } - return JSON.parse(response, parseDates) as ExecuteGridAssistantCommandResult; + return JSON.parse(response) as ExecuteGridAssistantCommandResult; } const actions = typeof response.actions === 'string' - ? JSON.parse(response.actions, parseDates) + ? JSON.parse(response.actions) : response.actions; return { actions }; diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts deleted file mode 100644 index 7734722095d9..000000000000 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/utils.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - describe, - expect, - it, -} from '@jest/globals'; - -import { parseDates } from './utils'; - -describe('parseDates', () => { - it('converts valid AIDate string to Date object', () => { - const result = parseDates('key', 'AIDate(2024, 5, 10)'); - expect(result).toEqual(new Date(2024, 4, 10)); - }); - - it('handles single-digit month and day', () => { - const result = parseDates('key', 'AIDate(2024, 1, 1)'); - expect(result).toEqual(new Date(2024, 0, 1)); - }); - - it('handles December 31', () => { - const result = parseDates('key', 'AIDate(2024, 12, 31)'); - expect(result).toEqual(new Date(2024, 11, 31)); - }); - - it('returns original string for invalid date (month 13)', () => { - const result = parseDates('key', 'AIDate(2024, 13, 1)'); - expect(result).toBe('AIDate(2024, 13, 1)'); - }); - - it('returns original string for invalid date (day 32)', () => { - const result = parseDates('key', 'AIDate(2024, 1, 32)'); - expect(result).toBe('AIDate(2024, 1, 32)'); - }); - - it('returns original string for February 30', () => { - const result = parseDates('key', 'AIDate(2024, 2, 30)'); - expect(result).toBe('AIDate(2024, 2, 30)'); - }); - - it('passes through non-AIDate strings unchanged', () => { - expect(parseDates('key', 'hello')).toBe('hello'); - expect(parseDates('key', '2024-05-10')).toBe('2024-05-10'); - }); - - it('passes through non-string values unchanged', () => { - expect(parseDates('key', 42)).toBe(42); - expect(parseDates('key', null)).toBe(null); - expect(parseDates('key', true)).toBe(true); - }); - - it('works as JSON.parse reviver', () => { - const json = '{"date":"AIDate(2024, 5, 10)","name":"test","count":5}'; - const result = JSON.parse(json, parseDates); - expect(result).toEqual({ - date: new Date(2024, 4, 10), - name: 'test', - count: 5, - }); - }); - - it('handles AIDate without spaces after commas', () => { - const result = parseDates('key', 'AIDate(2024,5,10)'); - expect(result).toEqual(new Date(2024, 4, 10)); - }); -}); diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts deleted file mode 100644 index 21b96a47f0ba..000000000000 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Matches "AIDate(year, month, day)" format used by the filtering command. - * The year is the full year; month and day are 1-based. - */ -const AI_DATE_REGEX = /^AIDate\((\d+),\s*(\d+),\s*(\d+)\)$/; - -export function parseDates(_key: string, value: unknown): unknown { - if (typeof value === 'string') { - const match = AI_DATE_REGEX.exec(value); - if (match) { - const year = Number(match[1]); - const month = Number(match[2]) - 1; - const day = Number(match[3]); - const date = new Date(year, month, day); - - const isValid = date.getFullYear() === year - && date.getMonth() === month - && date.getDate() === day; - - if (!isValid) { - return value; - } - - return date; - } - } - return value; -} diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts index 14261cf83b84..9d2a076107d1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts @@ -1,6 +1,9 @@ import type { SearchOperation } from '@js/common/data.types'; -import type { FilterExprNode, FilterExprTree } from '@js/common/grids'; +import type { BasicFilterExpr, FilterExprNode, FilterExprTree } from '@js/common/grids'; +import { dateUtilsTs } from '@ts/core/utils/date'; import type { CommandResult } from '@ts/grids/grid_core/ai_assistant/types'; +import type { InternalGrid } from '@ts/grids/grid_core/m_types'; +import { isDateType } from '@ts/grids/grid_core/m_utils'; import { z } from 'zod'; import { defineGridCommand } from './defineGridCommand'; @@ -10,7 +13,9 @@ const FILTER_OPS = [ 'contains', 'notcontains', 'startswith', 'endswith', ] as const satisfies readonly SearchOperation[]; -type FilterExprArray = | [string, SearchOperation, string | number | boolean | Date | null] +type FilterExprValue = BasicFilterExpr['value']; + +type FilterExprArray = | [string, SearchOperation, FilterExprValue] | [FilterExprArray, 'and' | 'or', FilterExprArray] | ['!', FilterExprArray]; @@ -18,7 +23,7 @@ const filterOpSchema = z.enum(FILTER_OPS); const filterValueScalarSchema = z.union([ z.string().describe( - 'A plain string value. If the value represents a date, it must use the AIDate(year, month, day) format where month is 1-based (1=January, 12=December), instead of ISO string or natural language.', + 'A plain string value. Date values should be in "YYYY-MM-DDTHH:mm:ss" format (e.g. "2024-05-10T00:00:00", "2024-05-10T14:30:00"). The time part is always required. The "Z" suffix or timezone offset should not be appended unless the user explicitly requests it.', ), z.number().describe('A numeric filter value.'), z.boolean().describe('A boolean filter value.'), @@ -64,7 +69,27 @@ const filterValueCommandSchema = z.object({ expression: filterExprTreeSchema.nullable(), }).strict(); -function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray { +function resolveFilterValue( + component: InternalGrid, + field: string, + value: FilterExprValue, +): FilterExprValue { + if (typeof value === 'string') { + const dataType = component.columnOption(field, 'dataType'); + if (isDateType(dataType)) { + if (!dateUtilsTs.isValidDate(value)) { + return value; + } + return new Date(value); + } + } + return value; +} + +function convertFilterExprToArray( + component: InternalGrid, + tree: FilterExprTree, +): FilterExprArray { const byId = new Map(); for (const node of tree.nodes) { if (byId.has(node.id)) { @@ -86,8 +111,10 @@ function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray { try { const { expr } = node; switch (expr.type) { - case 'basic': - return [expr.field, expr.operator, expr.value]; + case 'basic': { + const resolved = resolveFilterValue(component, expr.field, expr.value); + return [expr.field, expr.operator, resolved]; + } case 'combined': return [walk(expr.leftId), expr.combiner, walk(expr.rightId)]; case 'negated': @@ -105,7 +132,7 @@ function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray { export const filterValueCommand = defineGridCommand({ name: 'filterValue', - description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":,"expr":}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date, encode it as "AIDate(year, month, day)" where year is the full year, month is 1-based (1=January, 12=December), day is the day of the month. Example: May 10, 2024 → "AIDate(2024, 5, 10)". Do NOT use ISO strings or any other date format. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.', + description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":,"expr":}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date or datetime, always use "YYYY-MM-DDTHH:mm:ss" format without timezone suffix, e.g. "2024-05-10T00:00:00" for midnight or "2024-05-10T14:30:00" for a specific time. Always include the "T" and time part. Do NOT use date-only format like "2024-05-10" without time. Do NOT append "Z" or any timezone offset unless the user explicitly requests it. Do NOT use natural language for dates. If a column stores both date and time (dataType is "datetime"), filtering by a specific day requires a range: field >= "YYYY-MM-DDT00:00:00" AND field <= "YYYY-MM-DDT23:59:59". If a column stores only date without time (dataType is "date"), a simple equality "=" is sufficient. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.', schema: filterValueCommandSchema, execute: (component, { success, failure }) => (args): Promise => { const defaultMessage = args.expression === null @@ -115,7 +142,7 @@ export const filterValueCommand = defineGridCommand({ try { const filterValue = args.expression === null ? undefined - : convertFilterExprToArray(args.expression); + : convertFilterExprToArray(component, args.expression); // Handles remote operations via data controller listening for the `filtering` change component.option('filterValue', filterValue); 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 925e1544cc69..f03a205a92c4 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -66,6 +66,10 @@ const DATE_INTERVAL_SELECTORS = { const DEFAULT_COLUMN_WIDTH = 50; +export function isDateType(dataType: string | undefined): boolean { + return dataType === 'date' || dataType === 'datetime'; +} + const getIntervalSelector = function () { const data = arguments[1]; const value = this.calculateCellValue(data); @@ -81,10 +85,6 @@ const getIntervalSelector = function () { } }; -function isDateType(dataType) { - return dataType === 'date' || dataType === 'datetime'; -} - const getGlobalFormat = (dataType) => { const globalFormat = getGlobalFormatByDataType(dataType); From 6ea87edbd01e0e85350b149bac14a48672fe185c Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 3 Jun 2026 11:50:52 +0300 Subject: [PATCH 08/12] Remove describing types from prompt --- .../grids/grid_core/ai_assistant/commands/filtering.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts index 9d2a076107d1..9b9924300383 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts @@ -132,7 +132,7 @@ function convertFilterExprToArray( export const filterValueCommand = defineGridCommand({ name: 'filterValue', - description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":,"expr":}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date or datetime, always use "YYYY-MM-DDTHH:mm:ss" format without timezone suffix, e.g. "2024-05-10T00:00:00" for midnight or "2024-05-10T14:30:00" for a specific time. Always include the "T" and time part. Do NOT use date-only format like "2024-05-10" without time. Do NOT append "Z" or any timezone offset unless the user explicitly requests it. Do NOT use natural language for dates. If a column stores both date and time (dataType is "datetime"), filtering by a specific day requires a range: field >= "YYYY-MM-DDT00:00:00" AND field <= "YYYY-MM-DDT23:59:59". If a column stores only date without time (dataType is "date"), a simple equality "=" is sufficient. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.', + description: 'Apply a filter expression to the grid. Replaces any existing filter; pass null for expression to clear. The expression is a flat node list: {"rootId":id,"nodes":[...]}. Each node is {"id":,"expr":}, where "expr" is one of: basic {"type":"basic","field":dataField,"operator":op,"value":val}, combined {"type":"combined","combiner":"and"|"or","leftId":nodeId,"rightId":nodeId}, negated {"type":"negated","expressionId":nodeId}. "rootId" MUST be the "id" of the outermost node (the top of the expression tree) and must match one of the node ids exactly — never invent a value like "root". Every "leftId"/"rightId"/"expressionId" must also match a node "id". Ids must be unique and must not form cycles. The "field" is the column dataField (not the caption). Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith". DATE VALUES: When a value is a date or datetime, always use "YYYY-MM-DDTHH:mm:ss" format without timezone suffix, e.g. "2024-05-10T00:00:00" for midnight or "2024-05-10T14:30:00" for a specific time. Always include the "T" and time part. Do NOT use date-only format like "2024-05-10" without time. Do NOT append "Z" or any timezone offset unless the user explicitly requests it. Do NOT use natural language for dates. To express "not and" / "not or", add a negated node whose expressionId points at a combined node. Example for name = "Alpha" AND age > 10 (rootId is "n3", the combined node): {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"age","operator":">","value":10}},{"id":"n3","expr":{"type":"combined","combiner":"and","leftId":"n1","rightId":"n2"}}]}.', schema: filterValueCommandSchema, execute: (component, { success, failure }) => (args): Promise => { const defaultMessage = args.expression === null From d1c20dc0cf9ea1b3e78d69fc865c2359ba999e1b Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 3 Jun 2026 11:53:14 +0300 Subject: [PATCH 09/12] Remove tests --- .../commands/executeGridAssistant.test.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts index d90e8a6e9bfd..137155d3868f 100644 --- a/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts +++ b/packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.test.ts @@ -118,30 +118,6 @@ describe('ExecuteGridAssistantCommand', () => { actions: [{ name: 'sort', args: { columnName: 'name', sortOrder: 'asc' } }], }); }); - - it('should convert AIDate strings to Date objects in string response', () => { - const response = '{"actions":[{"name":"filterValue","args":{"expression":{"field":"SaleDate","operator":"=","value":"AIDate(2024, 5, 10)"}}}]}'; - // @ts-expect-error Access to protected property for a test - const result = command.parseResult(response); - - const { args } = result.actions[0]; - const expression = args.expression as Record; - expect(expression.value).toBeInstanceOf(Date); - expect(expression.value).toStrictEqual(new Date(2024, 4, 10)); - }); - - it('should convert AIDate strings to Date objects in stringified actions', () => { - const response = { - actions: '[{"name":"filterValue","args":{"expression":{"field":"SaleDate","operator":"=","value":"AIDate(2024, 12, 1)"}}}]', - }; - // @ts-expect-error Access to protected property for a test - const result = command.parseResult(response); - - const { args } = result.actions[0]; - const expression = args.expression as Record; - expect(expression.value).toBeInstanceOf(Date); - expect(expression.value).toStrictEqual(new Date(2024, 11, 1)); - }); }); describe('execute', () => { From 13307afcbc34ab2bccadcf7fb11dd956b324c8fe Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 3 Jun 2026 11:57:13 +0300 Subject: [PATCH 10/12] Update test --- .../commands/__tests__/filtering.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts index b8b495d0f7f9..be8d839711dc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts @@ -240,11 +240,10 @@ describe('filterValueCommand', () => { expect(result.status).toBe('success'); }); - it('passes Date values through to the filter array', async () => { - const date = new Date(2024, 4, 10); + it('converts ISO date string to Date object for date columns', async () => { const instance = await createGrid({ dataSource: [ - { id: 1, SaleDate: date }, + { id: 1, SaleDate: new Date(2024, 4, 10) }, ], columns: [ { dataField: 'id', dataType: 'number' }, @@ -255,10 +254,13 @@ describe('filterValueCommand', () => { const callbacks = createCallbacks(); const result = await filterValueCommand.execute(instance, callbacks)({ - expression: singleBasic('SaleDate', '=', date), + expression: singleBasic('SaleDate', '=', '2024-05-10T00:00:00'), }); - expect(spy).toHaveBeenCalledWith('filterValue', ['SaleDate', '=', date]); + expect(spy).toHaveBeenCalledWith( + 'filterValue', + ['SaleDate', '=', new Date('2024-05-10T00:00:00')], + ); expect(result.status).toBe('success'); }); From e3e49b36675fa7e8eb6b1e3b8611077acb922ff2 Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 3 Jun 2026 12:16:58 +0300 Subject: [PATCH 11/12] Extract resolveFilterValue as utils method --- .../commands/__tests__/filtering.test.ts | 24 +++++++++++ .../commands/__tests__/utils.test.ts | 42 ++++++++++++++++++- .../ai_assistant/commands/filtering.ts | 27 ++---------- .../grid_core/ai_assistant/commands/utils.ts | 19 ++++++++- 4 files changed, 86 insertions(+), 26 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts index be8d839711dc..1544d70bec5a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts @@ -264,6 +264,30 @@ describe('filterValueCommand', () => { expect(result.status).toBe('success'); }); + it('does not convert invalid date string for date columns', async () => { + const instance = await createGrid({ + dataSource: [ + { id: 1, SaleDate: new Date(2024, 4, 10) }, + ], + columns: [ + { dataField: 'id', dataType: 'number' }, + { dataField: 'SaleDate', dataType: 'date' }, + ], + }); + const spy = jest.spyOn(instance, 'option'); + const callbacks = createCallbacks(); + + const result = await filterValueCommand.execute(instance, callbacks)({ + expression: singleBasic('SaleDate', '=', 'not-a-date'), + }); + + expect(spy).toHaveBeenCalledWith( + 'filterValue', + ['SaleDate', '=', 'not-a-date'], + ); + expect(result.status).toBe('success'); + }); + it('converts a combined node into the legacy array form', async () => { const instance = await createGrid(); const spy = jest.spyOn(instance, 'option'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/utils.test.ts index d18d87b3caaf..ea3ff6065cf7 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/utils.test.ts @@ -5,8 +5,10 @@ import { } from '@jest/globals'; import { z } from 'zod'; -// eslint-disable-next-line spellcheck/spell-checker -import { isKeyShapeValid, normalizeKey, optionalNullish } from '../utils'; +import { + // eslint-disable-next-line spellcheck/spell-checker + isKeyShapeValid, normalizeKey, optionalNullish, resolveFilterValue, +} from '../utils'; describe('normalizeKey', () => { it('returns a string key as-is', () => { @@ -130,3 +132,39 @@ describe('isKeyShapeValid', () => { }); }); }); + +describe('resolveFilterValue', () => { + it('converts a valid ISO date string to Date for "date" dataType', () => { + const result = resolveFilterValue('date', '2024-05-10T00:00:00'); + expect(result).toEqual(new Date('2024-05-10T00:00:00')); + }); + + it('converts a valid ISO date string to Date for "datetime" dataType', () => { + const result = resolveFilterValue('datetime', '2024-05-10T14:30:00'); + expect(result).toEqual(new Date('2024-05-10T14:30:00')); + }); + + it('returns the original string for an invalid date with "date" dataType', () => { + expect(resolveFilterValue('date', 'not-a-date')).toBe('not-a-date'); + }); + + it('returns the original string when dataType is "string"', () => { + expect(resolveFilterValue('string', '2024-05-10T00:00:00')).toBe('2024-05-10T00:00:00'); + }); + + it('returns the original string when dataType is undefined', () => { + expect(resolveFilterValue(undefined, '2024-05-10T00:00:00')).toBe('2024-05-10T00:00:00'); + }); + + it('returns number values as-is regardless of dataType', () => { + expect(resolveFilterValue('date', 42)).toBe(42); + }); + + it('returns null as-is regardless of dataType', () => { + expect(resolveFilterValue('date', null)).toBeNull(); + }); + + it('returns boolean values as-is regardless of dataType', () => { + expect(resolveFilterValue('date', true)).toBe(true); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts index 9b9924300383..4392b2d521e3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts @@ -1,21 +1,18 @@ import type { SearchOperation } from '@js/common/data.types'; import type { BasicFilterExpr, FilterExprNode, FilterExprTree } from '@js/common/grids'; -import { dateUtilsTs } from '@ts/core/utils/date'; import type { CommandResult } from '@ts/grids/grid_core/ai_assistant/types'; import type { InternalGrid } from '@ts/grids/grid_core/m_types'; -import { isDateType } from '@ts/grids/grid_core/m_utils'; import { z } from 'zod'; import { defineGridCommand } from './defineGridCommand'; +import { resolveFilterValue } from './utils'; const FILTER_OPS = [ '=', '<>', '<', '<=', '>', '>=', 'contains', 'notcontains', 'startswith', 'endswith', ] as const satisfies readonly SearchOperation[]; -type FilterExprValue = BasicFilterExpr['value']; - -type FilterExprArray = | [string, SearchOperation, FilterExprValue] +type FilterExprArray = | [string, SearchOperation, BasicFilterExpr['value']] | [FilterExprArray, 'and' | 'or', FilterExprArray] | ['!', FilterExprArray]; @@ -69,23 +66,6 @@ const filterValueCommandSchema = z.object({ expression: filterExprTreeSchema.nullable(), }).strict(); -function resolveFilterValue( - component: InternalGrid, - field: string, - value: FilterExprValue, -): FilterExprValue { - if (typeof value === 'string') { - const dataType = component.columnOption(field, 'dataType'); - if (isDateType(dataType)) { - if (!dateUtilsTs.isValidDate(value)) { - return value; - } - return new Date(value); - } - } - return value; -} - function convertFilterExprToArray( component: InternalGrid, tree: FilterExprTree, @@ -112,7 +92,8 @@ function convertFilterExprToArray( const { expr } = node; switch (expr.type) { case 'basic': { - const resolved = resolveFilterValue(component, expr.field, expr.value); + const dataType = component.columnOption(expr.field, 'dataType'); + const resolved = resolveFilterValue(dataType, expr.value); return [expr.field, expr.operator, resolved]; } case 'combined': diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/utils.ts index 8a4bd45ea407..4a2e7c521faf 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/utils.ts @@ -1,5 +1,7 @@ -import type { CompositeKeyPair } from '@js/common/grids'; +import type { BasicFilterExpr, CompositeKeyPair } from '@js/common/grids'; import { isString } from '@js/core/utils/type'; +import { dateUtilsTs } from '@ts/core/utils/date'; +import { isDateType } from '@ts/grids/grid_core/m_utils'; import { z } from 'zod'; type RowKey = string | number | Record; @@ -64,3 +66,18 @@ export const isKeyShapeValid = ( return keyExpr.every((field) => field in key); }; + +type FilterExprValue = BasicFilterExpr['value']; + +export function resolveFilterValue( + dataType: string | undefined, + value: FilterExprValue, +): FilterExprValue { + if (typeof value === 'string' && isDateType(dataType)) { + if (!dateUtilsTs.isValidDate(value)) { + return value; + } + return new Date(value); + } + return value; +} From 990dee96c069e07ed1585a103663be85edec0a31 Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 3 Jun 2026 12:26:56 +0300 Subject: [PATCH 12/12] Add describe stub --- packages/devextreme/testing/helpers/stubs/zodStub.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/testing/helpers/stubs/zodStub.js b/packages/devextreme/testing/helpers/stubs/zodStub.js index 8bfbb1045240..668e5fd8ecd2 100644 --- a/packages/devextreme/testing/helpers/stubs/zodStub.js +++ b/packages/devextreme/testing/helpers/stubs/zodStub.js @@ -34,6 +34,7 @@ min: function() { return z; }, max: function() { return z; }, transform: function() { return z; }, + describe: function() { return z; }, // validation safeParse: function() { return { success: true, data: {} }; }, };