Skip to content

Commit fa1deaf

Browse files
r-farkhutdinovRuslan Farkhutdinovksercs
authored
HtmlEditor: Add AI Toolbar Item & AI Dialog components for a Text Transform feature (#29501)
Co-authored-by: Ruslan Farkhutdinov <[email protected]> Co-authored-by: ksercs <[email protected]>
1 parent db489c9 commit fa1deaf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1733
-166
lines changed

packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,29 @@ $transparent-border: 1px solid transparent;
352352
min-width: 360px;
353353
}
354354

355+
.dx-aidialog-controls {
356+
display: flex;
357+
gap: 8px;
358+
359+
.dx-selectbox {
360+
flex: 1 0 0;
361+
max-width: calc(50% - 4px);
362+
}
363+
}
364+
365+
.dx-aidialog-content {
366+
display: flex;
367+
flex-direction: column;
368+
gap: 12px;
369+
}
370+
371+
.dx-aidialog-title {
372+
display: flex;
373+
align-items: center;
374+
gap: 6px;
375+
font-weight: 600;
376+
}
377+
355378
.dx-overlay-content {
356379
&.dx-popup-fullscreen {
357380
.dx-formdialog-form {

packages/devextreme-themebuilder/tests/data/dependencies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const dependencies: FlatStylesDependencies = {
3636
gallery: [],
3737
toolbar: ['validation', 'button', 'loadindicator', 'loadpanel', 'scrollview', 'popup'],
3838
contextmenu: ['validation', 'button', 'loadindicator', 'textbox'],
39-
htmleditor: ['validation', 'button', 'loadindicator', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'textbox', 'list', 'checkbox', 'selectbox', 'numberbox', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'calendar', 'datebox', 'form', 'buttongroup', 'colorbox', 'progressbar', 'fileuploader', 'contextmenu'],
39+
htmleditor: ['validation', 'button', 'loadindicator', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'textbox', 'list', 'checkbox', 'selectbox', 'numberbox', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'calendar', 'datebox', 'form', 'buttongroup', 'colorbox', 'progressbar', 'fileuploader', 'contextmenu', 'textarea'],
4040
sortable: [],
4141
lookup: ['validation', 'button', 'loadindicator', 'textbox', 'popup', 'loadpanel', 'scrollview', 'list', 'popover'],
4242
map: [],
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1);

packages/devextreme/js/__internal/grids/pivot_grid/fields_area/m_fields_area.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import { each } from '@js/core/utils/iterator';
66
import { setHeight, setWidth } from '@js/core/utils/style';
77
import Button from '@js/ui/button';
88
import Popup from '@js/ui/popup/ui.popup';
9+
import { capitalize } from '@ts/core/utils/capitalize';
910

1011
import { AreaItem } from '../area_item/m_area_item';
11-
import { capitalizeFirstLetter } from '../m_widget_utils';
1212

1313
const DIV = '<div>';
1414

1515
const AREA_DRAG_CLASS = 'dx-pivotgrid-drag-action';
1616

1717
function renderGroupConnector(field, nextField, prevField, $container) {
18-
if (prevField && prevField.groupName && prevField.groupName === field.groupName) {
18+
if (prevField?.groupName && prevField.groupName === field.groupName) {
1919
$(DIV).addClass('dx-group-connector').addClass('dx-group-connector-prev').appendTo($container);
2020
}
2121

22-
if (nextField && nextField.groupName && nextField.groupName === field.groupName) {
22+
if (nextField?.groupName && nextField.groupName === field.groupName) {
2323
$(DIV).addClass('dx-group-connector').addClass('dx-group-connector-next').appendTo($container);
2424
}
2525
}
@@ -44,7 +44,7 @@ const FieldsArea = AreaItem.inherit({
4444
},
4545

4646
isVisible() {
47-
return !!this.option('fieldPanel.visible') && this.option(`fieldPanel.show${capitalizeFirstLetter(this._area)}Fields`);
47+
return !!this.option('fieldPanel.visible') && this.option(`fieldPanel.show${capitalize(this._area)}Fields`);
4848
},
4949

5050
_renderButton(element) {

packages/devextreme/js/__internal/grids/pivot_grid/m_widget_utils.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ function foreachDataLevel(data, callback, index, childrenField?) {
182182

183183
for (let i = 0; i < data.length; i += 1) {
184184
const item = data[i];
185-
if (item[childrenField] && item[childrenField].length) {
185+
if (item[childrenField]?.length) {
186186
foreachDataLevel(item[childrenField], callback, index + 1, childrenField);
187187
}
188188
}
@@ -365,10 +365,6 @@ const storeDrillDownMixin = {
365365
},
366366
};
367367

368-
function capitalizeFirstLetter(string) {
369-
return string.charAt(0).toUpperCase() + string.slice(1);
370-
}
371-
372368
const getScrollbarWidth = (
373369
containerElement,
374370
): number => containerElement.offsetWidth - containerElement.clientWidth;
@@ -408,14 +404,12 @@ export default {
408404
setDefaultFieldValueFormatting,
409405
getFiltersByPath,
410406
storeDrillDownMixin,
411-
capitalizeFirstLetter,
412407
getScrollbarWidth,
413408
calculateScrollbarWidth,
414409
};
415410

416411
export {
417412
calculateScrollbarWidth,
418-
capitalizeFirstLetter,
419413
createPath,
420414
discoverObjectFields,
421415
findField,

packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { Deferred, when } from '@js/core/utils/deferred';
66
import { extend } from '@js/core/utils/extend';
77
import { each } from '@js/core/utils/iterator';
88
import { isDefined, isString } from '@js/core/utils/type';
9+
import { capitalize } from '@ts/core/utils/capitalize';
910

1011
import pivotGridUtils, {
11-
capitalizeFirstLetter,
1212
getExpandedLevel,
1313
getFiltersByPath,
1414
setDefaultFieldValueFormatting,
@@ -38,7 +38,7 @@ function getFieldFilterSelector(field) {
3838
if (groupInterval.toLowerCase() === 'quarter') {
3939
groupInterval = 'Month';
4040
}
41-
selector = `${selector}.${capitalizeFirstLetter(groupInterval)}`;
41+
selector = `${selector}.${capitalize(groupInterval)}`;
4242
}
4343

4444
return selector;
@@ -99,7 +99,7 @@ function createFieldFilterExpressions(field, operation?) {
9999
let currentExpression: any = [];
100100

101101
if (Array.isArray(filterValue)) {
102-
const parseLevelsRecursive = field.levels && field.levels.length;
102+
const parseLevelsRecursive = field.levels?.length;
103103

104104
if (parseLevelsRecursive) {
105105
currentExpression = createFieldFilterExpressions({
@@ -249,7 +249,7 @@ function parseResult(data, total, descriptions, result) {
249249
const { rowHash } = result;
250250
const { columnHash } = result;
251251

252-
if (total && total.summary) {
252+
if (total?.summary) {
253253
each(total.summary, (index, summary) => {
254254
setValue(
255255
result.values,
@@ -331,10 +331,10 @@ function parseResult(data, total, descriptions, result) {
331331
result.rows.push({});
332332
}
333333

334-
const currentRowIndex = rowItem && rowItem.index || result.grandTotalRowIndex;
335-
const currentColumnIndex = columnItem && columnItem.index || result.grandTotalColumnIndex;
334+
const currentRowIndex = rowItem?.index || result.grandTotalRowIndex;
335+
const currentColumnIndex = columnItem?.index || result.grandTotalColumnIndex;
336336

337-
each(item && item.summary || [], (i, summary) => {
337+
each(item?.summary || [], (i, summary) => {
338338
setValue(result.values, summary, currentRowIndex, currentColumnIndex, i);
339339
});
340340
});

packages/devextreme/js/__internal/ui/html_editor/m_html_editor.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import getWordMatcher from '@ts/ui/html_editor/matchers/m_wordLists';
3535
import FormDialog from '@ts/ui/html_editor/ui/m_formDialog';
3636
import { prepareScrollData } from '@ts/ui/text_box/m_utils.scroll';
3737

38+
import type { AiDialogResult, AiDialogShowPayload } from './ui/aiDialog';
39+
import AiDialog from './ui/aiDialog';
40+
3841
const HTML_EDITOR_CLASS = 'dx-htmleditor';
3942
const QUILL_CONTAINER_CLASS = 'dx-quill-container';
4043
const QUILL_CLIPBOARD_CLASS = 'ql-clipboard';
@@ -52,6 +55,8 @@ class HtmlEditor extends Editor<Properties> {
5255

5356
_formDialog!: FormDialog;
5457

58+
_aiDialog?: AiDialog;
59+
5560
_quillInstance?: any;
5661

5762
_cleanCallback!: Callback;
@@ -349,6 +354,8 @@ class HtmlEditor extends Editor<Properties> {
349354
super._renderContentImpl();
350355
this._renderHtmlEditor();
351356
this._renderFormDialog();
357+
358+
this._renderAiDialog();
352359
this._addKeyPressHandler();
353360

354361
return renderContentPromise;
@@ -547,7 +554,18 @@ class HtmlEditor extends Editor<Properties> {
547554
hideOnOutsideClick: true,
548555
}, this.option('formDialogOptions'));
549556

550-
this._formDialog = new FormDialog(this, userOptions);
557+
this._formDialog = new FormDialog(this.$element(), userOptions);
558+
}
559+
560+
_renderAiDialog(): void {
561+
// @ts-expect-error ts-error
562+
const { aiIntegration } = this.option();
563+
564+
if (!aiIntegration) {
565+
return;
566+
}
567+
568+
this._aiDialog = new AiDialog(this.$element(), aiIntegration);
551569
}
552570

553571
_getStylingModePrefix(): string {
@@ -815,6 +833,10 @@ class HtmlEditor extends Editor<Properties> {
815833
return this._formDialog.show(formConfig);
816834
}
817835

836+
showAiDialog(payload: AiDialogShowPayload): Promise<AiDialogResult> | undefined {
837+
return this._aiDialog?.show(payload);
838+
}
839+
818840
// eslint-disable-next-line @typescript-eslint/no-unused-vars
819841
formDialogOption(optionName, optionValue): void {
820842
// @ts-expect-error ts-error

packages/devextreme/js/__internal/ui/html_editor/modules/m_toolbar.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ import { each } from '@js/core/utils/iterator';
1313
import {
1414
isDefined, isEmptyObject, isObject, isString,
1515
} from '@js/core/utils/type';
16+
import type { AICommandName, AICustomCommand, AIToolbarItem } from '@js/ui/html_editor';
1617
import type { Item } from '@js/ui/toolbar';
1718
import Toolbar from '@js/ui/toolbar';
1819
import errors from '@js/ui/widget/ui.errors';
20+
import { capitalize } from '@ts/core/utils/capitalize';
1921
import Quill from 'devextreme-quill';
2022

23+
import {
24+
buildCommandsMap, defaultCommandNames, getDefaultOptionsByCommand,
25+
} from '../utils/ai';
2126
import { getTableFormats, TABLE_OPERATIONS } from '../utils/m_table_helper';
2227
import {
2328
applyFormat, getDefaultClickHandler, getFormatHandlers, ICON_MAP,
@@ -55,6 +60,8 @@ if (Quill) {
5560
u: 85,
5661
};
5762

63+
const TOOLBAR_AI_ITEM_NAME = 'ai';
64+
5865
const localize = (name) => localizationMessage.format(`dxHtmlEditor-${camelize(name)}`);
5966

6067
const localizeValue = (value, name) => {
@@ -288,6 +295,8 @@ if (Quill) {
288295
this._detectRenamedOptions(item);
289296
if (isObject(item)) {
290297
newItem = this._handleObjectItem(item);
298+
} else if (item === TOOLBAR_AI_ITEM_NAME) {
299+
resultItems.push(this._getToolbarItem(this._prepareAIMenuItemConfig(item)));
291300
} else if (isString(item)) {
292301
const buttonItemConfig = this._prepareButtonItemConfig(item);
293302
newItem = this._getToolbarItem(buttonItemConfig);
@@ -301,16 +310,23 @@ if (Quill) {
301310
}
302311

303312
_handleObjectItem(item) {
313+
if (item.name === TOOLBAR_AI_ITEM_NAME) {
314+
return this._getToolbarItem(this._prepareAIMenuItemConfig(item));
315+
}
316+
304317
if (item.name && item.acceptedValues && this._isAcceptableItem(item.widget, 'dxSelectBox')) {
305318
const selectItemConfig = this._prepareSelectItemConfig(item);
306319

307320
return this._getToolbarItem(selectItemConfig);
308-
} if (item.name && this._isAcceptableItem(item.widget, 'dxButton')) {
321+
}
322+
323+
if (item.name && this._isAcceptableItem(item.widget, 'dxButton')) {
309324
const defaultButtonItemConfig = this._prepareButtonItemConfig(item.name);
310325
const buttonItemConfig = extend(true, defaultButtonItemConfig, item);
311326

312327
return this._getToolbarItem(buttonItemConfig);
313328
}
329+
314330
return this._getToolbarItem(item);
315331
}
316332

@@ -358,6 +374,92 @@ if (Quill) {
358374
}, item);
359375
}
360376

377+
private _createCommandMenuItem(
378+
command: AICommandName,
379+
text?: string,
380+
commandOptions?: string[],
381+
) {
382+
const options = commandOptions ?? getDefaultOptionsByCommand(command)?.map(capitalize);
383+
384+
return {
385+
id: command,
386+
text: text ?? defaultCommandNames[command],
387+
items: options?.map((option) => ({
388+
id: option,
389+
text: option,
390+
parentCommand: command,
391+
options: options?.map(capitalize),
392+
})),
393+
};
394+
}
395+
396+
private _buildMenuItems(commands: AIToolbarItem['commands']) {
397+
return commands?.map((command) => {
398+
if (typeof command === 'object') {
399+
if (command.name === 'custom') {
400+
return {
401+
id: 'custom',
402+
text: command.text,
403+
items: command.options?.map((option) => ({
404+
parentCommand: 'custom',
405+
id: option,
406+
text: option,
407+
options: command.options.map(capitalize),
408+
prompt,
409+
})),
410+
prompt: (command as AICustomCommand).prompt,
411+
};
412+
}
413+
414+
return this._createCommandMenuItem(command.name, command.text, command.options);
415+
}
416+
417+
return this._createCommandMenuItem(command);
418+
});
419+
}
420+
421+
_prepareAIMenuItemConfig(item: AIToolbarItem) {
422+
const {
423+
name = TOOLBAR_AI_ITEM_NAME,
424+
commands = Object.keys(defaultCommandNames) as AICommandName[],
425+
} = item;
426+
427+
const commandsMap = buildCommandsMap(commands);
428+
const menuItems = this._buildMenuItems(commands);
429+
430+
const dataSource = [{
431+
id: 'root',
432+
icon: 'sparkle',
433+
items: menuItems,
434+
}];
435+
436+
const options = {
437+
dataSource,
438+
onItemClick: (e): void => {
439+
const { itemData } = e;
440+
441+
if (itemData.items?.length) {
442+
return;
443+
}
444+
445+
const aiDialogOptions = {
446+
command: itemData.id,
447+
parentCommand: itemData.parentCommand,
448+
commandsMap,
449+
prompt: itemData.prompt,
450+
};
451+
452+
this._formatHandlers[name](aiDialogOptions);
453+
},
454+
};
455+
456+
return extend(true, {
457+
widget: 'dxMenu',
458+
name,
459+
options,
460+
}, typeof item === 'string' ? {} : item);
461+
}
462+
361463
_hideAdaptiveMenu() {
362464
if (this.toolbarInstance.option('overflowMenuVisible')) {
363465
this.toolbarInstance.option('overflowMenuVisible', false);

0 commit comments

Comments
 (0)