Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
08155a7
Implement the AiPromptEditor base
Oct 1, 2025
1e02630
Add Regenerate Data and Apply buttons
Oct 2, 2025
897fe94
Add localizations
Oct 2, 2025
bcd71c6
Add title, height and width for Popup
Oct 2, 2025
324723f
Add localizations for popup title
Oct 2, 2025
6a40316
Implement updating disabled state for buttons
Oct 3, 2025
2dc710d
Code refactoring - remove async operations. Change height to 'auto'. …
Oct 6, 2025
b6fd0de
Implement positioning relative to headers
Oct 7, 2025
32f61bc
Rename updateApplyAndStopButtonsVisibility -> toggleApplyButtonVisibi…
Oct 8, 2025
f1221c5
Add progressbar
Oct 8, 2025
4788dbe
Prepare AiColumnController and AiColumnIntegrationController for use …
Oct 9, 2025
313a1c0
Implement AiPromptEditor state update
Oct 9, 2025
407859b
Add cancellation of the request when hiding the AiPromptEditor. Add s…
Oct 9, 2025
d88305a
Add icons to buttons. Enable resizing and dragging for popup. Add min…
Oct 10, 2025
6722108
Show stop button when clicking on the Regenerate Data button
Oct 10, 2025
be5c54b
Fix tests
Oct 10, 2025
557c597
Fix the sendAiColumnRequest method
Oct 10, 2025
62a9032
Add storybook example
Oct 10, 2025
758fde1
Add placeholder for TextArea
Oct 13, 2025
c33fe9e
Rename AIPrompEditor.value -> AIPrompEditor.prompt
Oct 13, 2025
01ada70
add the 'wrapInstance' helper for jest tests
Oct 13, 2025
d4f29aa
Fix test
Oct 13, 2025
8bf66cb
Fix test (part 2)
Oct 14, 2025
6ad2133
Update localization messages after review
Oct 14, 2025
4f9fece
Fix minor issues. And a little code refactoring
Oct 15, 2025
c800bda
Fix popup positioning issue after reordering AI column
Oct 15, 2025
4331e7c
Create POM for textarea, button and progressbar components
Oct 15, 2025
e9d271f
Add the 'dx-aidialog' class
Oct 16, 2025
f9e44e9
Disable the resizeEnabled option
Oct 16, 2025
6b42e17
Set a constant height for TextArea
Oct 16, 2025
aaff650
Fix updating states for the Apply and Regenerate Data buttons
Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import DataGrid, {
} from "devextreme-react/data-grid";
import DiscountCell from "./DiscountCell";
import ODataStore from "devextreme/data/odata/store";
import { AIIntegration } from 'devextreme-react/common/ai-integration';
Comment thread
Raushen marked this conversation as resolved.

const columnOptions = {
regularColumns: [
Expand Down Expand Up @@ -252,22 +253,85 @@ export const ColumnReordering: Story = {
allowColumnDragging: true,
},
}
}
};

const generatedData = generateData(10, 100);
const generatedData = generateData(10, 100);

export const ColumnReorderingWithVirtualColumns: Story = {
argTypes: {
columns: {
control: 'object',
mapping: null,
},
export const ColumnReorderingWithVirtualColumns: Story = {
argTypes: {
columns: {
control: 'object',
mapping: null,
},
},
args: {
allowColumnReordering: true,
rtlEnabled: false,
columnWidth: 100,
dataSource: generatedData,
columns: Object.keys(generatedData[0]),
}
}

export const AiColumn: Story = {
args: {
dataSource: countries,
columns: [
{
caption: 'AI Column 1',
type: 'ai',
name: 'test',
ai: {
aiIntegration: new AIIntegration({
sendRequest() {
return {
promise: new Promise((resolve) => {
setTimeout(() => {
resolve('{"text":"Test response from AI Column 1"}');
}, 5000);
}),
abort: () => {
},
};
},
}),
},
},
'Country', 'Area', 'Population_Urban', 'Population_Rural',
{
caption: 'AI Column 2',
type: 'ai',
name: 'test',
ai: {
aiIntegration: new AIIntegration({
sendRequest() {
return {
promise: new Promise((resolve) => {
setTimeout(() => {
resolve('{"text":"Test response from AI Column 2"}');
}, 5000);
}),
abort: () => {
},
};
},
}),
},
}
],
allowColumnResizing: true,
allowColumnReordering: true,
rtlEnabled: false,
columnWidth: 100,
dataSource: generatedData,
columns: Object.keys(generatedData[0]),
onContextMenuPreparing: (e) => {
if (e.target === 'header' && e.column?.type === 'ai') {
e.items = e.items || [];
e.items.push({
text: 'Show AI Prompt Editor',
onItemClick: () => {
// @ts-expect-error
e.component.getView('aiColumnView').showPromptEditor(e.event.target, e.column);
},
});
}
}
}
}
};
8 changes: 8 additions & 0 deletions packages/devextreme-scss/scss/widgets/base/_gridBase.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1239,4 +1239,12 @@
cursor: pointer;
}
}

