From f016eae997bf21faaaa1d1b4624c49062f3faedf Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 1 Apr 2026 17:07:58 +0400 Subject: [PATCH 1/8] feat: add AIChat class with Popup and Chat integration --- .../grids/grid_core/ai_chat/ai_chat.ts | 53 +++++++++++++++++++ .../grids/grid_core/ai_chat/const.ts | 11 ++++ .../grids/grid_core/ai_chat/types.ts | 16 ++++++ 3 files changed, 80 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts new file mode 100644 index 000000000000..0705555bcf12 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.ts @@ -0,0 +1,53 @@ +import $ from '@js/core/renderer'; +import type { Properties as ChatProperties } from '@js/ui/chat'; +import Chat from '@js/ui/chat'; +import type { Properties as PopupProperties } from '@js/ui/popup'; +import Popup from '@js/ui/popup'; + +import { CLASSES, DEFAULT_POPUP_OPTIONS } from './const'; +import type { AIChatOptions } from './types'; + +export class AIChat { + private readonly popupInstance: Popup; + + private chatInstance!: Chat; + + constructor( + private readonly options: AIChatOptions, + ) { + const { container, createComponent } = options; + + container.addClass(CLASSES.aiChat); + this.popupInstance = createComponent(container, Popup, this.getPopupConfig()); + } + + private getChatConfig(): ChatProperties { + return {}; + } + + private getPopupConfig(): PopupProperties { + return { + ...DEFAULT_POPUP_OPTIONS, + wrapperAttr: { class: `${CLASSES.aiChat} ${CLASSES.aiDialog}` }, + contentTemplate: ($container): void => { + const $editorContainer = $('
') + .addClass(CLASSES.aiChatContent) + .appendTo($container); + + this.chatInstance = this.options.createComponent( + $editorContainer, + Chat, + this.getChatConfig(), + ); + }, + }; + } + + public show(): Promise { + return this.popupInstance.show(); + } + + public hide(): Promise { + return this.popupInstance.hide(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts new file mode 100644 index 000000000000..14657ab88fd8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts @@ -0,0 +1,11 @@ +export const DEFAULT_POPUP_OPTIONS = { + width: 360, + height: 'auto', + visible: false, +}; + +export const CLASSES = { + aiChat: 'dx-ai-chat', + aiDialog: 'dx-ai-dialog', + aiChatContent: 'dx-ai-chat__content', +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts new file mode 100644 index 000000000000..ec50b55fb31a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/types.ts @@ -0,0 +1,16 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import type { Properties as ChatProperties } from '@js/ui/chat'; +import type { Properties as PopupProperties } from '@js/ui/popup'; + +import type { CreateComponent } from '../m_types'; + +export interface AIChatOptions { + container: dxElementWrapper; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createComponent: CreateComponent; + onMessageEntered?: () => void; + onChatCleared?: () => void; + onRegenerate?: () => void; + popupOptions?: PopupProperties; + chatOptions?: ChatProperties; +} From 32d3288e95642d5ca09e24746d6153ab1bc38813 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 1 Apr 2026 17:08:13 +0400 Subject: [PATCH 2/8] feat: add AIAssistantView and AIAssistantViewController with grid registration --- .../js/__internal/grids/data_grid/m_widget.ts | 1 + .../grids/data_grid/m_widget_base.ts | 3 +- .../module_not_extended/ai_assistant.ts | 13 ++++++++ .../ai_assistant/m_ai_assistant_view.ts | 30 +++++++++++++++++++ .../m_ai_assistant_view_controller.ts | 18 +++++++++++ .../js/__internal/grids/grid_core/m_types.ts | 2 ++ .../grids/grid_core/views/m_grid_view.ts | 1 + .../js/__internal/grids/tree_list/m_widget.ts | 1 + .../grids/tree_list/m_widget_base.ts | 1 + .../module_not_extended/ai_assistant.ts | 13 ++++++++ 10 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.ts create mode 100644 packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_widget.ts b/packages/devextreme/js/__internal/grids/data_grid/m_widget.ts index 6db581e934f1..c4aae3a92e8a 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_widget.ts @@ -34,5 +34,6 @@ import './export/m_export'; import './focus/m_focus'; import './module_not_extended/row_dragging'; import './module_not_extended/toast'; +import './module_not_extended/ai_assistant'; export default DataGrid; diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts index 1f89a28c8262..3a2726af2e44 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts @@ -43,6 +43,7 @@ gridCore.registerModulesOrder([ 'columnHeaders', 'filterRow', 'headerPanel', + 'aiAssistant', 'headerFilter', 'sorting', 'search', @@ -130,7 +131,7 @@ class DataGrid extends GridCoreWidget { gridCoreUtils.logHeaderFilterDeprecatedWarningIfNeed(that); // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + gridCore.processModules(that, gridCore as any); gridCore.callModuleItemsMethod(that, 'init'); diff --git a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts new file mode 100644 index 000000000000..d17168f3e673 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_assistant.ts @@ -0,0 +1,13 @@ +import { AIAssistantView } from '@ts/grids/grid_core/ai_assistant/m_ai_assistant_view'; +import { AIAssistantViewController } from '@ts/grids/grid_core/ai_assistant/m_ai_assistant_view_controller'; + +import gridCore from '../m_core'; + +gridCore.registerModule('aiAssistant', { + controllers: { + aiAssistant: AIAssistantViewController, + }, + views: { + aiAssistantView: AIAssistantView, + }, +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts new file mode 100644 index 000000000000..b00ec2a881ef --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts @@ -0,0 +1,30 @@ +import { AIChat } from '../ai_chat/ai_chat'; +import type { AIChatOptions } from '../ai_chat/types'; +import { View } from '../m_modules'; + +export class AIAssistantView extends View { + private aiChatInstance!: AIChat; + + private getAIPromptEditorConfig(): AIChatOptions { + return { + container: this.element(), + createComponent: this._createComponent.bind(this), + }; + } + + protected _renderCore(): void { + const config = this.getAIPromptEditorConfig(); + + if (!this.aiChatInstance) { + this.aiChatInstance = new AIChat(config); + } + } + + public show(): Promise { + return this.aiChatInstance?.show() ?? Promise.resolve(false); + } + + public hide(): Promise { + return this.aiChatInstance?.hide() ?? Promise.resolve(false); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.ts new file mode 100644 index 000000000000..dfb0e345b13b --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.ts @@ -0,0 +1,18 @@ +import { ViewController } from '../m_modules'; +import type { AIAssistantView } from './m_ai_assistant_view'; + +export class AIAssistantViewController extends ViewController { + private aiAssistantView!: AIAssistantView; + + public init(): void { + this.aiAssistantView = this.getView('aiAssistantView'); + } + + public show(): Promise { + return this.aiAssistantView.show(); + } + + public hide(): Promise { + return this.aiAssistantView.hide(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 605f88240eaa..b2311a123c62 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -205,6 +205,7 @@ export interface Controllers { toastViewController: import('./toast/m_toast_controller').ToastViewController; aiColumn: import('./ai_column/controllers/m_ai_column_controller').AIColumnController; aiPromptEditor: import('./ai_column/controllers/m_ai_prompt_editor_view_controller').AIPromptEditorViewController; + aiAssistant: import('./ai_assistant/m_ai_assistant_view_controller').AIAssistantViewController; } type ControllerTypes = { @@ -229,6 +230,7 @@ export interface Views { filterPanelView: import('./filter/m_filter_panel').FilterPanelView; toastView: import('./toast/m_toast_view').ToastView; aiPromptEditorView: import('./ai_column/views/m_ai_prompt_editor_view').AIPromptEditorView; + aiAssistantView: import('./ai_assistant/m_ai_assistant_view').AIAssistantView; } export interface EditingControllerRequired { diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 3240d317ebbd..1ef32df7ad38 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -52,6 +52,7 @@ const VIEW_NAMES = [ 'filterBuilderView', 'toastView', 'aiPromptEditorView', + 'aiAssistantView', ]; const E2E_ATTRIBUTES = { diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_widget.ts b/packages/devextreme/js/__internal/grids/tree_list/m_widget.ts index 574c60903f3c..81fbd8d7b797 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_widget.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_widget.ts @@ -30,5 +30,6 @@ import './module_not_extended/virtual_columns'; import './m_focus'; import './module_not_extended/row_dragging'; import './module_not_extended/toast'; +import './module_not_extended/ai_assistant'; export default TreeList; diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts index c9ec6a1a9716..c933bd272e7e 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_widget_base.ts @@ -38,6 +38,7 @@ treeListCore.registerModulesOrder([ 'columnHeaders', 'filterRow', 'headerPanel', + 'aiAssistant', 'headerFilter', 'sorting', 'search', diff --git a/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts new file mode 100644 index 000000000000..d17168f3e673 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_assistant.ts @@ -0,0 +1,13 @@ +import { AIAssistantView } from '@ts/grids/grid_core/ai_assistant/m_ai_assistant_view'; +import { AIAssistantViewController } from '@ts/grids/grid_core/ai_assistant/m_ai_assistant_view_controller'; + +import gridCore from '../m_core'; + +gridCore.registerModule('aiAssistant', { + controllers: { + aiAssistant: AIAssistantViewController, + }, + views: { + aiAssistantView: AIAssistantView, + }, +}); From e3a3badacc3ca9579ee5904c8eef9b4918d5b240 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 1 Apr 2026 17:08:27 +0400 Subject: [PATCH 3/8] test: add unit tests for AIChat, AIAssistantView, and AIAssistantViewController --- .../ai_assistant/m_ai_assistant_view.test.ts | 149 ++++++++++++++++++ .../m_ai_assistant_view_controller.test.ts | 79 ++++++++++ .../grids/grid_core/ai_chat/ai_chat.test.ts | 111 +++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.test.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts new file mode 100644 index 000000000000..ee2a760690d5 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts @@ -0,0 +1,149 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import fx from '@js/common/core/animation/fx'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import wrapInstanceWithMocks from '@ts/grids/grid_core/__tests__/__mock__/helpers/wrapInstance'; + +import { AIChat } from '../ai_chat/ai_chat'; +import { AIAssistantView } from './m_ai_assistant_view'; + +jest.mock('../ai_chat/ai_chat', (): any => { + const original = jest.requireActual('../ai_chat/ai_chat'); + + return { + ...original, + AIChat: jest.fn((...args: any[]) => { + const instance: AIChat = new original.AIChat(...args); + return wrapInstanceWithMocks(instance); + }), + }; +}); + +const createComponentMock = jest.fn(( + el: dxElementWrapper, + Widget: any, + options: any, +): any => new Widget(el, options)); + +const createAIAssistantView = (): { + $container: dxElementWrapper; + aiAssistantView: AIAssistantView; +} => { + const $container = $('
').appendTo(document.body); + const mockComponent = { + element: (): any => $container.get(0), + _createComponent: createComponentMock, + _controllers: {}, + }; + + const aiAssistantView = new AIAssistantView(mockComponent); + aiAssistantView.render($container); + + return { $container, aiAssistantView }; +}; + +const beforeTest = (): void => { + fx.off = true; + jest.useFakeTimers(); + jest.clearAllMocks(); +}; + +const afterTest = (): void => { + document.body.innerHTML = ''; + fx.off = false; + jest.useRealTimers(); +}; + +describe('AIAssistantView', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('initialization', () => { + it('should create AIChat instance on first render', () => { + createAIAssistantView(); + + expect(AIChat).toHaveBeenCalledTimes(1); + }); + + it('should pass container and createComponent to AIChat', () => { + const { aiAssistantView } = createAIAssistantView(); + + expect(AIChat).toHaveBeenCalledWith( + expect.objectContaining({ + container: aiAssistantView.element(), + createComponent: expect.any(Function), + }), + ); + }); + + it('should not create a new AIChat instance on subsequent renders', () => { + const { $container, aiAssistantView } = createAIAssistantView(); + + aiAssistantView.render($container); + + expect(AIChat).toHaveBeenCalledTimes(1); + }); + }); + + describe('show', () => { + it('should delegate to AIChat show method', async () => { + const { aiAssistantView } = createAIAssistantView(); + + await aiAssistantView.show(); + + const aiChatInstance = (AIChat as jest.Mock) + .mock.results[0].value as { show: jest.Mock; hide: jest.Mock }; + + expect(aiChatInstance.show).toHaveBeenCalledTimes(1); + }); + }); + + describe('hide', () => { + it('should delegate to AIChat hide method', async () => { + const { aiAssistantView } = createAIAssistantView(); + + await aiAssistantView.hide(); + + const aiChatInstance = (AIChat as jest.Mock) + .mock.results[0].value as { show: jest.Mock; hide: jest.Mock }; + + expect(aiChatInstance.hide).toHaveBeenCalledTimes(1); + }); + }); + + describe('show when not initialized', () => { + it('should return resolved false promise when aiChatInstance is not created', () => { + const mockComponent = { + element: (): any => $('
').get(0), + _createComponent: createComponentMock, + _controllers: {}, + }; + + const aiAssistantView = new AIAssistantView(mockComponent); + + return expect(aiAssistantView.show()).resolves.toBe(false); + }); + }); + + describe('hide when not initialized', () => { + it('should return resolved false promise when aiChatInstance is not created', () => { + const mockComponent = { + element: (): any => $('
').get(0), + _createComponent: createComponentMock, + _controllers: {}, + }; + + const aiAssistantView = new AIAssistantView(mockComponent); + + return expect(aiAssistantView.hide()).resolves.toBe(false); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.test.ts new file mode 100644 index 000000000000..1dcb7d198766 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view_controller.test.ts @@ -0,0 +1,79 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; + +import { AIAssistantViewController } from './m_ai_assistant_view_controller'; + +interface MockAIAssistantView { + show: jest.Mock<() => Promise>; + hide: jest.Mock<() => Promise>; +} + +const createMockAIAssistantView = (): MockAIAssistantView => ({ + show: jest.fn<() => Promise>().mockResolvedValue(true), + hide: jest.fn<() => Promise>().mockResolvedValue(true), +}); + +const createAIAssistantViewController = (): { + controller: AIAssistantViewController; + mockView: ReturnType; +} => { + const mockView = createMockAIAssistantView(); + const mockComponent = { + _views: { + aiAssistantView: mockView, + }, + _controllers: {}, + }; + + const controller = new AIAssistantViewController(mockComponent); + controller.init(); + + return { controller, mockView }; +}; + +const beforeTest = (): void => { + jest.clearAllMocks(); +}; + +describe('AIAssistantViewController', () => { + beforeEach(beforeTest); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('init', () => { + it('should get aiAssistantView reference', () => { + const { controller } = createAIAssistantViewController(); + + expect(controller).toBeDefined(); + }); + }); + + describe('show', () => { + it('should delegate to aiAssistantView show method', async () => { + const { controller, mockView } = createAIAssistantViewController(); + + const result = await controller.show(); + + expect(mockView.show).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + }); + + describe('hide', () => { + it('should delegate to aiAssistantView hide method', async () => { + const { controller, mockView } = createAIAssistantViewController(); + + const result = await controller.hide(); + + expect(mockView.hide).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts new file mode 100644 index 000000000000..319dc494630f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts @@ -0,0 +1,111 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import $ from '@js/core/renderer'; +import Chat from '@js/ui/chat'; +import Popup from '@js/ui/popup'; + +import { AIChat } from './ai_chat'; +import { CLASSES, DEFAULT_POPUP_OPTIONS } from './const'; +import type { AIChatOptions } from './types'; + +const mockPopupInstance = { + show: jest.fn<() => Promise>().mockResolvedValue(true), + hide: jest.fn<() => Promise>().mockResolvedValue(true), +}; + +const mockChatInstance = {}; + +const createComponentMock = jest.fn(( + _el: any, + Widget: any, +): any => { + if (Widget === Popup) { + return mockPopupInstance; + } + if (Widget === Chat) { + return mockChatInstance; + } + return {}; +}); + +const createAIChat = (optionsOverride: Partial = {}): { + $container: ReturnType; + aiChat: AIChat; +} => { + const $container = $('
').appendTo(document.body); + + const options: AIChatOptions = { + container: $container, + createComponent: createComponentMock as any, + ...optionsOverride, + }; + + const aiChat = new AIChat(options); + + return { $container, aiChat }; +}; + +const beforeTest = (): void => { + jest.clearAllMocks(); +}; + +const afterTest = (): void => { + document.body.innerHTML = ''; +}; + +describe('AIChat', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('initialization', () => { + it('should add ai chat class to container', () => { + const { $container } = createAIChat(); + + expect($container.hasClass(CLASSES.aiChat)).toBe(true); + }); + + it('should create popup instance via createComponent', () => { + createAIChat(); + + expect(createComponentMock).toHaveBeenCalledTimes(1); + expect(createComponentMock).toHaveBeenCalledWith( + expect.any(Object), + Popup, + expect.objectContaining({ + ...DEFAULT_POPUP_OPTIONS, + wrapperAttr: { class: `${CLASSES.aiChat} ${CLASSES.aiDialog}` }, + contentTemplate: expect.any(Function), + }), + ); + }); + }); + + describe('show', () => { + it('should call popup show method', async () => { + const { aiChat } = createAIChat(); + + const result = await aiChat.show(); + + expect(mockPopupInstance.show).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + }); + + describe('hide', () => { + it('should call popup hide method', async () => { + const { aiChat } = createAIChat(); + + const result = await aiChat.hide(); + + expect(mockPopupInstance.hide).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + }); +}); From e7d00da986e531f729910cb20e28e14e62943a06 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 1 Apr 2026 18:40:58 +0400 Subject: [PATCH 4/8] Fix themebuilder tests --- packages/devextreme-themebuilder/tests/data/dependencies.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 8e1319ccd981..634355fcd57b 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -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', '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'], + 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'], 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', 'textarea'], + 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'], 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', 'toast', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist', 'progressbar', 'textarea', 'buttongroup', 'dropdownbutton'], + 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'], }; From 5ed6170d738aa56aa59009caf13ee121821f2728 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 1 Apr 2026 18:59:22 +0400 Subject: [PATCH 5/8] Fix copilot comments --- .../devextreme/js/__internal/grids/data_grid/m_widget_base.ts | 1 - .../grids/grid_core/ai_assistant/m_ai_assistant_view.ts | 4 ++-- .../devextreme/js/__internal/grids/grid_core/ai_chat/const.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts index 3a2726af2e44..1710af383abd 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_widget_base.ts @@ -131,7 +131,6 @@ class DataGrid extends GridCoreWidget { gridCoreUtils.logHeaderFilterDeprecatedWarningIfNeed(that); // @ts-expect-error - gridCore.processModules(that, gridCore as any); gridCore.callModuleItemsMethod(that, 'init'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts index b00ec2a881ef..c0a9b6a4b29c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts @@ -5,7 +5,7 @@ import { View } from '../m_modules'; export class AIAssistantView extends View { private aiChatInstance!: AIChat; - private getAIPromptEditorConfig(): AIChatOptions { + private getAIChatConfig(): AIChatOptions { return { container: this.element(), createComponent: this._createComponent.bind(this), @@ -13,7 +13,7 @@ export class AIAssistantView extends View { } protected _renderCore(): void { - const config = this.getAIPromptEditorConfig(); + const config = this.getAIChatConfig(); if (!this.aiChatInstance) { this.aiChatInstance = new AIChat(config); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts index 14657ab88fd8..b775aa9570c5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_chat/const.ts @@ -6,6 +6,6 @@ export const DEFAULT_POPUP_OPTIONS = { export const CLASSES = { aiChat: 'dx-ai-chat', - aiDialog: 'dx-ai-dialog', + aiDialog: 'dx-aidialog', aiChatContent: 'dx-ai-chat__content', }; From f4437d0acef9fe48145305aa26957726a5476b59 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 1 Apr 2026 21:39:04 +0400 Subject: [PATCH 6/8] feat: add aiAssistant visibility to AIAssistantView --- .../ai_assistant/m_ai_assistant_view.test.ts | 79 +++++++++++++++---- .../ai_assistant/m_ai_assistant_view.ts | 4 + 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts index ee2a760690d5..92a7c8afd9cb 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts @@ -33,21 +33,47 @@ const createComponentMock = jest.fn(( options: any, ): any => new Widget(el, options)); -const createAIAssistantView = (): { +const createAIAssistantView = ({ + initialVisible = true, + render = true, +}: { + initialVisible?: boolean; + render?: boolean; +} = {}): { $container: dxElementWrapper; aiAssistantView: AIAssistantView; + optionMock: jest.Mock<(name: string) => boolean | undefined>; + setVisible: (value: boolean) => void; } => { const $container = $('
').appendTo(document.body); + let isVisible = initialVisible; + const optionMock = jest.fn((name: string): boolean | undefined => { + if (name === 'aiAssistant.visible') { + return isVisible; + } + + return undefined; + }); const mockComponent = { element: (): any => $container.get(0), _createComponent: createComponentMock, _controllers: {}, + option: optionMock, }; const aiAssistantView = new AIAssistantView(mockComponent); - aiAssistantView.render($container); + if (render) { + aiAssistantView.render($container); + } - return { $container, aiAssistantView }; + return { + $container, + aiAssistantView, + optionMock, + setVisible: (value: boolean) => { + isVisible = value; + }, + }; }; const beforeTest = (): void => { @@ -66,6 +92,20 @@ describe('AIAssistantView', () => { beforeEach(beforeTest); afterEach(afterTest); + describe('isVisible', () => { + it('should return aiAssistant.visible option value', () => { + const { aiAssistantView, optionMock, setVisible } = createAIAssistantView({ render: false }); + + expect(aiAssistantView.isVisible()).toBe(true); + + setVisible(false); + + expect(aiAssistantView.isVisible()).toBe(false); + expect(optionMock).toHaveBeenNthCalledWith(1, 'aiAssistant.visible'); + expect(optionMock).toHaveBeenNthCalledWith(2, 'aiAssistant.visible'); + }); + }); + describe('initialization', () => { it('should create AIChat instance on first render', () => { createAIAssistantView(); @@ -91,6 +131,23 @@ describe('AIAssistantView', () => { expect(AIChat).toHaveBeenCalledTimes(1); }); + + it('should not create AIChat instance when view is hidden', () => { + const { aiAssistantView } = createAIAssistantView({ initialVisible: false }); + + expect(AIChat).not.toHaveBeenCalled(); + expect(aiAssistantView.element().hasClass('dx-hidden')).toBe(true); + }); + + it('should create AIChat instance when view becomes visible', () => { + const { $container, aiAssistantView, setVisible } = createAIAssistantView({ initialVisible: false }); + + setVisible(true); + aiAssistantView.render($container); + + expect(AIChat).toHaveBeenCalledTimes(1); + expect(aiAssistantView.element().hasClass('dx-hidden')).toBe(false); + }); }); describe('show', () => { @@ -121,13 +178,7 @@ describe('AIAssistantView', () => { describe('show when not initialized', () => { it('should return resolved false promise when aiChatInstance is not created', () => { - const mockComponent = { - element: (): any => $('
').get(0), - _createComponent: createComponentMock, - _controllers: {}, - }; - - const aiAssistantView = new AIAssistantView(mockComponent); + const { aiAssistantView } = createAIAssistantView({ render: false }); return expect(aiAssistantView.show()).resolves.toBe(false); }); @@ -135,13 +186,7 @@ describe('AIAssistantView', () => { describe('hide when not initialized', () => { it('should return resolved false promise when aiChatInstance is not created', () => { - const mockComponent = { - element: (): any => $('
').get(0), - _createComponent: createComponentMock, - _controllers: {}, - }; - - const aiAssistantView = new AIAssistantView(mockComponent); + const { aiAssistantView } = createAIAssistantView({ render: false }); return expect(aiAssistantView.hide()).resolves.toBe(false); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts index c0a9b6a4b29c..c7af649400ed 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts @@ -20,6 +20,10 @@ export class AIAssistantView extends View { } } + public isVisible(): boolean { + return this.option('aiAssistant.visible'); + } + public show(): Promise { return this.aiChatInstance?.show() ?? Promise.resolve(false); } From 33a10f95a5de07c8e1203c227ff67266ddb0695b Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 2 Apr 2026 12:03:49 +0400 Subject: [PATCH 7/8] fix: use aiAssistant enabled option in AIAssistantView --- .../grids/grid_core/ai_assistant/m_ai_assistant_view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts index c7af649400ed..4e19e58f0425 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.ts @@ -21,7 +21,7 @@ export class AIAssistantView extends View { } public isVisible(): boolean { - return this.option('aiAssistant.visible'); + return this.option('aiAssistant.enabled'); } public show(): Promise { From 122434ac113293fb680c218302a6a106ea7433fd Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 2 Apr 2026 13:24:33 +0400 Subject: [PATCH 8/8] Fix jest tests --- .../ai_assistant/m_ai_assistant_view.test.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts index 92a7c8afd9cb..1ea0905daaaf 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_assistant/m_ai_assistant_view.test.ts @@ -34,22 +34,22 @@ const createComponentMock = jest.fn(( ): any => new Widget(el, options)); const createAIAssistantView = ({ - initialVisible = true, + initialEnabled = true, render = true, }: { - initialVisible?: boolean; + initialEnabled?: boolean; render?: boolean; } = {}): { $container: dxElementWrapper; aiAssistantView: AIAssistantView; optionMock: jest.Mock<(name: string) => boolean | undefined>; - setVisible: (value: boolean) => void; + setEnabled: (value: boolean) => void; } => { const $container = $('
').appendTo(document.body); - let isVisible = initialVisible; + let isEnabled = initialEnabled; const optionMock = jest.fn((name: string): boolean | undefined => { - if (name === 'aiAssistant.visible') { - return isVisible; + if (name === 'aiAssistant.enabled') { + return isEnabled; } return undefined; @@ -70,8 +70,8 @@ const createAIAssistantView = ({ $container, aiAssistantView, optionMock, - setVisible: (value: boolean) => { - isVisible = value; + setEnabled: (value: boolean): void => { + isEnabled = value; }, }; }; @@ -93,16 +93,16 @@ describe('AIAssistantView', () => { afterEach(afterTest); describe('isVisible', () => { - it('should return aiAssistant.visible option value', () => { - const { aiAssistantView, optionMock, setVisible } = createAIAssistantView({ render: false }); + it('should return aiAssistant.enabled option value', () => { + const { aiAssistantView, optionMock, setEnabled } = createAIAssistantView({ render: false }); expect(aiAssistantView.isVisible()).toBe(true); - setVisible(false); + setEnabled(false); expect(aiAssistantView.isVisible()).toBe(false); - expect(optionMock).toHaveBeenNthCalledWith(1, 'aiAssistant.visible'); - expect(optionMock).toHaveBeenNthCalledWith(2, 'aiAssistant.visible'); + expect(optionMock).toHaveBeenNthCalledWith(1, 'aiAssistant.enabled'); + expect(optionMock).toHaveBeenNthCalledWith(2, 'aiAssistant.enabled'); }); }); @@ -132,17 +132,19 @@ describe('AIAssistantView', () => { expect(AIChat).toHaveBeenCalledTimes(1); }); - it('should not create AIChat instance when view is hidden', () => { - const { aiAssistantView } = createAIAssistantView({ initialVisible: false }); + it('should not create AIChat instance when aiAssistant is disabled', () => { + const { aiAssistantView } = createAIAssistantView({ initialEnabled: false }); expect(AIChat).not.toHaveBeenCalled(); expect(aiAssistantView.element().hasClass('dx-hidden')).toBe(true); }); - it('should create AIChat instance when view becomes visible', () => { - const { $container, aiAssistantView, setVisible } = createAIAssistantView({ initialVisible: false }); + it('should create AIChat instance when aiAssistant becomes enabled', () => { + const { $container, aiAssistantView, setEnabled } = createAIAssistantView({ + initialEnabled: false, + }); - setVisible(true); + setEnabled(true); aiAssistantView.render($container); expect(AIChat).toHaveBeenCalledTimes(1);