Skip to content

Commit 1710f03

Browse files
authored
DataGrid/TreeList - AI Assistant: Create skeletons for classes and register new modules (#33121)
Co-authored-by: Alyar <>
1 parent 56beee9 commit 1710f03

17 files changed

Lines changed: 554 additions & 4 deletions

File tree

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', 'toast', 'contextmenu', 'scrollview', 'popup', 'progressbar', 'toolbar', 'checkbox', 'treeview', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable', 'textarea'],
60+
datagrid: ['loadindicator', 'loadpanel', 'validation', 'button', 'textbox', 'toast', 'contextmenu', 'scrollview', 'popup', 'progressbar', 'toolbar', 'checkbox', 'treeview', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable', 'textarea', 'chat', 'speechtotext'],
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', 'textarea'],
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', 'chat', 'speechtotext'],
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', 'toast', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist', 'progressbar', 'textarea', 'buttongroup', 'dropdownbutton'],
66+
gantt: ['loadindicator', 'loadpanel', 'validation', 'button', 'popup', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'toast', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist', 'progressbar', 'textarea', 'buttongroup', 'dropdownbutton', 'chat', 'speechtotext'],
6767
};

packages/devextreme/js/__internal/grids/data_grid/m_widget.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ import './export/m_export';
3434
import './focus/m_focus';
3535
import './module_not_extended/row_dragging';
3636
import './module_not_extended/toast';
37+
import './module_not_extended/ai_assistant';
3738

3839
export default DataGrid;

packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ gridCore.registerModulesOrder([
4343
'columnHeaders',
4444
'filterRow',
4545
'headerPanel',
46+
'aiAssistant',
4647
'headerFilter',
4748
'sorting',
4849
'search',
@@ -130,7 +131,6 @@ class DataGrid extends GridCoreWidget<Properties> {
130131
gridCoreUtils.logHeaderFilterDeprecatedWarningIfNeed(that);
131132

132133
// @ts-expect-error
133-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
134134
gridCore.processModules(that, gridCore as any);
135135

136136
gridCore.callModuleItemsMethod(that, 'init');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { AIAssistantView } from '@ts/grids/grid_core/ai_assistant/m_ai_assistant_view';
2+
import { AIAssistantViewController } from '@ts/grids/grid_core/ai_assistant/m_ai_assistant_view_controller';
3+
4+
import gridCore from '../m_core';
5+
6+
gridCore.registerModule('aiAssistant', {
7+
controllers: {
8+
aiAssistant: AIAssistantViewController,
9+
},
10+
views: {
11+
aiAssistantView: AIAssistantView,
12+
},
13+
});
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import {
3+
afterEach,
4+
beforeEach,
5+
describe,
6+
expect,
7+
it,
8+
jest,
9+
} from '@jest/globals';
10+
import fx from '@js/common/core/animation/fx';
11+
import type { dxElementWrapper } from '@js/core/renderer';
12+
import $ from '@js/core/renderer';
13+
import wrapInstanceWithMocks from '@ts/grids/grid_core/__tests__/__mock__/helpers/wrapInstance';
14+
15+
import { AIChat } from '../ai_chat/ai_chat';
16+
import { AIAssistantView } from './m_ai_assistant_view';
17+
18+
jest.mock('../ai_chat/ai_chat', (): any => {
19+
const original = jest.requireActual<any>('../ai_chat/ai_chat');
20+
21+
return {
22+
...original,
23+
AIChat: jest.fn((...args: any[]) => {
24+
const instance: AIChat = new original.AIChat(...args);
25+
return wrapInstanceWithMocks(instance);
26+
}),
27+
};
28+
});
29+
30+
const createComponentMock = jest.fn((
31+
el: dxElementWrapper,
32+
Widget: any,
33+
options: any,
34+
): any => new Widget(el, options));
35+
36+
const createAIAssistantView = ({
37+
initialEnabled = true,
38+
render = true,
39+
}: {
40+
initialEnabled?: boolean;
41+
render?: boolean;
42+
} = {}): {
43+
$container: dxElementWrapper;
44+
aiAssistantView: AIAssistantView;
45+
optionMock: jest.Mock<(name: string) => boolean | undefined>;
46+
setEnabled: (value: boolean) => void;
47+
} => {
48+
const $container = $('<div>').appendTo(document.body);
49+
let isEnabled = initialEnabled;
50+
const optionMock = jest.fn((name: string): boolean | undefined => {
51+
if (name === 'aiAssistant.enabled') {
52+
return isEnabled;
53+
}
54+
55+
return undefined;
56+
});
57+
const mockComponent = {
58+
element: (): any => $container.get(0),
59+
_createComponent: createComponentMock,
60+
_controllers: {},
61+
option: optionMock,
62+
};
63+
64+
const aiAssistantView = new AIAssistantView(mockComponent);
65+
if (render) {
66+
aiAssistantView.render($container);
67+
}
68+
69+
return {
70+
$container,
71+
aiAssistantView,
72+
optionMock,
73+
setEnabled: (value: boolean): void => {
74+
isEnabled = value;
75+
},
76+
};
77+
};
78+
79+
const beforeTest = (): void => {
80+
fx.off = true;
81+
jest.useFakeTimers();
82+
jest.clearAllMocks();
83+
};
84+
85+
const afterTest = (): void => {
86+
document.body.innerHTML = '';
87+
fx.off = false;
88+
jest.useRealTimers();
89+
};
90+
91+
describe('AIAssistantView', () => {
92+
beforeEach(beforeTest);
93+
afterEach(afterTest);
94+
95+
describe('isVisible', () => {
96+
it('should return aiAssistant.enabled option value', () => {
97+
const { aiAssistantView, optionMock, setEnabled } = createAIAssistantView({ render: false });
98+
99+
expect(aiAssistantView.isVisible()).toBe(true);
100+
101+
setEnabled(false);
102+
103+
expect(aiAssistantView.isVisible()).toBe(false);
104+
expect(optionMock).toHaveBeenNthCalledWith(1, 'aiAssistant.enabled');
105+
expect(optionMock).toHaveBeenNthCalledWith(2, 'aiAssistant.enabled');
106+
});
107+
});
108+
109+
describe('initialization', () => {
110+
it('should create AIChat instance on first render', () => {
111+
createAIAssistantView();
112+
113+
expect(AIChat).toHaveBeenCalledTimes(1);
114+
});
115+
116+
it('should pass container and createComponent to AIChat', () => {
117+
const { aiAssistantView } = createAIAssistantView();
118+
119+
expect(AIChat).toHaveBeenCalledWith(
120+
expect.objectContaining({
121+
container: aiAssistantView.element(),
122+
createComponent: expect.any(Function),
123+
}),
124+
);
125+
});
126+
127+
it('should not create a new AIChat instance on subsequent renders', () => {
128+
const { $container, aiAssistantView } = createAIAssistantView();
129+
130+
aiAssistantView.render($container);
131+
132+
expect(AIChat).toHaveBeenCalledTimes(1);
133+
});
134+
135+
it('should not create AIChat instance when aiAssistant is disabled', () => {
136+
const { aiAssistantView } = createAIAssistantView({ initialEnabled: false });
137+
138+
expect(AIChat).not.toHaveBeenCalled();
139+
expect(aiAssistantView.element().hasClass('dx-hidden')).toBe(true);
140+
});
141+
142+
it('should create AIChat instance when aiAssistant becomes enabled', () => {
143+
const { $container, aiAssistantView, setEnabled } = createAIAssistantView({
144+
initialEnabled: false,
145+
});
146+
147+
setEnabled(true);
148+
aiAssistantView.render($container);
149+
150+
expect(AIChat).toHaveBeenCalledTimes(1);
151+
expect(aiAssistantView.element().hasClass('dx-hidden')).toBe(false);
152+
});
153+
});
154+
155+
describe('show', () => {
156+
it('should delegate to AIChat show method', async () => {
157+
const { aiAssistantView } = createAIAssistantView();
158+
159+
await aiAssistantView.show();
160+
161+
const aiChatInstance = (AIChat as jest.Mock)
162+
.mock.results[0].value as { show: jest.Mock; hide: jest.Mock };
163+
164+
expect(aiChatInstance.show).toHaveBeenCalledTimes(1);
165+
});
166+
});
167+
168+
describe('hide', () => {
169+
it('should delegate to AIChat hide method', async () => {
170+
const { aiAssistantView } = createAIAssistantView();
171+
172+
await aiAssistantView.hide();
173+
174+
const aiChatInstance = (AIChat as jest.Mock)
175+
.mock.results[0].value as { show: jest.Mock; hide: jest.Mock };
176+
177+
expect(aiChatInstance.hide).toHaveBeenCalledTimes(1);
178+
});
179+
});
180+
181+
describe('show when not initialized', () => {
182+
it('should return resolved false promise when aiChatInstance is not created', () => {
183+
const { aiAssistantView } = createAIAssistantView({ render: false });
184+
185+
return expect(aiAssistantView.show()).resolves.toBe(false);
186+
});
187+
});
188+
189+
describe('hide when not initialized', () => {
190+
it('should return resolved false promise when aiChatInstance is not created', () => {
191+
const { aiAssistantView } = createAIAssistantView({ render: false });
192+
193+
return expect(aiAssistantView.hide()).resolves.toBe(false);
194+
});
195+
});
196+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { AIChat } from '../ai_chat/ai_chat';
2+
import type { AIChatOptions } from '../ai_chat/types';
3+
import { View } from '../m_modules';
4+
5+
export class AIAssistantView extends View {
6+
private aiChatInstance!: AIChat;
7+
8+
private getAIChatConfig(): AIChatOptions {
9+
return {
10+
container: this.element(),
11+
createComponent: this._createComponent.bind(this),
12+
};
13+
}
14+
15+
protected _renderCore(): void {
16+
const config = this.getAIChatConfig();
17+
18+
if (!this.aiChatInstance) {
19+
this.aiChatInstance = new AIChat(config);
20+
}
21+
}
22+
23+
public isVisible(): boolean {
24+
return this.option('aiAssistant.enabled');
25+
}
26+
27+
public show(): Promise<boolean> {
28+
return this.aiChatInstance?.show() ?? Promise.resolve(false);
29+
}
30+
31+
public hide(): Promise<boolean> {
32+
return this.aiChatInstance?.hide() ?? Promise.resolve(false);
33+
}
34+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
afterEach,
3+
beforeEach,
4+
describe,
5+
expect,
6+
it,
7+
jest,
8+
} from '@jest/globals';
9+
10+
import { AIAssistantViewController } from './m_ai_assistant_view_controller';
11+
12+
interface MockAIAssistantView {
13+
show: jest.Mock<() => Promise<boolean>>;
14+
hide: jest.Mock<() => Promise<boolean>>;
15+
}
16+
17+
const createMockAIAssistantView = (): MockAIAssistantView => ({
18+
show: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
19+
hide: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
20+
});
21+
22+
const createAIAssistantViewController = (): {
23+
controller: AIAssistantViewController;
24+
mockView: ReturnType<typeof createMockAIAssistantView>;
25+
} => {
26+
const mockView = createMockAIAssistantView();
27+
const mockComponent = {
28+
_views: {
29+
aiAssistantView: mockView,
30+
},
31+
_controllers: {},
32+
};
33+
34+
const controller = new AIAssistantViewController(mockComponent);
35+
controller.init();
36+
37+
return { controller, mockView };
38+
};
39+
40+
const beforeTest = (): void => {
41+
jest.clearAllMocks();
42+
};
43+
44+
describe('AIAssistantViewController', () => {
45+
beforeEach(beforeTest);
46+
afterEach(() => {
47+
jest.clearAllMocks();
48+
});
49+
50+
describe('init', () => {
51+
it('should get aiAssistantView reference', () => {
52+
const { controller } = createAIAssistantViewController();
53+
54+
expect(controller).toBeDefined();
55+
});
56+
});
57+
58+
describe('show', () => {
59+
it('should delegate to aiAssistantView show method', async () => {
60+
const { controller, mockView } = createAIAssistantViewController();
61+
62+
const result = await controller.show();
63+
64+
expect(mockView.show).toHaveBeenCalledTimes(1);
65+
expect(result).toBe(true);
66+
});
67+
});
68+
69+
describe('hide', () => {
70+
it('should delegate to aiAssistantView hide method', async () => {
71+
const { controller, mockView } = createAIAssistantViewController();
72+
73+
const result = await controller.hide();
74+
75+
expect(mockView.hide).toHaveBeenCalledTimes(1);
76+
expect(result).toBe(true);
77+
});
78+
});
79+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ViewController } from '../m_modules';
2+
import type { AIAssistantView } from './m_ai_assistant_view';
3+
4+
export class AIAssistantViewController extends ViewController {
5+
private aiAssistantView!: AIAssistantView;
6+
7+
public init(): void {
8+
this.aiAssistantView = this.getView('aiAssistantView');
9+
}
10+
11+
public show(): Promise<boolean> {
12+
return this.aiAssistantView.show();
13+
}
14+
15+
public hide(): Promise<boolean> {
16+
return this.aiAssistantView.hide();
17+
}
18+
}

0 commit comments

Comments
 (0)