Skip to content

Commit 9432cf0

Browse files
Merge branch '26_1' into issue-4222_26_1
2 parents 3d5ad48 + 7e7dfbd commit 9432cf0

6 files changed

Lines changed: 138 additions & 38 deletions

File tree

apps/demos/Demos/DataGrid/AIAssistant/description.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
The AI Assistant for the DevExtreme [DataGrid](/Documentation/Guide/UI_Components/DataGrid/Overview/) allows you to interact with the component using natural language. The Chat also supports [speech-to-text input](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#speechToTextEnabled), ideal for hands-free interactions or entering longer prompts.
1+
The AI Assistant for the DevExtreme [DataGrid](/Documentation/Guide/UI_Components/DataGrid/Overview/) allows you to interact with the component using natural language. Our Chat implementation supports [speech-to-text input](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#speechToTextEnabled), ideal for hands-free interaction or entering longer prompts.
22

3-
You can use/configure the following DataGrid features in the AI Assistant Chat prompts:
3+
You can use/configure the following DataGrid features via AI Assistant Chat prompts:
44

55
- [Filtering and Searching](/Documentation/Guide/UI_Components/DataGrid/Filtering_and_Searching/)
66
- [Sorting](/Documentation/Guide/UI_Components/DataGrid/Sorting/)
@@ -11,7 +11,7 @@ You can use/configure the following DataGrid features in the AI Assistant Chat p
1111
- [Summaries](/Documentation/Guide/UI_Components/DataGrid/Summaries/Predefined_Aggregate_Functions/)
1212
- [Column Fixing](/Documentation/Guide/UI_Components/DataGrid/Columns/Column_Fixing/), [Resizing](/Documentation/Guide/UI_Components/DataGrid/Columns/Column_Sizing/), and [Reordering](/Documentation/Guide/UI_Components/DataGrid/Columns/Column_Reordering/)
1313

14-
The AI Assistant feature is also available for the DevExtreme [TreeList](/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/#aiAssistant) component.
14+
This AI Assistant feature is also available in the DevExtreme [TreeList](/Documentation/ApiReference/UI_Components/dxTreeList/Configuration/#aiAssistant) component.
1515
<!--split-->
1616

1717
In this demo, the AI Assistant is enabled for a DataGrid that displays mock sales data with over 1500 records.
@@ -24,6 +24,6 @@ When connected to your own AI model/service without rate and data limits, the AI
2424

2525
[/note]
2626

27-
To enable the AI Assistant, configure the [aiIntegration](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#aiIntegration) or **aiAssistant**.[aiIntegration](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/aiAssistant/#aiIntegration) object and set the **aiAssistant**.[enabled](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/aiAssistant/#enabled) property to `true`. Once activated, the DataGrid adds a predefined item (*"aiAssistantButton"*) to the toolbar. This button opens the AI Assistant Chat in a draggable pop-up window.
27+
To enable the AI Assistant, configure the [aiIntegration](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/#aiIntegration) or **aiAssistant**.[aiIntegration](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/aiAssistant/#aiIntegration) object and set the **aiAssistant**.[enabled](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/aiAssistant/#enabled) property to `true`. Once activated, the DataGrid adds a predefined item (*"aiAssistantButton"*) to the toolbar. This button opens our AI Assistant Chat in a draggable pop-up window.
2828

2929
This demo also configures [suggestions](/Documentation/ApiReference/UI_Components/dxChat/Configuration/#suggestions) for the AI Assistant Chat. These buttons allow you to interact with the assistant in one click using predefined prompts. For additional information about suggestions, refer to the following demo: [DevExtreme Chat - Prompt Suggestions](/Demos/WidgetsGallery/Demo/Chat/PromptSuggestions/).

packages/devextreme/js/__internal/ui/m_tag_box.ts

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ function xor(a: boolean, b: boolean): boolean {
3333
return (a || b) && !(a && b);
3434
}
3535

36+
type TagBoxItem = string | number | any;
37+
type SelectedItemsMap = Record<string, TagBoxItem>;
38+
3639
const TAGBOX_TAG_DATA_KEY = 'dxTagData';
3740
const TAGBOX_TAG_DISPLAY_VALUE = 'dxTagDisplayValue';
3841

@@ -51,13 +54,13 @@ const TEXTEDITOR_INPUT_CONTAINER_CLASS = 'dx-texteditor-input-container';
5154

5255
const TAGBOX_MOUSE_WHEEL_DELTA_MULTIPLIER = -0.3;
5356

54-
export interface TagBoxProperties extends Omit<Properties,
55-
'onCustomItemCreating'
56-
| 'onItemClick' | 'onSelectionChanged'
57-
| 'onOpened' | 'onClosed'
58-
| 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste'
59-
| 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> {
60-
57+
export interface TagBoxProperties extends Omit<
58+
Properties,
59+
'onCustomItemCreating'
60+
| 'onItemClick' | 'onSelectionChanged'
61+
| 'onOpened' | 'onClosed'
62+
| 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste'
63+
| 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> {
6164
}
6265

6366
class TagBox<
@@ -860,7 +863,7 @@ class TagBox<
860863
// @ts-expect-error ts-error
861864
const isListItemsLoaded = !!listSelectedItems && this._list._dataController.isLoaded();
862865
const selectedItems = listSelectedItems || this.option('selectedItems');
863-
// @ts-expect-error ts-error
866+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
864867
const clientFilterFunction = creator.getLocalFilter(this._valueGetter);
865868
// @ts-expect-error ts-error
866869
const filteredItems = selectedItems.filter(clientFilterFunction);
@@ -907,13 +910,13 @@ class TagBox<
907910
_createTagsData(values, filteredItems) {
908911
const items = [];
909912
const cache = {};
910-
// @ts-expect-error ts-error
913+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
911914
const isValueExprSpecified = this._valueGetterExpr() === 'this';
912915
const { acceptCustomValue } = this.option();
913916
const filteredValues = {};
914917

915918
filteredItems.forEach((filteredItem) => {
916-
// @ts-expect-error
919+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
917920
const filteredItemValue = isValueExprSpecified ? JSON.stringify(filteredItem) : this._valueGetter(filteredItem);
918921

919922
filteredValues[filteredItemValue] = filteredItem;
@@ -967,7 +970,7 @@ class TagBox<
967970
return item;
968971
}
969972
const selectedItem = this.option('selectedItem');
970-
// @ts-expect-error
973+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
971974
const customItem = this._valueGetter(selectedItem) === value ? selectedItem : value;
972975

973976
return customItem;
@@ -1046,7 +1049,10 @@ class TagBox<
10461049
this._selectedItems = this._getItemsFromPlain(this._valuesToUpdate);
10471050

10481051
if (this._selectedItems.length === this._valuesToUpdate.length) {
1049-
this._tagsToRender = this._selectedItems;
1052+
this._tagsToRender = this._sortSelectedItemsByValues(
1053+
this._selectedItems,
1054+
this._valuesToUpdate,
1055+
);
10501056
this._renderTagsImpl();
10511057
isPlainDataUsed = true;
10521058
d.resolve();
@@ -1113,6 +1119,45 @@ class TagBox<
11131119
return selectedItems;
11141120
}
11151121

1122+
_shouldUseClickOrderForTags(values: TagBox['_valuesToUpdate']): boolean {
1123+
const { maxDisplayedTags, showMultiTagOnly } = this.option();
1124+
1125+
return !showMultiTagOnly
1126+
&& isDefined(maxDisplayedTags)
1127+
&& values.length > maxDisplayedTags;
1128+
}
1129+
1130+
_sortSelectedItemsByValues(
1131+
selectedItems: TagBoxItem[],
1132+
values: TagBoxItem[],
1133+
): TagBoxItem[] {
1134+
if (!this._shouldUseClickOrderForTags(values) || !selectedItems.length) {
1135+
return selectedItems;
1136+
}
1137+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
1138+
const isValueExprDefault = this._valueGetterExpr() === 'this';
1139+
1140+
const mappedSelectedItems = selectedItems.reduce<SelectedItemsMap>((result, item) => {
1141+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
1142+
const itemValue = isValueExprDefault ? JSON.stringify(item) : this._valueGetter(item);
1143+
result[itemValue] = item;
1144+
1145+
return result;
1146+
}, {});
1147+
1148+
const selectedByOrderItems: TagBoxItem[] = values.reduce((result, currentValue) => {
1149+
const normalizedValue = isValueExprDefault ? JSON.stringify(currentValue) : currentValue;
1150+
const item = mappedSelectedItems[normalizedValue];
1151+
if (isDefined(item)) {
1152+
result.push(item);
1153+
}
1154+
1155+
return result;
1156+
}, []);
1157+
1158+
return selectedByOrderItems;
1159+
}
1160+
11161161
_filterSelectedItems(plainItems, values) {
11171162
const selectedItems = plainItems.filter((dataItem) => {
11181163
let currentValue;
@@ -1180,8 +1225,7 @@ class TagBox<
11801225

11811226
_renderTagsElements(items): void {
11821227
const $multiTag = this._multiTagRequired() && this._renderMultiTag(this._input());
1183-
const showMultiTagOnly = this.option('showMultiTagOnly');
1184-
const maxDisplayedTags = this.option('maxDisplayedTags');
1228+
const { showMultiTagOnly, maxDisplayedTags } = this.option();
11851229

11861230
items.forEach((item, index) => {
11871231
// @ts-expect-error ts-error
@@ -1206,7 +1250,7 @@ class TagBox<
12061250
const $tags = this._tagElements();
12071251

12081252
const selectedItems = this.option('selectedItems') ?? [];
1209-
// @ts-expect-error ts-error
1253+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
12101254
const values = selectedItems.map((item) => this._valueGetter(item));
12111255

12121256
each($tags, (_, tag) => {
@@ -1252,7 +1296,7 @@ class TagBox<
12521296
}
12531297

12541298
_renderTag(item, $input): void {
1255-
// @ts-expect-error ts-error
1299+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
12561300
const value = this._valueGetter(item);
12571301

12581302
if (!isDefined(value)) {
@@ -1399,12 +1443,12 @@ class TagBox<
13991443
const value = this._getValue().slice();
14001444

14011445
each(e.removedItems || [], (_, removedItem) => {
1402-
// @ts-expect-error ts-error
1446+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
14031447
this._removeTag(value, this._valueGetter(removedItem));
14041448
});
14051449

14061450
each(e.addedItems || [], (_, addedItem) => {
1407-
// @ts-expect-error ts-error
1451+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
14081452
this._addTag(value, this._valueGetter(addedItem));
14091453
});
14101454

@@ -1562,7 +1606,7 @@ class TagBox<
15621606
}
15631607

15641608
const dataController = this._dataController;
1565-
// @ts-expect-error ts-error
1609+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
15661610
const valueGetterExpr = this._valueGetterExpr();
15671611

15681612
if (isString(valueGetterExpr) && valueGetterExpr !== 'this') {
@@ -1584,14 +1628,14 @@ class TagBox<
15841628

15851629
_dataSourceFilterExpr() {
15861630
const filter = [];
1587-
// @ts-expect-error
1631+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
15881632
this._getValue().forEach((value) => filter.push(['!', [this._valueGetterExpr(), value]]));
15891633

15901634
return filter;
15911635
}
15921636

15931637
_dataSourceFilterFunction(itemData) {
1594-
// @ts-expect-error ts-error
1638+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
15951639
const itemValue = this._valueGetter(itemData);
15961640
let result = true;
15971641

@@ -1640,7 +1684,7 @@ class TagBox<
16401684

16411685
return this
16421686
._getPlainItems(this._list.option('selectedItems'))
1643-
// @ts-expect-error
1687+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
16441688
.map((item) => this._valueGetter(item));
16451689
}
16461690

@@ -1699,15 +1743,15 @@ class TagBox<
16991743
}
17001744

17011745
const previousItemsValuesMap = previousItems.reduce((map, item) => {
1702-
// @ts-expect-error ts-error
1746+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
17031747
const value = this._valueGetter(item);
17041748
map[value] = item;
17051749
return map;
17061750
}, {});
17071751

17081752
const addedItems = [];
17091753
newItems.forEach((item) => {
1710-
// @ts-expect-error ts-error
1754+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
17111755
const value = this._valueGetter(item);
17121756
if (!previousItemsValuesMap[value]) {
17131757
addedItems.push(item as never);

packages/devextreme/playground/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ window.addEventListener('load', () =>
2929
cardMinWidth: 320,
3030
columns: ['Company', 'Address', 'City', 'State', 'Zipcode', 'Phone'],
3131
});
32-
}));
32+
}));

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,62 @@ QUnit.module('multi tag support', {
10781078
assert.deepEqual(this.getTexts($tagBox.find('.' + TAGBOX_TAG_CLASS)), ['1', '2'], 'tags have correct text');
10791079
});
10801080

1081+
1082+
QUnit.test('TagBox should preserve reverse click order in leading tag when showMultiTagOnly is false', function(assert) {
1083+
const $tagBox = $('#tagBox').dxTagBox({
1084+
items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
1085+
showSelectionControls: true,
1086+
maxDisplayedTags: 2,
1087+
showMultiTagOnly: false,
1088+
opened: true
1089+
});
1090+
1091+
const tagBox = $tagBox.dxTagBox('instance');
1092+
1093+
this.clock.tick(TIME_TO_WAIT);
1094+
1095+
const $listItems = getListItems(tagBox);
1096+
1097+
$listItems.last().trigger('dxclick');
1098+
$listItems.eq(7).trigger('dxclick');
1099+
$listItems.eq(6).trigger('dxclick');
1100+
1101+
1102+
assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), '10', 'leading tag has correct text');
1103+
});
1104+
1105+
QUnit.test('TagBox should work correctly with string ID\'s in item when valueExpr is used', function(assert) {
1106+
const items = [
1107+
{ ID: 'a', Name: 'HD Video Player' },
1108+
{ ID: 'b', Name: 'SuperHD Video Player' },
1109+
{ ID: 'c', Name: 'SuperPlasma 50' },
1110+
{ ID: 'd', Name: 'SuperLED 50' }
1111+
];
1112+
const $tagBox = $('#tagBox').dxTagBox({
1113+
items: items,
1114+
valueExpr: 'ID',
1115+
displayExpr: 'Name',
1116+
showSelectionControls: true,
1117+
maxDisplayedTags: 2,
1118+
showMultiTagOnly: false,
1119+
opened: true
1120+
});
1121+
1122+
const tagBox = $tagBox.dxTagBox('instance');
1123+
1124+
this.clock.tick(TIME_TO_WAIT);
1125+
1126+
const $listItems = getListItems(tagBox);
1127+
1128+
$listItems.last().trigger('dxclick');
1129+
$listItems.eq(2).trigger('dxclick');
1130+
$listItems.eq(1).trigger('dxclick');
1131+
1132+
1133+
assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), items[items.length - 1].Name, 'leading tag has correct text');
1134+
});
1135+
1136+
10811137
QUnit.test('only one multi tag should be rendered when selectAll checked and value changind on runtime', function(assert) {
10821138
let suppressSelectionChanged = false;
10831139

pnpm-lock.yaml

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ catalog:
1111
inferno-create-element: ^8.2.3
1212
inferno-server: ^8.2.3
1313
devexpress-diagram: 2.2.29
14-
devexpress-gantt: 4.1.68
14+
devexpress-gantt: 4.1.69
1515
devextreme-exceljs-fork: 4.4.9
1616
devextreme-quill: 1.7.9
1717
eslint: 9.39.4

0 commit comments

Comments
 (0)