-
Notifications
You must be signed in to change notification settings - Fork 672
AI Assistant: Filter command does not create Date objects in filter expression #33756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 26_1
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggested scenario covers only date, not date-time cases, it also does not take into account timezones. |
||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,13 +10,13 @@ const FILTER_OPS = [ | |
| 'contains', 'notcontains', 'startswith', 'endswith', | ||
| ] as const satisfies readonly SearchOperation[]; | ||
|
|
||
| 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()]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI can't return an object of type Date, it'll always be a string, therefore that schema is incorrect
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better use a separate description for filter value, like following: |
||
|
|
||
| const basicFilterExprSchema = z.object({ | ||
| type: z.enum(['basic']), | ||
|
|
@@ -98,7 +98,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":<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"}}]}.', | ||
| 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, 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<CommandResult> => { | ||
| const defaultMessage = args.expression === null | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.