Skip to content

Commit eaaa974

Browse files
Merge branch '26_1' into 26_1_dx_scss_nx_infra_p
2 parents 3c1ae29 + a0e0e4d commit eaaa974

12 files changed

Lines changed: 496 additions & 25 deletions

File tree

packages/devextreme-scss/scss/widgets/base/gridBase/layout/aiChat/_index.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@
6666
bottom: 0;
6767
line-height: 0;
6868
}
69+
70+
.dx-ai-chat--disabled .dx-ai-chat__message-regenerate-button {
71+
pointer-events: none;
72+
}

packages/devextreme-scss/scss/widgets/fluent/gridBase/layout/aiChat/_index.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@use '../../../colors' as *;
33
@use '../../../button/colors' as *;
44
@use './sizes' as *;
5+
@use "../../../../base/widget" as *;
56
@include ai-chat-messagelist-empty(
67
$button-default-bg,
78
$button-default-outlined-hover-bg,
@@ -12,3 +13,7 @@
1213
@include ai-chat-messagebubble-border($base-border-color);
1314
@include ai-chat-message-regenerate-button($button-normal-active-bg);
1415
@include ai-chat-message-icon($ai-chat-message-icon-size);
16+
17+
.dx-ai-chat--disabled .dx-ai-chat__message-regenerate-button {
18+
@include disabled-widget();
19+
}

packages/devextreme-scss/scss/widgets/generic/gridBase/layout/aiChat/_index.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@use "../../../colors" as *;
33
@use "../../../button/colors" as *;
44
@use './sizes' as *;
5+
@use "../../../../base/widget" as *;
56
@include ai-chat-messagelist-empty(
67
$button-default-bg,
78
$button-default-outlined-bg-hover,
@@ -12,3 +13,7 @@
1213
@include ai-chat-messagebubble-border($base-border-color);
1314
@include ai-chat-message-regenerate-button($button-normal-outlined-bg-active);
1415
@include ai-chat-message-icon($ai-chat-message-icon-size);
16+
17+
.dx-ai-chat--disabled .dx-ai-chat__message-regenerate-button {
18+
@include disabled-widget();
19+
}

packages/devextreme-scss/scss/widgets/material/gridBase/layout/aiChat/_index.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@use "../../../colors" as *;
33
@use "../../../button/colors" as *;
44
@use './sizes' as *;
5+
@use "../../../../base/widget" as *;
56
@include ai-chat-messagelist-empty(
67
$button-default-bg,
78
$button-default-outlined-hover-bg,
@@ -12,3 +13,7 @@
1213
@include ai-chat-messagebubble-border($base-border-color);
1314
@include ai-chat-message-regenerate-button($button-normal-active-bg);
1415
@include ai-chat-message-icon($ai-chat-message-icon-size);
16+
17+
.dx-ai-chat--disabled .dx-ai-chat__message-regenerate-button {
18+
@include disabled-widget();
19+
}

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ describe('AIAssistantController', () => {
8282
const timestamp = '2026-04-16T10:00:00.000Z';
8383
const expectedTimestamp = Date.parse(timestamp);
8484

85+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
8586
controller.sendRequestToAI({
8687
author: { id: 'user', name: 'User' },
8788
text: 'Generate values',
@@ -104,6 +105,7 @@ describe('AIAssistantController', () => {
104105
it('should keep message as pending when AI integration is not configured', async () => {
105106
const controller = createController();
106107

108+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
107109
controller.sendRequestToAI({
108110
author: { id: 'user', name: 'User' },
109111
text: 'Generate values',
@@ -124,6 +126,7 @@ describe('AIAssistantController', () => {
124126
'aiAssistant.aiIntegration': mockAIIntegration,
125127
});
126128

129+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
127130
controller.sendRequestToAI({
128131
author: { id: 'user', name: 'User' },
129132
text: 'Generate values',
@@ -153,7 +156,7 @@ describe('AIAssistantController', () => {
153156
'aiAssistant.aiIntegration': mockAIIntegration,
154157
});
155158

156-
controller.sendRequestToAI({
159+
const promise = controller.sendRequestToAI({
157160
author: { id: 'user', name: 'User' },
158161
text: 'Generate values',
159162
timestamp: '2026-04-16T10:00:00.000Z',
@@ -169,14 +172,16 @@ describe('AIAssistantController', () => {
169172
text: 'Network error',
170173
}),
171174
]);
175+
176+
await expect(promise).rejects.toThrow('Network error');
172177
});
173178

174179
it('should fail message when response has no actions', async () => {
175180
const controller = createController({
176181
'aiAssistant.aiIntegration': mockAIIntegration,
177182
});
178183

179-
controller.sendRequestToAI({
184+
const promise = controller.sendRequestToAI({
180185
author: { id: 'user', name: 'User' },
181186
text: 'Generate values',
182187
timestamp: '2026-04-16T10:00:00.000Z',
@@ -196,6 +201,57 @@ describe('AIAssistantController', () => {
196201
text: 'Default error message',
197202
}),
198203
]);
204+
205+
await expect(promise).rejects.toThrow('Default error message');
206+
});
207+
208+
it('should resolve promise when command succeeds', async () => {
209+
const controller = createController({
210+
'aiAssistant.aiIntegration': mockAIIntegration,
211+
});
212+
213+
const promise = controller.sendRequestToAI({
214+
author: { id: 'user', name: 'User' },
215+
text: 'Generate values',
216+
timestamp: '2026-04-16T10:00:00.000Z',
217+
} as Message);
218+
219+
const actions = [{ name: 'sort', args: { column: 'Name' } }];
220+
sendRequestCallbacks.onComplete?.({ actions });
221+
222+
await expect(promise).resolves.toBeUndefined();
223+
});
224+
225+
it('should reject promise when onError is called', async () => {
226+
const controller = createController({
227+
'aiAssistant.aiIntegration': mockAIIntegration,
228+
});
229+
230+
const promise = controller.sendRequestToAI({
231+
author: { id: 'user', name: 'User' },
232+
text: 'Generate values',
233+
timestamp: '2026-04-16T10:00:00.000Z',
234+
} as Message);
235+
236+
sendRequestCallbacks.onError?.(new Error('Network error'));
237+
238+
await expect(promise).rejects.toThrow('Network error');
239+
});
240+
241+
it('should reject promise when response has no actions', async () => {
242+
const controller = createController({
243+
'aiAssistant.aiIntegration': mockAIIntegration,
244+
});
245+
246+
const promise = controller.sendRequestToAI({
247+
author: { id: 'user', name: 'User' },
248+
text: 'Generate values',
249+
timestamp: '2026-04-16T10:00:00.000Z',
250+
} as Message);
251+
252+
sendRequestCallbacks.onComplete?.({} as ExecuteGridAssistantCommandResult);
253+
254+
await expect(promise).rejects.toThrow('Default error message');
199255
});
200256
});
201257
});

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ describe('AIAssistantView', () => {
283283
describe('chat event handlers', () => {
284284
describe('onMessageEntered', () => {
285285
it('should send request to AI with the entered message', () => {
286+
mockAIAssistantController.sendRequestToAI.mockReturnValue(Promise.resolve());
286287
createAIAssistantView();
287288

288289
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
@@ -296,6 +297,82 @@ describe('AIAssistantView', () => {
296297
expect(mockAIAssistantController.sendRequestToAI).toHaveBeenCalledTimes(1);
297298
expect(mockAIAssistantController.sendRequestToAI).toHaveBeenCalledWith(message);
298299
});
300+
301+
it('should not send request when chat is disabled', () => {
302+
createAIAssistantView();
303+
304+
const aiChatInstance = (AIChat as jest.Mock)
305+
.mock.results[0].value as { isDisabled: jest.Mock; setDisabled: jest.Mock };
306+
aiChatInstance.isDisabled.mockReturnValue(true);
307+
308+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
309+
const message = {
310+
author: { id: 'user', name: 'User' },
311+
text: 'Generate summary',
312+
};
313+
314+
aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any);
315+
316+
expect(mockAIAssistantController.sendRequestToAI).not.toHaveBeenCalled();
317+
});
318+
319+
it('should call setDisabled(true) before sending request', () => {
320+
mockAIAssistantController.sendRequestToAI.mockReturnValue(Promise.resolve());
321+
createAIAssistantView();
322+
323+
const aiChatInstance = (AIChat as jest.Mock)
324+
.mock.results[0].value as { setDisabled: jest.Mock };
325+
326+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
327+
const message = {
328+
author: { id: 'user', name: 'User' },
329+
text: 'Generate summary',
330+
};
331+
332+
aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any);
333+
334+
expect(aiChatInstance.setDisabled).toHaveBeenCalledWith(true);
335+
});
336+
337+
it('should call setDisabled(false) after request completes successfully', async () => {
338+
mockAIAssistantController.sendRequestToAI.mockReturnValue(Promise.resolve());
339+
createAIAssistantView();
340+
341+
const aiChatInstance = (AIChat as jest.Mock)
342+
.mock.results[0].value as { setDisabled: jest.Mock };
343+
344+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
345+
const message = {
346+
author: { id: 'user', name: 'User' },
347+
text: 'Generate summary',
348+
};
349+
350+
aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any);
351+
await Promise.resolve();
352+
353+
expect(aiChatInstance.setDisabled).toHaveBeenLastCalledWith(false);
354+
});
355+
356+
it('should call setDisabled(false) after request fails', async () => {
357+
mockAIAssistantController.sendRequestToAI.mockReturnValue(
358+
Promise.reject(new Error('Network error')),
359+
);
360+
createAIAssistantView();
361+
362+
const aiChatInstance = (AIChat as jest.Mock)
363+
.mock.results[0].value as { setDisabled: jest.Mock };
364+
365+
const aiChatConfig = (AIChat as jest.Mock).mock.calls[0][0] as AIChatOptions;
366+
const message = {
367+
author: { id: 'user', name: 'User' },
368+
text: 'Generate summary',
369+
};
370+
371+
aiChatConfig.chatOptions?.onMessageEntered?.({ message } as any);
372+
await Promise.resolve();
373+
374+
expect(aiChatInstance.setDisabled).toHaveBeenLastCalledWith(false);
375+
});
299376
});
300377
});
301378

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,26 +102,31 @@ export class AIAssistantController extends Controller {
102102
};
103103
}
104104

105-
public sendRequestToAI(message: Message): void {
105+
public sendRequestToAI(message: Message): Promise<void> {
106106
const aiMessageId = this.createPendingAIMessage(message);
107107

108-
this.aiAssistantIntegrationController?.sendRequest(message.text, {
109-
onComplete: (response: ExecuteGridAssistantCommandResult): void => {
110-
fromPromise(this.processResponse(response))
111-
.done((commands: CommandResults) => {
112-
this.completeAIMessage(aiMessageId, commands);
113-
})
114-
.fail((errorMessage) => {
115-
const error = errorMessage instanceof Error
116-
? errorMessage
117-
: new Error(String(errorMessage));
118-
119-
this.failAIMessage(aiMessageId, error);
120-
});
121-
},
122-
onError: (error: Error): void => {
123-
this.failAIMessage(aiMessageId, error);
124-
},
108+
return new Promise((resolve, reject) => {
109+
this.aiAssistantIntegrationController?.sendRequest(message.text, {
110+
onComplete: (response: ExecuteGridAssistantCommandResult): void => {
111+
fromPromise(this.processResponse(response))
112+
.done((commands: CommandResults) => {
113+
this.completeAIMessage(aiMessageId, commands);
114+
resolve();
115+
})
116+
.fail((errorMessage) => {
117+
const error = errorMessage instanceof Error
118+
? errorMessage
119+
: new Error(String(errorMessage));
120+
121+
this.failAIMessage(aiMessageId, error);
122+
reject(error);
123+
});
124+
},
125+
onError: (error: Error): void => {
126+
this.failAIMessage(aiMessageId, error);
127+
reject(error);
128+
},
129+
});
125130
});
126131
}
127132

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Callback } from '@js/core/utils/callbacks';
33
import { getHeight } from '@js/core/utils/size';
44
import type { Properties as ChatProperties } from '@js/ui/chat';
55
import type { Properties as PopupProperties } from '@js/ui/popup';
6+
import { fromPromise } from '@ts/core/utils/m_deferred';
67
import { AI_ASSISTANT_POPUP_OFFSET } from '@ts/grids/grid_core/ai_assistant/const';
78
import {
89
isChatOptions,
@@ -90,7 +91,14 @@ export class AIAssistantView extends View {
9091
dataSource: this.aiAssistantController.getMessageDataSource(),
9192
reloadOnChange: true,
9293
onMessageEntered: (e): void => {
93-
this.aiAssistantController.sendRequestToAI(e.message);
94+
if (this.aiChatInstance?.isDisabled()) {
95+
return;
96+
}
97+
98+
this.aiChatInstance?.setDisabled(true);
99+
fromPromise(this.aiAssistantController.sendRequestToAI(e.message)).always(() => {
100+
this.aiChatInstance?.setDisabled(false);
101+
});
94102
},
95103
...this.option('aiAssistant.chat'),
96104
};

0 commit comments

Comments
 (0)