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..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,33 +271,62 @@ 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 mode deselect', () => { + expect(selectByIndexesCommand.schema.safeParse({ + 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('rejects when mode is missing', () => { + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [1], + }).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: [] }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [], mode: 'select', + }).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], mode: 'select', + }).success).toBe(false); }); it('rejects negative indexes', () => { - expect(selectByIndexesCommand.schema.safeParse({ indexes: [-1] }).success).toBe(false); + expect(selectByIndexesCommand.schema.safeParse({ + indexes: [-1], mode: 'select', + }).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], mode: 'select', + }).success).toBe(false); }); it('rejects unknown properties', () => { expect(selectByIndexesCommand.schema.safeParse({ indexes: [1], + mode: 'select', extra: 1, }).success).toBe(false); }); @@ -309,7 +338,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], mode: 'select', + }); expect(result.status).toBe('failure'); expect(selectSpy).not.toHaveBeenCalled(); @@ -322,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'); @@ -341,7 +372,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], mode: 'select', + }); expect(result.status).toBe('failure'); expect(selectSpy).not.toHaveBeenCalled(); @@ -352,12 +385,44 @@ 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], mode: 'select', + }); expect(selectSpy).toHaveBeenCalledWith([0, 2]); expect(result.status).toBe('success'); }); + 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], mode: 'select', + }); + + 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); + const selectSpy = jest.spyOn(instance, 'selectRowsByIndexes'); + const callbacks = createCallbacks(); + + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], mode: 'deselect', + }); + + 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 +430,9 @@ describe('selectByIndexesCommand', () => { }); const callbacks = createCallbacks(); - const result = await selectByIndexesCommand.execute(instance, callbacks)({ indexes: [1] }); + const result = await selectByIndexesCommand.execute(instance, callbacks)({ + indexes: [1], mode: 'select', + }); expect(result.status).toBe('failure'); }); @@ -376,28 +443,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], mode: 'select', + }); + + 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], mode: 'deselect', + }); 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], mode: 'select', + }); 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], mode: 'deselect', + }); + + 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], 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 6c5a5cf9ded4..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 @@ -2,7 +2,9 @@ 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 { + compositeKeyPairSchema, isKeyShapeValid, normalizeKey, +} from './utils'; const selectByKeysCommandSchema = z.object({ keys: z.array(z.union([ @@ -45,15 +47,22 @@ export const selectByKeysCommand = defineGridCommand({ const selectByIndexesCommandSchema = z.object({ indexes: z.array(z.number().int().min(1)).min(1), + mode: z.enum(['select', 'deselect']), }).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 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 defaultMessage = `Select row(s) number ${rowIndexes} on the current page.`; + const action = args.mode === '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 +79,18 @@ export const selectByIndexesCommand = defineGridCommand({ } try { - 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); } catch { diff --git a/packages/devextreme/js/common/grids.d.ts b/packages/devextreme/js/common/grids.d.ts index 2002bbe7f8ba..72d53ff536c5 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[]; + mode: 'select' | 'deselect'; }; selectAll: {}; deselectAll: {}; diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 0a793a6a963f..be00d6be8554 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[]; + mode: 'select' | 'deselect'; }; selectAll: {}; deselectAll: {};