Skip to content

Commit d947e65

Browse files
Merge branch '26_1' into 26_1_WIP_check_ng22
2 parents 1aac645 + ff8e184 commit d947e65

18 files changed

Lines changed: 605 additions & 371 deletions

File tree

packages/devextreme/js/__internal/core/ai_integration/commands/executeGridAssistant.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export class ExecuteGridAssistantCommand extends BaseCommand<
2323
};
2424
}
2525

26-
// TODO: check response more carefully
2726
protected parseResult(
2827
response: ExecuteGridAssistantCommandResponse,
2928
): ExecuteGridAssistantCommandResult {

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/filtering.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,54 @@ describe('filterValueCommand', () => {
240240
expect(result.status).toBe('success');
241241
});
242242

243+
it('converts ISO date string to Date object for date columns', async () => {
244+
const instance = await createGrid({
245+
dataSource: [
246+
{ id: 1, SaleDate: new Date(2024, 4, 10) },
247+
],
248+
columns: [
249+
{ dataField: 'id', dataType: 'number' },
250+
{ dataField: 'SaleDate', dataType: 'date' },
251+
],
252+
});
253+
const spy = jest.spyOn(instance, 'option');
254+
const callbacks = createCallbacks();
255+
256+
const result = await filterValueCommand.execute(instance, callbacks)({
257+
expression: singleBasic('SaleDate', '=', '2024-05-10T00:00:00'),
258+
});
259+
260+
expect(spy).toHaveBeenCalledWith(
261+
'filterValue',
262+
['SaleDate', '=', new Date('2024-05-10T00:00:00')],
263+
);
264+
expect(result.status).toBe('success');
265+
});
266+
267+
it('does not convert invalid date string for date columns', async () => {
268+
const instance = await createGrid({
269+
dataSource: [
270+
{ id: 1, SaleDate: new Date(2024, 4, 10) },
271+
],
272+
columns: [
273+
{ dataField: 'id', dataType: 'number' },
274+
{ dataField: 'SaleDate', dataType: 'date' },
275+
],
276+
});
277+
const spy = jest.spyOn(instance, 'option');
278+
const callbacks = createCallbacks();
279+
280+
const result = await filterValueCommand.execute(instance, callbacks)({
281+
expression: singleBasic('SaleDate', '=', 'not-a-date'),
282+
});
283+
284+
expect(spy).toHaveBeenCalledWith(
285+
'filterValue',
286+
['SaleDate', '=', 'not-a-date'],
287+
);
288+
expect(result.status).toBe('success');
289+
});
290+
243291
it('converts a combined node into the legacy array form', async () => {
244292
const instance = await createGrid();
245293
const spy = jest.spyOn(instance, 'option');

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/utils.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
} from '@jest/globals';
66
import { z } from 'zod';
77

8-
// eslint-disable-next-line spellcheck/spell-checker
9-
import { isKeyShapeValid, normalizeKey, optionalNullish } from '../utils';
8+
import {
9+
// eslint-disable-next-line spellcheck/spell-checker
10+
isKeyShapeValid, normalizeKey, optionalNullish, resolveFilterValue,
11+
} from '../utils';
1012

1113
describe('normalizeKey', () => {
1214
it('returns a string key as-is', () => {
@@ -130,3 +132,39 @@ describe('isKeyShapeValid', () => {
130132
});
131133
});
132134
});
135+
136+
describe('resolveFilterValue', () => {
137+
it('converts a valid ISO date string to Date for "date" dataType', () => {
138+
const result = resolveFilterValue('date', '2024-05-10T00:00:00');
139+
expect(result).toEqual(new Date('2024-05-10T00:00:00'));
140+
});
141+
142+
it('converts a valid ISO date string to Date for "datetime" dataType', () => {
143+
const result = resolveFilterValue('datetime', '2024-05-10T14:30:00');
144+
expect(result).toEqual(new Date('2024-05-10T14:30:00'));
145+
});
146+
147+
it('returns the original string for an invalid date with "date" dataType', () => {
148+
expect(resolveFilterValue('date', 'not-a-date')).toBe('not-a-date');
149+
});
150+
151+
it('returns the original string when dataType is "string"', () => {
152+
expect(resolveFilterValue('string', '2024-05-10T00:00:00')).toBe('2024-05-10T00:00:00');
153+
});
154+
155+
it('returns the original string when dataType is undefined', () => {
156+
expect(resolveFilterValue(undefined, '2024-05-10T00:00:00')).toBe('2024-05-10T00:00:00');
157+
});
158+
159+
it('returns number values as-is regardless of dataType', () => {
160+
expect(resolveFilterValue('date', 42)).toBe(42);
161+
});
162+
163+
it('returns null as-is regardless of dataType', () => {
164+
expect(resolveFilterValue('date', null)).toBeNull();
165+
});
166+
167+
it('returns boolean values as-is regardless of dataType', () => {
168+
expect(resolveFilterValue('date', true)).toBe(true);
169+
});
170+
});

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/filtering.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
import type { SearchOperation } from '@js/common/data.types';
2-
import type { FilterExprNode, FilterExprTree } from '@js/common/grids';
2+
import type { BasicFilterExpr, FilterExprNode, FilterExprTree } from '@js/common/grids';
33
import type { CommandResult } from '@ts/grids/grid_core/ai_assistant/types';
4+
import type { InternalGrid } from '@ts/grids/grid_core/m_types';
45
import { z } from 'zod';
56

67
import { defineGridCommand } from './defineGridCommand';
8+
import { resolveFilterValue } from './utils';
79

810
const FILTER_OPS = [
911
'=', '<>', '<', '<=', '>', '>=',
1012
'contains', 'notcontains', 'startswith', 'endswith',
1113
] as const satisfies readonly SearchOperation[];
1214

13-
type FilterExprArray = | [string, SearchOperation, string | number | boolean | null]
15+
type FilterExprArray = | [string, SearchOperation, BasicFilterExpr['value']]
1416
| [FilterExprArray, 'and' | 'or', FilterExprArray]
1517
| ['!', FilterExprArray];
1618

1719
const filterOpSchema = z.enum(FILTER_OPS);
1820

19-
const filterValueScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
21+
const filterValueScalarSchema = z.union([
22+
z.string().describe(
23+
'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.',
24+
),
25+
z.number().describe('A numeric filter value.'),
26+
z.boolean().describe('A boolean filter value.'),
27+
z.null().describe('A null filter value.'),
28+
]);
2029

2130
const basicFilterExprSchema = z.object({
2231
type: z.enum(['basic']),
@@ -57,7 +66,10 @@ const filterValueCommandSchema = z.object({
5766
expression: filterExprTreeSchema.nullable(),
5867
}).strict();
5968

60-
function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray {
69+
function convertFilterExprToArray(
70+
component: InternalGrid,
71+
tree: FilterExprTree,
72+
): FilterExprArray {
6173
const byId = new Map<string, FilterExprNode>();
6274
for (const node of tree.nodes) {
6375
if (byId.has(node.id)) {
@@ -79,8 +91,11 @@ function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray {
7991
try {
8092
const { expr } = node;
8193
switch (expr.type) {
82-
case 'basic':
83-
return [expr.field, expr.operator, expr.value];
94+
case 'basic': {
95+
const dataType = component.columnOption(expr.field, 'dataType');
96+
const resolved = resolveFilterValue(dataType, expr.value);
97+
return [expr.field, expr.operator, resolved];
98+
}
8499
case 'combined':
85100
return [walk(expr.leftId), expr.combiner, walk(expr.rightId)];
86101
case 'negated':
@@ -98,7 +113,7 @@ function convertFilterExprToArray(tree: FilterExprTree): FilterExprArray {
98113

99114
export const filterValueCommand = defineGridCommand({
100115
name: 'filterValue',
101-
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":<unique string like "n1">,"expr":<expression>}, 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"}}]}.',
116+
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":<unique string like "n1">,"expr":<expression>}, 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"}}]}.',
102117
schema: filterValueCommandSchema,
103118
execute: (component, { success, failure }) => (args): Promise<CommandResult> => {
104119
const defaultMessage = args.expression === null
@@ -108,7 +123,7 @@ export const filterValueCommand = defineGridCommand({
108123
try {
109124
const filterValue = args.expression === null
110125
? undefined
111-
: convertFilterExprToArray(args.expression);
126+
: convertFilterExprToArray(component, args.expression);
112127

113128
// Handles remote operations via data controller listening for the `filtering` change
114129
component.option('filterValue', filterValue);

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/utils.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { CompositeKeyPair } from '@js/common/grids';
1+
import type { BasicFilterExpr, CompositeKeyPair } from '@js/common/grids';
22
import { isString } from '@js/core/utils/type';
3+
import { dateUtilsTs } from '@ts/core/utils/date';
4+
import { isDateType } from '@ts/grids/grid_core/m_utils';
35
import { z } from 'zod';
46

57
type RowKey = string | number | Record<string, string | number>;
@@ -64,3 +66,18 @@ export const isKeyShapeValid = (
6466

6567
return keyExpr.every((field) => field in key);
6668
};
69+
70+
type FilterExprValue = BasicFilterExpr['value'];
71+
72+
export function resolveFilterValue(
73+
dataType: string | undefined,
74+
value: FilterExprValue,
75+
): FilterExprValue {
76+
if (typeof value === 'string' && isDateType(dataType)) {
77+
if (!dateUtilsTs.isValidDate(value)) {
78+
return value;
79+
}
80+
return new Date(value);
81+
}
82+
return value;
83+
}

packages/devextreme/js/__internal/grids/grid_core/m_utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ const DATE_INTERVAL_SELECTORS = {
6666

6767
const DEFAULT_COLUMN_WIDTH = 50;
6868

69+
export function isDateType(dataType: string | undefined): boolean {
70+
return dataType === 'date' || dataType === 'datetime';
71+
}
72+
6973
const getIntervalSelector = function () {
7074
const data = arguments[1];
7175
const value = this.calculateCellValue(data);
@@ -81,10 +85,6 @@ const getIntervalSelector = function () {
8185
}
8286
};
8387

84-
function isDateType(dataType) {
85-
return dataType === 'date' || dataType === 'datetime';
86-
}
87-
8888
const getGlobalFormat = (dataType) => {
8989
const globalFormat = getGlobalFormatByDataType(dataType);
9090

packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ class ListStrategy extends DateBoxStrategy {
324324
}
325325

326326
_updatePopupHeight(): void {
327-
const dropDownOptionsHeight = getSizeValue(this.dateBox.option('dropDownOptions.height'));
327+
const { dropDownOptions } = this.dateBox.option();
328+
const dropDownOptionsHeight = getSizeValue(dropDownOptions?.height);
328329

329330
if (dropDownOptionsHeight === undefined || dropDownOptionsHeight === 'auto') {
330331
this.dateBox._setPopupOption('height', 'auto');

0 commit comments

Comments
 (0)