Skip to content

Commit 45124c6

Browse files
committed
set ai assistant button to active state when popup visible, refactor
1 parent 94a38e7 commit 45124c6

File tree

8 files changed

+210
-109
lines changed

8 files changed

+210
-109
lines changed

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view.test.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import $ from '@js/core/renderer';
1313
import wrapInstanceWithMocks from '@ts/grids/grid_core/__tests__/__mock__/helpers/wrapInstance';
1414

1515
import { AIChat } from '../../ai_chat/ai_chat';
16+
import type { AIChatOptions } from '../../ai_chat/types';
1617
import { AIAssistantView } from '../ai_assistant_view';
1718

1819
jest.mock('../../ai_chat/ai_chat', (): any => {
@@ -152,45 +153,61 @@ describe('AIAssistantView', () => {
152153
});
153154
});
154155

155-
describe('show', () => {
156-
it('should delegate to AIChat show method', async () => {
156+
describe('toggle', () => {
157+
it('should delegate to AIChat toggle method', async () => {
157158
const { aiAssistantView } = createAIAssistantView();
158159

159-
await aiAssistantView.show();
160+
await aiAssistantView.toggle();
160161

161162
const aiChatInstance = (AIChat as jest.Mock)
162-
.mock.results[0].value as { show: jest.Mock; hide: jest.Mock };
163+
.mock.results[0].value as { toggle: jest.Mock };
163164

164-
expect(aiChatInstance.show).toHaveBeenCalledTimes(1);
165+
expect(aiChatInstance.toggle).toHaveBeenCalledTimes(1);
166+
});
167+
168+
it('should return resolved false promise when aiChatInstance is not created', () => {
169+
const { aiAssistantView } = createAIAssistantView({ render: false });
170+
171+
return expect(aiAssistantView.toggle()).resolves.toBe(false);
165172
});
166173
});
167174

168-
describe('hide', () => {
169-
it('should delegate to AIChat hide method', async () => {
175+
describe('isShown', () => {
176+
it('should delegate to AIChat isShown method', () => {
170177
const { aiAssistantView } = createAIAssistantView();
171178

172-
await aiAssistantView.hide();
173-
174179
const aiChatInstance = (AIChat as jest.Mock)
175-
.mock.results[0].value as { show: jest.Mock; hide: jest.Mock };
180+
.mock.results[0].value as { isShown: jest.Mock };
176181

177-
expect(aiChatInstance.hide).toHaveBeenCalledTimes(1);
182+
aiChatInstance.isShown.mockReturnValue(true);
183+
expect(aiAssistantView.isShown()).toBe(true);
184+
185+
aiChatInstance.isShown.mockReturnValue(false);
186+
expect(aiAssistantView.isShown()).toBe(false);
178187
});
179-
});
180188

181-
describe('show when not initialized', () => {
182-
it('should return resolved false promise when aiChatInstance is not created', () => {
189+
it('should return false when aiChatInstance is not created', () => {
183190
const { aiAssistantView } = createAIAssistantView({ render: false });
184191

185-
return expect(aiAssistantView.show()).resolves.toBe(false);
192+
expect(aiAssistantView.isShown()).toBe(false);
186193
});
187194
});
188195

189-
describe('hide when not initialized', () => {
190-
it('should return resolved false promise when aiChatInstance is not created', () => {
191-
const { aiAssistantView } = createAIAssistantView({ render: false });
196+
describe('onVisibilityChanged', () => {
197+
it('should fire onVisibilityChanged callback when popup visibility changes', () => {
198+
const { aiAssistantView } = createAIAssistantView();
199+
const callback = jest.fn();
200+
201+
aiAssistantView.onVisibilityChanged = callback;
202+
203+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
204+
aiChatConfig.onVisibilityChanged?.(true);
205+
206+
expect(callback).toHaveBeenCalledWith(true);
207+
208+
aiChatConfig.onVisibilityChanged?.(false);
192209

193-
return expect(aiAssistantView.hide()).resolves.toBe(false);
210+
expect(callback).toHaveBeenCalledWith(false);
194211
});
195212
});
196213
});

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/__tests__/ai_assistant_view_controller.integration.test.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -143,51 +143,4 @@ describe('AIAssistantViewController', () => {
143143
expect(button?.getAttribute('title')).toBe('My Custom Title');
144144
});
145145
});
146-
147-
describe('show / hide / toggle', () => {
148-
it('should delegate show to aiAssistantView', async () => {
149-
const { instance } = await createDataGrid({
150-
dataSource: [{ id: 1 }],
151-
aiAssistant: { enabled: true, title: 'AI Assistant' },
152-
});
153-
154-
const controller = instance.getController('aiAssistant');
155-
const view = (controller as any).aiAssistantView;
156-
const showSpy = jest.spyOn(view, 'show');
157-
158-
await controller.show();
159-
160-
expect(showSpy).toHaveBeenCalledTimes(1);
161-
});
162-
163-
it('should delegate hide to aiAssistantView', async () => {
164-
const { instance } = await createDataGrid({
165-
dataSource: [{ id: 1 }],
166-
aiAssistant: { enabled: true, title: 'AI Assistant' },
167-
});
168-
169-
const controller = instance.getController('aiAssistant');
170-
const view = (controller as any).aiAssistantView;
171-
const hideSpy = jest.spyOn(view, 'hide');
172-
173-
await controller.hide();
174-
175-
expect(hideSpy).toHaveBeenCalledTimes(1);
176-
});
177-
178-
it('should delegate toggle to aiAssistantView', async () => {
179-
const { instance } = await createDataGrid({
180-
dataSource: [{ id: 1 }],
181-
aiAssistant: { enabled: true, title: 'AI Assistant' },
182-
});
183-
184-
const controller = instance.getController('aiAssistant');
185-
const view = (controller as any).aiAssistantView;
186-
const toggleSpy = jest.spyOn(view, 'toggle');
187-
188-
await controller.toggle();
189-
190-
expect(toggleSpy).toHaveBeenCalledTimes(1);
191-
});
192-
});
193146
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
afterEach,
3+
beforeEach,
4+
describe,
5+
expect,
6+
it,
7+
jest,
8+
} from '@jest/globals';
9+
10+
import { AIAssistantViewController } from '../ai_assistant_view_controller';
11+
12+
interface MockAIAssistantView {
13+
toggle: jest.Mock<() => Promise<boolean>>;
14+
isShown: jest.Mock<() => boolean>;
15+
onVisibilityChanged?: (visible: boolean) => void;
16+
}
17+
18+
interface MockHeaderPanel {
19+
registerToolbarItem: jest.Mock;
20+
applyToolbarItem: jest.Mock;
21+
removeToolbarItem: jest.Mock;
22+
}
23+
24+
const createMockAIAssistantView = (): MockAIAssistantView => ({
25+
toggle: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
26+
isShown: jest.fn<() => boolean>().mockReturnValue(false),
27+
});
28+
29+
const createMockHeaderPanel = (): MockHeaderPanel => ({
30+
registerToolbarItem: jest.fn(),
31+
applyToolbarItem: jest.fn(),
32+
removeToolbarItem: jest.fn(),
33+
});
34+
35+
const createAIAssistantViewController = (
36+
options: Record<string, unknown> = {},
37+
): {
38+
controller: AIAssistantViewController;
39+
mockView: MockAIAssistantView;
40+
mockHeaderPanel: MockHeaderPanel;
41+
} => {
42+
const mockView = createMockAIAssistantView();
43+
const mockHeaderPanel = createMockHeaderPanel();
44+
45+
const mockComponent = {
46+
NAME: 'dxDataGrid',
47+
_views: {
48+
aiAssistantView: mockView,
49+
headerPanel: mockHeaderPanel,
50+
},
51+
_controllers: {},
52+
option(name?: string) {
53+
if (name !== undefined) {
54+
return options[name];
55+
}
56+
return options;
57+
},
58+
};
59+
60+
const controller = new AIAssistantViewController(mockComponent);
61+
controller.init();
62+
63+
return { controller, mockView, mockHeaderPanel };
64+
};
65+
66+
describe('AIAssistantViewController', () => {
67+
beforeEach(() => {
68+
jest.clearAllMocks();
69+
});
70+
afterEach(() => {
71+
jest.clearAllMocks();
72+
});
73+
74+
describe('init', () => {
75+
it('should get aiAssistantView reference', () => {
76+
const { controller } = createAIAssistantViewController();
77+
78+
expect(controller).toBeDefined();
79+
});
80+
});
81+
82+
describe('toggle', () => {
83+
it('should delegate to aiAssistantView toggle', async () => {
84+
const { controller, mockView } = createAIAssistantViewController({
85+
'aiAssistant.enabled': true,
86+
});
87+
88+
await controller.toggle();
89+
90+
expect(mockView.toggle).toHaveBeenCalledTimes(1);
91+
});
92+
});
93+
94+
describe('onVisibilityChanged subscription', () => {
95+
it('should subscribe to aiAssistantView.onVisibilityChanged on init', () => {
96+
const { mockView } = createAIAssistantViewController();
97+
98+
expect(mockView.onVisibilityChanged).toBeDefined();
99+
expect(typeof mockView.onVisibilityChanged).toBe('function');
100+
});
101+
});
102+
103+
describe('optionChanged', () => {
104+
it('should set handled to true for aiAssistant options', () => {
105+
const { controller } = createAIAssistantViewController();
106+
107+
const args = {
108+
name: 'aiAssistant' as const,
109+
fullName: 'aiAssistant.enabled' as const,
110+
value: true,
111+
previousValue: false,
112+
handled: false,
113+
};
114+
115+
controller.optionChanged(args);
116+
117+
expect(args.handled).toBe(true);
118+
});
119+
});
120+
});

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ import { View } from '../m_modules';
55
export class AIAssistantView extends View {
66
private aiChatInstance!: AIChat;
77

8+
public onVisibilityChanged?: (visible: boolean) => void;
9+
810
private getAIChatConfig(): AIChatOptions {
911
return {
1012
container: this.element(),
1113
createComponent: this._createComponent.bind(this),
14+
onVisibilityChanged: (visible: boolean): void => {
15+
this.onVisibilityChanged?.(visible);
16+
},
1217
};
1318
}
1419

@@ -24,12 +29,8 @@ export class AIAssistantView extends View {
2429
return !!this.option('aiAssistant.enabled');
2530
}
2631

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);
32+
public isShown(): boolean {
33+
return this.aiChatInstance?.isShown() ?? false;
3334
}
3435

3536
public toggle(): Promise<boolean> {

packages/devextreme/js/__internal/grids/grid_core/ai_assistant/ai_assistant_view_controller.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import type { dxElementWrapper } from '@js/core/renderer';
12
import $ from '@js/core/renderer';
23
import type { InitializedEvent as ButtonInitializedEvent } from '@js/ui/button';
4+
import { ACTIVE_STATE_CLASS } from '@ts/core/widget/widget';
35
import type { HeaderPanel } from '@ts/grids/grid_core/header_panel/m_header_panel';
46
import type { OptionChanged } from '@ts/grids/grid_core/m_types';
57
import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types';
@@ -16,39 +18,34 @@ export class AIAssistantViewController extends ViewController {
1618

1719
private headerPanel?: HeaderPanel;
1820

21+
private $aiAssistantButton?: dxElementWrapper;
22+
1923
public init(): void {
2024
this.aiAssistantView = this.getView('aiAssistantView');
2125
this.headerPanel = this.getView('headerPanel');
2226

27+
this.aiAssistantView.onVisibilityChanged = (visible: boolean): void => {
28+
this.$aiAssistantButton?.toggleClass(ACTIVE_STATE_CLASS, visible);
29+
};
30+
2331
const isAiAssistantEnabled = this.option('aiAssistant.enabled'); // TODO clarify option name
2432

2533
if (isAiAssistantEnabled) {
26-
const aiAssistantToolbarItem = this.getAiAssistantToolbarItem();
34+
const aiAssistantToolbarItem = this.createAiAssistantToolbarItem();
2735

2836
this.headerPanel?.registerToolbarItem(AI_ASSISTANT_BUTTON_NAME, aiAssistantToolbarItem);
2937
}
3038
}
3139

3240
public optionChanged(args: OptionChanged): void {
3341
if (args.name === 'aiAssistant') {
34-
if (args.fullName === 'aiAssistant.enabled') { // TODO clarify option name
35-
this.syncAiAssistantItem();
36-
}
37-
42+
this.syncAiAssistantItem();
3843
args.handled = true;
3944
} else {
4045
super.optionChanged(args);
4146
}
4247
}
4348

44-
public show(): Promise<boolean> {
45-
return this.aiAssistantView.show();
46-
}
47-
48-
public hide(): Promise<boolean> {
49-
return this.aiAssistantView.hide();
50-
}
51-
5249
public toggle(): Promise<boolean> {
5350
return this.aiAssistantView.toggle();
5451
}
@@ -57,28 +54,32 @@ export class AIAssistantViewController extends ViewController {
5754
const isAiAssistantEnabled = this.option('aiAssistant.enabled'); // TODO clarify option name
5855

5956
if (isAiAssistantEnabled) {
60-
const aiAssistantToolbarItem = this.getAiAssistantToolbarItem();
57+
const aiAssistantToolbarItem = this.createAiAssistantToolbarItem();
6158

6259
this.headerPanel?.applyToolbarItem(AI_ASSISTANT_BUTTON_NAME, aiAssistantToolbarItem);
6360
} else {
6461
this.headerPanel?.removeToolbarItem(AI_ASSISTANT_BUTTON_NAME);
6562
}
6663
}
6764

68-
private getAiAssistantToolbarItem(): ToolbarItem {
65+
private createAiAssistantToolbarItem(): ToolbarItem {
6966
const onClickHandler = (): Promise<boolean> => this.toggle();
7067

7168
const onInitialized = (e: ButtonInitializedEvent): void => {
69+
this.$aiAssistantButton = $(e.element);
70+
7271
if (this.headerPanel) {
73-
const asAssistantClass = this.addWidgetPrefix(AI_ASSISTANT_BUTTON_CLASS);
74-
$(e.element).addClass(this.headerPanel._getToolbarButtonClass(asAssistantClass));
72+
const aiAssistantClass = this.addWidgetPrefix(AI_ASSISTANT_BUTTON_CLASS);
73+
this.$aiAssistantButton.addClass(this.headerPanel._getToolbarButtonClass(aiAssistantClass));
7574
}
7675
};
7776
const hintText = this.option('aiAssistant.title'); // TODO clarify option name
77+
7878
return {
7979
widget: 'dxButton',
8080
options: {
8181
icon: AI_ASSISTANT_ICON_NAME,
82+
activeStateEnabled: false,
8283
onClick: onClickHandler,
8384
hint: hintText,
8485
text: hintText,

0 commit comments

Comments
 (0)