Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
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', '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'],
Comment thread
Alyar666 marked this conversation as resolved.
pivotgrid: ['validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'popup', 'loadpanel', 'checkbox', 'treeview', 'scrollview', 'list'],
Comment thread
Alyar666 marked this conversation as resolved.
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'],
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ gridCore.registerModulesOrder([
'columnHeaders',
'filterRow',
'headerPanel',
'aiAssistant',
'headerFilter',
'sorting',
'search',
Expand Down Expand Up @@ -130,7 +131,6 @@ class DataGrid extends GridCoreWidget<Properties> {
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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/* 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<any>('../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 = ({
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 = $('<div>').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);
if (render) {
aiAssistantView.render($container);
}

return {
$container,
aiAssistantView,
optionMock,
setVisible: (value: boolean) => {
isVisible = value;
},
};
};

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('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();

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);
});

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', () => {
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 { aiAssistantView } = createAIAssistantView({ render: false });

return expect(aiAssistantView.show()).resolves.toBe(false);
});
});

describe('hide when not initialized', () => {
it('should return resolved false promise when aiChatInstance is not created', () => {
const { aiAssistantView } = createAIAssistantView({ render: false });

return expect(aiAssistantView.hide()).resolves.toBe(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;
Comment thread
Alyar666 marked this conversation as resolved.

private getAIChatConfig(): AIChatOptions {
return {
container: this.element(),
createComponent: this._createComponent.bind(this),
};
}

protected _renderCore(): void {
const config = this.getAIChatConfig();

if (!this.aiChatInstance) {
this.aiChatInstance = new AIChat(config);
}
}

public isVisible(): boolean {
return this.option('aiAssistant.enabled');
}

public show(): Promise<boolean> {
return this.aiChatInstance?.show() ?? Promise.resolve(false);
}

public hide(): Promise<boolean> {
return this.aiChatInstance?.hide() ?? Promise.resolve(false);
}
}
Original file line number Diff line number Diff line change
@@ -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<boolean>>;
hide: jest.Mock<() => Promise<boolean>>;
}

const createMockAIAssistantView = (): MockAIAssistantView => ({
show: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
hide: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
});

const createAIAssistantViewController = (): {
controller: AIAssistantViewController;
mockView: ReturnType<typeof createMockAIAssistantView>;
} => {
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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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<boolean> {
return this.aiAssistantView.show();
}

public hide(): Promise<boolean> {
return this.aiAssistantView.hide();
}
}
Loading
Loading