Skip to content

Commit 70501df

Browse files
authored
DataGrid: Fix extra editCellTemplate/onEditorPreparing call after clicking on a cell in a different row and column (T1291087) (DevExpress#30722)
1 parent facba66 commit 70501df

File tree

7 files changed

+213
-45
lines changed

7 files changed

+213
-45
lines changed

e2e/testcafe-devextreme/tests/dataGrid/common/editing.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2807,3 +2807,153 @@ test('DataGrid - A new row is added above the existing row if the data source is
28072807
})
28082808
.after(async () => changeTheme(Themes.genericLight));
28092809
});
2810+
2811+
// T1291087
2812+
test('The editCellTemplate template should not be called after clicking on a cell in another row and column', async (t) => {
2813+
// arrange
2814+
const dataGrid = new DataGrid('#container');
2815+
const firstCellOfFirstRow = dataGrid.getDataCell(0, 0);
2816+
const secondCellOfSecondRow = dataGrid.getDataCell(1, 1);
2817+
const checkEditCellTemplateCallArgs = async () => {
2818+
const templateCallArgs = await ClientFunction(() => (window as any).editCellTemplateCallArgs)();
2819+
2820+
await t
2821+
.expect(templateCallArgs.length)
2822+
.eql(1, 'editCellTemplate should be called only once')
2823+
.expect(templateCallArgs[0])
2824+
.eql('Column1', 'editCellTemplate should be called for the first column only');
2825+
};
2826+
2827+
// act
2828+
await t.click(firstCellOfFirstRow.element);
2829+
2830+
// assert
2831+
await t
2832+
.expect(firstCellOfFirstRow.isEditCell)
2833+
.ok('The first cell of the first row should be in edit mode')
2834+
.expect(firstCellOfFirstRow.getEditor().element.focused)
2835+
.ok('The first cell of the first row should be focused');
2836+
2837+
await checkEditCellTemplateCallArgs();
2838+
2839+
// act
2840+
await t.click(secondCellOfSecondRow.element);
2841+
2842+
// assert
2843+
await t
2844+
.expect(secondCellOfSecondRow.isEditCell)
2845+
.ok('The second cell of the second row should be in edit mode')
2846+
.expect(secondCellOfSecondRow.getEditor().element.focused)
2847+
.ok('The second cell of the second row should be focused');
2848+
2849+
await checkEditCellTemplateCallArgs();
2850+
}).before(async () => {
2851+
await ClientFunction(() => {
2852+
(window as any).editCellTemplateCallArgs = [];
2853+
})();
2854+
2855+
return createWidget('dxDataGrid', {
2856+
dataSource: [
2857+
{
2858+
ID: 1, Column1: 'a', Column2: 'b', Column3: 'c',
2859+
},
2860+
{
2861+
ID: 2, Column1: 'd', Column2: 'e', Column3: 'f',
2862+
},
2863+
{
2864+
ID: 3, Column1: 'g', Column2: 'h', Column3: 'i',
2865+
},
2866+
],
2867+
keyExpr: 'ID',
2868+
columns: [{
2869+
dataField: 'Column1',
2870+
editCellTemplate(_, cellInfo) {
2871+
(window as any).editCellTemplateCallArgs.push(cellInfo.column.dataField);
2872+
2873+
return ($('<div>') as any).dxTextBox({
2874+
value: cellInfo.value,
2875+
onValueChanged: (args) => cellInfo.setValue(args.value),
2876+
});
2877+
},
2878+
}, 'Column2', 'Column3'],
2879+
showBorders: true,
2880+
editing: { mode: 'batch', allowUpdating: true },
2881+
});
2882+
}).after(async () => {
2883+
await ClientFunction(() => {
2884+
delete (window as any).editCellTemplateCallArgs;
2885+
})();
2886+
});
2887+
2888+
// T1291087
2889+
test('The onEditorPreparing event should be called once after clicking on a cell in another row and column', async (t) => {
2890+
// arrange
2891+
const dataGrid = new DataGrid('#container');
2892+
const firstCellOfFirstRow = dataGrid.getDataCell(0, 0);
2893+
const secondCellOfSecondRow = dataGrid.getDataCell(1, 1);
2894+
const checkOnEditorPreparingCallArgs = async (expectedCallCount, expectedArgs) => {
2895+
const eventCallArgs = await ClientFunction(() => (window as any).onEditorPreparingCallArgs)();
2896+
2897+
await t
2898+
.expect(eventCallArgs.length)
2899+
.eql(expectedCallCount, 'number of the onEditorPreparing event calls is correct')
2900+
.expect(eventCallArgs[expectedCallCount - 1])
2901+
.eql(expectedArgs, 'onEditorPreparing event called for expected cell');
2902+
};
2903+
2904+
// act
2905+
await t.click(firstCellOfFirstRow.element);
2906+
2907+
// assert
2908+
await t
2909+
.expect(firstCellOfFirstRow.isEditCell)
2910+
.ok('The first cell of the first row should be in edit mode')
2911+
.expect(firstCellOfFirstRow.getEditor().element.focused)
2912+
.ok('The first cell of the first row should be focused');
2913+
2914+
await checkOnEditorPreparingCallArgs(1, { dataField: 'Column1', rowIndex: 0 });
2915+
2916+
// act
2917+
await t.click(secondCellOfSecondRow.element);
2918+
2919+
// assert
2920+
await t
2921+
.expect(secondCellOfSecondRow.isEditCell)
2922+
.ok('The second cell of the second row should be in edit mode')
2923+
.expect(secondCellOfSecondRow.getEditor().element.focused)
2924+
.ok('The second cell of the second row should be focused');
2925+
2926+
await checkOnEditorPreparingCallArgs(2, { dataField: 'Column2', rowIndex: 1 });
2927+
}).before(async () => {
2928+
await ClientFunction(() => {
2929+
(window as any).onEditorPreparingCallArgs = [];
2930+
})();
2931+
2932+
return createWidget('dxDataGrid', {
2933+
dataSource: [
2934+
{
2935+
ID: 1, Column1: 'a', Column2: 'b', Column3: 'c',
2936+
},
2937+
{
2938+
ID: 2, Column1: 'd', Column2: 'e', Column3: 'f',
2939+
},
2940+
{
2941+
ID: 3, Column1: 'g', Column2: 'h', Column3: 'i',
2942+
},
2943+
],
2944+
keyExpr: 'ID',
2945+
columns: ['Column1', 'Column2', 'Column3'],
2946+
showBorders: true,
2947+
editing: { mode: 'batch', allowUpdating: true },
2948+
onEditorPreparing(e) {
2949+
(window as any).onEditorPreparingCallArgs.push({
2950+
dataField: e.dataField,
2951+
rowIndex: e.row?.rowIndex,
2952+
});
2953+
},
2954+
});
2955+
}).after(async () => {
2956+
await ClientFunction(() => {
2957+
delete (window as any).onEditorPreparingCallArgs;
2958+
})();
2959+
});

