From 755be16286d18b906a9cd8da2b131985edf79b6b Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Tue, 2 Jun 2026 16:25:29 +0400 Subject: [PATCH 1/4] AI Assistant: add deselect option to selectByIndexes command --- .../commands/__tests__/selection.test.ts | 105 +++++++++++++++--- .../ai_assistant/commands/selection.ts | 18 ++- packages/devextreme/js/common/grids.d.ts | 3 +- packages/devextreme/ts/dx.all.d.ts | 1 + 4 files changed, 107 insertions(+), 20 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts index 6b382a06f4d7..627136a6b813 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts @@ -271,33 +271,52 @@ describe('selectByIndexesCommand', () => { afterEach(() => afterTest()); describe('schema', () => { - it('accepts an array of positive integers', () => { - expect(selectByIndexesCommand.schema.safeParse({ indexes: [1, 2, 3] }).success).toBe(true); + it('accepts an array of positive integers with deselect', () => { + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [1, 2, 3], deselect: false, + }).success).toBe(true); }); it('rejects when indexes is missing', () => { - expect(selectByIndexesCommand.schema.safeParse({}).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + deselect: false, + }).success).toBe(false); + }); + + it('rejects when deselect is missing', () => { + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [1], + }).success).toBe(false); }); it('rejects when indexes is an empty array', () => { - expect(selectByIndexesCommand.schema.safeParse({ indexes: [] }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [], deselect: false, + }).success).toBe(false); }); it('rejects zero (indexes are 1-based)', () => { - expect(selectByIndexesCommand.schema.safeParse({ indexes: [0] }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [0], deselect: false, + }).success).toBe(false); }); it('rejects negative indexes', () => { - expect(selectByIndexesCommand.schema.safeParse({ indexes: [-1] }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [-1], deselect: false, + }).success).toBe(false); }); it('rejects non-integer indexes', () => { - expect(selectByIndexesCommand.schema.safeParse({ indexes: [1.5] }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [1.5], deselect: false, + }).success).toBe(false); }); it('rejects unknown properties', () => { expect(selectByIndexesCommand.schema.safeParse({ indexes: [1], + deselect: false, extra: 1, }).success).toBe(false); }); @@ -309,7 +328,9 @@ describe('selectByIndexesCommand', () => { const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes'); const callbacks = createCallbacks(); - const result = await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [0] }); + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: false, + }); expect(result.status).toBe('failure'); expect(selectSpy).not.toHaveBeenCalled(); @@ -322,7 +343,7 @@ describe('selectByIndexesCommand', () => { // Three rows in createGrid; 1-based index 100 has no row on the current page. const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 100], + indexes: [1, 100], deselect: false, }); expect(result.status).toBe('failure'); @@ -341,7 +362,9 @@ describe('selectByIndexesCommand', () => { const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes'); const callbacks = createCallbacks(); - const result = await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1] }); + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: false, + }); expect(result.status).toBe('failure'); expect(selectSpy).not.toHaveBeenCalled(); @@ -352,12 +375,29 @@ describe('selectByIndexesCommand', () => { const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes').mockReturnValue(Promise.resolve([]) as never); const callbacks = createCallbacks(); - const result = await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1, 3] }); + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1, 3], deselect: false, + }); expect(selectSpy).toHaveBeenCalledWith([0, 2]); expect(result.status).toBe('success'); }); + it('resolves indexes to row keys and calls deselectRows when deselecting', async () => { + const instance = await createGrid(); + const deselectSpy = jest.spyOn(instance, 'deselectRows').mockReturnValue(Promise.resolve([]) as never); + const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes'); + const callbacks = createCallbacks(); + + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: true, + }); + + expect(deselectSpy).toHaveBeenCalledWith([1]); + expect(selectSpy).not.toHaveBeenCalled(); + expect(result.status).toBe('success'); + }); + it('returns failure when selectRowsByIndexes throws', async () => { const instance = await createGrid(); jest.spyOn(instance, 'selectRowsByIndexes').mockImplementation(() => { @@ -365,7 +405,9 @@ describe('selectByIndexesCommand', () => { }); const callbacks = createCallbacks(); - const result = await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1] }); + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: false, + }); expect(result.status).toBe('failure'); }); @@ -376,28 +418,59 @@ describe('selectByIndexesCommand', () => { .mockReturnValue(Promise.reject(new Error('Error')) as never); const callbacks = createCallbacks(); - const result = await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1] }); + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: false, + }); + + expect(result.status).toBe('failure'); + }); + + it('returns failure when deselectRows rejects', async () => { + const instance = await createGrid(); + jest.spyOn(instance, 'deselectRows') + .mockReturnValue(Promise.reject(new Error('Error')) as never); + const callbacks = createCallbacks(); + + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: true, + }); expect(result.status).toBe('failure'); }); }); describe('default message', () => { - it('reports the 1-based row numbers on the current page on success', async () => { + it('reports the 1-based row numbers on the current page on select', async () => { const instance = await createGrid(); jest.spyOn(instance, 'selectRowsByIndexes').mockReturnValue(Promise.resolve([]) as never); const callbacks = createCallbacks(); - await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1, 3] }); + await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1, 3], deselect: false, + }); expect(callbacks.success).toHaveBeenCalledWith('Select row(s) number 1, 3 on the current page.'); }); + it('reports the 1-based row numbers on the current page on deselect', async () => { + const instance = await createGrid(); + jest.spyOn(instance, 'deselectRows').mockReturnValue(Promise.resolve([]) as never); + const callbacks = createCallbacks(); + + await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: true, + }); + + expect(callbacks.success).toHaveBeenCalledWith('Deselect row(s) number 1 on the current page.'); + }); + it('passes the same default message to failure', async () => { const instance = await createGrid({ selection: { mode: 'none' } }); const callbacks = createCallbacks(); - await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1] }); + await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], deselect: false, + }); expect(callbacks.failure).toHaveBeenCalledWith('Select row(s) number 1 on the current page.'); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts index 6c5a5cf9ded4..326273ebde6d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts @@ -45,15 +45,22 @@ export const selectByKeysCommand = defineGridCommand({ const selectByIndexesCommandSchema = z.object({ indexes: z.array(z.number().int().min(1)).min(1), + deselect: z.boolean(), }).strict(); export const selectByIndexesCommand = defineGridCommand({ name: 'selectByIndexes', - description: 'Select rows by their 1-based indexes within the current page. Index 1 is the first row on the visible page; group/header rows are not selectable. To select rows that are not on the current page, use selectByKeys, or call pageIndex first to switch the page.', + description: 'Select or deselect specific rows by their 1-based indexes within the current page. ' + + 'Index 1 is the first row on the visible page; group/header rows are not addressable. ' + + 'Set deselect to true to remove the listed rows from the current selection (e.g. "unselect row 1"); set deselect to false to select them. ' + + 'When deselect is false, the listed rows replace the current selection. ' + + 'To target rows that are not on the current page, use selectByKeys, or call pageIndex first to switch the page. ' + + 'To clear the whole selection, use deselectAll or clearSelection.', schema: selectByIndexesCommandSchema, execute: (component, { success, failure }) => async (args): Promise => { const rowIndexes = args.indexes.join(', '); - const defaultMessage = `Select row(s) number ${rowIndexes} on the current page.`; + const action = args.deselect ? 'Deselect' : 'Select'; + const defaultMessage = `${action} row(s) number ${rowIndexes} on the current page.`; if (component.option('selection.mode') === 'none') { return failure(defaultMessage); @@ -70,7 +77,12 @@ export const selectByIndexesCommand = defineGridCommand({ } try { - await component.selectRowsByIndexes(normalizedRowIndexes); + if (args.deselect) { + const itemKeys = normalizedRowIndexes.map((index) => items[index].key); + await component.deselectRows(itemKeys); + } else { + await component.selectRowsByIndexes(normalizedRowIndexes); + } return success(defaultMessage); } catch { diff --git a/packages/devextreme/js/common/grids.d.ts b/packages/devextreme/js/common/grids.d.ts index 2002bbe7f8ba..f26ee6c6e77f 100644 --- a/packages/devextreme/js/common/grids.d.ts +++ b/packages/devextreme/js/common/grids.d.ts @@ -221,7 +221,8 @@ export type PredefinedCommands = { preserve: boolean; }; selectByIndexes: { - indexes: number[] + indexes: number[]; + deselect: boolean; }; selectAll: {}; deselectAll: {}; diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 0a793a6a963f..1406614b06d6 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -6611,6 +6611,7 @@ declare module DevExpress.common.grids { }; selectByIndexes: { indexes: number[]; + deselect: boolean; }; selectAll: {}; deselectAll: {}; From 90a343194b3b105ad19c344a5e9526b9fc77184e Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Tue, 2 Jun 2026 17:25:35 +0400 Subject: [PATCH 2/4] make "deselect" optional for backward compatibility --- .../commands/__tests__/selection.test.ts | 50 ++++++++++++------- .../ai_assistant/commands/selection.ts | 8 +-- packages/devextreme/js/common/grids.d.ts | 2 +- packages/devextreme/ts/dx.all.d.ts | 2 +- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts index 627136a6b813..9a8f98efeb5d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts @@ -273,50 +273,47 @@ describe('selectByIndexesCommand', () => { describe('schema', () => { it('accepts an array of positive integers with deselect', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [1, 2, 3], deselect: false, + indexes: [1, 2, 3], deselect: true, }).success).toBe(true); }); it('rejects when indexes is missing', () => { - expect(selectByIndexesCommand.schema.safeParse({ - deselect: false, - }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({}).success).toBe(false); }); - it('rejects when deselect is missing', () => { + it('accepts when deselect is omitted (optional)', () => { expect(selectByIndexesCommand.schema.safeParse({ indexes: [1], - }).success).toBe(false); + }).success).toBe(true); }); it('rejects when indexes is an empty array', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [], deselect: false, + indexes: [], }).success).toBe(false); }); it('rejects zero (indexes are 1-based)', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [0], deselect: false, + indexes: [0], }).success).toBe(false); }); it('rejects negative indexes', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [-1], deselect: false, + indexes: [-1], }).success).toBe(false); }); it('rejects non-integer indexes', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [1.5], deselect: false, + indexes: [1.5], }).success).toBe(false); }); it('rejects unknown properties', () => { expect(selectByIndexesCommand.schema.safeParse({ indexes: [1], - deselect: false, extra: 1, }).success).toBe(false); }); @@ -329,7 +326,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: false, + indexes: [1], }); expect(result.status).toBe('failure'); @@ -343,7 +340,7 @@ describe('selectByIndexesCommand', () => { // Three rows in createGrid; 1-based index 100 has no row on the current page. const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 100], deselect: false, + indexes: [1, 100], }); expect(result.status).toBe('failure'); @@ -363,7 +360,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: false, + indexes: [1], }); expect(result.status).toBe('failure'); @@ -376,13 +373,28 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 3], deselect: false, + indexes: [1, 3], }); expect(selectSpy).toHaveBeenCalledWith([0, 2]); expect(result.status).toBe('success'); }); + it('selects when deselect is omitted (defaults to selecting)', async () => { + const instance = await createGrid(); + const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes').mockReturnValue(Promise.resolve([]) as never); + const deselectSpy = jest.spyOn(instance, 'deselectRows'); + const callbacks = createCallbacks(); + + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1, 3], + }); + + expect(selectSpy).toHaveBeenCalledWith([0, 2]); + expect(deselectSpy).not.toHaveBeenCalled(); + expect(result.status).toBe('success'); + }); + it('resolves indexes to row keys and calls deselectRows when deselecting', async () => { const instance = await createGrid(); const deselectSpy = jest.spyOn(instance, 'deselectRows').mockReturnValue(Promise.resolve([]) as never); @@ -406,7 +418,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: false, + indexes: [1], }); expect(result.status).toBe('failure'); @@ -419,7 +431,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: false, + indexes: [1], }); expect(result.status).toBe('failure'); @@ -446,7 +458,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 3], deselect: false, + indexes: [1, 3], }); expect(callbacks.success).toHaveBeenCalledWith('Select row(s) number 1, 3 on the current page.'); @@ -469,7 +481,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: false, + indexes: [1], }); expect(callbacks.failure).toHaveBeenCalledWith('Select row(s) number 1 on the current page.'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts index 326273ebde6d..c1c4b7aaf39d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts @@ -45,17 +45,17 @@ export const selectByKeysCommand = defineGridCommand({ const selectByIndexesCommandSchema = z.object({ indexes: z.array(z.number().int().min(1)).min(1), - deselect: z.boolean(), + deselect: z.boolean().optional(), }).strict(); export const selectByIndexesCommand = defineGridCommand({ name: 'selectByIndexes', description: 'Select or deselect specific rows by their 1-based indexes within the current page. ' + 'Index 1 is the first row on the visible page; group/header rows are not addressable. ' - + 'Set deselect to true to remove the listed rows from the current selection (e.g. "unselect row 1"); set deselect to false to select them. ' - + 'When deselect is false, the listed rows replace the current selection. ' + + 'Set deselect to true to remove the listed rows from the current selection (e.g. "unselect row 1"); omit it or set it to false to select them. ' + + 'When deselect is false or omitted, the listed rows replace the current selection. ' + 'To target rows that are not on the current page, use selectByKeys, or call pageIndex first to switch the page. ' - + 'To clear the whole selection, use deselectAll or clearSelection.', + + 'To clear selection only within the current selectAll scope, use deselectAll; to clear selection across all pages regardless of selectAllMode, use clearSelection.', schema: selectByIndexesCommandSchema, execute: (component, { success, failure }) => async (args): Promise => { const rowIndexes = args.indexes.join(', '); diff --git a/packages/devextreme/js/common/grids.d.ts b/packages/devextreme/js/common/grids.d.ts index f26ee6c6e77f..e38178afeed9 100644 --- a/packages/devextreme/js/common/grids.d.ts +++ b/packages/devextreme/js/common/grids.d.ts @@ -222,7 +222,7 @@ export type PredefinedCommands = { }; selectByIndexes: { indexes: number[]; - deselect: boolean; + deselect?: boolean; }; selectAll: {}; deselectAll: {}; diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 1406614b06d6..f8528417e4ea 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -6611,7 +6611,7 @@ declare module DevExpress.common.grids { }; selectByIndexes: { indexes: number[]; - deselect: boolean; + deselect?: boolean; }; selectAll: {}; deselectAll: {}; From 8948bd0eda191e2a74717b837956470d6d156ed3 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Wed, 3 Jun 2026 11:51:15 +0400 Subject: [PATCH 3/4] wrap "deselect" into optionalNullish --- .../grids/grid_core/ai_assistant/commands/selection.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts index c1c4b7aaf39d..fa6d47b46205 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts @@ -2,7 +2,10 @@ import type { CommandResult } from '@ts/grids/grid_core/ai_assistant/types'; import { z } from 'zod'; import { defineGridCommand } from './defineGridCommand'; -import { compositeKeyPairSchema, isKeyShapeValid, normalizeKey } from './utils'; +import { + // eslint-disable-next-line spellcheck/spell-checker + compositeKeyPairSchema, isKeyShapeValid, normalizeKey, optionalNullish, +} from './utils'; const selectByKeysCommandSchema = z.object({ keys: z.array(z.union([ @@ -45,7 +48,8 @@ export const selectByKeysCommand = defineGridCommand({ const selectByIndexesCommandSchema = z.object({ indexes: z.array(z.number().int().min(1)).min(1), - deselect: z.boolean().optional(), + // eslint-disable-next-line spellcheck/spell-checker + deselect: optionalNullish(z.boolean()), }).strict(); export const selectByIndexesCommand = defineGridCommand({ From f06666c254cb311b8a8be8c0eaa29e76b1e6b785 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Wed, 3 Jun 2026 18:07:55 +0400 Subject: [PATCH 4/4] use required argument "mode" instead of optional "deselect" --- .../commands/__tests__/selection.test.ts | 57 ++++++++++++------- .../ai_assistant/commands/selection.ts | 28 +++++---- packages/devextreme/js/common/grids.d.ts | 2 +- packages/devextreme/ts/dx.all.d.ts | 2 +- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts index 9a8f98efeb5d..56c842003265 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/__tests__/selection.test.ts @@ -271,49 +271,62 @@ describe('selectByIndexesCommand', () => { afterEach(() => afterTest()); describe('schema', () => { - it('accepts an array of positive integers with deselect', () => { + it('accepts an array of positive integers with mode deselect', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [1, 2, 3], deselect: true, + indexes: [1, 2, 3], mode: 'deselect', + }).success).toBe(true); + }); + + it('accepts mode select', () => { + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [1], mode: 'select', }).success).toBe(true); }); it('rejects when indexes is missing', () => { - expect(selectByIndexesCommand.schema.safeParse({}).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ mode: 'select' }).success).toBe(false); }); - it('accepts when deselect is omitted (optional)', () => { + it('rejects when mode is missing', () => { expect(selectByIndexesCommand.schema.safeParse({ indexes: [1], - }).success).toBe(true); + }).success).toBe(false); + }); + + it('rejects an invalid mode value', () => { + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [1], mode: 'toggle', + }).success).toBe(false); }); it('rejects when indexes is an empty array', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [], + indexes: [], mode: 'select', }).success).toBe(false); }); it('rejects zero (indexes are 1-based)', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [0], + indexes: [0], mode: 'select', }).success).toBe(false); }); it('rejects negative indexes', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [-1], + indexes: [-1], mode: 'select', }).success).toBe(false); }); it('rejects non-integer indexes', () => { expect(selectByIndexesCommand.schema.safeParse({ - indexes: [1.5], + indexes: [1.5], mode: 'select', }).success).toBe(false); }); it('rejects unknown properties', () => { expect(selectByIndexesCommand.schema.safeParse({ indexes: [1], + mode: 'select', extra: 1, }).success).toBe(false); }); @@ -326,7 +339,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], + indexes: [1], mode: 'select', }); expect(result.status).toBe('failure'); @@ -340,7 +353,7 @@ describe('selectByIndexesCommand', () => { // Three rows in createGrid; 1-based index 100 has no row on the current page. const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 100], + indexes: [1, 100], mode: 'select', }); expect(result.status).toBe('failure'); @@ -360,7 +373,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], + indexes: [1], mode: 'select', }); expect(result.status).toBe('failure'); @@ -373,21 +386,21 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 3], + indexes: [1, 3], mode: 'select', }); expect(selectSpy).toHaveBeenCalledWith([0, 2]); expect(result.status).toBe('success'); }); - it('selects when deselect is omitted (defaults to selecting)', async () => { + it('selects when mode is select', async () => { const instance = await createGrid(); const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes').mockReturnValue(Promise.resolve([]) as never); const deselectSpy = jest.spyOn(instance, 'deselectRows'); const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 3], + indexes: [1, 3], mode: 'select', }); expect(selectSpy).toHaveBeenCalledWith([0, 2]); @@ -402,7 +415,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: true, + indexes: [1], mode: 'deselect', }); expect(deselectSpy).toHaveBeenCalledWith([1]); @@ -418,7 +431,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], + indexes: [1], mode: 'select', }); expect(result.status).toBe('failure'); @@ -431,7 +444,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], + indexes: [1], mode: 'select', }); expect(result.status).toBe('failure'); @@ -444,7 +457,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); const result = await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: true, + indexes: [1], mode: 'deselect', }); expect(result.status).toBe('failure'); @@ -458,7 +471,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1, 3], + indexes: [1, 3], mode: 'select', }); expect(callbacks.success).toHaveBeenCalledWith('Select row(s) number 1, 3 on the current page.'); @@ -470,7 +483,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], deselect: true, + indexes: [1], mode: 'deselect', }); expect(callbacks.success).toHaveBeenCalledWith('Deselect row(s) number 1 on the current page.'); @@ -481,7 +494,7 @@ describe('selectByIndexesCommand', () => { const callbacks = createCallbacks(); await selectByIndexesCommand.execute(instance, callbacks)({ - indexes: [1], + indexes: [1], mode: 'select', }); expect(callbacks.failure).toHaveBeenCalledWith('Select row(s) number 1 on the current page.'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts index fa6d47b46205..da2c1173aa76 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/commands/selection.ts @@ -3,8 +3,7 @@ import { z } from 'zod'; import { defineGridCommand } from './defineGridCommand'; import { - // eslint-disable-next-line spellcheck/spell-checker - compositeKeyPairSchema, isKeyShapeValid, normalizeKey, optionalNullish, + compositeKeyPairSchema, isKeyShapeValid, normalizeKey, } from './utils'; const selectByKeysCommandSchema = z.object({ @@ -48,22 +47,21 @@ export const selectByKeysCommand = defineGridCommand({ const selectByIndexesCommandSchema = z.object({ indexes: z.array(z.number().int().min(1)).min(1), - // eslint-disable-next-line spellcheck/spell-checker - deselect: optionalNullish(z.boolean()), + mode: z.enum(['select', 'deselect']), }).strict(); export const selectByIndexesCommand = defineGridCommand({ name: 'selectByIndexes', description: 'Select or deselect specific rows by their 1-based indexes within the current page. ' + 'Index 1 is the first row on the visible page; group/header rows are not addressable. ' - + 'Set deselect to true to remove the listed rows from the current selection (e.g. "unselect row 1"); omit it or set it to false to select them. ' - + 'When deselect is false or omitted, the listed rows replace the current selection. ' + + 'Set mode to "deselect" to remove the listed rows from the current selection (e.g. "unselect row 1"); set it to "select" to select them. ' + + 'When mode is "select", the listed rows replace the current selection. ' + 'To target rows that are not on the current page, use selectByKeys, or call pageIndex first to switch the page. ' + 'To clear selection only within the current selectAll scope, use deselectAll; to clear selection across all pages regardless of selectAllMode, use clearSelection.', schema: selectByIndexesCommandSchema, execute: (component, { success, failure }) => async (args): Promise => { const rowIndexes = args.indexes.join(', '); - const action = args.deselect ? 'Deselect' : 'Select'; + const action = args.mode === 'deselect' ? 'Deselect' : 'Select'; const defaultMessage = `${action} row(s) number ${rowIndexes} on the current page.`; if (component.option('selection.mode') === 'none') { @@ -81,11 +79,17 @@ export const selectByIndexesCommand = defineGridCommand({ } try { - if (args.deselect) { - const itemKeys = normalizedRowIndexes.map((index) => items[index].key); - await component.deselectRows(itemKeys); - } else { - await component.selectRowsByIndexes(normalizedRowIndexes); + switch (args.mode) { + case 'deselect': { + const itemKeys = normalizedRowIndexes.map((index) => items[index].key); + await component.deselectRows(itemKeys); + break; + } + case 'select': + await component.selectRowsByIndexes(normalizedRowIndexes); + break; + default: + return failure(defaultMessage); } return success(defaultMessage); diff --git a/packages/devextreme/js/common/grids.d.ts b/packages/devextreme/js/common/grids.d.ts index e38178afeed9..72d53ff536c5 100644 --- a/packages/devextreme/js/common/grids.d.ts +++ b/packages/devextreme/js/common/grids.d.ts @@ -222,7 +222,7 @@ export type PredefinedCommands = { }; selectByIndexes: { indexes: number[]; - deselect?: boolean; + mode: 'select' | 'deselect'; }; selectAll: {}; deselectAll: {}; diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index f8528417e4ea..be00d6be8554 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -6611,7 +6611,7 @@ declare module DevExpress.common.grids { }; selectByIndexes: { indexes: number[]; - deselect?: boolean; + mode: 'select' | 'deselect'; }; selectAll: {}; deselectAll: {};