From f0635b555482f8c44bd015f5e5560748a67180f5 Mon Sep 17 00:00:00 2001 From: Raushen Date: Mon, 22 Dec 2025 18:11:55 +0200 Subject: [PATCH 1/4] SmartPaste spike --- .../grid_core/editing/m_editing_form_based.ts | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts index 05a530e0f618..2c029ceadf52 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts @@ -135,7 +135,6 @@ const editingControllerExtender = (Base: ModuleType) => class } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _updateEditRowCore(row, skipCurrentRow, isCustomSetCellValue) { const editForm = this._editForm; @@ -158,19 +157,38 @@ const editingControllerExtender = (Base: ModuleType) => class protected _showEditPopup(rowIndex, repaintForm?) { const isMobileDevice = devices.current().deviceType !== 'desktop'; const editPopupClass = this.addWidgetPrefix(EDIT_POPUP_CLASS); + const toolbarItems = [ + { + toolbar: 'bottom', location: 'after', widget: 'dxButton', options: this._getSaveButtonConfig(), + }, + { + toolbar: 'bottom', location: 'after', widget: 'dxButton', options: this._getCancelButtonConfig(), + }, + ]; + + // Add Smart Paste button if aiIntegration is configured + const editFormOptions = this.option(EDITING_FORM_OPTION_NAME); + if (editFormOptions?.aiIntegration) { + toolbarItems.unshift({ + toolbar: 'bottom', + location: 'before', + widget: 'dxButton', + options: { + text: 'Smart Paste 22', + icon: 'smartPaste', + onClick: () => { + this._editForm?.smartPaste(); + }, + }, + }); + } + const popupOptions = extend( { showTitle: false, fullScreen: isMobileDevice, wrapperAttr: { class: editPopupClass }, - toolbarItems: [ - { - toolbar: 'bottom', location: 'after', widget: 'dxButton', options: this._getSaveButtonConfig(), - }, - { - toolbar: 'bottom', location: 'after', widget: 'dxButton', options: this._getCancelButtonConfig(), - }, - ], + toolbarItems, contentTemplate: this._getPopupEditFormTemplate(rowIndex), }, this.option(EDITING_POPUP_OPTION_NAME), @@ -394,6 +412,24 @@ const editingControllerExtender = (Base: ModuleType) => class return extend({}, editFormOptions, { items, formID: `dx-${new Guid()}`, + onSmartPasted: (e) => { + // Update grid editors with AI-generated values + if (e.aiResult) { + Object.keys(e.aiResult).forEach((dataField) => { + const column = this._columnsController.columnOption(`dataField:${dataField}`); + if (column && detailOptions.data) { + const oldValue = detailOptions.data[dataField]; + const newValue = e.aiResult[dataField]; + detailOptions.data[dataField] = newValue; + + // eslint-disable-next-line no-console + console.log(`Smart Paste: Updated ${dataField} from "${oldValue}" to "${newValue}"`); + } + }); + // Trigger form refresh to update editors + this._editForm?.repaint(); + } + }, customizeItem: (item) => { let column; const itemId = item.name || item.dataField; @@ -448,6 +484,18 @@ const editingControllerExtender = (Base: ModuleType) => class if (!isPopupForm) { const $buttonsContainer = $('
').addClass(this.addWidgetPrefix(FORM_BUTTONS_CONTAINER_CLASS)).appendTo($container); + + // Add Smart Paste button if aiIntegration is configured + if (editFormOptions?.aiIntegration) { + this._createComponent($('
').appendTo($buttonsContainer), Button, { + text: 'Smart Paste', + icon: 'paste', + onClick: () => { + this._editForm?.smartPaste(); + }, + }); + } + this._createComponent($('
').appendTo($buttonsContainer), Button, this._getSaveButtonConfig()); this._createComponent($('
').appendTo($buttonsContainer), Button, this._getCancelButtonConfig()); } From 1d632ff0ffe10d1703b1d85195cf3a3b810aeff3 Mon Sep 17 00:00:00 2001 From: Raushen Date: Mon, 22 Dec 2025 18:28:56 +0200 Subject: [PATCH 2/4] Refactoring --- .../grid_core/editing/m_editing_form_based.ts | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts index 2c029ceadf52..a9f3a325f41b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts @@ -154,6 +154,16 @@ const editingControllerExtender = (Base: ModuleType) => class } } + protected _getSmartPasteButtonConfig() { + return { + text: 'Smart Paste', + icon: 'clipboardpastesparkle', + onClick: () => { + this._editForm?.smartPaste(); + }, + }; + } + protected _showEditPopup(rowIndex, repaintForm?) { const isMobileDevice = devices.current().deviceType !== 'desktop'; const editPopupClass = this.addWidgetPrefix(EDIT_POPUP_CLASS); @@ -166,20 +176,13 @@ const editingControllerExtender = (Base: ModuleType) => class }, ]; - // Add Smart Paste button if aiIntegration is configured const editFormOptions = this.option(EDITING_FORM_OPTION_NAME); if (editFormOptions?.aiIntegration) { toolbarItems.unshift({ toolbar: 'bottom', location: 'before', widget: 'dxButton', - options: { - text: 'Smart Paste 22', - icon: 'smartPaste', - onClick: () => { - this._editForm?.smartPaste(); - }, - }, + options: this._getSmartPasteButtonConfig(), }); } @@ -485,15 +488,8 @@ const editingControllerExtender = (Base: ModuleType) => class if (!isPopupForm) { const $buttonsContainer = $('
').addClass(this.addWidgetPrefix(FORM_BUTTONS_CONTAINER_CLASS)).appendTo($container); - // Add Smart Paste button if aiIntegration is configured if (editFormOptions?.aiIntegration) { - this._createComponent($('
').appendTo($buttonsContainer), Button, { - text: 'Smart Paste', - icon: 'paste', - onClick: () => { - this._editForm?.smartPaste(); - }, - }); + this._createComponent($('
').appendTo($buttonsContainer), Button, this._getSmartPasteButtonConfig()); } this._createComponent($('
').appendTo($buttonsContainer), Button, this._getSaveButtonConfig()); From 3e63b418cf30c4efefa294fcf9d8a7417b56515d Mon Sep 17 00:00:00 2001 From: Raushen Date: Mon, 22 Dec 2025 20:25:13 +0200 Subject: [PATCH 3/4] Spike --- .../m_editing_form_smart_paste.test.ts | 487 ++++++++++++++++++ .../grid_core/editing/m_editing_form_based.ts | 50 +- 2 files changed, 525 insertions(+), 12 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/editing/__tests__/m_editing_form_smart_paste.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/__tests__/m_editing_form_smart_paste.test.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/__tests__/m_editing_form_smart_paste.test.ts new file mode 100644 index 000000000000..15abc30e8d61 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/__tests__/m_editing_form_smart_paste.test.ts @@ -0,0 +1,487 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import $ from '@js/core/renderer'; +import DataGrid from '@js/ui/data_grid'; +import { AIIntegration } from '@ts/core/ai_integration/core/ai_integration'; + +const GRID_CONTAINER_ID = 'gridContainer'; + +interface RequestResult { + promise: Promise; + abort: () => void; +} + +const dataSource = [{ + ID: 1, + FirstName: 'John', + LastName: 'Heart', + Position: 'CEO', + BirthDate: new Date('1964/03/16'), + HireDate: new Date('1995/01/15'), + City: 'Los Angeles', + State: 'CA', + Email: 'john.heart@example.com', + Phone: '555-0100', +}, { + ID: 2, + FirstName: 'Olivia', + LastName: 'Peyton', + Position: 'Sales Assistant', + BirthDate: new Date('1981/06/03'), + HireDate: new Date('2012/05/14'), + City: 'San Diego', + State: 'CA', + Email: 'olivia.peyton@example.com', + Phone: '555-0200', +}]; + +const flushAsync = async (): Promise => { + jest.runOnlyPendingTimers(); + await Promise.resolve(); +}; + +const getEditorValue = (editForm: any, dataField: string): any => { + const $formElement = editForm.$element(); + const itemID = editForm.getItemID(dataField); + const escapedID = itemID.replace(/([.:!])/g, '\\$1'); + const $input = $formElement.find(`#${escapedID}`); + if ($input.length) { + const $widget = $input.closest('.dx-widget'); + if ($widget.length) { + const widgetNames = $widget.data('dxComponents'); + if (widgetNames && widgetNames.length > 0) { + const widget = $widget.data(widgetNames[0]); + return widget?.option('value'); + } + } + } + return undefined; +}; + +describe('DataGrid - Form-based editing with Smart Paste', () => { + let $gridContainer; + let gridInstance: DataGrid; + + beforeEach(() => { + jest.useFakeTimers(); + $gridContainer = $('
') + .attr('id', GRID_CONTAINER_ID) + .appendTo(document.body); + + Object.defineProperty(navigator, 'clipboard', { + value: { + readText: jest.fn<() => Promise>().mockResolvedValue('Jane Doe, CTO, San Francisco, jane.doe@example.com, 555-9999'), + } as Partial, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + gridInstance?.dispose(); + $gridContainer.remove(); + jest.useRealTimers(); + }); + + it('should update editors when Smart Paste is triggered', async () => { + const mockAIResultString = 'FirstName:::Jane;;;LastName:::Doe;;;Position:::CTO;;;City:::San Francisco;;;Email:::jane.doe@example.com;;;Phone:::555-9999'; + + gridInstance = new DataGrid($gridContainer.get(0) as HTMLDivElement, { + dataSource: [...dataSource], + keyExpr: 'ID', + editing: { + mode: 'form', + allowUpdating: true, + form: { + aiIntegration: new AIIntegration({ + sendRequest(): RequestResult { + return { + promise: Promise.resolve(mockAIResultString), + abort: (): void => {}, + }; + }, + }), + }, + }, + columns: ['FirstName', 'LastName', 'Position', 'City', 'Email', 'Phone'], + }); + + await flushAsync(); + + gridInstance.editRow(0); + await flushAsync(); + + const editForm = (gridInstance as any).getController('editing')._editForm; + expect(editForm).toBeDefined(); + + const formAIIntegration = editForm.option('aiIntegration'); + expect(formAIIntegration).toBeDefined(); + + const $formElement = editForm.$element(); + const $buttonsContainer = $formElement.parent().find('.dx-datagrid-form-buttons-container'); + const $allButtons = $buttonsContainer.find('.dx-button'); + + const $smartPasteButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Smart Paste'; + }).first(); + + expect($smartPasteButton.length).toBe(1); + + const dataController = (gridInstance as any).getController('data'); + const fireErrorSpy = jest.spyOn(dataController, 'fireError'); + + ($smartPasteButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + expect(fireErrorSpy).not.toHaveBeenCalledWith('E1043'); + fireErrorSpy.mockRestore(); + + expect(getEditorValue(editForm, 'FirstName')).toBe('Jane'); + expect(getEditorValue(editForm, 'LastName')).toBe('Doe'); + expect(getEditorValue(editForm, 'Position')).toBe('CTO'); + expect(getEditorValue(editForm, 'City')).toBe('San Francisco'); + expect(getEditorValue(editForm, 'Email')).toBe('jane.doe@example.com'); + expect(getEditorValue(editForm, 'Phone')).toBe('555-9999'); + + const editingController = (gridInstance as any).getController('editing'); + const changes = editingController.getChanges(); + expect(changes.length).toBe(1); + expect(changes[0].data.FirstName).toBe('Jane'); + expect(changes[0].data.LastName).toBe('Doe'); + expect(changes[0].data.Position).toBe('CTO'); + + expect(changes[0].key).toBe(1); + expect(changes[0].type).toBe('update'); + + const $saveButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Save'; + }).first(); + + expect($saveButton.length).toBe(1); + ($saveButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + const $gridElement = $gridContainer; + const $firstNameCell = $gridElement.find('.dx-data-row').first().find('td').eq(0); + const $lastNameCell = $gridElement.find('.dx-data-row').first().find('td').eq(1); + const $positionCell = $gridElement.find('.dx-data-row').first().find('td').eq(2); + + expect($firstNameCell.text()).toBe('Jane'); + expect($lastNameCell.text()).toBe('Doe'); + expect($positionCell.text()).toBe('CTO'); + }); + + it('should not save changes when Cancel button is clicked after Smart Paste', async () => { + const mockAIResultString = 'FirstName:::Alice;;;LastName:::Brown;;;Position:::Manager;;;City:::New York;;;Email:::alice.brown@example.com;;;Phone:::555-3333'; + + gridInstance = new DataGrid($gridContainer.get(0) as HTMLDivElement, { + dataSource: [...dataSource], + keyExpr: 'ID', + editing: { + mode: 'form', + allowUpdating: true, + form: { + aiIntegration: new AIIntegration({ + sendRequest(): RequestResult { + return { + promise: Promise.resolve(mockAIResultString), + abort: (): void => {}, + }; + }, + }), + }, + }, + columns: ['FirstName', 'LastName', 'Position', 'City', 'Email', 'Phone'], + }); + + await flushAsync(); + + gridInstance.editRow(1); + await flushAsync(); + + const editForm = (gridInstance as any).getController('editing')._editForm; + expect(editForm).toBeDefined(); + + const $formElement = editForm.$element(); + const $buttonsContainer = $formElement.parent().find('.dx-datagrid-form-buttons-container'); + const $allButtons = $buttonsContainer.find('.dx-button'); + + const $smartPasteButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Smart Paste'; + }).first(); + + expect($smartPasteButton.length).toBe(1); + + ($smartPasteButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + expect(getEditorValue(editForm, 'FirstName')).toBe('Alice'); + expect(getEditorValue(editForm, 'LastName')).toBe('Brown'); + + const $cancelButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Cancel'; + }).first(); + + expect($cancelButton.length).toBe(1); + ($cancelButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + const editingController = (gridInstance as any).getController('editing'); + const changes = editingController.getChanges(); + expect(changes.length).toBe(0); + + const $gridElement = $gridContainer; + const $secondRow = $gridElement.find('.dx-data-row').eq(1); + const $firstNameCell = $secondRow.find('td').eq(0); + const $lastNameCell = $secondRow.find('td').eq(1); + const $positionCell = $secondRow.find('td').eq(2); + + expect($firstNameCell.text()).toBe('Olivia'); + expect($lastNameCell.text()).toBe('Peyton'); + expect($positionCell.text()).toBe('Sales Assistant'); + }); + + it('should update editors when Smart Paste is triggered in popup mode', async () => { + const mockAIResultString = 'FirstName:::Jane;;;LastName:::Doe;;;Position:::CTO;;;City:::San Francisco;;;Email:::jane.doe@example.com;;;Phone:::555-9999'; + + gridInstance = new DataGrid($gridContainer.get(0) as HTMLDivElement, { + dataSource: [...dataSource], + keyExpr: 'ID', + editing: { + mode: 'popup', + allowUpdating: true, + form: { + aiIntegration: new AIIntegration({ + sendRequest(): RequestResult { + return { + promise: Promise.resolve(mockAIResultString), + abort: (): void => {}, + }; + }, + }), + }, + }, + columns: ['FirstName', 'LastName', 'Position', 'City', 'Email', 'Phone'], + }); + + await flushAsync(); + + gridInstance.editRow(0); + await flushAsync(); + + const editForm = (gridInstance as any).getController('editing')._editForm; + expect(editForm).toBeDefined(); + + const formAIIntegration = editForm.option('aiIntegration'); + expect(formAIIntegration).toBeDefined(); + + const editingController = (gridInstance as any).getController('editing'); + const $popupContent = $(editingController.getPopupContent()); + expect($popupContent.length).toBeGreaterThan(0); + + const $overlayContent = $popupContent.closest('.dx-overlay-content'); + const $allButtons = $overlayContent.find('.dx-button'); + // @ts-expect-error jQuery filter accepts function but types are incorrect + const $smartPasteButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Smart Paste'; + }).first(); + + expect($smartPasteButton.length).toBe(1); + + const dataController = (gridInstance as any).getController('data'); + const fireErrorSpy = jest.spyOn(dataController, 'fireError'); + + ($smartPasteButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + expect(fireErrorSpy).not.toHaveBeenCalledWith('E1043'); + fireErrorSpy.mockRestore(); + + expect(getEditorValue(editForm, 'FirstName')).toBe('Jane'); + expect(getEditorValue(editForm, 'LastName')).toBe('Doe'); + expect(getEditorValue(editForm, 'Position')).toBe('CTO'); + expect(getEditorValue(editForm, 'City')).toBe('San Francisco'); + expect(getEditorValue(editForm, 'Email')).toBe('jane.doe@example.com'); + expect(getEditorValue(editForm, 'Phone')).toBe('555-9999'); + + const changes = editingController.getChanges(); + expect(changes.length).toBe(1); + expect(changes[0].data.FirstName).toBe('Jane'); + expect(changes[0].data.LastName).toBe('Doe'); + expect(changes[0].data.Position).toBe('CTO'); + + expect(changes[0].key).toBe(1); + expect(changes[0].type).toBe('update'); + }); + + it('should save changes when Save button is clicked after Smart Paste in popup mode', async () => { + const mockAIResultString = 'FirstName:::Alice;;;LastName:::Smith;;;Position:::Manager;;;City:::New York;;;Email:::alice.smith@example.com;;;Phone:::555-7777'; + + gridInstance = new DataGrid($gridContainer.get(0) as HTMLDivElement, { + dataSource: [...dataSource], + keyExpr: 'ID', + editing: { + mode: 'popup', + allowUpdating: true, + form: { + aiIntegration: new AIIntegration({ + sendRequest(): RequestResult { + return { + promise: Promise.resolve(mockAIResultString), + abort: (): void => {}, + }; + }, + }), + }, + }, + columns: ['FirstName', 'LastName', 'Position', 'City', 'Email', 'Phone'], + }); + + await flushAsync(); + + gridInstance.editRow(0); + await flushAsync(); + + const editForm = (gridInstance as any).getController('editing')._editForm; + expect(editForm).toBeDefined(); + + const editingController = (gridInstance as any).getController('editing'); + const $popupContent = $(editingController.getPopupContent()); + expect($popupContent.length).toBeGreaterThan(0); + + const $overlayContent = $popupContent.closest('.dx-overlay-content'); + const $allButtons = $overlayContent.find('.dx-button'); + // @ts-expect-error jQuery filter accepts function but types are incorrect + const $smartPasteButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Smart Paste'; + }).first(); + + expect($smartPasteButton.length).toBe(1); + + ($smartPasteButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + expect(getEditorValue(editForm, 'FirstName')).toBe('Alice'); + expect(getEditorValue(editForm, 'LastName')).toBe('Smith'); + expect(getEditorValue(editForm, 'Position')).toBe('Manager'); + + // @ts-expect-error jQuery filter accepts function but types are incorrect + const $saveButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Save'; + }).first(); + + expect($saveButton.length).toBe(1); + ($saveButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + const $gridElement = $gridContainer; + const $firstNameCell = $gridElement.find('.dx-data-row').first().find('td').eq(0); + const $lastNameCell = $gridElement.find('.dx-data-row').first().find('td').eq(1); + const $positionCell = $gridElement.find('.dx-data-row').first().find('td').eq(2); + + expect($firstNameCell.text()).toBe('Alice'); + expect($lastNameCell.text()).toBe('Smith'); + expect($positionCell.text()).toBe('Manager'); + }); + + it('should not save changes when Cancel button is clicked after Smart Paste in popup mode', async () => { + const mockAIResultString = 'FirstName:::Bob;;;LastName:::Johnson;;;Position:::Developer;;;City:::Seattle;;;Email:::bob.johnson@example.com;;;Phone:::555-8888'; + + gridInstance = new DataGrid($gridContainer.get(0) as HTMLDivElement, { + dataSource: [...dataSource], + keyExpr: 'ID', + editing: { + mode: 'popup', + allowUpdating: true, + form: { + aiIntegration: new AIIntegration({ + sendRequest(): RequestResult { + return { + promise: Promise.resolve(mockAIResultString), + abort: (): void => {}, + }; + }, + }), + }, + }, + columns: ['FirstName', 'LastName', 'Position', 'City', 'Email', 'Phone'], + }); + + await flushAsync(); + + gridInstance.editRow(1); + await flushAsync(); + + const editForm = (gridInstance as any).getController('editing')._editForm; + expect(editForm).toBeDefined(); + + const editingController = (gridInstance as any).getController('editing'); + const $popupContent = $(editingController.getPopupContent()); + expect($popupContent.length).toBeGreaterThan(0); + + const $overlayContent = $popupContent.closest('.dx-overlay-content'); + const $allButtons = $overlayContent.find('.dx-button'); + // @ts-expect-error jQuery filter accepts function but types are incorrect + const $smartPasteButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Smart Paste'; + }).first(); + + expect($smartPasteButton.length).toBe(1); + + ($smartPasteButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + expect(getEditorValue(editForm, 'FirstName')).toBe('Bob'); + expect(getEditorValue(editForm, 'LastName')).toBe('Johnson'); + + // @ts-expect-error jQuery filter accepts function but types are incorrect + const $cancelButton = $allButtons.filter((_, el) => { + const $el = $(el); + const buttonInstance = ($el as any).dxButton('instance'); + return buttonInstance?.option('text') === 'Cancel'; + }).first(); + + expect($cancelButton.length).toBe(1); + ($cancelButton.get(0) as HTMLElement)?.click(); + + await flushAsync(); + + const changes = editingController.getChanges(); + expect(changes.length).toBe(0); + + const $gridElement = $gridContainer; + const $secondRow = $gridElement.find('.dx-data-row').eq(1); + const $firstNameCell = $secondRow.find('td').eq(0); + const $lastNameCell = $secondRow.find('td').eq(1); + const $positionCell = $secondRow.find('td').eq(2); + + expect($firstNameCell.text()).toBe('Olivia'); + expect($lastNameCell.text()).toBe('Peyton'); + expect($positionCell.text()).toBe('Sales Assistant'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts index a9f3a325f41b..861aedaf1ed6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts @@ -389,6 +389,7 @@ const editingControllerExtender = (Base: ModuleType) => class const editFormItemClass = this.addWidgetPrefix(EDIT_FORM_ITEM_CLASS); let items: any = this.option('editing.form.items'); const isCustomEditorType = {}; + const formAIIntegration = this.option('editing.form.aiIntegration'); if (!items) { const columns = this._columnsController.getColumns(); @@ -415,22 +416,47 @@ const editingControllerExtender = (Base: ModuleType) => class return extend({}, editFormOptions, { items, formID: `dx-${new Guid()}`, + aiIntegration: formAIIntegration, onSmartPasted: (e) => { - // Update grid editors with AI-generated values - if (e.aiResult) { + if (e.aiResult && this._editForm) { Object.keys(e.aiResult).forEach((dataField) => { - const column = this._columnsController.columnOption(`dataField:${dataField}`); - if (column && detailOptions.data) { - const oldValue = detailOptions.data[dataField]; - const newValue = e.aiResult[dataField]; - detailOptions.data[dataField] = newValue; - - // eslint-disable-next-line no-console - console.log(`Smart Paste: Updated ${dataField} from "${oldValue}" to "${newValue}"`); + const newValue = e.aiResult[dataField]; + + const column = this._columnsController.columnOption(dataField); + if (column) { + const cellOptions = { + data: detailOptions.data, + column, + row: { data: detailOptions.data }, + columnIndex: column.index, + key: detailOptions.key, + }; + + this.updateFieldValue(cellOptions, newValue, undefined); + } + + const itemID = this._editForm.getItemID(dataField); + const $formElement = this._editForm.$element(); + + const escapedID = itemID.replace(/([.:!])/g, '\\$1'); + const $fieldItem = $formElement.find(`#${escapedID}`).first(); + + if ($fieldItem.length) { + const $widget = $fieldItem.closest('.dx-widget'); + + if ($widget.length) { + const widgetNames = $widget.data('dxComponents'); + + if (widgetNames && widgetNames.length > 0) { + const widgetInstance = $widget.data(widgetNames[0]); + + if (widgetInstance && widgetInstance.option) { + widgetInstance.option('value', newValue); + } + } + } } }); - // Trigger form refresh to update editors - this._editForm?.repaint(); } }, customizeItem: (item) => { From d09d8f4e0d6fe3f32d4cdc54a89e35ee10cc0ee1 Mon Sep 17 00:00:00 2001 From: Raushen Date: Mon, 22 Dec 2025 21:18:49 +0200 Subject: [PATCH 4/4] Story book --- .../examples/datagrid/DataGrid.stories.tsx | 170 +++++++++++++++++- 1 file changed, 168 insertions(+), 2 deletions(-) diff --git a/apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx b/apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx index c15ff00ff869..76256142cca4 100644 --- a/apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx +++ b/apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx @@ -5,6 +5,8 @@ import { countries, generateData } from './data'; import DataGrid, { Column, DataGridTypes, + Editing, + Form, Grouping, GroupPanel, Pager, @@ -15,6 +17,7 @@ import DiscountCell from "./DiscountCell"; import ODataStore from "devextreme/data/odata/store"; import { AIIntegration } from 'devextreme-react/common/ai-integration'; import { AzureOpenAI } from 'openai'; +import notify from 'devextreme/ui/notify'; const columnOptions = { regularColumns: [ @@ -241,8 +244,7 @@ export const ColumnReordering: Story = { rtlEnabled: false, columnHidingEnabled: true, dataSource: countries, - // @ts-expect-error - columns: 'regularColumns', + columns: 'regularColumns' as any, columnFixing: { enabled: false }, @@ -353,3 +355,167 @@ export const AiColumn: Story = { allowColumnReordering: true, }, }; + +const employees = [ + { + ID: 1, + FirstName: 'John', + LastName: 'Heart', + Position: 'CEO', + BirthDate: new Date('1964/03/16'), + HireDate: new Date('1995/01/15'), + City: 'Los Angeles', + State: 'CA', + Email: 'jheart@dx-email.com', + Phone: '+1(213) 555-9392' + }, + { + ID: 2, + FirstName: 'Olivia', + LastName: 'Peyton', + Position: 'Sales Assistant', + BirthDate: new Date('1981/06/03'), + HireDate: new Date('2012/05/14'), + City: 'Atlanta', + State: 'GA', + Email: 'oliviap@dx-email.com', + Phone: '+1(310) 555-2728' + }, + { + ID: 3, + FirstName: 'Robert', + LastName: 'Reagan', + Position: 'CMO', + BirthDate: new Date('1974/09/07'), + HireDate: new Date('2002/11/08'), + City: 'Bentonville', + State: 'AR', + Email: 'robertr@dx-email.com', + Phone: '+1(818) 555-2387' + }, +]; + +export const SmartPaste: Story = { + argTypes: { + editingMode: { + control: 'radio', + options: ['form', 'popup'], + description: 'Editing mode for the DataGrid', + }, + }, + args: { + dataSource: employees, + keyExpr: 'ID', + showBorders: true, + editingMode: "popup", + }, + render: (args) => { + const { editingMode, ...gridArgs } = args; + + const sampleText = `Name: Sarah Johnson +Position: Senior Developer +Email: sarah.johnson@company.com +Phone: +1(555) 123-4567 +Born: 1985/06/15 +Hired: 2020/03/01 +Location: San Francisco, CA`; + + const copyToClipboard = () => { + navigator.clipboard.writeText(sampleText).then(() => { + notify({ + message: 'Text copied to clipboard! Now click "Add" and then "Smart Paste" in the form.', + position: { + my: 'bottom center', + at: 'bottom', + of: window, + }, + width: 'auto', + }, 'success', 2500); + }).catch(() => { + notify({ + message: 'Failed to copy text to clipboard', + position: { + my: 'bottom center', + at: 'bottom', + of: window, + }, + width: 'auto', + }, 'error', 2000); + }); + }; + + return ( +
+
+

AI Integration - Smart Paste Demo

+

How to use:

+
    +
  1. Change editing mode in Storybook Controls: form (inline) or popup (modal)
  2. +
  3. Click the "Copy Sample Text" button below
  4. +
  5. Click "Add" button in the DataGrid to open the edit form
  6. +
  7. In the edit form, click the "Smart Paste" button
  8. +
  9. Watch the AI automatically fill all fields from the copied text! ✨
  10. +
+

+ Sample text: +

+
+ {sampleText} +
+ +
+ + + + + + + + + + + + +
+ + + + +
+ ); + } +};