Skip to content

Commit c9842e1

Browse files
authored
DataGrid/TreeList: Implement AiPromptEditor (#31289)
Co-authored-by: Alyar <>
1 parent 76fdf51 commit c9842e1

50 files changed

Lines changed: 2155 additions & 86 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/react-storybook/stories/examples/datagrid/DataGrid.stories.tsx

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import DataGrid, {
1313
} from "devextreme-react/data-grid";
1414
import DiscountCell from "./DiscountCell";
1515
import ODataStore from "devextreme/data/odata/store";
16+
import { AIIntegration } from 'devextreme-react/common/ai-integration';
1617

1718
const columnOptions = {
1819
regularColumns: [
@@ -252,22 +253,85 @@ export const ColumnReordering: Story = {
252253
allowColumnDragging: true,
253254
},
254255
}
255-
}
256+
};
256257

257-
const generatedData = generateData(10, 100);
258+
const generatedData = generateData(10, 100);
258259

259-
export const ColumnReorderingWithVirtualColumns: Story = {
260-
argTypes: {
261-
columns: {
262-
control: 'object',
263-
mapping: null,
264-
},
260+
export const ColumnReorderingWithVirtualColumns: Story = {
261+
argTypes: {
262+
columns: {
263+
control: 'object',
264+
mapping: null,
265265
},
266+
},
267+
args: {
268+
allowColumnReordering: true,
269+
rtlEnabled: false,
270+
columnWidth: 100,
271+
dataSource: generatedData,
272+
columns: Object.keys(generatedData[0]),
273+
}
274+
}
275+
276+
export const AiColumn: Story = {
266277
args: {
278+
dataSource: countries,
279+
columns: [
280+
{
281+
caption: 'AI Column 1',
282+
type: 'ai',
283+
name: 'test',
284+
ai: {
285+
aiIntegration: new AIIntegration({
286+
sendRequest() {
287+
return {
288+
promise: new Promise((resolve) => {
289+
setTimeout(() => {
290+
resolve('{"text":"Test response from AI Column 1"}');
291+
}, 5000);
292+
}),
293+
abort: () => {
294+
},
295+
};
296+
},
297+
}),
298+
},
299+
},
300+
'Country', 'Area', 'Population_Urban', 'Population_Rural',
301+
{
302+
caption: 'AI Column 2',
303+
type: 'ai',
304+
name: 'test',
305+
ai: {
306+
aiIntegration: new AIIntegration({
307+
sendRequest() {
308+
return {
309+
promise: new Promise((resolve) => {
310+
setTimeout(() => {
311+
resolve('{"text":"Test response from AI Column 2"}');
312+
}, 5000);
313+
}),
314+
abort: () => {
315+
},
316+
};
317+
},
318+
}),
319+
},
320+
}
321+
],
322+
allowColumnResizing: true,
267323
allowColumnReordering: true,
268-
rtlEnabled: false,
269-
columnWidth: 100,
270-
dataSource: generatedData,
271-
columns: Object.keys(generatedData[0]),
324+
onContextMenuPreparing: (e) => {
325+
if (e.target === 'header' && e.column?.type === 'ai') {
326+
e.items = e.items || [];
327+
e.items.push({
328+
text: 'Show AI Prompt Editor',
329+
onItemClick: () => {
330+
// @ts-expect-error
331+
e.component.getView('aiColumnView').showPromptEditor(e.event.target, e.column);
332+
},
333+
});
334+
}
335+
}
272336
}
273-
}
337+
};

packages/devextreme-scss/scss/widgets/base/_gridBase.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,4 +1239,12 @@
12391239
cursor: pointer;
12401240
}
12411241
}
1242+
1243+
.dx-ai-prompt-editor__progressbar {
1244+
position: absolute;
1245+
left: 0;
1246+
right: 0;
1247+
bottom: 0;
1248+
line-height: 0;
1249+
}
12421250
}

