Skip to content

Commit 4f58e30

Browse files
Merge branch '26_1' of https://github.com/DevExpress/DevExtreme into 26_1_dx_scss_nx_infra_p
2 parents 7604f86 + 1fd7c09 commit 4f58e30

14 files changed

Lines changed: 1171 additions & 18383 deletions

File tree

apps/react/public/js/app/bundle.js

Lines changed: 0 additions & 18265 deletions
This file was deleted.

apps/react/public/js/app/bundle.js.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
) {
3535
.dx-ai-chat__message--error .dx-ai-chat__message-icon,
3636
.dx-ai-chat__action-list-item--error,
37+
.dx-ai-chat__action-list-item--aborted,
3738
.dx-ai-chat__message-error-text {
3839
color: $error-message-color;
3940
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,50 @@ describe('AIAssistantController', () => {
166166
]);
167167
});
168168

169+
it('should complete message as failure when commands contain aborted items', async () => {
170+
(MockedGridCommands.mockImplementation as jest.Mock).call(
171+
MockedGridCommands,
172+
() => ({
173+
validate: jest.fn().mockReturnValue(true),
174+
executeCommands: jest.fn<() => Promise<CommandResult[]>>().mockResolvedValue([
175+
{ status: 'success', message: 'sort' },
176+
{ status: 'aborted', message: 'filter aborted' },
177+
]),
178+
abort: jest.fn(),
179+
}),
180+
);
181+
182+
const controller = createController({
183+
'aiAssistant.aiIntegration': mockAIIntegration,
184+
});
185+
186+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
187+
controller.sendRequestToAI({
188+
author: { id: 'user', name: 'User' },
189+
text: 'Sort and filter',
190+
timestamp: '2026-04-16T10:00:00.000Z',
191+
} as Message);
192+
193+
const actions = [
194+
{ name: 'sort', args: { column: 'Name' } },
195+
{ name: 'filter', args: { column: 'Age' } },
196+
];
197+
sendRequestCallbacks.onComplete?.({ actions });
198+
await Promise.resolve();
199+
200+
const messages = await getStore(controller).load();
201+
202+
expect(messages).toEqual([
203+
expect.objectContaining({
204+
status: MessageStatus.Failure,
205+
commands: [
206+
{ status: 'success', message: 'sort' },
207+
{ status: 'aborted', message: 'filter aborted' },
208+
],
209+
}),
210+
]);
211+
});
212+
169213
it('should fail message when onError callback is called', async () => {
170214
const controller = createController({
171215
'aiAssistant.aiIntegration': mockAIIntegration,

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {
33
} from '@jest/globals';
44
import type { Message } from '@js/ui/chat';
55

6-
import { AI_ASSISTANT_AUTHOR_ID } from '../const';
6+
import { AI_ASSISTANT_AUTHOR_ID, MessageStatus } from '../const';
77
import {
8+
getMessageStatus,
89
isAIMessage,
910
isChatOptions,
1011
isEnabledOption,
@@ -117,3 +118,45 @@ describe('isChatOptions', () => {
117118
})).toBe(false);
118119
});
119120
});
121+
122+
describe('getMessageStatus', () => {
123+
it('should return Success when all commands are successful', () => {
124+
const commands = [
125+
{ status: 'success' as const, message: 'Sorted' },
126+
{ status: 'success' as const, message: 'Filtered' },
127+
];
128+
129+
expect(getMessageStatus(commands)).toBe(MessageStatus.Success);
130+
});
131+
132+
it('should return Failure when commands contain errors', () => {
133+
const commands = [
134+
{ status: 'success' as const, message: 'Sorted' },
135+
{ status: 'failure' as const, message: 'Failed to filter' },
136+
];
137+
138+
expect(getMessageStatus(commands)).toBe(MessageStatus.Failure);
139+
});
140+
141+
it('should return Failure when commands contain aborted items', () => {
142+
const commands = [
143+
{ status: 'success' as const, message: 'Sorted' },
144+
{ status: 'aborted' as const, message: 'Filter was aborted' },
145+
];
146+
147+
expect(getMessageStatus(commands)).toBe(MessageStatus.Failure);
148+
});
149+
150+
it('should return Failure when commands contain both errors and aborted items', () => {
151+
const commands = [
152+
{ status: 'failure' as const, message: 'Failed' },
153+
{ status: 'aborted' as const, message: 'Aborted' },
154+
];
155+
156+
expect(getMessageStatus(commands)).toBe(MessageStatus.Failure);
157+
});
158+
159+
it('should return Success when commands array is empty', () => {
160+
expect(getMessageStatus([])).toBe(MessageStatus.Success);
161+
});
162+
});

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type { DataSourceLike } from '@js/data/data_source';
77
import type { Message } from '@js/ui/chat';
88
import { fromPromise } from '@ts/core/utils/m_deferred';
99

