diff --git a/js/__internal/grids/grid_core/editing/m_editing.ts b/js/__internal/grids/grid_core/editing/m_editing.ts index bf0248e84d51..0fb06661215c 100644 --- a/js/__internal/grids/grid_core/editing/m_editing.ts +++ b/js/__internal/grids/grid_core/editing/m_editing.ts @@ -74,6 +74,7 @@ import type { ICellBasedEditingControllerExtender } from './m_editing_cell_based import type { IFormBasedEditingControllerExtender } from './m_editing_form_based'; import { createFailureHandler, + generateNewRowTempKey, getButtonIndex, getButtonName, getEditingTexts, @@ -780,12 +781,8 @@ class EditingControllerImpl extends modules.ViewController { _addInsertInfo(change, parentKey?) { let insertInfo; - let { key } = change; - - if (!isDefined(key)) { - key = String(new Guid()); - change.key = key; - } + change.key = this.getChangeKeyValue(change); + const { key } = change; insertInfo = this._getInternalData(key)?.insertInfo; if (!isDefined(insertInfo)) { @@ -803,6 +800,23 @@ class EditingControllerImpl extends modules.ViewController { return { insertInfo, key }; } + private getChangeKeyValue(change: any): unknown { + if (isDefined(change.key)) { + return change.key; + } + + const keyExpr = this._dataController.key(); + let keyValue; + if (change.data && keyExpr && !Array.isArray(keyExpr)) { + keyValue = change.data[keyExpr]; + } + if (!isDefined(keyValue)) { + keyValue = generateNewRowTempKey(); + } + + return keyValue; + } + _setInsertAfterOrBeforeKey(change, parentKey) { const dataController = this._dataController; const allItems = dataController.items(true); diff --git a/js/__internal/grids/grid_core/editing/m_editing_utils.ts b/js/__internal/grids/grid_core/editing/m_editing_utils.ts index 05967147ce9a..ddffd2ed0859 100644 --- a/js/__internal/grids/grid_core/editing/m_editing_utils.ts +++ b/js/__internal/grids/grid_core/editing/m_editing_utils.ts @@ -1,5 +1,9 @@ +import Guid from '@js/core/guid'; import { isObject } from '@js/core/utils/type'; +const NEW_ROW_TEMP_KEY_PREFIX = '_DX_KEY_'; +const GUID_LENGTH = 36; + export const createFailureHandler = function (deferred) { return function (arg) { const error = arg instanceof Error ? arg : new Error(arg && String(arg) || 'Unknown error'); @@ -31,6 +35,12 @@ export const getEditingTexts = (options) => { }; }; +export const generateNewRowTempKey = (): string => `${NEW_ROW_TEMP_KEY_PREFIX}${new Guid()}`; + +export const isNewRowTempKey = (key: string): boolean => typeof key === 'string' + && key.startsWith(NEW_ROW_TEMP_KEY_PREFIX) + && key.length === NEW_ROW_TEMP_KEY_PREFIX.length + GUID_LENGTH; + export const getButtonIndex = (buttons, name) => { let result = -1; diff --git a/js/__internal/grids/grid_core/filter/m_filter_row.ts b/js/__internal/grids/grid_core/filter/m_filter_row.ts index e01343e3f8d6..1ace07c5280f 100644 --- a/js/__internal/grids/grid_core/filter/m_filter_row.ts +++ b/js/__internal/grids/grid_core/filter/m_filter_row.ts @@ -558,7 +558,7 @@ const ColumnHeadersViewFilterRowExtender = (function () { selectable: false, items: that._getFilterOperationMenuItems(column), }], - onItemRendered({ itemElement }) { + onItemRendered: ({ itemElement }) => { this.setAria('label', ARIA_SEARCH_BOX, $(itemElement)); }, onItemClick(properties) { diff --git a/js/__internal/grids/grid_core/focus/m_focus.ts b/js/__internal/grids/grid_core/focus/m_focus.ts index 885642c04c53..64ff302709dd 100644 --- a/js/__internal/grids/grid_core/focus/m_focus.ts +++ b/js/__internal/grids/grid_core/focus/m_focus.ts @@ -4,6 +4,7 @@ import { Deferred, when } from '@js/core/utils/deferred'; import { each } from '@js/core/utils/iterator'; import { isBoolean, isDefined } from '@js/core/utils/type'; +import { isNewRowTempKey } from '../editing/m_editing_utils'; import core from '../m_modules'; import gridCoreUtils from '../m_utils'; import { UiGridCoreFocusUtils } from './m_focus_utils'; @@ -635,7 +636,7 @@ export const focusModule = { const deferred = new Deferred(); const dataSource = that._dataSource; - if (Array.isArray(key)) { + if (Array.isArray(key) || isNewRowTempKey(key)) { return deferred.resolve(-1).promise(); } diff --git a/testing/testcafe/model/dataGrid/data/cell.ts b/testing/testcafe/model/dataGrid/data/cell.ts index 5daf4966a13d..1c740b0b79b6 100644 --- a/testing/testcafe/model/dataGrid/data/cell.ts +++ b/testing/testcafe/model/dataGrid/data/cell.ts @@ -16,6 +16,7 @@ const CLASS = { overlay: 'dx-overlay', checkbox: 'dx-checkbox', linkEdit: 'dx-link-edit', + linkSave: 'dx-link-save', }; export default class DataCell extends FocusableElement { @@ -60,6 +61,10 @@ export default class DataCell extends FocusableElement { return this.element.find(`.${CLASS.linkEdit}`); } + getLinkSave(): Selector { + return this.element.find(`.${CLASS.linkSave}`); + } + getIconByTitle(title: string): Selector { return this.element.find(`a[title=${title}]`); } diff --git a/testing/testcafe/tests/dataGrid/editing.ts b/testing/testcafe/tests/dataGrid/editing.ts index 95a6a2cc0356..df3c22bc87b4 100644 --- a/testing/testcafe/tests/dataGrid/editing.ts +++ b/testing/testcafe/tests/dataGrid/editing.ts @@ -2372,3 +2372,140 @@ test('Popup EditForm screenshot', async (t) => { await changeTheme('generic.light'); }); }); + +test('Component sends unexpected filtering request after inserting a new row if focusedRowEnabled is true and key set in data source (T1181477)', async (t) => { + const dataGrid = new DataGrid('#container'); + + await t + .click(dataGrid.getHeaderPanel().getAddRowButton()) + .click(dataGrid.getDataCell(0, 2).getLinkSave()) + + .expect(dataGrid.getDataCell(3, 1).element.innerText) + .notContains('Name 3') + + .expect(Selector('#otherContainer').innerText) + .eql(''); +}).before(async () => { + await createWidget('dxDataGrid', ClientFunction(() => { + const dataSourceCore = [ + { ID: 1, Name: 'Name 1' }, + { ID: 2, Name: 'Name 2' }, + { ID: 3, Name: 'Name 3' }, + ]; + + const sampleAPI = { + load() { + const data = dataSourceCore; + return new Promise((resolve) => { + setTimeout(() => { + resolve(data); + }, 100); + }); + }, + totalCount() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(dataSourceCore.length); + }, 100); + }); + }, + insert(values) { + return new Promise((resolve) => { + setTimeout(() => { + const newID = dataSourceCore.length + 1; + values.ID = newID; + dataSourceCore.push(values); + resolve(newID); + }, 100); + }); + }, + }; + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'ID', + load(o) { + if (o.filter) { + $('#otherContainer').append('Fail'); + } + + return Promise.all([sampleAPI.load(), sampleAPI.totalCount()]).then((res) => ({ + data: res[0], + totalCount: res[1], + })); + }, + insert(values) { + return sampleAPI.insert(values); + }, + }); + + return { + dataSource: store, + showBorders: true, + focusedRowEnabled: true, + autoNavigateToFocusedRow: true, + editing: { + allowAdding: true, + }, + remoteOperations: true, + }; + })); +}); + +test('Component sends unexpected filtering request after inserting a new row if focusedRowEnabled is true and key set on event (T1181477)', async (t) => { + const dataGrid = new DataGrid('#container'); + + await t + .click(dataGrid.getHeaderPanel().getAddRowButton()) + .click(dataGrid.getDataCell(0, 2).getLinkSave()) + + .expect(dataGrid.getDataCell(3, 1).element.innerText) + .notContains('Name 3') + + .expect(Selector('#otherContainer').innerText) + .eql(''); +}).before(async () => { + await createWidget('dxDataGrid', ClientFunction(() => { + const dataSourceCore = [ + { ID: 1, Name: 'Name 1' }, + { ID: 2, Name: 'Name 2' }, + { ID: 3, Name: 'Name 3' }, + ]; + + const sampleAPI = new (window as any).DevExpress.data.ArrayStore(dataSourceCore); + + const store = new (window as any).DevExpress.data.CustomStore({ + key: 'ID', + load(o) { + if (o.filter) { + $('#otherContainer').append('Fail'); + } + + return Promise.all([sampleAPI.load(), sampleAPI.totalCount()]).then((res) => ({ + data: res[0], + totalCount: res[1], + })); + }, + insert(values) { + return sampleAPI.insert(values); + }, + }); + + return { + dataSource: store, + showBorders: true, + focusedRowEnabled: true, + autoNavigateToFocusedRow: true, + editing: { + allowAdding: true, + }, + onInitNewRow(e) { + e.promise = new Promise((resolve) => { + const newId = dataSourceCore.length + 1; + e.data.ID = newId; + resolve(undefined); + }); + }, + remoteOperations: true, + }; + })); +}); diff --git a/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js b/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js index f8a97b70879f..6a2d3ef08801 100644 --- a/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js +++ b/testing/tests/DevExpress.ui.widgets.dataGrid/focus.tests.js @@ -3534,7 +3534,7 @@ QUnit.module('Focused row', getModuleConfig(true), () => { // assert assert.ok($(this.getRowElement(0)).find('.dx-texteditor-input').is(':focus'), 'input is focused'); - assert.equal(this.option('focusedRowKey').length, 36, 'focusedRowKey is tmp "guid" key'); + assert.equal(this.option('focusedRowKey').length, 44, 'focusedRowKey is tmp "guid" key'); assert.equal(this.option('focusedRowIndex'), 0, 'focusedRowIndex'); }); @@ -4112,7 +4112,7 @@ QUnit.module('Focused row', getModuleConfig(true), () => { // assert assert.strictEqual(onFocusedRowChangedSpy.callCount, 1, 'onFocusedRowChanged event is called for a new row'); assert.ok($(this.getRowElement(newRowIndex)).find('.dx-texteditor-input').is(':focus'), 'input is focused'); - assert.equal(this.option('focusedRowKey').length, 36, 'focusedRowKey is tmp "guid" key'); + assert.equal(this.option('focusedRowKey').length, 44, 'focusedRowKey is tmp "guid" key'); // Skip this check // focusedRowIndex different with shadow-dom and without it // Uncomment after fix of the T1160487 ticket.