.dx-ai-prompt-editor__progressbar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
line-height: 0;
}
}
6 changes: 3 additions & 3 deletions packages/devextreme-themebuilder/tests/data/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ export const dependencies: FlatStylesDependencies = {
treeview: ['validation', 'button', 'loadindicator', 'textbox', 'checkbox'],
menu: ['validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'checkbox', 'treeview'],
filterbuilder: ['validation', 'button', 'loadindicator', 'textbox', 'checkbox', 'treeview', 'popup', 'numberbox', 'loadpanel', 'scrollview', 'list', 'selectbox', 'calendar', 'box', 'datebox'],
datagrid: ['loadindicator', 'loadpanel', 'validation', 'button', 'textbox', 'contextmenu', 'scrollview', 'popup', 'toolbar', 'checkbox', 'treeview', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable'],
datagrid: ['loadindicator', 'loadpanel', 'validation', 'button', 'textbox', 'contextmenu', 'scrollview', 'popup', 'progressbar', 'toolbar', 'checkbox', 'treeview', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable', 'textarea'],
treelist: ['loadindicator', 'loadpanel', 'validation', 'button', 'textbox', 'contextmenu', 'scrollview', 'popup', 'toolbar'],
pivotgrid: ['validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'popup', 'loadpanel', 'checkbox', 'treeview', 'scrollview', 'list'],
scheduler: ['validation', 'button', 'popup', 'loadindicator', 'loadpanel', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'buttongroup', 'radiogroup', 'textarea', 'tagbox', 'switch', 'dropdownbutton', 'popover', 'tooltip', 'toolbar'],
filemanager: ['toast', 'validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'checkbox', 'treeview', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable', 'datagrid', 'drawer', 'progressbar', 'fileuploader'],
filemanager: ['toast', 'validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'checkbox', 'treeview', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable', 'datagrid', 'drawer', 'progressbar', 'fileuploader', 'textarea'],
diagram: ['loadindicator', 'validation', 'button', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'textbox', 'contextmenu', 'list', 'checkbox', 'selectbox', 'numberbox', 'colorbox', 'popover', 'accordion', 'tooltip', 'multiview', 'tabs', 'tabpanel', 'progressbar', 'fileuploader'],
gantt: ['loadindicator', 'loadpanel', 'validation', 'button', 'popup', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist'],
gantt: ['loadindicator', 'loadpanel', 'validation', 'button', 'popup', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist', 'progressbar', 'textarea'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { jest } from '@jest/globals';

export default function wrapInstance<T extends object>(instance: T): T {
const proto = Object.getPrototypeOf(instance);

(Object.getOwnPropertyNames(proto) as (keyof T)[]).forEach((key): void => {
const originalValue = instance[key];
if (typeof originalValue === 'function' && key !== 'constructor') {
const originalMethod = originalValue as (...a: unknown[]) => unknown;

instance[key] = jest.fn(originalMethod.bind(instance)) as T[typeof key];
}
});

return instance;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Popup from '@js/ui/popup';
import { ProgressBarModel } from '@ts/ui/__tests__/__mock__/model/progress_bar';
import { TextAreaModel } from '@ts/ui/__tests__/__mock__/model/text_area';
import { ButtonModel } from '@ts/ui/button/__tests__/__mock__/model/button';

const CLASSES = {
wrapper: 'dx-overlay-wrapper',
toolbarItem: 'dx-toolbar-item',
stateInvisible: 'dx-state-invisible',
stateDisabled: 'dx-state-disabled',
aiPromptEditor: 'dx-ai-prompt-editor',
aiPromptEditorTextArea: 'dx-ai-prompt-editor__text-area',
aiPromptEditorRefreshButton: 'dx-ai-prompt-editor__refresh-button',
aiPromptEditorApplyButton: 'dx-ai-prompt-editor__apply-button',
aiPromptEditorStopButton: 'dx-ai-prompt-editor__stop-button',
aiPromptEditorProgressBar: 'dx-ai-prompt-editor__progressbar',
};

export class AiPromptEditorModel {
constructor(protected readonly root: HTMLElement) {}

public isVisible(): boolean {
return this.getWrapperElement() !== null;
}

public getPopupInstance(): Popup {
return Popup.getInstance(this.root) as Popup;
}

public getTextArea(): TextAreaModel {
return new TextAreaModel(
this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorTextArea}`) as HTMLElement,
);
}

public getWrapperElement(): HTMLElement {
return document.body.querySelector(`.${CLASSES.aiPromptEditor}.${CLASSES.wrapper}`) as HTMLElement;
}

public getRefreshButton(): ButtonModel {
return new ButtonModel(this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorRefreshButton}`) as HTMLElement);
}

public getApplyButton(): ButtonModel {
return new ButtonModel(this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorApplyButton}`) as HTMLElement);
}

public getStopButton(): ButtonModel {
return new ButtonModel(this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorStopButton}`) as HTMLElement);
}

public getProgressBar(): ProgressBarModel {
return new ProgressBarModel(
this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorProgressBar}`) as HTMLElement,
);
}

public isApplyToolbarItemVisible(): boolean {
Copy link
Copy Markdown
Contributor

@Raushen Raushen Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: I'd suggest to avoid this method too. There are two ways:

  1. Create Toolbar and ToolbarItem POM where create methods Toolbar.getItemByText and ToolbarItem.isVisible
  2. On the Button POM level create method areParentsVisible where it iterates all parents up to body and check its visibility by checking display: none. It seems it would be easier way.

const applyButton = this.getApplyButton().getElement();
const toolbarItem = applyButton.closest(`.${CLASSES.toolbarItem}`);

return !!toolbarItem && !toolbarItem.classList.contains(CLASSES.stateInvisible);
}

public isStopToolbarItemVisible(): boolean {
const stopButton = this.getStopButton().getElement();
const toolbarItem = stopButton.closest(`.${CLASSES.toolbarItem}`);

return !!toolbarItem && !toolbarItem.classList.contains(CLASSES.stateInvisible);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const createDataGrid = async (
const instance = new DataGrid($container.get(0) as HTMLDivElement, options);
const component = new DataGridModel($container.get(0) as HTMLElement);

jest.runOnlyPendingTimers();
jest.runAllTimers();

resolve({
$container,
component,
Expand Down Expand Up @@ -548,6 +549,7 @@ describe('aiIntegration', () => {
return aiIntegrationResult();
},
});

const columnAiIntegration = new AIIntegration({
sendRequest(): RequestResult {
columnSendRequestSpy();
Expand Down Expand Up @@ -578,6 +580,7 @@ describe('aiIntegration', () => {
expect(rootSendRequestSpy).toHaveBeenCalled();
expect(columnSendRequestSpy).not.toHaveBeenCalled();
});

it('should be taken from grid level if it set up (dynamic update)', async () => {
const { instance } = await createDataGrid({
dataSource: [
Expand All @@ -600,6 +603,7 @@ describe('aiIntegration', () => {
expect(rootSendRequestSpy).toHaveBeenCalled();
expect(columnSendRequestSpy).not.toHaveBeenCalled();
});

it('should be taken from column level if it set up (first load)', async () => {
const { instance } = await createDataGrid({
dataSource: [
Expand Down Expand Up @@ -800,6 +804,7 @@ describe('aiMode', () => {
const aiMode = instance.columnOption('myColumn', 'ai.mode');
expect(aiMode).toBe('auto');
});

it('should call aiIntegration.sendRequest with every visible rows change', async () => {
const dataSource = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
Expand Down
Loading
Loading