Skip to content

Commit 52be080

Browse files
Form SmartPaste: add smartPaste method to Form (#30878)
1 parent dfff4a1 commit 52be080

File tree

7 files changed

+631
-14
lines changed

7 files changed

+631
-14
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { isObject } from '@js/core/utils/type';
2+
import type { FormItemComponent, SimpleItem } from '@js/ui/form';
3+
4+
const getEditorTypeInfo = (editorType: FormItemComponent | undefined): string => {
5+
switch (editorType) {
6+
case 'dxDateBox':
7+
case 'dxCalendar':
8+
return 'date in ISO format';
9+
case 'dxDateRangeBox':
10+
return 'date range in ISO format, use pattern {start}:::{end}';
11+
case 'dxColorBox':
12+
return 'color in hex format';
13+
case 'dxCheckBox':
14+
case 'dxSwitch':
15+
return 'boolean value, true or false';
16+
case 'dxNumberBox':
17+
case 'dxSlider':
18+
return 'numeric value';
19+
case 'dxRangeSlider':
20+
return 'numeric range, use pattern {start}:::{end}';
21+
default:
22+
return 'text';
23+
}
24+
};
25+
26+
const getItemsAcceptedValuesInfo = (editorOptions: SimpleItem['editorOptions']): string => {
27+
if (!editorOptions?.items) {
28+
return '';
29+
}
30+
31+
const items = editorOptions.items.map((item: { text?: string } | string) => {
32+
if (isObject(item)) {
33+
return item.text;
34+
}
35+
36+
return item;
37+
});
38+
39+
const acceptedValues = `, accepted values: ${items.join(', ')}, split values with :::`;
40+
const customItemsAllowed = editorOptions?.acceptCustomValue ? ' (custom values are allowed)' : '';
41+
42+
return `${acceptedValues}${customItemsAllowed}`;
43+
};
44+
45+
export const getItemFormatInfo = ({ editorType, editorOptions }: SimpleItem): string => {
46+
const dataType = getEditorTypeInfo(editorType);
47+
const acceptedValues = getItemsAcceptedValuesInfo(editorOptions);
48+
49+
return `${dataType}${acceptedValues}`;
50+
};

packages/devextreme/js/__internal/ui/form/form.items_runtime_info.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type PreparedItem<T = Item> = T & {
2323

2424
export type TabItem = NonNullable<TabbedItem['tabs']>[number];
2525
export type PreparedTabItem = PreparedItem<TabItem>;
26+
export type SimpleItemWithDataField = SimpleItem & Required<Pick<SimpleItem, 'dataField'>>;
2627

2728
export interface PreparedGroupedItem extends PreparedItem<GroupItem> {
2829
_prepareGroupCaptionTemplate?: (captionTemplate?: template | ((
@@ -212,4 +213,35 @@ export default class FormItemsRunTimeInfo {
212213
});
213214
filteredKeys.forEach((key) => this.removeItemByKey(key));
214215
}
216+
217+
_isEditableItem(item: SimpleItem): boolean {
218+
const { visible: itemVisible, editorOptions } = item;
219+
const { readOnly, disabled, visible } = editorOptions ?? {};
220+
221+
return itemVisible !== false && !readOnly && !disabled && visible !== false;
222+
}
223+
224+
_isItemAIEnabled(item: SimpleItem): boolean {
225+
// @ts-expect-error
226+
return !item.aiOptions?.disabled;
227+
}
228+
229+
_isDataItem(item: PreparedItem): item is SimpleItemWithDataField {
230+
return 'dataField' in item;
231+
}
232+
233+
getVisibleItems(): FormItemRuntimeInfo[] {
234+
const allItems = Object.values(this._map);
235+
236+
return allItems.filter(({ $itemContainer }) => $itemContainer?.css('visibility') === 'visible');
237+
}
238+
239+
getItemsForDataExtraction(): SimpleItemWithDataField[] {
240+
const visibleItems = this.getVisibleItems().map(({ item }) => item);
241+
242+
return visibleItems
243+
.filter(this._isDataItem)
244+
.filter(this._isItemAIEnabled)
245+
.filter(this._isEditableItem);
246+
}
215247
}

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

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import '@js/ui/validation_summary';
22
import '@js/ui/validation_group';
33

44
import type { EditorStyle } from '@js/common';
5+
import type { RequestCallbacks, SmartPasteCommandParams, SmartPasteCommandResult } from '@js/common/ai-integration';
56
import eventsEngine from '@js/common/core/events/core/events_engine';
67
import { triggerResizeEvent, triggerShownEvent } from '@js/common/core/events/visibility_change';
78
import messageLocalization from '@js/common/core/localization/message';
@@ -26,7 +27,12 @@ import type { ChangedOptionInfo, EventInfo } from '@js/events';
2627
import type {
2728
FieldDataChangedEvent,
2829
FormItemType,
29-
GroupItem, Item, LabelLocation, Properties, SimpleItemTemplateData, TabbedItem,
30+
GroupItem,
31+
Item,
32+
LabelLocation,
33+
Properties,
34+
SimpleItemTemplateData,
35+
TabbedItem,
3036
} from '@js/ui/form';
3137
import { current, isMaterial, isMaterialBased } from '@js/ui/themes';
3238
import type { ValidationResult } from '@js/ui/validation_group';
@@ -37,9 +43,7 @@ import Widget, { FOCUSED_STATE_CLASS } from '@ts/core/widget/widget';
3743
import type { Button } from '@ts/ui/button/button';
3844
import { DROP_DOWN_EDITOR_CLASS } from '@ts/ui/drop_down_editor/m_drop_down_editor';
3945
import Editor from '@ts/ui/editor/editor';
40-
import {
41-
setLabelWidthByMaxLabelWidth,
42-
} from '@ts/ui/form/components/label';
46+
import { setLabelWidthByMaxLabelWidth } from '@ts/ui/form/components/label';
4347
import {
4448
FIELD_ITEM_CLASS,
4549
FIELD_ITEM_CONTENT_CLASS,
@@ -58,6 +62,7 @@ import {
5862
GROUP_COL_COUNT_CLASS,
5963
ROOT_SIMPLE_ITEM_CLASS,
6064
} from '@ts/ui/form/constants';
65+
import { getItemFormatInfo } from '@ts/ui/form/form.ai.utils';
6166
import type { ItemOptionActionType } from '@ts/ui/form/form.item_options_actions';
6267
import tryCreateItemOptionAction from '@ts/ui/form/form.item_options_actions';
6368
import type {
@@ -91,6 +96,21 @@ import TabPanel from '@ts/ui/tab_panel/tab_panel';
9196
import { TEXTEDITOR_CLASS, TEXTEDITOR_INPUT_CLASS } from '@ts/ui/text_box/m_text_editor.base';
9297
import { TOOLBAR_CLASS } from '@ts/ui/toolbar/constants';
9398

99+
export type FormAICommandName = 'smartPaste';
100+
export interface AICommandParamsMap {
101+
smartPaste: SmartPasteCommandParams;
102+
}
103+
104+
export interface AICommandResultMap {
105+
smartPaste: SmartPasteCommandResult;
106+
}
107+
108+
interface AICommandWithParams<T extends FormAICommandName> {
109+
command: T;
110+
params: AICommandParamsMap[T];
111+
callbacks: RequestCallbacks<AICommandResultMap[T]>;
112+
}
113+
94114
const ITEM_OPTIONS_FOR_VALIDATION_UPDATING = ['items', 'isRequired', 'validationRules', 'visible'];
95115

96116
export interface FormProperties extends Properties {
@@ -104,6 +124,10 @@ export interface FormProperties extends Properties {
104124
}
105125

106126
class Form extends Widget<FormProperties> {
127+
private _abort?: () => void;
128+
129+
private _currentAICommand?: AICommandWithParams<FormAICommandName> = undefined;
130+
107131
_targetScreenFactor?: ScreenSizeQualifier;
108132

109133
_lastMarkupScreenFactor!: ScreenSizeQualifier;
@@ -1100,6 +1124,11 @@ class Form extends Widget<FormProperties> {
11001124
ValidationEngine.removeGroup(args.previousValue || this);
11011125
this._invalidate();
11021126
break;
1127+
// @ts-expect-error
1128+
case 'aiIntegration': {
1129+
this._processAIIntegrationUpdate();
1130+
break;
1131+
}
11031132
default:
11041133
super._optionChanged(args);
11051134
}
@@ -1760,6 +1789,71 @@ class Form extends Widget<FormProperties> {
17601789
getTargetScreenFactor(): ScreenSizeQualifier | undefined {
17611790
return this._targetScreenFactor;
17621791
}
1792+
1793+
private _processCommandCompletion(): void {
1794+
this._abort?.();
1795+
this._abort = undefined;
1796+
this._currentAICommand = undefined;
1797+
}
1798+
1799+
private _processAIIntegrationUpdate(): void {
1800+
if (this._currentAICommand) {
1801+
const { command, params, callbacks } = this._currentAICommand;
1802+
1803+
this._processCommandCompletion();
1804+
this._executeAICommand(command, params, callbacks);
1805+
}
1806+
}
1807+
1808+
private _executeAICommand<T extends FormAICommandName>(
1809+
command: T,
1810+
params: AICommandParamsMap[T],
1811+
callbacks: RequestCallbacks<AICommandResultMap[T]>,
1812+
): void {
1813+
// @ts-expect-error
1814+
const { aiIntegration } = this.option();
1815+
1816+
this._currentAICommand = {
1817+
command,
1818+
params,
1819+
callbacks,
1820+
};
1821+
this._abort = aiIntegration?.[command](params, callbacks);
1822+
}
1823+
1824+
private _getSmartPasteCommandCallbacks(): RequestCallbacks<SmartPasteCommandResult> {
1825+
return {
1826+
onComplete: (fieldsData: SmartPasteCommandResult): void => {
1827+
this.beginUpdate();
1828+
fieldsData.forEach(({ name, value }: SmartPasteCommandResult[number]) => {
1829+
this._updateFieldValue(name, value);
1830+
});
1831+
this.endUpdate();
1832+
this._processCommandCompletion();
1833+
},
1834+
onError: (): void => {
1835+
this._processCommandCompletion();
1836+
},
1837+
};
1838+
}
1839+
1840+
async smartPaste(text?: string): Promise<void> {
1841+
const dataItems = this._itemsRunTimeInfo.getItemsForDataExtraction();
1842+
const fields = dataItems.map((item) => ({
1843+
name: item.dataField,
1844+
format: getItemFormatInfo(item),
1845+
// @ts-expect-error
1846+
instruction: item.aiOptions?.instruction,
1847+
}));
1848+
1849+
const smartPasteParams = {
1850+
text: text ?? await navigator.clipboard.readText(),
1851+
fields,
1852+
};
1853+
const smartPasteCallbacks = this._getSmartPasteCommandCallbacks();
1854+
1855+
this._executeAICommand('smartPaste', smartPasteParams, smartPasteCallbacks);
1856+
}
17631857
}
17641858

17651859
registerComponent('dxForm', Form);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { getItemFormatInfo } from '__internal/ui/form/form.ai.utils';
2+
3+
const formItemComponents = [
4+
'dxAutocomplete',
5+
'dxCalendar',
6+
'dxCheckBox',
7+
'dxColorBox',
8+
'dxDateBox',
9+
'dxDateRangeBox',
10+
'dxDropDownBox',
11+
'dxHtmlEditor',
12+
'dxLookup',
13+
'dxNumberBox',
14+
'dxRadioGroup',
15+
'dxRangeSlider',
16+
'dxSelectBox',
17+
'dxSlider',
18+
'dxSwitch',
19+
'dxTagBox',
20+
'dxTextArea',
21+
'dxTextBox',
22+
];
23+
24+
QUnit.module('getItemFormatInfo', () => {
25+
formItemComponents.forEach((editorType) => {
26+
QUnit.test(`should return correct format for ${editorType}`, function(assert) {
27+
const format = getItemFormatInfo({ editorType });
28+
const expectedFormats = {
29+
dxDateBox: 'date in ISO format',
30+
dxCalendar: 'date in ISO format',
31+
dxDateRangeBox: 'date range in ISO format, use pattern {start}:::{end}',
32+
dxColorBox: 'color in hex format',
33+
dxCheckBox: 'boolean value, true or false',
34+
dxSwitch: 'boolean value, true or false',
35+
dxNumberBox: 'numeric value',
36+
dxSlider: 'numeric value',
37+
dxRangeSlider: 'numeric range, use pattern {start}:::{end}',
38+
};
39+
40+
assert.strictEqual(format, expectedFormats[editorType] || 'text', `${editorType} format is correct`);
41+
});
42+
});
43+
44+
QUnit.test('should list acceptable values if editorOptions.items defined', function(assert) {
45+
const format = getItemFormatInfo({
46+
editorType: 'dxSelectBox',
47+
editorOptions: { items: ['item1', 'item2', 'item3'] },
48+
});
49+
const expectedFormat = 'text, accepted values: item1, item2, item3, split values with :::';
50+
51+
assert.strictEqual(format, expectedFormat, 'list of acceptable values is correct');
52+
});
53+
54+
QUnit.test('should list acceptable values if editorOptions.items passed as objects', function(assert) {
55+
const format = getItemFormatInfo({
56+
editorType: 'dxSelectBox',
57+
editorOptions: {
58+
items: [
59+
{ text: 'item1', value: 1 },
60+
{ text: 'item2', value: 2 },
61+
{ text: 'item3', value: 3 },
62+
]
63+
},
64+
});
65+
const expectedFormat = 'text, accepted values: item1, item2, item3, split values with :::';
66+
67+
assert.strictEqual(format, expectedFormat, 'list of acceptable values is correct');
68+
});
69+
70+
QUnit.test('should allow custom values if editorOptions.acceptCustomValue = true', function(assert) {
71+
const format = getItemFormatInfo({
72+
editorType: 'dxSelectBox',
73+
editorOptions: {
74+
items: ['item1', 'item2', 'item3'],
75+
acceptCustomValue: true,
76+
},
77+
});
78+
const expectedFormat = 'text, accepted values: item1, item2, item3, split values with ::: (custom values are allowed)';
79+
80+
assert.strictEqual(format, expectedFormat, 'list of acceptable values is correct, custom values are allowed');
81+
});
82+
});

0 commit comments

Comments
 (0)