10-
import { hasCommandErrors } from '../ai_chat/utils';
1110
import { Controller } from '../m_modules';
1211
import { AIAssistantIntegrationController } from './ai_assistant_integration_controller';
1312
import { AI_ASSISTANT_AUTHOR, AI_ASSISTANT_AUTHOR_ID, MessageStatus } from './const';
@@ -16,7 +15,7 @@ import type {
1615
AIMessage,
1716
CommandResults,
1817
} from './types';
19-
import { isAIMessage } from './utils';
18+
import { getMessageStatus, isAIMessage } from './utils';
2019

2120
export class AIAssistantController extends Controller {
2221
private gridCommands?: GridCommands;
@@ -85,9 +84,7 @@ export class AIAssistantController extends Controller {
8584
}
8685

8786
private completeAIMessage(messageId: string, commands: CommandResults): void {
88-
const messageStatus = hasCommandErrors(commands)
89-
? MessageStatus.Failure
90-
: MessageStatus.Success;
87+
const messageStatus = getMessageStatus(commands);
9188

9289
this.updateAIMessage(messageId, {
9390
headerText: this.getCustomizedResponseTitle(),

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { isObject } from '@js/core/utils/type';
22
import type { Message } from '@js/ui/chat';
33

4-
import { AI_ASSISTANT_AUTHOR_ID } from './const';
5-
import type { AIMessage } from './types';
4+
import { hasAbortedCommands, hasCommandErrors } from '../ai_chat/utils';
5+
import { AI_ASSISTANT_AUTHOR_ID, MessageStatus } from './const';
6+
import type { AIMessage, CommandResults } from './types';
67

78
export const isAIMessage = (
89
message: Message,
@@ -19,3 +20,11 @@ export const isPopupOptions = (optionName: string, value: unknown): boolean => o
1920

2021
export const isChatOptions = (optionName: string, value: unknown): boolean => optionName.startsWith('aiAssistant.chat')
2122
|| (optionName === 'aiAssistant' && isObject(value) && 'chat' in value);
23+
24+
export const getMessageStatus = (commands: CommandResults): MessageStatus => {
25+
if (hasCommandErrors(commands) || hasAbortedCommands(commands)) {
26+
return MessageStatus.Failure;
27+
}
28+
29+
return MessageStatus.Success;
30+
};

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

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Popup from '@ts/ui/popup/m_popup';
1515

1616
import { AIChat } from './ai_chat';
1717
import {
18+
ABORTED_ITEM_EMOJI,
1819
CLASSES, CLEAR_CHAT_ICON, DEFAULT_POPUP_OPTIONS,
1920
ERROR_ITEM_EMOJI, REGENERATE_ICON, SUCCESS_ITEM_EMOJI,
2021
} from './const';
@@ -375,6 +376,7 @@ describe('AIChat', () => {
375376
const commands: CommandResults = [
376377
{ status: 'success', message: 'Sorted Name.' },
377378
{ status: 'failure', message: 'Failed to group.' },
379+
{ status: 'aborted', message: 'Aborted filter.' },
378380
];
379381

380382
renderMessageTemplate(chatConfig, {
@@ -386,9 +388,10 @@ describe('AIChat', () => {
386388

387389
const icons = container.querySelectorAll(`.${CLASSES.actionListItemIcon}`);
388390

389-
expect(icons).toHaveLength(2);
391+
expect(icons).toHaveLength(3);
390392
expect(icons[0].textContent).toBe(SUCCESS_ITEM_EMOJI);
391393
expect(icons[1].textContent).toBe(ERROR_ITEM_EMOJI);
394+
expect(icons[2].textContent).toBe(ABORTED_ITEM_EMOJI);
392395
});
393396

394397
it('should render error icon when commands contain errors', () => {
@@ -582,6 +585,99 @@ describe('AIChat', () => {
582585
});
583586
});
584587

588+
describe('aborted state', () => {
589+
it('should render command list with aborted class for aborted command items', () => {
590+
createAIChat();
591+
triggerContentTemplate();
592+
593+
const chatConfig = getChatConfig();
594+
const container = document.createElement('div');
595+
const commands: CommandResults = [
596+
{ status: 'success', message: 'Sorted Name.' },
597+
{ status: 'aborted', message: 'Filter was aborted.' },
598+
];
599+
600+
renderMessageTemplate(chatConfig, {
601+
author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' },
602+
text: 'Sorting and Filtering',
603+
status: 'success',
604+
commands,
605+
}, container);
606+
607+
expect(container.querySelector(`.${CLASSES.actionList}`)).not.toBeNull();
608+
expect(container.querySelectorAll(`.${CLASSES.actionListItem}`)).toHaveLength(2);
609+
expect(container.querySelectorAll(`.${CLASSES.actionListItemSuccess}`)).toHaveLength(1);
610+
expect(container.querySelectorAll(`.${CLASSES.actionListItemAborted}`)).toHaveLength(1);
611+
});
612+
613+
it('should render aborted emoji for aborted command items', () => {
614+
createAIChat();
615+
triggerContentTemplate();
616+
617+
const chatConfig = getChatConfig();
618+
const container = document.createElement('div');
619+
const commands: CommandResults = [
620+
{ status: 'aborted', message: 'Filter was aborted.' },
621+
];
622+
623+
renderMessageTemplate(chatConfig, {
624+
author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' },
625+
text: 'Filtering',
626+
status: 'success',
627+
commands,
628+
}, container);
629+
630+
const icons = container.querySelectorAll(`.${CLASSES.actionListItemIcon}`);
631+
632+
expect(icons).toHaveLength(1);
633+
expect(icons[0].textContent).toBe(ABORTED_ITEM_EMOJI);
634+
});
635+
636+
it('should render error icon when commands contain aborted items', () => {
637+
createAIChat();
638+
triggerContentTemplate();
639+
640+
const chatConfig = getChatConfig();
641+
const container = document.createElement('div');
642+
643+
renderMessageTemplate(chatConfig, {
644+
author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' },
645+
text: 'Aborted',
646+
status: 'success',
647+
commands: [
648+
{ status: 'success', message: 'OK' },
649+
{ status: 'aborted', message: 'Aborted' },
650+
],
651+
}, container);
652+
653+
expect(container.querySelector(`.${CLASSES.messageIcon}`)?.classList.contains('dx-icon-errorcircle')).toBe(true);
654+
});
655+
656+
it('should render regenerate button when commands contain aborted items', () => {
657+
const onRegenerate = jest.fn();
658+
createAIChat({ onRegenerate });
659+
triggerContentTemplate();
660+
661+
const chatConfig = getChatConfig();
662+
const container = document.createElement('div');
663+
664+
renderMessageTemplate(chatConfig, {
665+
author: { id: AI_ASSISTANT_AUTHOR_ID, name: 'AI Assistant' },
666+
text: 'Aborted results',
667+
status: 'success',
668+
commands: [
669+
{ status: 'success', message: 'OK' },
670+
{ status: 'aborted', message: 'Aborted' },
671+
],
672+
}, container);
673+
674+
const regenerateButton = container.querySelector(`.${CLASSES.messageRegenerateButton}`);
675+
676+
expect(regenerateButton).not.toBeNull();
677+
expect(regenerateButton?.classList.contains(`dx-icon-${REGENERATE_ICON}`)).toBe(true);
678+
});
679+
});
680+
585681
describe('general', () => {
586682
it('should not render anything when message is undefined', () => {
587683
createAIChat();

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,17 @@ import {
2525
CLASSES, CLEAR_CHAT_ICON,
2626
DEFAULT_CHAT_OPTIONS,
2727
DEFAULT_POPUP_OPTIONS,
28-
ERROR_ITEM_EMOJI,
2928
REGENERATE_ICON,
30-
SUCCESS_ITEM_EMOJI,
3129
} from './const';
3230
import type {
3331
AIChatOptions, CommandResult, CommandResults,
3432
} from './types';
3533
import {
3634
findMessageById,
35+
getCommandItemStyle,
3736
getMessageIconName,
3837
getMessageStateClass,
39-
hasCommandErrors,
38+
needToRenderCommandList,
4039
needToShowRegenerateButton,
4140
} from './utils';
4241

@@ -178,7 +177,7 @@ export class AIChat {
178177

179178
private renderMessageStateContent($parent: dxElementWrapper, message: AIMessage): void {
180179
switch (true) {
181-
case (message.status === MessageStatus.Success || hasCommandErrors(message.commands)):
180+
case (needToRenderCommandList(message)):
182181
this.renderCommandList($parent, message.commands);
183182
break;
184183
case message.status === MessageStatus.Failure:
@@ -212,16 +211,12 @@ export class AIChat {
212211
$parent: dxElementWrapper,
213212
command: CommandResult,
214213
): void {
215-
const commandStateClass = command.status === MessageStatus.Failure
216-
? CLASSES.actionListItemError
217-
: CLASSES.actionListItemSuccess;
214+
const { stateClass, emoji } = getCommandItemStyle(command.status);
218215

219216
const $item = $('<li>')
220-
.addClass(`${CLASSES.actionListItem} ${commandStateClass}`)
217+
.addClass(`${CLASSES.actionListItem} ${stateClass}`)
221218
.appendTo($parent);
222219

223-
const emoji = command.status === MessageStatus.Failure ? ERROR_ITEM_EMOJI : SUCCESS_ITEM_EMOJI;
224-
225220
$('<span>')
226221
.addClass(CLASSES.actionListItemIcon)
227222
.text(emoji)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const CLASSES = {
3535
actionListItem: 'dx-ai-chat__action-list-item',
3636
actionListItemSuccess: 'dx-ai-chat__action-list-item--success',
3737
actionListItemError: 'dx-ai-chat__action-list-item--error',
38+
actionListItemAborted: 'dx-ai-chat__action-list-item--aborted',
3839
actionListItemIcon: 'dx-ai-chat__action-list-item-icon',
3940
actionListItemText: 'dx-ai-chat__action-list-item-text',
4041
messageErrorText: 'dx-ai-chat__message-error-text',
@@ -52,3 +53,4 @@ export const REGENERATE_ICON = 'restore';
5253

5354
export const SUCCESS_ITEM_EMOJI = '✅';
5455
export const ERROR_ITEM_EMOJI = '❌';
56+
export const ABORTED_ITEM_EMOJI = '⚠️';

0 commit comments

Comments
 (0)