e2e/testcafe-devextreme/tests/dataGrid/common/keyboardNavigation/keyboardNavigation.functional.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ test('Changing keyboardNavigation options should not invalidate the entire conte
6565
.expect(ClientFunction(() => (window as any).invalidateCounter)())
6666
.eql(0)
6767
.expect(ClientFunction(() => (window as any).renderTableCounter)())
68-
.eql(10);
68+
.eql(9);
6969
}).before(async () => {
7070
await ClientFunction(() => {
7171
(window as any).invalidateCounter = 0;

packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ interface HandleDataChangedArguments {
7373

7474
type UserData = Record<string, unknown>;
7575

76-
interface Item {
76+
export interface Item {
7777
rowType: 'data' | 'group' | 'groupFooter' | 'detailAdaptive';
7878
data: UserData;
79+
key: unknown;
80+
oldData?: UserData;
7981
dataIndex?: number;
8082
values?: unknown[];
8183
visible?: boolean;
@@ -85,8 +87,8 @@ interface Item {
8587
rowIndex?: number;
8688
cells?: unknown[];
8789
loadIndex?: number;
88-
key: unknown;
8990
isSelected?: boolean;
91+
removed?: boolean;
9092
}
9193

9294
export type Filter = any;

packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import {
9292
isEditingCell,
9393
isEditingOrShowEditorAlwaysDataCell,
9494
} from './m_editing_utils';
95+
import type { NormalizedEditCellOptions } from './types';
9596

9697
class EditingControllerImpl extends modules.ViewController {
9798
protected _columnsController!: Controllers['columns'];
@@ -652,17 +653,6 @@ class EditingControllerImpl extends modules.ViewController {
652653
}
653654
}
654655

655-
private _setEditRowKeyByIndex(rowIndex, silent) {
656-
const key = this._dataController.getKeyByRowIndex(rowIndex);
657-
658-
if (key === undefined) {
659-
this._dataController.fireError('E1043');
660-
return;
661-
}
662-
663-
this._setEditRowKey(key, silent);
664-
}
665-
666656
public getEditRowIndex() {
667657
return this._getVisibleEditRowIndex();
668658
}
@@ -2477,6 +2467,8 @@ class EditingControllerImpl extends modules.ViewController {
24772467

24782468
protected _isRowDeleteAllowed(): any {}
24792469

2470+
protected _prepareEditCell(parameters: NormalizedEditCellOptions): boolean { return false; }
2471+
24802472
public shouldHighlightCell(parameters) {
24812473
const cellModified = this.isCellModified(parameters);
24822474
return cellModified

packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_cell_based.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
} from './const';
3434
import type { EditingController } from './m_editing';
3535
import { isEditable } from './m_editing_utils';
36+
import type { NormalizedEditCellOptions } from './types';
3637

3738
export interface ICellBasedEditingControllerExtender {
3839
// eslint-disable-next-line @typescript-eslint/method-signature-style
@@ -234,36 +235,31 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
234235
return coreResult !== undefined ? coreResult : d.promise();
235236
}
236237

237-
private _editCellCore(options) {
238-
const dataController = this._dataController;
239-
const isEditByOptionChanged = isDefined(options.oldColumnIndex) || isDefined(options.oldRowIndex);
238+
private _editCellCore(options): boolean | DeferredObj<boolean> | undefined {
239+
const editCellOptions: NormalizedEditCellOptions = this._getNormalizedEditCellOptions(options);
240240
const {
241-
columnIndex, rowIndex, column, item,
242-
} = this._getNormalizedEditCellOptions(options) as any;
243-
const params = {
244-
data: item?.data,
245-
cancel: false,
241+
columnIndex,
242+
rowIndex,
246243
column,
247-
};
244+
item,
245+
} = editCellOptions;
248246

249247
if (item.key === undefined) {
250248
this._dataController.fireError('E1043');
251249
return;
252250
}
253251

254252
if (column && (item.rowType === 'data' || item.rowType === 'detailAdaptive') && !item.removed && this.isCellOrBatchEditMode()) {
255-
if (!isEditByOptionChanged && this.isEditCell(rowIndex, columnIndex)) {
253+
if (this.isEditCell(rowIndex, columnIndex)) {
256254
return true;
257255
}
258256

259-
const editRowIndex = rowIndex + dataController.getRowIndexOffset();
260-
261257
return when(this._beforeEditCell(rowIndex, columnIndex, item)).done((cancel) => {
262258
if (cancel) {
263259
return;
264260
}
265261

266-
if (!this._prepareEditCell(params, item, columnIndex, editRowIndex)) {
262+
if (!this._prepareEditCell(editCellOptions)) {
267263
this._processCanceledEditingCell();
268264
}
269265
});
@@ -294,8 +290,16 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
294290
}
295291

296292
private _getNormalizedEditCellOptions({
297-
oldColumnIndex, oldRowIndex, columnIndex, rowIndex,
298-
}) {
293+
oldColumnIndex,
294+
oldRowIndex,
295+
columnIndex,
296+
rowIndex,
297+
}: {
298+
oldColumnIndex: number;
299+
oldRowIndex: number;
300+
columnIndex: number;
301+
rowIndex: number;
302+
}): NormalizedEditCellOptions {
299303
const columnsController = this._columnsController;
300304
const visibleColumns = columnsController.getVisibleColumns();
301305
const items = this._dataController.items();
@@ -324,22 +328,31 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
324328
};
325329
}
326330

327-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
328-
private _prepareEditCell(params, item, editColumnIndex, editRowIndex) {
329-
if (!item.isNewRow) {
330-
params.key = item.key;
331-
}
331+
protected _prepareEditCell({
332+
item,
333+
column,
334+
oldColumn,
335+
columnIndex,
336+
oldRowIndex,
337+
}: NormalizedEditCellOptions): boolean {
338+
const editingStartParams = {
339+
data: item?.data,
340+
cancel: false,
341+
column,
342+
key: !item.isNewRow ? item.key : undefined,
343+
};
332344

333-
if (this._isEditingStart(params)) {
345+
if (this._isEditingStart(editingStartParams)) {
334346
return false;
335347
}
336348

337349
this._pageIndex = this._dataController.pageIndex();
338350

339-
this._setEditRowKey(item.key);
340-
this._setEditColumnNameByIndex(editColumnIndex);
351+
this._setEditRowKey(item.key, true);
352+
this._setEditColumnNameByIndex(columnIndex, true);
353+
this._repaintEditCell(column, oldColumn, oldRowIndex);
341354

342-
if (!params.column.showEditorAlways) {
355+
if (!column.showEditorAlways) {
343356
this._addInternalData({
344357
key: item.key,
345358
oldData: item.oldData ?? item.data,
@@ -419,7 +432,6 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
419432
}
420433
}
421434

422-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
423435
protected _prepareChange(options, value, text) {
424436
const $cellElement = $(options.cellElement);
425437

@@ -516,7 +528,6 @@ const editingControllerExtender = (Base: ModuleType<EditingController>) => class
516528
}
517529
}
518530

519-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
520531
protected _allowRowAdding(params) {
521532
if (this.isBatchEditMode()) {
522533
return true;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Column } from '../columns_controller/m_columns_controller';
2+
import type { Item } from '../data_controller/m_data_controller';
3+
4+
export interface NormalizedEditCellOptions {
5+
item: Item;
6+
oldColumn: Column;
7+
column: Column;
8+
columnIndex: number;
9+
oldRowIndex: number;
10+
rowIndex: number;
11+
}

packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view';
3333

3434
import { EDITORS_INPUT_SELECTOR } from '../editing/const';
3535
import type { EditingController } from '../editing/m_editing';
36+
import type { NormalizedEditCellOptions } from '../editing/types';
3637
import modules from '../m_modules';
3738
import type { ModuleType } from '../m_types';
3839
import gridCoreUtils from '../m_utils';
@@ -710,12 +711,13 @@ export const validatingEditingExtender = (Base: ModuleType<EditingController>) =
710711
super._validateEditFormAfterUpdate.apply(this, arguments as any);
711712
}
712713

713-
private _prepareEditCell(params) {
714-
// @ts-expect-error
715-
const isNotCanceled = super._prepareEditCell.apply(this, arguments as any);
714+
protected _prepareEditCell(parameters: NormalizedEditCellOptions): boolean {
715+
const { column, item } = parameters;
716+
const isNotCanceled: boolean = super._prepareEditCell(parameters);
717+
const key = !item.isNewRow ? item.key : undefined;
716718

717-
if (isNotCanceled && params.column.showEditorAlways) {
718-
this._validatingController.updateValidationState({ key: params.key });
719+
if (isNotCanceled && column.showEditorAlways) {
720+
this._validatingController.updateValidationState({ key });
719721
}
720722

721723
return isNotCanceled;

0 commit comments

Comments
 (0)