Skip to content

Commit 9ccf37e

Browse files
authored
DataGrid: Editing - fix duplication of an undeleted row while creating a new row (T1293181) (#31948)
1 parent ea57982 commit 9ccf37e

File tree

4 files changed

+192
-2
lines changed

4 files changed

+192
-2
lines changed

packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { GridBase } from '@js/common/grids';
44
import type { dxElementWrapper } from '@js/core/renderer';
55
import $ from '@js/core/renderer';
66

7+
import { DataRowModel } from './row/data_row';
8+
79
const SELECTORS = {
810
headerRowClass: 'dx-header-row',
911
dataRowClass: 'dx-data-row',
@@ -45,5 +47,13 @@ export abstract class GridCoreModel<TInstance extends GridBase = GridBase> {
4547
return $(Array.from(this.getHeaderCells()).find((el) => $(el).text().includes(text)));
4648
}
4749

50+
public getDataRows(): NodeListOf<HTMLElement> {
51+
return this.root.querySelectorAll(`.${SELECTORS.dataRowClass}`);
52+
}
53+
54+
public getDataRow(rowIndex: number): DataRowModel {
55+
return new DataRowModel(this.getDataRows()[rowIndex]);
56+
}
57+
4858
public abstract getInstance(): TInstance;
4959
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const SELECTORS = {
2+
editRow: 'dx-edit-row',
3+
deleteRowButton: 'dx-link-delete',
4+
undeleteRowButton: 'dx-link-undelete',
5+
};
6+
7+
export class DataRowModel {
8+
public readonly isEditRow: boolean;
9+
10+
constructor(protected readonly root: HTMLElement | null) {
11+
this.isEditRow = !!this.root?.classList.contains(SELECTORS.editRow);
12+
}
13+
14+
public getElement(): HTMLElement | null {
15+
return this.root;
16+
}
17+
18+
public getDeleteButton(): HTMLElement {
19+
const row = this.getElement() as HTMLElement;
20+
21+
return row.querySelector(`.${SELECTORS.deleteRowButton}`) as HTMLElement;
22+
}
23+
24+
public getRecoverButton(): HTMLElement {
25+
const row = this.getElement() as HTMLElement;
26+
27+
return row.querySelector(`.${SELECTORS.undeleteRowButton}`) as HTMLElement;
28+
}
29+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, it, jest,
3+
} from '@jest/globals';
4+
import type { dxElementWrapper } from '@js/core/renderer';
5+
import $ from '@js/core/renderer';
6+
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
7+
import DataGrid from '@js/ui/data_grid';
8+
import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid';
9+
10+
const GRID_CONTAINER_ID = 'gridContainer';
11+
12+
const SELECTORS = {
13+
gridContainer: `#${GRID_CONTAINER_ID}`,
14+
};
15+
16+
const dataSource = [{
17+
ID: 1,
18+
FirstName: 'John',
19+
LastName: 'Heart',
20+
Prefix: 'Mr.',
21+
Position: 'CEO',
22+
BirthDate: '1964/03/16',
23+
HireDate: '1995/01/15',
24+
Notes: 'John has been in the Audio/Video industry since 1990. He has led DevAv as its CEO since 2003.\r\n\r\nWhen not working hard as the CEO, John loves to golf and bowl. He once bowled a perfect game of 300.',
25+
Address: '351 S Hill St.',
26+
}, {
27+
ID: 2,
28+
FirstName: 'Olivia',
29+
LastName: 'Peyton',
30+
Prefix: 'Mrs.',
31+
Position: 'Sales Assistant',
32+
BirthDate: '1981/06/03',
33+
HireDate: '2012/05/14',
34+
Notes: 'Olivia loves to sell. She has been selling DevAV products since 2012. \r\n\r\nOlivia was homecoming queen in high school. She is expecting her first child in 6 months. Good Luck Olivia.',
35+
Address: '807 W Paseo Del Mar',
36+
}, {
37+
ID: 3,
38+
FirstName: 'Robert',
39+
LastName: 'Reagan',
40+
Prefix: 'Mr.',
41+
Position: 'CMO',
42+
BirthDate: '1974/09/07',
43+
HireDate: '2002/11/08',
44+
Notes: 'Robert was recently voted the CMO of the year by CMO Magazine. He is a proud member of the DevAV Management Team.\r\n\r\nRobert is a championship BBQ chef, so when you get the chance ask him for his secret recipe.',
45+
Address: '4 Westmoreland Pl.',
46+
}];
47+
48+
const flushAsync = async (): Promise<void> => {
49+
jest.runOnlyPendingTimers();
50+
await Promise.resolve();
51+
};
52+
53+
const createDataGrid = async (
54+
options: DataGridProperties = {},
55+
): Promise<{
56+
$container: dxElementWrapper;
57+
component: DataGridModel;
58+
instance: DataGrid;
59+
}> => new Promise((resolve) => {
60+
const $container = $('<div>')
61+
.attr('id', GRID_CONTAINER_ID)
62+
.appendTo(document.body);
63+
64+
const instance = new DataGrid($container.get(0) as HTMLDivElement, options);
65+
const component = new DataGridModel($container.get(0) as HTMLElement);
66+
67+
jest.runAllTimers();
68+
resolve({
69+
$container,
70+
component,
71+
instance,
72+
});
73+
});
74+
75+
const beforeTest = (): void => {
76+
jest.useFakeTimers();
77+
};
78+
79+
const afterTest = (): void => {
80+
const $container = $(SELECTORS.gridContainer);
81+
const dataGrid = (
82+
$container as dxElementWrapper & { dxDataGrid: (command: string) => DataGrid }
83+
).dxDataGrid('instance');
84+
85+
dataGrid.dispose();
86+
$container.remove();
87+
jest.clearAllMocks();
88+
jest.useRealTimers();
89+
};
90+
91+
describe('DataGrid editing', () => {
92+
beforeEach(beforeTest);
93+
afterEach(afterTest);
94+
95+
// T1293181
96+
describe('Recovered (undeleted) row', () => {
97+
it('should have correct data when placed before inserted row in batch editing', async () => {
98+
const recoveringRowIndex = dataSource.length - 1;
99+
const { component, instance } = await createDataGrid({
100+
keyExpr: 'ID',
101+
dataSource,
102+
columns: [
103+
{
104+
dataField: 'Prefix',
105+
caption: 'Title',
106+
width: 70,
107+
},
108+
'FirstName',
109+
'LastName', {
110+
dataField: 'Position',
111+
width: 170,
112+
}, {
113+
dataField: 'BirthDate',
114+
dataType: 'date' as const,
115+
},
116+
],
117+
editing: {
118+
mode: 'batch',
119+
allowDeleting: true,
120+
allowAdding: true,
121+
newRowPosition: 'pageBottom',
122+
texts: {
123+
deleteRow: 'Delete',
124+
undeleteRow: 'Undelete',
125+
},
126+
},
127+
});
128+
129+
await flushAsync();
130+
131+
await instance.addRow();
132+
await flushAsync();
133+
134+
const rowDeleteButton = component.getDataRow(recoveringRowIndex).getDeleteButton();
135+
rowDeleteButton.click();
136+
await flushAsync();
137+
138+
const rowRecoverButton = component.getDataRow(recoveringRowIndex).getRecoverButton();
139+
rowRecoverButton.click();
140+
await flushAsync();
141+
142+
const rows = instance.getVisibleRows();
143+
expect(rows).toHaveLength(dataSource.length + 1);
144+
expect(rows[recoveringRowIndex].data).toEqual(dataSource[recoveringRowIndex]);
145+
});
146+
});
147+
});

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,13 +1346,17 @@ class EditingControllerImpl extends modules.ViewController {
13461346
protected _removeChange(index) {
13471347
if (index >= 0) {
13481348
const changes = [...this.getChanges()];
1349-
const { key } = changes[index];
1349+
const { key, type } = changes[index];
13501350

13511351
this._removeInternalData(key);
13521352

1353-
this._updateInsertAfterOrBeforeKeys(changes, index);
1353+
if (type !== DATA_EDIT_DATA_REMOVE_TYPE) {
1354+
this._updateInsertAfterOrBeforeKeys(changes, index);
1355+
}
1356+
13541357
changes.splice(index, 1);
13551358
this._silentOption(EDITING_CHANGES_OPTION_NAME, changes);
1359+
13561360
if (equalByValue(this.option(EDITING_EDITROWKEY_OPTION_NAME), key)) {
13571361
this._resetEditIndices();
13581362
}

0 commit comments

Comments
 (0)