diff --git a/packages/devextreme-angular/src/common/data/index.ts b/packages/devextreme-angular/src/common/data/index.ts index 5a681f8150bf..8494d824255d 100644 --- a/packages/devextreme-angular/src/common/data/index.ts +++ b/packages/devextreme-angular/src/common/data/index.ts @@ -32,6 +32,7 @@ export type { LoadResult, LoadResultObject, LocalStoreOptions, + MultiValueSearchOperation, ODataContextOptions, ODataStoreOptions, Query, diff --git a/packages/devextreme-angular/src/common/grids/index.ts b/packages/devextreme-angular/src/common/grids/index.ts index 3dfa383d0540..0d2283dfd538 100644 --- a/packages/devextreme-angular/src/common/grids/index.ts +++ b/packages/devextreme-angular/src/common/grids/index.ts @@ -42,6 +42,7 @@ export type { FilterPanelTexts, FilterRow, FilterRowOperationDescriptions, + FilterScalarValue, FilterType, FixedPosition, GridBase, @@ -57,6 +58,7 @@ export type { KeyboardNavigation, KeyDownInfo, LoadPanel, + MultiValueFilterExpr, NegatedFilterExpr, NewRowInfo, NewRowPosition, diff --git a/packages/devextreme-angular/src/common/index.ts b/packages/devextreme-angular/src/common/index.ts index 279b8990e09d..b366339487d5 100644 --- a/packages/devextreme-angular/src/common/index.ts +++ b/packages/devextreme-angular/src/common/index.ts @@ -232,6 +232,7 @@ export namespace Data { export const LocalStore = DataModule.LocalStore; export type LocalStore = DataModule.LocalStore; export type LocalStoreOptions = DataModule.LocalStoreOptions; + export type MultiValueSearchOperation = DataModule.MultiValueSearchOperation; export const ODataContext = DataModule.ODataContext; export type ODataContext = DataModule.ODataContext; export type ODataContextOptions = DataModule.ODataContextOptions; @@ -317,6 +318,7 @@ export namespace Grids { export type FilterPanelTexts = GridsModule.FilterPanelTexts; export type FilterRow = GridsModule.FilterRow; export type FilterRowOperationDescriptions = GridsModule.FilterRowOperationDescriptions; + export type FilterScalarValue = GridsModule.FilterScalarValue; export type FilterType = GridsModule.FilterType; export type FixedPosition = GridsModule.FixedPosition; export type GridBase = GridsModule.GridBase; @@ -332,6 +334,7 @@ export namespace Grids { export type KeyboardNavigation = GridsModule.KeyboardNavigation; export type KeyDownInfo = GridsModule.KeyDownInfo; export type LoadPanel = GridsModule.LoadPanel; + export type MultiValueFilterExpr = GridsModule.MultiValueFilterExpr; export type NegatedFilterExpr = GridsModule.NegatedFilterExpr; export type NewRowInfo = GridsModule.NewRowInfo; export type NewRowPosition = GridsModule.NewRowPosition; diff --git a/packages/devextreme-angular/src/index.ts b/packages/devextreme-angular/src/index.ts index 65432c562db2..3624a2e345a9 100644 --- a/packages/devextreme-angular/src/index.ts +++ b/packages/devextreme-angular/src/index.ts @@ -314,6 +314,7 @@ export namespace Common { export const LocalStore = (DataModule as any).LocalStore as typeof import('devextreme/common/data').LocalStore; export type LocalStore = import('devextreme/common/data').LocalStore; export type LocalStoreOptions = import('devextreme/common/data').LocalStoreOptions; + export type MultiValueSearchOperation = import('devextreme/common/data').MultiValueSearchOperation; export const ODataContext = (DataModule as any).ODataContext as typeof import('devextreme/common/data').ODataContext; export type ODataContext = import('devextreme/common/data').ODataContext; export type ODataContextOptions = import('devextreme/common/data').ODataContextOptions; diff --git a/packages/devextreme-react/src/common/data.ts b/packages/devextreme-react/src/common/data.ts index 9dc779507085..a8b4bcb0582c 100644 --- a/packages/devextreme-react/src/common/data.ts +++ b/packages/devextreme-react/src/common/data.ts @@ -32,6 +32,7 @@ export type { LoadResult, LoadResultObject, LocalStoreOptions, + MultiValueSearchOperation, ODataContextOptions, ODataStoreOptions, Query, diff --git a/packages/devextreme-react/src/common/grids.ts b/packages/devextreme-react/src/common/grids.ts index f90801f9d45e..1467bd0c324c 100644 --- a/packages/devextreme-react/src/common/grids.ts +++ b/packages/devextreme-react/src/common/grids.ts @@ -42,6 +42,7 @@ export type { FilterPanelTexts, FilterRow, FilterRowOperationDescriptions, + FilterScalarValue, FilterType, FixedPosition, GridBase, @@ -57,6 +58,7 @@ export type { KeyboardNavigation, KeyDownInfo, LoadPanel, + MultiValueFilterExpr, NegatedFilterExpr, NewRowInfo, NewRowPosition, diff --git a/packages/devextreme-react/src/common/index.ts b/packages/devextreme-react/src/common/index.ts index 680fbb24f36a..f7c84a02403c 100644 --- a/packages/devextreme-react/src/common/index.ts +++ b/packages/devextreme-react/src/common/index.ts @@ -232,6 +232,7 @@ export namespace Data { export const LocalStore = DataModule.LocalStore; export type LocalStore = DataModule.LocalStore; export type LocalStoreOptions = DataModule.LocalStoreOptions; + export type MultiValueSearchOperation = DataModule.MultiValueSearchOperation; export const ODataContext = DataModule.ODataContext; export type ODataContext = DataModule.ODataContext; export type ODataContextOptions = DataModule.ODataContextOptions; @@ -317,6 +318,7 @@ export namespace Grids { export type FilterPanelTexts = GridsModule.FilterPanelTexts; export type FilterRow = GridsModule.FilterRow; export type FilterRowOperationDescriptions = GridsModule.FilterRowOperationDescriptions; + export type FilterScalarValue = GridsModule.FilterScalarValue; export type FilterType = GridsModule.FilterType; export type FixedPosition = GridsModule.FixedPosition; export type GridBase = GridsModule.GridBase; @@ -332,6 +334,7 @@ export namespace Grids { export type KeyboardNavigation = GridsModule.KeyboardNavigation; export type KeyDownInfo = GridsModule.KeyDownInfo; export type LoadPanel = GridsModule.LoadPanel; + export type MultiValueFilterExpr = GridsModule.MultiValueFilterExpr; export type NegatedFilterExpr = GridsModule.NegatedFilterExpr; export type NewRowInfo = GridsModule.NewRowInfo; export type NewRowPosition = GridsModule.NewRowPosition; diff --git a/packages/devextreme-vue/src/common/data.ts b/packages/devextreme-vue/src/common/data.ts index 9dc779507085..a8b4bcb0582c 100644 --- a/packages/devextreme-vue/src/common/data.ts +++ b/packages/devextreme-vue/src/common/data.ts @@ -32,6 +32,7 @@ export type { LoadResult, LoadResultObject, LocalStoreOptions, + MultiValueSearchOperation, ODataContextOptions, ODataStoreOptions, Query, diff --git a/packages/devextreme-vue/src/common/grids.ts b/packages/devextreme-vue/src/common/grids.ts index f90801f9d45e..1467bd0c324c 100644 --- a/packages/devextreme-vue/src/common/grids.ts +++ b/packages/devextreme-vue/src/common/grids.ts @@ -42,6 +42,7 @@ export type { FilterPanelTexts, FilterRow, FilterRowOperationDescriptions, + FilterScalarValue, FilterType, FixedPosition, GridBase, @@ -57,6 +58,7 @@ export type { KeyboardNavigation, KeyDownInfo, LoadPanel, + MultiValueFilterExpr, NegatedFilterExpr, NewRowInfo, NewRowPosition, diff --git a/packages/devextreme-vue/src/common/index.ts b/packages/devextreme-vue/src/common/index.ts index 680fbb24f36a..f7c84a02403c 100644 --- a/packages/devextreme-vue/src/common/index.ts +++ b/packages/devextreme-vue/src/common/index.ts @@ -232,6 +232,7 @@ export namespace Data { export const LocalStore = DataModule.LocalStore; export type LocalStore = DataModule.LocalStore; export type LocalStoreOptions = DataModule.LocalStoreOptions; + export type MultiValueSearchOperation = DataModule.MultiValueSearchOperation; export const ODataContext = DataModule.ODataContext; export type ODataContext = DataModule.ODataContext; export type ODataContextOptions = DataModule.ODataContextOptions; @@ -317,6 +318,7 @@ export namespace Grids { export type FilterPanelTexts = GridsModule.FilterPanelTexts; export type FilterRow = GridsModule.FilterRow; export type FilterRowOperationDescriptions = GridsModule.FilterRowOperationDescriptions; + export type FilterScalarValue = GridsModule.FilterScalarValue; export type FilterType = GridsModule.FilterType; export type FixedPosition = GridsModule.FixedPosition; export type GridBase = GridsModule.GridBase; @@ -332,6 +334,7 @@ export namespace Grids { export type KeyboardNavigation = GridsModule.KeyboardNavigation; export type KeyDownInfo = GridsModule.KeyDownInfo; export type LoadPanel = GridsModule.LoadPanel; + export type MultiValueFilterExpr = GridsModule.MultiValueFilterExpr; export type NegatedFilterExpr = GridsModule.NegatedFilterExpr; export type NewRowInfo = GridsModule.NewRowInfo; export type NewRowPosition = GridsModule.NewRowPosition; 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 c98d22cc2868..ab42f8ff6fa4 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 @@ -61,6 +61,11 @@ const singleBasic = (field: string, operator: string, value: unknown): any => tr basicNode('n1', field, operator, value), ]); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const singleMultiValue = (field: string, operator: string, values: unknown[]): any => tree('n1', [ + basicNode('n1', field, operator, values), +]); + const createCallbacks = (): { success: jest.Mock<(message?: string) => CommandResult>; failure: jest.Mock<(message?: string) => CommandResult>; @@ -117,6 +122,44 @@ describe('filterValueCommand', () => { expect(filterValueCommand.schema.safeParse({ expression }).success).toBe(true); }); + it.each([ + ['anyof'], ['noneof'], + ])('accepts multi-value op "%s"', (op) => { + expect(filterValueCommand.schema.safeParse({ + expression: singleMultiValue('name', op, ['Alpha', 'Beta']), + }).success).toBe(true); + }); + + it('accepts an anyof expression with mixed scalar types in array', () => { + expect(filterValueCommand.schema.safeParse({ + expression: singleMultiValue('name', 'anyof', ['Alpha', 1, true, null]), + }).success).toBe(true); + }); + + it('accepts an anyof expression with an empty array', () => { + expect(filterValueCommand.schema.safeParse({ + expression: singleMultiValue('name', 'anyof', []), + }).success).toBe(true); + }); + + it('rejects anyof with a scalar value instead of array', () => { + expect(filterValueCommand.schema.safeParse({ + expression: singleBasic('name', 'anyof', 'Alpha'), + }).success).toBe(false); + }); + + it('rejects noneof with a scalar value instead of array', () => { + expect(filterValueCommand.schema.safeParse({ + expression: singleBasic('name', 'noneof', 'Alpha'), + }).success).toBe(false); + }); + + it('rejects basic op with an array value', () => { + expect(filterValueCommand.schema.safeParse({ + expression: singleMultiValue('name', '=', ['Alpha', 'Beta']), + }).success).toBe(false); + }); + it('accepts a combined expression with "and"', () => { expect(filterValueCommand.schema.safeParse({ expression: tree('n3', [ @@ -288,6 +331,55 @@ describe('filterValueCommand', () => { expect(result.status).toBe('success'); }); + it('converts anyof expression to the legacy array form', async () => { + const instance = await createGrid(); + const spy = jest.spyOn(instance, 'option'); + const callbacks = createCallbacks(); + + const result = await filterValueCommand.execute(instance, callbacks)({ + expression: singleMultiValue('name', 'anyof', ['Alpha', 'Beta']), + }); + + expect(spy).toHaveBeenCalledWith('filterValue', ['name', 'anyof', ['Alpha', 'Beta']]); + expect(result.status).toBe('success'); + }); + + it('converts noneof expression to the legacy array form', async () => { + const instance = await createGrid(); + const spy = jest.spyOn(instance, 'option'); + const callbacks = createCallbacks(); + + const result = await filterValueCommand.execute(instance, callbacks)({ + expression: singleMultiValue('name', 'noneof', ['Alpha', 'Beta']), + }); + + expect(spy).toHaveBeenCalledWith('filterValue', ['name', 'noneof', ['Alpha', 'Beta']]); + expect(result.status).toBe('success'); + }); + + it('resolves date values inside anyof array 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: singleMultiValue('SaleDate', 'anyof', ['2024-05-10T00:00:00', '2024-06-01T00:00:00']), + }); + + expect(spy).toHaveBeenCalledWith('filterValue', [ + 'SaleDate', 'anyof', [new Date('2024-05-10T00:00:00'), new Date('2024-06-01T00:00:00')], + ]); + 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'); @@ -449,6 +541,19 @@ describe('filterValueCommand', () => { expect(result.status).toBe('failure'); }); + it('returns failure when anyof field has no corresponding column', async () => { + const instance = await createGrid(); + const spy = jest.spyOn(instance, 'option'); + const callbacks = createCallbacks(); + + const result = await filterValueCommand.execute(instance, callbacks)({ + expression: singleMultiValue('nonexistent', 'anyof', ['Alpha']), + }); + + expect(spy).not.toHaveBeenCalled(); + expect(result.status).toBe('failure'); + }); + it('succeeds when a field maps to a hidden but existing column', async () => { const instance = await createGrid({ columns: [ 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 ea3ff6065cf7..a5af2cf9888b 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 @@ -7,7 +7,7 @@ import { z } from 'zod'; import { // eslint-disable-next-line spellcheck/spell-checker - isKeyShapeValid, normalizeKey, optionalNullish, resolveFilterValue, + isKeyShapeValid, isMultiValueExpr, normalizeKey, optionalNullish, resolveFilterValue, } from '../utils'; describe('normalizeKey', () => { @@ -168,3 +168,71 @@ describe('resolveFilterValue', () => { expect(resolveFilterValue('date', true)).toBe(true); }); }); + +describe('isMultiValueExpr', () => { + it('returns true for an expression with an array value', () => { + const expr = { + type: 'basic' as const, + field: 'status', + operator: 'anyof' as const, + value: ['open', 'closed'], + }; + + expect(isMultiValueExpr(expr)).toBe(true); + }); + + it('returns true for an expression with an empty array value', () => { + const expr = { + type: 'basic' as const, + field: 'status', + operator: 'noneof' as const, + value: [] as string[], + }; + + expect(isMultiValueExpr(expr)).toBe(true); + }); + + it('returns false for an expression with a string value', () => { + const expr = { + type: 'basic' as const, + field: 'name', + operator: '=' as const, + value: 'Alice', + }; + + expect(isMultiValueExpr(expr)).toBe(false); + }); + + it('returns false for an expression with a number value', () => { + const expr = { + type: 'basic' as const, + field: 'age', + operator: '>' as const, + value: 18, + }; + + expect(isMultiValueExpr(expr)).toBe(false); + }); + + it('returns false for an expression with a null value', () => { + const expr = { + type: 'basic' as const, + field: 'name', + operator: '=' as const, + value: null, + }; + + expect(isMultiValueExpr(expr)).toBe(false); + }); + + it('returns false for an expression with a boolean value', () => { + const expr = { + type: 'basic' as const, + field: 'active', + operator: '=' as const, + value: true, + }; + + expect(isMultiValueExpr(expr)).toBe(false); + }); +}); 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 8896d9e1463c..dcc5e8479134 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,5 +1,7 @@ -import type { SearchOperation } from '@js/common/data.types'; -import type { BasicFilterExpr, FilterExprNode, FilterExprTree } from '@js/common/grids'; +import type { MultiValueSearchOperation, SearchOperation } from '@js/common/data.types'; +import type { + FilterExprNode, FilterExprTree, FilterScalarValue, +} from '@js/common/grids'; import { when } from '@js/core/utils/deferred'; import { isDefined } from '@js/core/utils/type'; import type { CommandResult } from '@ts/grids/grid_core/ai_assistant/types'; @@ -7,18 +9,24 @@ import type { InternalGrid } from '@ts/grids/grid_core/m_types'; import { z } from 'zod'; import { defineGridCommand } from './defineGridCommand'; -import { resolveFilterValue } from './utils'; +import { isMultiValueExpr, resolveFilterValue } from './utils'; const FILTER_OPS = [ '=', '<>', '<', '<=', '>', '>=', 'contains', 'notcontains', 'startswith', 'endswith', ] as const satisfies readonly SearchOperation[]; -type FilterExprArray = | [string, SearchOperation, BasicFilterExpr['value']] +const MULTI_VALUE_FILTER_OPS = [ + 'anyof', 'noneof', +] as const satisfies readonly MultiValueSearchOperation[]; + +type FilterExprArray = [string, SearchOperation, FilterScalarValue] + | [string, MultiValueSearchOperation, FilterScalarValue[]] | [FilterExprArray, 'and' | 'or', FilterExprArray] | ['!', FilterExprArray]; const filterOpSchema = z.enum(FILTER_OPS); +const multiValueFilterOpSchema = z.enum(MULTI_VALUE_FILTER_OPS); const filterValueScalarSchema = z.union([ z.string().describe( @@ -36,6 +44,13 @@ const basicFilterExprSchema = z.object({ value: filterValueScalarSchema, }).strict(); +const multiValueFilterExprSchema = z.object({ + type: z.enum(['basic']), + field: z.string(), + operator: multiValueFilterOpSchema, + value: z.array(filterValueScalarSchema), +}).strict(); + const combinedFilterExprSchema = z.object({ type: z.enum(['combined']), combiner: z.enum(['and', 'or']), @@ -50,6 +65,7 @@ const negatedFilterExprSchema = z.object({ const filterExprSchema = z.union([ basicFilterExprSchema, + multiValueFilterExprSchema, combinedFilterExprSchema, negatedFilterExprSchema, ]); @@ -101,6 +117,11 @@ function convertFilterExprToArray( throw new Error(`Unknown column: ${expr.field}`); } + if (isMultiValueExpr(expr)) { + const resolved = expr.value.map((v) => resolveFilterValue(column.dataType, v)); + return [expr.field, expr.operator, resolved]; + } + const resolved = resolveFilterValue(column.dataType, expr.value); return [expr.field, expr.operator, resolved]; @@ -125,9 +146,9 @@ const getFilterSuccessMessage = async ( filterValue: FilterExprArray, ): Promise => { try { + const customOperations = component.getController('filterSync').getCustomFilterOperations(); const filterText: string = await when( - // Custom filter operations are omitted as not supported in command - component.getView('filterPanelView').getFilterText(filterValue, []), + component.getView('filterPanelView').getFilterText(filterValue, customOperations), ); return `Apply a filter: ${filterText}.`; @@ -138,7 +159,40 @@ const getFilterSuccessMessage = async ( 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" of every basic expression MUST be the dataField of a column that exists in the grid (not the caption); the column may be hidden, but it must exist. Never filter on a field that has no corresponding column. 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"}}]}.', + 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" of every basic expression MUST be + the dataField of a column that exists in the grid (not the caption); the column may be hidden, but it + must exist. Never filter on a field that has no corresponding column. + + 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. + + Supported operators: "=", "<>", "<", "<=", ">", ">=", "contains", "notcontains", "startswith", "endswith", "anyof", "noneof". + + It is needed to use the anyof operator for filtering multiple values. It allows syncing with HeaderFilter. + For example, instead of using + {"rootId":"n3","nodes":[{"id":"n1","expr":{"type":"basic","field":"name","operator":"=","value":"Alpha"}},{"id":"n2","expr":{"type":"basic","field":"name","operator":"=","value":"Beta"}},{"id":"n3","expr":{"type":"combined","combiner":"or","leftId":"n1","rightId":"n2"}}]} + you need to use: + {"rootId": "n1", "nodes":[{"id":"n1", "expr":{"type":"basic", "field": "name", "operator":"anyof", "value":["Alpha", "Beta"]}}]} + + Use the noneof operator to exclude multiple values. For example, to filter rows where name is neither "Alpha" nor "Beta": + {"rootId": "n1", "nodes":[{"id":"n1", "expr":{"type":"basic", "field": "name", "operator":"noneof", "value":["Alpha", "Beta"]}}]} + + 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 }) => async (args): Promise => { let defaultMessage = args.expression === null 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 4a2e7c521faf..717403cfa1c9 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,4 +1,6 @@ -import type { BasicFilterExpr, CompositeKeyPair } from '@js/common/grids'; +import type { + BasicFilterExpr, CompositeKeyPair, FilterScalarValue, MultiValueFilterExpr, +} 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'; @@ -67,12 +69,10 @@ export const isKeyShapeValid = ( return keyExpr.every((field) => field in key); }; -type FilterExprValue = BasicFilterExpr['value']; - export function resolveFilterValue( dataType: string | undefined, - value: FilterExprValue, -): FilterExprValue { + value: FilterScalarValue, +): FilterScalarValue { if (typeof value === 'string' && isDateType(dataType)) { if (!dateUtilsTs.isValidDate(value)) { return value; @@ -81,3 +81,9 @@ export function resolveFilterValue( } return value; } + +export function isMultiValueExpr( + expr: BasicFilterExpr | MultiValueFilterExpr, +): expr is MultiValueFilterExpr { + return Array.isArray(expr.value); +} diff --git a/packages/devextreme/js/common/data.d.ts b/packages/devextreme/js/common/data.d.ts index e377d84765e4..6abb950631b6 100644 --- a/packages/devextreme/js/common/data.d.ts +++ b/packages/devextreme/js/common/data.d.ts @@ -9,6 +9,7 @@ import type { import type { SearchOperation as SearchOperationInternal, + MultiValueSearchOperation as MultiValueSearchOperationInternal, GroupingInterval as GroupingIntervalInternal, SortDescriptor as SortDescriptorInternal, GroupDescriptor as GroupDescriptorInternal, @@ -24,6 +25,12 @@ import type { */ export type SearchOperation = SearchOperationInternal; +/** + * @namespace DevExpress.common.data + * @public + */ +export type MultiValueSearchOperation = MultiValueSearchOperationInternal; + /** * @namespace DevExpress.common.data * @public diff --git a/packages/devextreme/js/common/data.types.d.ts b/packages/devextreme/js/common/data.types.d.ts index 3dcdfb98be1a..62f09a3cf5e1 100644 --- a/packages/devextreme/js/common/data.types.d.ts +++ b/packages/devextreme/js/common/data.types.d.ts @@ -9,6 +9,11 @@ import { */ export type SearchOperation = '=' | '<>' | '>' | '>=' | '<' | '<=' | 'startswith' | 'endswith' | 'contains' | 'notcontains'; +/** + * @namespace DevExpress.data + */ +export type MultiValueSearchOperation = 'anyof' | 'noneof'; + /** * @namespace DevExpress.data */ diff --git a/packages/devextreme/js/common/grids.d.ts b/packages/devextreme/js/common/grids.d.ts index 72d53ff536c5..3e9be5f807e0 100644 --- a/packages/devextreme/js/common/grids.d.ts +++ b/packages/devextreme/js/common/grids.d.ts @@ -29,6 +29,7 @@ import { import { DataSource, DataSourceOptions, + MultiValueSearchOperation, SearchOperation, } from './data'; @@ -100,6 +101,13 @@ export type ResponseStatusTexts = { failure?: string; }; +/** + * @docid + * @public + * @namespace DevExpress.common.grids + */ +export type FilterScalarValue = string | number | boolean | null | Date; + /** * @docid * @public @@ -109,7 +117,19 @@ export type BasicFilterExpr = { type: 'basic'; field: string; operator: SearchOperation; - value: string | number | boolean | null | Date; + value: FilterScalarValue; +}; + +/** + * @docid + * @public + * @namespace DevExpress.common.grids + */ +export type MultiValueFilterExpr = { + type: 'basic'; + field: string; + operator: MultiValueSearchOperation; + value: FilterScalarValue[]; }; /** @@ -139,7 +159,7 @@ export type NegatedFilterExpr = { * @public * @namespace DevExpress.common.grids */ -export type FilterExpr = BasicFilterExpr | CombinedFilterExpr | NegatedFilterExpr; +export type FilterExpr = BasicFilterExpr | MultiValueFilterExpr | CombinedFilterExpr | NegatedFilterExpr; /** * @docid diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index be00d6be8554..74ba296fdfc2 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -3830,6 +3830,8 @@ declare module DevExpress.common.data { */ name?: string; }; + export type MultiValueSearchOperation = + DevExpress.data.MultiValueSearchOperation; /** * [descr:ODataContext] */ @@ -4706,7 +4708,7 @@ declare module DevExpress.common.grids { type: 'basic'; field: string; operator: DevExpress.common.data.SearchOperation; - value: string | number | boolean | null | Date; + value: FilterScalarValue; }; /** * [descr:ColumnAIOptions] @@ -5457,6 +5459,7 @@ declare module DevExpress.common.grids { */ export type FilterExpr = | BasicFilterExpr + | MultiValueFilterExpr | CombinedFilterExpr | NegatedFilterExpr; /** @@ -5626,6 +5629,10 @@ declare module DevExpress.common.grids { */ startsWith?: string; }; + /** + * [descr:FilterScalarValue] + */ + export type FilterScalarValue = string | number | boolean | null | Date; export type FilterType = 'exclude' | 'include'; export type FixedPosition = 'left' | 'right' | 'sticky'; /** @@ -6497,6 +6504,15 @@ declare module DevExpress.common.grids { */ width?: number | string; }; + /** + * [descr:MultiValueFilterExpr] + */ + export type MultiValueFilterExpr = { + type: 'basic'; + field: string; + operator: DevExpress.common.data.MultiValueSearchOperation; + value: FilterScalarValue[]; + }; /** * [descr:NegatedFilterExpr] */ @@ -7709,6 +7725,10 @@ declare module DevExpress.data { */ userData?: any; } + /** + * @deprecated Attention! This type is for internal purposes only. If you used it previously, please submit a ticket to our {@link https://supportcenter.devexpress.com/ticket/create Support Center}. We will check if there is an alternative solution. + */ + export type MultiValueSearchOperation = 'anyof' | 'noneof'; /** * @deprecated Attention! This type is for internal purposes only. If you used it previously, please submit a ticket to our {@link https://supportcenter.devexpress.com/ticket/create Support Center}. We will check if there is an alternative solution. */