diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts index 16488785a4be..d124f6f8870a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/cell/data_cell.ts @@ -1,6 +1,9 @@ +import { TextBoxModel } from '@ts/ui/__tests__/__mock__/model/textbox'; + const SELECTORS = { editCell: 'dx-editor-cell', invalidCell: 'invalid', + textBox: 'dx-textbox', }; export class DataCellModel { @@ -27,4 +30,9 @@ export class DataCellModel { public getHTML(): string { return this.root?.innerHTML ?? ''; } + + public getEditor(): TextBoxModel { + const editorElement = this.root?.querySelector(`.${SELECTORS.textBox}`) as HTMLElement; + return new TextBoxModel(editorElement); + } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts index 0d93490374ae..cccec361732f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts @@ -25,6 +25,7 @@ const SELECTORS = { editForm: 'edit-form', headerCellIndicators: 'dx-column-indicators', headerCellFilter: 'dx-header-filter', + revertButton: 'dx-revert-button', }; export abstract class GridCoreModel { @@ -101,6 +102,10 @@ export abstract class GridCoreModel { return new ToastModel(this.getToastContainer()); } + public getRevertButton(): HTMLElement { + return document.body.querySelector(`.${SELECTORS.revertButton}`) as HTMLElement; + } + public addWidgetPrefix(classNames: string): string { const componentName = this.NAME; diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts new file mode 100644 index 000000000000..c4200ccd0795 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/__tests__/validating.integration.test.ts @@ -0,0 +1,73 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import { + afterTest, + beforeTest, + createDataGrid, +} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils'; + +describe('DataGrid Cell Editing', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + // T1308327 + describe('when showEditorAlways and repaintChangesOnly is enabled', () => { + it('should restore the value after canceling changes with validation error (T1308327)', async () => { + const data = [ + { id: 1, name: 'Job 1', article: 'Article A' }, + { id: 2, name: 'Job 2', article: 'Article B' }, + ]; + + const { instance, component } = await createDataGrid({ + dataSource: data, + keyExpr: 'id', + repaintChangesOnly: true, + editing: { + mode: 'cell', + allowUpdating: true, + }, + columns: [ + { + dataField: 'name', + showEditorAlways: true, + validationRules: [ + { type: 'required', message: 'Required field' }, + ], + }, + { + dataField: 'article', + }, + ], + }); + + const firstCell = component.getDataCell(0, 0); + const firstEditor = firstCell.getEditor(); + + firstEditor.setValue(''); + jest.runAllTimers(); + + expect(component.getDataCell(0, 0).isValidCell).toBe(false); + + component.getRevertButton().click(); + jest.runAllTimers(); + + expect(instance.cellValue(0, 'name')).toBe('Job 1'); + expect(component.getDataCell(0, 0).isValidCell).toBe(true); + + const secondCell = component.getDataCell(1, 0); + const secondEditor = secondCell.getEditor(); + + secondEditor.setValue(''); + jest.runAllTimers(); + + expect(component.getDataCell(1, 0).isValidCell).toBe(false); + + component.getRevertButton().click(); + jest.runAllTimers(); + + expect(instance.cellValue(1, 'name')).toBe('Job 2'); + expect(component.getDataCell(1, 0).isValidCell).toBe(true); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index 5c81455478b3..a8e5ac0d4485 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -115,6 +115,34 @@ export class ValidatingController extends modules.Controller { this._validationStateCache = {}; } + public resetValidationStateForChanges(changes) { + if (!changes?.length) { + return; + } + + changes.forEach(({ key }) => { + this._removeValidationData(key); + }); + } + + private _removeValidationData(key) { + if (!this._validationState?.length) { + return; + } + + const keyHash = getKeyHash(key); + const isObjectKeyHash = isObject(keyHash); + + if (!isObjectKeyHash && this._validationStateCache) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this._validationStateCache[keyHash]; + } + + this._validationState = this._validationState.filter((data) => ( + isObjectKeyHash ? !equalByValue(data.key, key) : data.key !== key + )); + } + public _rowIsValidated(change) { const validationData = this._getValidationData(change?.key); @@ -936,14 +964,7 @@ export const validatingEditingExtender = (Base: ModuleType) = }); this._focusEditingCell(); } else if (!cancel) { - let shouldResetValidationState = true; - - if (isCellEditMode) { - const columns = this._columnsController.getColumns(); - const columnsWithValidatingEditors = columns.filter((col) => col.showEditorAlways && col.validationRules?.length > 0).length > 0; - - shouldResetValidationState = !columnsWithValidatingEditors; - } + const shouldResetValidationState = this._shouldResetValidationState(); if (shouldResetValidationState) { this._validatingController.initValidationState(); @@ -979,11 +1000,34 @@ export const validatingEditingExtender = (Base: ModuleType) = } protected _beforeCancelEditData() { - this._validatingController.initValidationState(); + const validatingController = this._validatingController; + const shouldResetValidationState = this._shouldResetValidationState(); + + if (shouldResetValidationState) { + validatingController.initValidationState(); + } else { + const changes = this.getChanges(); + validatingController.resetValidationStateForChanges(changes); + } super._beforeCancelEditData(); } + private _shouldResetValidationState(): boolean { + const isCellEditMode = this.getEditMode() === EDIT_MODE_CELL; + + if (isCellEditMode) { + const columns = this._columnsController.getColumns(); + const columnsWithValidatingEditors = columns.filter( + (col) => col.showEditorAlways && col.validationRules?.length > 0, + ); + + return columnsWithValidatingEditors.length === 0; + } + + return true; + } + private _showErrorRow(change) { let $popupContent; const items = this._dataController.items(); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js index de02e485d012..154120751d7b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/editing.integration.tests.js @@ -2809,10 +2809,9 @@ QUnit.module('Editing', baseModuleConfig, () => { this.clock.tick(10); $(grid.getCellElement(0, 1)).trigger('dxclick'); this.clock.tick(10); - const callCount = action === 'close edit cell' ? 3 : 4; // assert - assert.equal(validationCallback.callCount, callCount, 'validation callback call count'); + assert.equal(validationCallback.callCount, 3, 'validation callback call count'); }); });