packages/devextreme-themebuilder/tests/data/dependencies.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ export const dependencies: FlatStylesDependencies = {
5757
treeview: ['validation', 'button', 'loadindicator', 'textbox', 'checkbox'],
5858
menu: ['validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'checkbox', 'treeview'],
5959
filterbuilder: ['validation', 'button', 'loadindicator', 'textbox', 'checkbox', 'treeview', 'popup', 'numberbox', 'loadpanel', 'scrollview', 'list', 'selectbox', 'calendar', 'box', 'datebox'],
60-
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'],
60+
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'],
6161
treelist: ['loadindicator', 'loadpanel', 'validation', 'button', 'textbox', 'contextmenu', 'scrollview', 'popup', 'toolbar'],
6262
pivotgrid: ['validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'popup', 'loadpanel', 'checkbox', 'treeview', 'scrollview', 'list'],
6363
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'],
64-
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'],
64+
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'],
6565
diagram: ['loadindicator', 'validation', 'button', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'textbox', 'contextmenu', 'list', 'checkbox', 'selectbox', 'numberbox', 'colorbox', 'popover', 'accordion', 'tooltip', 'multiview', 'tabs', 'tabpanel', 'progressbar', 'fileuploader'],
66-
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'],
66+
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'],
6767
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { jest } from '@jest/globals';
2+
3+
export default function wrapInstance<T extends object>(instance: T): T {
4+
const proto = Object.getPrototypeOf(instance);
5+
6+
(Object.getOwnPropertyNames(proto) as (keyof T)[]).forEach((key): void => {
7+
const originalValue = instance[key];
8+
if (typeof originalValue === 'function' && key !== 'constructor') {
9+
const originalMethod = originalValue as (...a: unknown[]) => unknown;
10+
11+
instance[key] = jest.fn(originalMethod.bind(instance)) as T[typeof key];
12+
}
13+
});
14+
15+
return instance;
16+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Popup from '@js/ui/popup';
2+
import { ProgressBarModel } from '@ts/ui/__tests__/__mock__/model/progress_bar';
3+
import { TextAreaModel } from '@ts/ui/__tests__/__mock__/model/text_area';
4+
import { ButtonModel } from '@ts/ui/button/__tests__/__mock__/model/button';
5+
6+
const CLASSES = {
7+
wrapper: 'dx-overlay-wrapper',
8+
toolbarItem: 'dx-toolbar-item',
9+
stateInvisible: 'dx-state-invisible',
10+
stateDisabled: 'dx-state-disabled',
11+
aiPromptEditor: 'dx-ai-prompt-editor',
12+
aiPromptEditorTextArea: 'dx-ai-prompt-editor__text-area',
13+
aiPromptEditorRefreshButton: 'dx-ai-prompt-editor__refresh-button',
14+
aiPromptEditorApplyButton: 'dx-ai-prompt-editor__apply-button',
15+
aiPromptEditorStopButton: 'dx-ai-prompt-editor__stop-button',
16+
aiPromptEditorProgressBar: 'dx-ai-prompt-editor__progressbar',
17+
};
18+
19+
export class AiPromptEditorModel {
20+
constructor(protected readonly root: HTMLElement) {}
21+
22+
public isVisible(): boolean {
23+
return this.getWrapperElement() !== null;
24+
}
25+
26+
public getPopupInstance(): Popup {
27+
return Popup.getInstance(this.root) as Popup;
28+
}
29+
30+
public getTextArea(): TextAreaModel {
31+
return new TextAreaModel(
32+
this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorTextArea}`) as HTMLElement,
33+
);
34+
}
35+
36+
public getWrapperElement(): HTMLElement {
37+
return document.body.querySelector(`.${CLASSES.aiPromptEditor}.${CLASSES.wrapper}`) as HTMLElement;
38+
}
39+
40+
public getRefreshButton(): ButtonModel {
41+
return new ButtonModel(this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorRefreshButton}`) as HTMLElement);
42+
}
43+
44+
public getApplyButton(): ButtonModel {
45+
return new ButtonModel(this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorApplyButton}`) as HTMLElement);
46+
}
47+
48+
public getStopButton(): ButtonModel {
49+
return new ButtonModel(this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorStopButton}`) as HTMLElement);
50+
}
51+
52+
public getProgressBar(): ProgressBarModel {
53+
return new ProgressBarModel(
54+
this.getWrapperElement().querySelector(`.${CLASSES.aiPromptEditorProgressBar}`) as HTMLElement,
55+
);
56+
}
57+
58+
public isApplyToolbarItemVisible(): boolean {
59+
const applyButton = this.getApplyButton().getElement();
60+
const toolbarItem = applyButton.closest(`.${CLASSES.toolbarItem}`);
61+
62+
return !!toolbarItem && !toolbarItem.classList.contains(CLASSES.stateInvisible);
63+
}
64+
65+
public isStopToolbarItemVisible(): boolean {
66+
const stopButton = this.getStopButton().getElement();
67+
const toolbarItem = stopButton.closest(`.${CLASSES.toolbarItem}`);
68+
69+
return !!toolbarItem && !toolbarItem.classList.contains(CLASSES.stateInvisible);
70+
}
71+
}

packages/devextreme/js/__internal/grids/grid_core/ai_column/ai_column.integration.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const createDataGrid = async (
3535
const instance = new DataGrid($container.get(0) as HTMLDivElement, options);
3636
const component = new DataGridModel($container.get(0) as HTMLElement);
3737

38-
jest.runOnlyPendingTimers();
38+
jest.runAllTimers();
39+
3940
resolve({
4041
$container,
4142
component,
@@ -548,6 +549,7 @@ describe('aiIntegration', () => {
548549
return aiIntegrationResult();
549550
},
550551
});
552+
551553
const columnAiIntegration = new AIIntegration({
552554
sendRequest(): RequestResult {
553555
columnSendRequestSpy();
@@ -578,6 +580,7 @@ describe('aiIntegration', () => {
578580
expect(rootSendRequestSpy).toHaveBeenCalled();
579581
expect(columnSendRequestSpy).not.toHaveBeenCalled();
580582
});
583+
581584
it('should be taken from grid level if it set up (dynamic update)', async () => {
582585
const { instance } = await createDataGrid({
583586
dataSource: [
@@ -600,6 +603,7 @@ describe('aiIntegration', () => {
600603
expect(rootSendRequestSpy).toHaveBeenCalled();
601604
expect(columnSendRequestSpy).not.toHaveBeenCalled();
602605
});
606+
603607
it('should be taken from column level if it set up (first load)', async () => {
604608
const { instance } = await createDataGrid({
605609
dataSource: [
@@ -800,6 +804,7 @@ describe('aiMode', () => {
800804
const aiMode = instance.columnOption('myColumn', 'ai.mode');
801805
expect(aiMode).toBe('auto');
802806
});
807+
803808
it('should call aiIntegration.sendRequest with every visible rows change', async () => {
804809
const dataSource = Array.from({ length: 100 }, (_, i) => ({
805810
id: i + 1,

0 commit comments

Comments
 (0)