Skip to content

Commit 1bc3374

Browse files
committed
AIChat: fix optionChanged to cover object values
1 parent 78477dc commit 1bc3374

File tree

9 files changed

+277
-25
lines changed

9 files changed

+277
-25
lines changed

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

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,11 @@ describe('AIAssistantView', () => {
290290
});
291291

292292
it('should call hide when aiAssistant.enabled changes to false', () => {
293-
const { aiAssistantView } = createAIAssistantView();
293+
const { aiAssistantView, setEnabled } = createAIAssistantView();
294294
const hideSpy = jest.spyOn(aiAssistantView, 'hide');
295295

296+
setEnabled(false);
297+
296298
aiAssistantView.optionChanged({
297299
name: 'aiAssistant' as const,
298300
fullName: 'aiAssistant.enabled' as const,
@@ -304,7 +306,7 @@ describe('AIAssistantView', () => {
304306
expect(hideSpy).toHaveBeenCalledTimes(1);
305307
});
306308

307-
it('should call updateOptions on aiChatInstance for non-enabled sub-options', () => {
309+
it('should call updateOptions on aiChatInstance for title change', () => {
308310
const { aiAssistantView } = createAIAssistantView();
309311

310312
const aiChatInstance = (AIChat as jest.Mock)
@@ -319,6 +321,55 @@ describe('AIAssistantView', () => {
319321
});
320322

321323
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
324+
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
325+
expect.any(Object),
326+
true,
327+
false,
328+
);
329+
});
330+
331+
it('should call updateOptions on aiChatInstance for chat options change', () => {
332+
const { aiAssistantView } = createAIAssistantView();
333+
334+
const aiChatInstance = (AIChat as jest.Mock)
335+
.mock.results[0].value as { updateOptions: jest.Mock };
336+
337+
aiAssistantView.optionChanged({
338+
name: 'aiAssistant' as const,
339+
fullName: 'aiAssistant.chat' as const,
340+
value: { speechToTextEnabled: false },
341+
previousValue: {},
342+
handled: false,
343+
});
344+
345+
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
346+
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
347+
expect.any(Object),
348+
false,
349+
true,
350+
);
351+
});
352+
353+
it('should call updateOptions with both flags when object value contains title and chat', () => {
354+
const { aiAssistantView } = createAIAssistantView();
355+
356+
const aiChatInstance = (AIChat as jest.Mock)
357+
.mock.results[0].value as { updateOptions: jest.Mock };
358+
359+
aiAssistantView.optionChanged({
360+
name: 'aiAssistant' as const,
361+
fullName: 'aiAssistant' as const,
362+
value: { title: 'New title', chat: { speechToTextEnabled: false } },
363+
previousValue: { title: 'Old title' },
364+
handled: false,
365+
});
366+
367+
expect(aiChatInstance.updateOptions).toHaveBeenCalledTimes(1);
368+
expect(aiChatInstance.updateOptions).toHaveBeenCalledWith(
369+
expect.any(Object),
370+
true,
371+
true,
372+
);
322373
});
323374

324375
it('should not throw when aiChatInstance is not created for non-enabled sub-options', () => {

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,59 @@ describe('AIAssistantViewController', () => {
210210

211211
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
212212
});
213+
214+
it('should set handled to true when object value contains enabled', () => {
215+
const options: Record<string, unknown> = { 'aiAssistant.enabled': false };
216+
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);
217+
218+
options['aiAssistant.enabled'] = true;
219+
220+
const args = {
221+
name: 'aiAssistant' as const,
222+
fullName: 'aiAssistant' as const,
223+
value: { enabled: true },
224+
previousValue: { enabled: false },
225+
handled: false,
226+
};
227+
228+
controller.optionChanged(args);
229+
230+
expect(args.handled).toBe(true);
231+
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
232+
});
233+
234+
it('should set handled to true when object value contains title', () => {
235+
const options: Record<string, unknown> = { 'aiAssistant.enabled': true };
236+
const { controller, mockHeaderPanel } = createAIAssistantViewController(options);
237+
238+
const args = {
239+
name: 'aiAssistant' as const,
240+
fullName: 'aiAssistant' as const,
241+
value: { title: 'New title', chat: { speechToTextEnabled: false } },
242+
previousValue: { title: 'Old title' },
243+
handled: false,
244+
};
245+
246+
controller.optionChanged(args);
247+
248+
expect(args.handled).toBe(true);
249+
expect(mockHeaderPanel.applyToolbarItem).toHaveBeenCalledTimes(1);
250+
});
251+
252+
it('should not set handled when object value contains only chat/popup options', () => {
253+
const { controller } = createAIAssistantViewController();
254+
255+
const args = {
256+
name: 'aiAssistant' as const,
257+
fullName: 'aiAssistant' as const,
258+
value: { chat: { speechToTextEnabled: false, showMessageTimestamp: true } },
259+
previousValue: {},
260+
handled: false,
261+
};
262+
263+
controller.optionChanged(args);
264+
265+
expect(args.handled).toBe(false);
266+
});
213267
});
214268
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
describe, expect, it,
3+
} from '@jest/globals';
4+
5+
import {
6+
isChatOptions,
7+
isEnabledOption,
8+
isPopupOptions,
9+
isTitleOption,
10+
} from '../utils';
11+
12+
describe('isEnabledOption', () => {
13+
it('should return true for enabled option names', () => {
14+
expect(isEnabledOption('aiAssistant.enabled', true)).toBe(true);
15+
expect(isEnabledOption('aiAssistant', {
16+
enabled: false,
17+
title: 'AI Assistant',
18+
})).toBe(true);
19+
});
20+
21+
it('should return false for non-enabled option names', () => {
22+
expect(isEnabledOption('aiAssistant.title', 'Title')).toBe(false);
23+
expect(isEnabledOption('aiAssistant.popup', {})).toBe(false);
24+
expect(isEnabledOption('aiAssistant', { title: 'Title' })).toBe(false);
25+
expect(isEnabledOption('aiAssistant', 'string')).toBe(false);
26+
});
27+
});
28+
29+
describe('isTitleOption', () => {
30+
it('should return true for title option names', () => {
31+
expect(isTitleOption('aiAssistant.title', 'New Title')).toBe(true);
32+
expect(isTitleOption('aiAssistant', {
33+
title: 'New Title',
34+
chat: { speechToTextEnabled: false },
35+
})).toBe(true);
36+
});
37+
38+
it('should return false for non-title option names', () => {
39+
expect(isTitleOption('aiAssistant.enabled', true)).toBe(false);
40+
expect(isTitleOption('aiAssistant.chat', {})).toBe(false);
41+
expect(isTitleOption('aiAssistant', { enabled: true })).toBe(false);
42+
expect(isTitleOption('aiAssistant', 'string')).toBe(false);
43+
});
44+
});
45+
46+
describe('isPopupOptions', () => {
47+
it('should return true for popup option names', () => {
48+
expect(isPopupOptions('aiAssistant.popup', {})).toBe(true);
49+
expect(isPopupOptions('aiAssistant.popup.width', 400)).toBe(true);
50+
expect(isPopupOptions('aiAssistant', { popup: { width: 400 } })).toBe(true);
51+
expect(isPopupOptions('aiAssistant', {
52+
popup: { width: 400 },
53+
title: 'AI Assistant',
54+
})).toBe(true);
55+
});
56+
57+
it('should return false for non-popup option names', () => {
58+
expect(isPopupOptions('aiAssistant.chat', {})).toBe(false);
59+
expect(isPopupOptions('aiAssistant.enabled', true)).toBe(false);
60+
expect(isPopupOptions('aiAssistant', { chat: {} })).toBe(false);
61+
expect(isPopupOptions('aiAssistant', {
62+
chat: { showAvatar: false },
63+
title: 'AI Assistant',
64+
})).toBe(false);
65+
});
66+
});
67+
68+
describe('isChatOptions', () => {
69+
it('should return true for chat option names', () => {
70+
expect(isChatOptions('aiAssistant.chat', {})).toBe(true);
71+
expect(isChatOptions('aiAssistant.chat.showAvatar', false)).toBe(true);
72+
expect(isChatOptions('aiAssistant', { chat: { showAvatar: false } })).toBe(true);
73+
expect(isChatOptions('aiAssistant', {
74+
chat: { speechToTextEnabled: false },
75+
title: 'AI Assistant',
76+
})).toBe(true);
77+
});
78+
79+
it('should return false for non-chat option names', () => {
80+
expect(isChatOptions('aiAssistant.popup', {})).toBe(false);
81+
expect(isChatOptions('aiAssistant.enabled', true)).toBe(false);
82+
expect(isChatOptions('aiAssistant', { popup: {} })).toBe(false);
83+
expect(isChatOptions('aiAssistant', {
84+
popup: { width: 400 },
85+
title: 'AI Assistant',
86+
})).toBe(false);
87+
});
88+
});

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { isString } from '@js/core/utils/type';
66
import type { Message, Properties as ChatProperties } from '@js/ui/chat';
77
import type { Properties as PopupProperties } from '@js/ui/popup';
88
import { AI_ASSISTANT_POPUP_OFFSET } from '@ts/grids/grid_core/ai_assistant/const';
9+
import { isChatOptions, isEnabledOption, isTitleOption } from '@ts/grids/grid_core/ai_assistant/utils';
10+
import { isPopupOptions } from '@ts/grids/grid_core/ai_column/utils';
911
import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers';
1012
import type { OptionChanged } from '@ts/grids/grid_core/m_types';
1113
import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view';
@@ -110,19 +112,29 @@ export class AIAssistantView extends View {
110112

111113
public optionChanged(args: OptionChanged): void {
112114
if (args.name === 'aiAssistant') {
113-
const [, secondLevel] = args.fullName.split('.');
114-
switch (secondLevel) {
115-
case 'enabled':
116-
if (args.value) {
117-
this?._invalidate();
118-
} else {
119-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
120-
this?.hide();
121-
}
122-
break;
123-
default:
124-
this.aiChatInstance?.updateOptions(this.getAIChatConfig());
115+
const enabledChanged = isEnabledOption(args.fullName, args.value);
116+
117+
if (enabledChanged) {
118+
if (this.isVisible()) {
119+
this?._invalidate();
120+
} else {
121+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
122+
this?.hide();
123+
}
125124
}
125+
126+
const popupOptionsChanged = isTitleOption(args.fullName, args.value)
127+
|| isPopupOptions(args.fullName, args.value);
128+
const chatOptionsChanged = isChatOptions(args.fullName, args.value);
129+
130+
if (popupOptionsChanged || chatOptionsChanged) {
131+
this.aiChatInstance?.updateOptions(
132+
this.getAIChatConfig(),
133+
popupOptionsChanged,
134+
chatOptionsChanged,
135+
);
136+
}
137+
126138
args.handled = true;
127139
} else {
128140
super.optionChanged(args);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { ToolbarItem } from '@ts/grids/new/grid_core/toolbar/types';
1111

1212
import { ViewController } from '../m_modules';
1313
import type { AIAssistantView } from './ai_assistant_view';
14+
import { isEnabledOption, isTitleOption } from './utils';
1415

1516
export class AIAssistantViewController extends ViewController {
1617
private aiAssistantView?: AIAssistantView;
@@ -45,7 +46,10 @@ export class AIAssistantViewController extends ViewController {
4546

4647
public optionChanged(args: OptionChanged): void {
4748
if (args.name === 'aiAssistant') {
48-
if (args.fullName === 'aiAssistant.enabled' || args.fullName === 'aiAssistant.title') {
49+
const enabledChanged = isEnabledOption(args.fullName, args.value);
50+
const titleChanged = isTitleOption(args.fullName, args.value);
51+
52+
if (enabledChanged || titleChanged) {
4953
this.syncAiAssistantItem();
5054
args.handled = true;
5155
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { isObject } from '@js/core/utils/type';
2+
3+
export const isEnabledOption = (optionName: string, value: unknown): boolean => optionName.startsWith('aiAssistant.enabled')
4+
|| (optionName === 'aiAssistant' && isObject(value) && 'enabled' in value);
5+
6+
export const isTitleOption = (optionName: string, value: unknown): boolean => optionName.startsWith('aiAssistant.title')
7+
|| (optionName === 'aiAssistant' && isObject(value) && 'title' in value);
8+
9+
export const isPopupOptions = (optionName: string, value: unknown): boolean => optionName.startsWith('aiAssistant.popup')
10+
|| (optionName === 'aiAssistant' && isObject(value) && 'popup' in value);
11+
12+
export const isChatOptions = (optionName: string, value: unknown): boolean => optionName.startsWith('aiAssistant.chat')
13+
|| (optionName === 'aiAssistant' && isObject(value) && 'chat' in value);

packages/devextreme/js/__internal/grids/grid_core/ai_chat/ai_chat.test.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,20 +178,32 @@ describe('AIChat', () => {
178178
popupConfig.contentTemplate($('<div>'));
179179
};
180180

181-
it('should call popupInstance.option with new popupOptions', () => {
181+
it('should call popupInstance.option with new popupOptions when updatePopup is true', () => {
182182
const { aiChat, $container } = createAIChat();
183183
const newPopupOptions = { title: 'Updated' };
184184

185185
aiChat.updateOptions({
186186
container: $container,
187187
createComponent: createComponentMock as any,
188188
popupOptions: newPopupOptions,
189-
});
189+
}, true, false);
190190

191191
expect(mockPopupInstance.option).toHaveBeenCalledWith(newPopupOptions);
192192
});
193193

194-
it('should call chatInstance.option with new chatOptions', () => {
194+
it('should not call popupInstance.option when updatePopup is false', () => {
195+
const { aiChat, $container } = createAIChat();
196+
197+
aiChat.updateOptions({
198+
container: $container,
199+
createComponent: createComponentMock as any,
200+
popupOptions: { title: 'Updated' },
201+
}, false, false);
202+
203+
expect(mockPopupInstance.option).not.toHaveBeenCalledWith({ title: 'Updated' });
204+
});
205+
206+
it('should call chatInstance.option with new chatOptions when updateChat is true', () => {
195207
const { aiChat, $container } = createAIChat();
196208
triggerContentTemplate();
197209

@@ -201,20 +213,33 @@ describe('AIChat', () => {
201213
container: $container,
202214
createComponent: createComponentMock as any,
203215
chatOptions: newChatOptions,
204-
});
216+
}, false, true);
205217

206218
expect(mockChatInstance.option).toHaveBeenCalledWith(newChatOptions);
207219
});
208220

209-
it('should not throw when chatInstance is not created', () => {
221+
it('should not call chatInstance.option when updateChat is false', () => {
222+
const { aiChat, $container } = createAIChat();
223+
triggerContentTemplate();
224+
225+
aiChat.updateOptions({
226+
container: $container,
227+
createComponent: createComponentMock as any,
228+
chatOptions: { showAvatar: true },
229+
}, false, false);
230+
231+
expect(mockChatInstance.option).not.toHaveBeenCalled();
232+
});
233+
234+
it('should not throw when chatInstance is not created and updateChat is true', () => {
210235
const { aiChat, $container } = createAIChat();
211236

212237
expect(() => {
213238
aiChat.updateOptions({
214239
container: $container,
215240
createComponent: createComponentMock as any,
216241
chatOptions: { showAvatar: true },
217-
});
242+
}, false, true);
218243
}).not.toThrow();
219244
});
220245
});

0 commit comments

Comments
 (0)