diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example22.ts b/examples/vite-demo-vanilla-bundle/src/examples/example22.ts index 791b19f42..2e9ababcf 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example22.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example22.ts @@ -7,8 +7,9 @@ import { } from '@slickgrid-universal/common'; import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; -import { ExampleGridOptions } from './example-grid-options'; +import { BindingEventService } from '@slickgrid-universal/binding'; +import { ExampleGridOptions } from './example-grid-options'; import './example22.scss'; import type { TranslateService } from '../translate.service'; @@ -25,11 +26,13 @@ export default class Example22 { fetchResult = ''; statusClass = 'is-success'; statusStyle = 'display: none'; + private _bindingEventService: BindingEventService; constructor() { this.translateService = (window).TranslateService; this.selectedLanguage = this.translateService.getCurrentLanguage(); this.selectedLanguageFile = `${this.selectedLanguage}.json`; + this._bindingEventService = new BindingEventService(); } attached() { @@ -44,10 +47,15 @@ export default class Example22 { { ...ExampleGridOptions, ...this.gridOptions }, this.dataset ); + + this._bindingEventService.bind(document.querySelector(`.grid1`)!, 'onvalidationerror', (event) => + alert((event as CustomEvent)?.detail.args.validationResults.msg) + ); } dispose() { this.sgb?.dispose(); + this._bindingEventService.unbindAll(); } /* Define grid Options and Columns */ @@ -80,7 +88,7 @@ export default class Example22 { minWidth: 100, filterable: true, type: FieldType.number, - editor: { model: Editors.text }, + editor: { model: Editors.text, validator: (val) => (val > 100 ? { msg: 'Max 100% allowed', valid: false} : { msg: '', valid: true}) }, }, { id: 'start', diff --git a/package.json b/package.json index 09fb32ad6..35df55a12 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@types/node": "^20.12.7", "cross-env": "^7.0.3", "cypress": "^13.7.3", + "cypress-real-events": "^1.12.0", "dotenv": "^16.4.5", "eslint": "^9.0.0", "eslint-plugin-cypress": "^2.15.1", diff --git a/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts b/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts index 9d1058c01..5a990b7d3 100644 --- a/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts +++ b/packages/common/src/extensions/__tests__/slickCellExternalCopyManager.spec.ts @@ -23,6 +23,7 @@ const mockGetSelectionModel = { const returnValueStub = jest.fn(); const gridStub = { getActiveCell: jest.fn(), + getActiveCellNode: jest.fn(), getColumns: jest.fn().mockReturnValue([ { id: 'firstName', field: 'firstName', name: 'First Name', }, { id: 'lastName', field: 'lastName', name: 'Last Name' }, @@ -47,6 +48,7 @@ const gridStub = { triggerEvent: jest.fn().mockReturnValue({ getReturnValue: returnValueStub }), onCellChange: new SlickEvent(), onKeyDown: new SlickEvent(), + onValidationError: new SlickEvent(), } as unknown as SlickGrid; const mockCellSelectionModel = { @@ -67,6 +69,7 @@ const mockTextEditor = { applyValue: jest.fn(), loadValue: jest.fn(), serializeValue: jest.fn(), + validate: jest.fn().mockReturnValue({ valid: true, msg: null }), } as unknown as InputEditor; const mockTextEditorImplementation = jest.fn().mockImplementation(() => mockTextEditor); @@ -80,9 +83,9 @@ describe('CellExternalCopyManager', () => { lastNameElm.textContent = 'Last Name'; const mockEventCallback = () => { }; const mockColumns = [ - { id: 'firstName', field: 'firstName', name: 'First Name', editor: Editors.text, editorClass: Editors.text }, + { id: 'firstName', field: 'firstName', name: 'First Name', editor: { model: Editors.text }, editorClass: Editors.text }, { id: 'lastName', field: 'lastName', name: lastNameElm, }, - { id: 'age', field: 'age', name: 'Age', editor: Editors.text, editorClass: Editors.text }, + { id: 'age', field: 'age', name: 'Age', editor: { model: Editors.text }, editorClass: Editors.text }, ] as Column[]; let plugin: SlickCellExternalCopyManager; const gridOptionsMock = { @@ -196,6 +199,23 @@ describe('CellExternalCopyManager', () => { expect(applyValSpy).toHaveBeenCalledWith(mockItem, 'some value'); }); + it('should call "setDataItemValueForColumn" and expect an onValidationError triggered if validation failed', () => { + const validationResults = { valid: false, msg: 'foobar' }; + const applyValSpy = jest.spyOn(mockTextEditor, 'applyValue'); + const loadValSpy = jest.spyOn(mockTextEditor, 'loadValue'); + const validationSpy = jest.spyOn(mockTextEditor, 'validate').mockReturnValue(validationResults); + jest.spyOn(gridStub, 'getSelectionModel').mockReturnValue(mockCellSelectionModel as any); + const notifySpy = jest.spyOn(gridStub.onValidationError, 'notify'); + const mockItem = { firstName: 'John', lastName: 'Doe' }; + plugin.init(gridStub); + plugin.setDataItemValueForColumn(mockItem, mockColumns[0], 'some value'); + + expect(loadValSpy).toHaveBeenCalledWith(mockItem); + expect(applyValSpy).toHaveBeenCalledWith(mockItem, 'some value'); + expect(validationSpy).toHaveBeenCalled(); + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ validationResults })); + }); + it('should call "setDataItemValueForColumn" and expect item last name to change with new value when no Editor is provided', () => { const mockItem = { firstName: 'John', lastName: 'Doe' }; plugin.init(gridStub); diff --git a/packages/common/src/extensions/slickCellExternalCopyManager.ts b/packages/common/src/extensions/slickCellExternalCopyManager.ts index 0c3d6e6b8..cf6d79a93 100644 --- a/packages/common/src/extensions/slickCellExternalCopyManager.ts +++ b/packages/common/src/extensions/slickCellExternalCopyManager.ts @@ -1,6 +1,6 @@ import { createDomElement, getHtmlStringOutput, stripTags } from '@slickgrid-universal/utils'; -import type { Column, ExcelCopyBufferOption, ExternalCopyClipCommand, OnEventArgs } from '../interfaces/index'; +import type { Column, Editor, ExcelCopyBufferOption, ExternalCopyClipCommand, OnEventArgs } from '../interfaces/index'; import { SlickEvent, SlickEventData, SlickEventHandler, type SlickGrid, SlickRange, type SlickDataView, Utils as SlickUtils } from '../core/index'; // using external SlickGrid JS libraries @@ -127,15 +127,15 @@ export class SlickCellExternalCopyManager { // if a custom getter is not defined, we call serializeValue of the editor to serialize if (columnDef) { - if (columnDef.editor) { + if (columnDef.editorClass) { const tmpP = document.createElement('p'); - const editor = new (columnDef as any).editor({ + const editor = new (columnDef as any).editorClass({ container: tmpP, // a dummy container column: columnDef, event, position: { top: 0, left: 0 }, // a dummy position required by some editors grid: this._grid, - }); + }) as Editor; editor.loadValue(item); retVal = editor.serializeValue(); editor.destroy(); @@ -155,15 +155,29 @@ export class SlickCellExternalCopyManager { } // if a custom setter is not defined, we call applyValue of the editor to unserialize - if (columnDef.editor) { + if (columnDef.editorClass) { const tmpDiv = document.createElement('div'); - const editor = new (columnDef as any).editor({ + const editor = new (columnDef as any).editorClass({ container: tmpDiv, // a dummy container column: columnDef, position: { top: 0, left: 0 }, // a dummy position required by some editors grid: this._grid - }); + }) as Editor; editor.loadValue(item); + const validationResults = editor.validate(undefined, value); + if (!validationResults.valid) { + const activeCell = this._grid.getActiveCell()!; + this._grid.onValidationError.notify({ + editor, + cellNode: this._grid.getActiveCellNode()!, + validationResults, + row: activeCell?.row, + cell: activeCell?.cell, + column: columnDef, + grid: this._grid, + }); + } + editor.applyValue(item, value); editor.destroy(); tmpDiv.remove(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad27177be..7800cae5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: cypress: specifier: ^13.7.3 version: 13.7.3 + cypress-real-events: + specifier: ^1.12.0 + version: 1.12.0(cypress@13.7.3) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -3944,6 +3947,14 @@ packages: dependencies: rrweb-cssom: 0.6.0 + /cypress-real-events@1.12.0(cypress@13.7.3): + resolution: {integrity: sha512-oiy+4kGKkzc2PT36k3GGQqkGxNiVypheWjMtfyi89iIk6bYmTzeqxapaLHS3pnhZOX1IEbTDUVxh8T4Nhs1tyQ==} + peerDependencies: + cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x + dependencies: + cypress: 13.7.3 + dev: true + /cypress@13.7.3: resolution: {integrity: sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} diff --git a/test/cypress/e2e/example22.cy.ts b/test/cypress/e2e/example22.cy.ts index 353044b16..2d157024b 100644 --- a/test/cypress/e2e/example22.cy.ts +++ b/test/cypress/e2e/example22.cy.ts @@ -62,7 +62,23 @@ describe('Example 22 - Row Based Editing', () => { cy.get('.slick-cell').first().should('have.class', 'slick-rbe-unsaved-cell'); }); - it('should stay in editmode if saving failed', () => { + it('should fire onvalidationerror event when pasting and resulting in invalid validation result', (done) => { + cy.reload(); + + cy.get('.action-btns--edit').first().click(); + + cy.get('.slick-cell.l1.r1').first().click().type('120{enter}'); + cy.get('.slick-cell.l1.r1').first().click().realPress(['Control', 'C']); + + cy.on('window:alert', (str) => { + expect(str).to.equal('Max 100% allowed'); + done(); + }); + cy.get('.slick-cell.l2.r2').first().click().realPress(['Control', 'V']); + cy.get('.slick-cell.active').type('{enter}'); + }); + + it('should stay in editmode if saving failed', (done) => { cy.reload(); cy.get('.action-btns--edit').first().click(); @@ -74,6 +90,7 @@ describe('Example 22 - Row Based Editing', () => { cy.on('window:confirm', () => true); cy.on('window:alert', (str) => { expect(str).to.equal('Sorry, 40 is the maximum allowed duration.'); + done(); }); cy.get('.slick-row.slick-rbe-editmode').should('have.length', 1); diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index 89a26336b..5ac907420 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -24,6 +24,7 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) import '@4tw/cypress-drag-drop'; +import 'cypress-real-events'; import { convertPosition } from './common'; declare global { diff --git a/test/tsconfig.json b/test/tsconfig.json index 6032fe3d9..16e7ce7a6 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -7,7 +7,8 @@ ], "types": [ "jest", - "node" + "node", + "cypress-real-events" ], "allowJs": true, "skipLibCheck": true,