From 1676b6c93664cc06fc9f4cc0511d9e2d865d15e4 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 16 Apr 2025 18:21:51 +0300 Subject: [PATCH 01/14] HTMLEditor: Add storybook story & update AI dialog controls --- .../stories/htmleditor/HtmlEditor.stories.tsx | 84 +++++++++++++++++++ .../stories/htmleditor/data.ts | 62 ++++++++++++++ .../__internal/ui/html_editor/ui/aiDialog.ts | 8 +- .../htmlEditorParts/aiDialog.tests.js | 2 +- 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx create mode 100644 apps/react-storybook/stories/htmleditor/data.ts diff --git a/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx new file mode 100644 index 000000000000..f71ab133787e --- /dev/null +++ b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { HtmlEditor, Toolbar, Item, IHtmlEditorOptions, IItemProps } from 'devextreme-react/html-editor'; +import type { Meta, StoryObj } from '@storybook/react'; +import { defaultToolbarItems, fullToolbarItems } from './data'; + +const meta: Meta = { + title: 'Editors/HtmlEditor', + component: HtmlEditor, +}; + +export default meta; + +type HtmlEditorRenderArgs = IHtmlEditorOptions & { + items: IItemProps[], +}; + +export const Overview: StoryObj = { + argTypes: { + items: { + options: ['default', 'full'], + mapping: { + default: defaultToolbarItems, + full: fullToolbarItems, + }, + control: { + type: 'select', + labels: { + default: 'Default Toolbar', + full: 'Full Toolbar', + }, + }, + }, + rtlEnabled: { control: 'boolean' }, + readOnly: { control: 'boolean' }, + disabled: { control: 'boolean'}, + height: { control: 'number' }, + width: { control: 'text' }, + }, + args: { + items: defaultToolbarItems, + rtlEnabled: false, + readOnly: false, + disabled: false, + height: 500, + width: '100%', + }, + render: ({ items, ...editorProps }: HtmlEditorRenderArgs) => ( +
+ + + {items.map((item, index) => ( + + ))} + + +
+ ), + } + +export const WithAI: StoryObj = { + args: { + items: [ + ...defaultToolbarItems, + { name: 'separator' }, + // { name: 'ai' }, + ], + height: 500, + width: '100%', + }, + render: ({ items, ...editorProps }: HtmlEditorRenderArgs) => ( +
+ + + {items.map((item, index) => ( + + ))} + + +
+ ), + } \ No newline at end of file diff --git a/apps/react-storybook/stories/htmleditor/data.ts b/apps/react-storybook/stories/htmleditor/data.ts new file mode 100644 index 000000000000..38e0fa1936b5 --- /dev/null +++ b/apps/react-storybook/stories/htmleditor/data.ts @@ -0,0 +1,62 @@ +export const defaultToolbarItems = [ + { name: 'bold' }, + { name: 'italic' }, + { name: 'underline' }, + { name: 'separator' }, + { name: 'undo' }, + { name: 'redo' }, + ]; + + export const fullToolbarItems = [ + { name: 'undo' }, + { name: 'redo' }, + { name: 'separator' }, + { + name: 'size', + acceptedValues: ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'], + options: { placeholder: 'Font size' }, + }, + { + name: 'font', + acceptedValues: ['Arial', 'Courier New', 'Georgia', 'Impact', 'Lucida Console', 'Tahoma', 'Times New Roman', 'Verdana'], + options: { placeholder: 'Font' }, + }, + { name: 'separator' }, + { name: 'bold' }, + { name: 'italic' }, + { name: 'strike' }, + { name: 'underline' }, + { name: 'separator' }, + { name: 'alignLeft' }, + { name: 'alignCenter' }, + { name: 'alignRight' }, + { name: 'alignJustify' }, + { name: 'separator' }, + { name: 'orderedList' }, + { name: 'bulletList' }, + { name: 'separator' }, + { + name: 'header', + acceptedValues: [false, 1, 2, 3, 4, 5], + options: { placeholder: 'Header' }, + }, + { name: 'separator' }, + { name: 'color' }, + { name: 'background' }, + { name: 'separator' }, + { name: 'link' }, + { name: 'image' }, + { name: 'separator' }, + { name: 'clear' }, + { name: 'codeBlock' }, + { name: 'blockquote' }, + { name: 'separator' }, + { name: 'insertTable' }, + { name: 'deleteTable' }, + { name: 'insertRowAbove' }, + { name: 'insertRowBelow' }, + { name: 'deleteRow' }, + { name: 'insertColumnLeft' }, + { name: 'insertColumnRight' }, + { name: 'deleteColumn' }, + ]; diff --git a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts index a230a152c79c..aacc0b62e3c2 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -23,6 +23,8 @@ const AI_DIALOG_TITLE_CLASS = 'dx-aidialog-title'; const AI_DIALOG_TITLE_TEXT_CLASS = 'dx-aidialog-title-text'; const ICON_CLASS = 'dx-icon'; const ICON_SPARKLE_CLASS = 'dx-icon-sparkle'; +const COPY_BUTTON_ICON = 'copy'; +const TRY_AGAIN_BUTTON_ICON = 'redo'; const POPUP_MIN_WIDTH = 288; const POPUP_MAX_WIDTH = 460; @@ -251,6 +253,8 @@ export default class AIDialog extends BaseDialog { location: 'after', widget: 'dxButton', options: { + stylingMode: 'outlined', + icon: COPY_BUTTON_ICON, text: localizationMessage.format('dxHtmlEditor-aiCopy'), onClick: async (): Promise => { await navigator?.clipboard?.writeText(this._resultText); @@ -266,6 +270,8 @@ export default class AIDialog extends BaseDialog { location: 'after', widget: 'dxButton', options: { + stylingMode: 'outlined', + icon: TRY_AGAIN_BUTTON_ICON, text: localizationMessage.format('dxHtmlEditor-aiTryAgain'), onClick: () => this._retryAIRequest(), }, @@ -308,8 +314,8 @@ export default class AIDialog extends BaseDialog { case DialogState.ResultReady: items.push( this._getReplaceButtonItem(), - this._getTryAgainButtonItem(), this._getCopyButtonItem(), + this._getTryAgainButtonItem(), ); break; case DialogState.Asking: diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js index b63f6784f9f6..9833f34c9ed7 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js @@ -248,7 +248,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(promptTextAreaInstance.option('readOnly'), true, 'prompt TextArea is readOnly'); assert.strictEqual(resultTextAreaInstance.option('visible'), true, 'result TextArea is visible'); - assert.deepEqual(buttonTexts, ['Replace', 'Try again', 'Copy'], 'Toolbar contains correct buttons after generation'); + assert.deepEqual(buttonTexts, ['Replace', 'Copy', 'Try again'], 'Toolbar contains correct buttons after generation'); assert.strictEqual(replaceButtonItem.disabled, undefined, 'replace button is not disabled'); assert.strictEqual(copyButtonItem.disabled, undefined, 'copy button is not disabled'); }); From eb165fd3caa2fa6bddfb00b2ce9561878d06e65e Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 17 Apr 2025 13:03:02 +0300 Subject: [PATCH 02/14] HTMLEditor: Disable AI menu if empty command provided & set up AI storybook --- .../react-storybook/stories/htmleditor/HtmlEditor.stories.tsx | 4 +++- .../js/__internal/ui/html_editor/modules/m_toolbar.ts | 2 ++ .../devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx index f71ab133787e..961d9661a0b3 100644 --- a/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx +++ b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { HtmlEditor, Toolbar, Item, IHtmlEditorOptions, IItemProps } from 'devextreme-react/html-editor'; import type { Meta, StoryObj } from '@storybook/react'; import { defaultToolbarItems, fullToolbarItems } from './data'; +import { AIIntegration } from 'devextreme/artifacts/npm/devextreme/common/ai-integration'; const meta: Meta = { title: 'Editors/HtmlEditor', @@ -62,7 +63,7 @@ export const WithAI: StoryObj = { items: [ ...defaultToolbarItems, { name: 'separator' }, - // { name: 'ai' }, + { name: 'ai' } ], height: 500, width: '100%', @@ -72,6 +73,7 @@ export const WithAI: StoryObj = { {items.map((item, index) => ( diff --git a/packages/devextreme/js/__internal/ui/html_editor/modules/m_toolbar.ts b/packages/devextreme/js/__internal/ui/html_editor/modules/m_toolbar.ts index 9d6993cb32ce..c6bb89c1650e 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/modules/m_toolbar.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/modules/m_toolbar.ts @@ -1,6 +1,7 @@ import '@js/ui/select_box'; import '@ts/ui/color_box/m_color_view'; import '@js/ui/number_box'; +import '@js/ui/menu'; import eventsEngine from '@js/common/core/events/core/events_engine'; import { addNamespace } from '@js/common/core/events/utils/index'; @@ -451,6 +452,7 @@ if (Quill) { this._formatHandlers[name](aiDialogOptions); }, + disabled: !dataSource[0].items?.length, }; return extend(true, { diff --git a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts index aacc0b62e3c2..b21f3bab7dab 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -1,3 +1,5 @@ +import '@js/ui/drop_down_button'; + import type { AIIntegration } from '@js/common/ai-integration'; import localizationMessage from '@js/common/core/localization/message'; import type { dxElementWrapper } from '@js/core/renderer'; From 14603144cce9f4cab0f425afd8a0b9f7cb256566 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 17 Apr 2025 13:21:29 +0300 Subject: [PATCH 03/14] HTMLEditor: Fix dependencies list --- packages/devextreme-themebuilder/tests/data/dependencies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 85810049a16d..1df5b1ed3f32 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -38,7 +38,7 @@ export const dependencies: FlatStylesDependencies = { gallery: [], toolbar: ['validation', 'button', 'loadindicator', 'loadpanel', 'scrollview', 'popup'], contextmenu: ['validation', 'button', 'loadindicator', 'textbox'], - 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'], + 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', 'menu', 'dropdownbutton', 'treeview'], sortable: [], lookup: ['validation', 'button', 'loadindicator', 'textbox', 'popup', 'loadpanel', 'scrollview', 'list', 'popover'], map: [], From 9624a9ea395d000b63a666a53b8aeed8ee2a42bc Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 17 Apr 2025 18:36:26 +0300 Subject: [PATCH 04/14] HTMLEditor: Update Copy button location; make replace button split --- .../js/__internal/ui/html_editor/ui/aiDialog.ts | 16 ++++++++++------ packages/devextreme/testing/helpers/aiDialog.js | 17 ++++++++++++----- .../htmlEditorParts/aiDialog.tests.js | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts index b21f3bab7dab..d9bf6f93c1e2 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -5,7 +5,7 @@ import localizationMessage from '@js/common/core/localization/message'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; -import type { ItemClickEvent } from '@js/ui/drop_down_button_types'; +import type { ButtonClickEvent, ItemClickEvent } from '@js/ui/drop_down_button_types'; import type { AICustomCommand } from '@js/ui/html_editor'; import type { Properties as PopupProperties, ToolbarItem } from '@js/ui/popup'; import type dxSelectBox from '@js/ui/select_box'; @@ -58,7 +58,7 @@ export interface AIDialogShowPayload { export interface AIDialogResult { resultText: string; - event: ItemClickEvent; + event: ItemClickEvent | ButtonClickEvent & ItemClickEvent['itemData']; } export default class AIDialog extends BaseDialog { @@ -235,14 +235,18 @@ export default class AIDialog extends BaseDialog { text: localizationMessage.format('dxHtmlEditor-aiReplace'), stylingMode: 'contained', type: 'default', + splitButton: true, + useSelectMode: false, items: [ - { id: ReplaceButtonActions.Replace, text: localizationMessage.format('dxHtmlEditor-aiReplace') }, { id: ReplaceButtonActions.InsertAbove, text: localizationMessage.format('dxHtmlEditor-aiInsertAbove') }, { id: ReplaceButtonActions.InsertBelow, text: localizationMessage.format('dxHtmlEditor-aiInsertBelow') }, ], dropDownOptions: { width: REPLACE_DROPDOWN_WIDTH, }, + onButtonClick: (e: ButtonClickEvent): void => { + this.replaceButtonAction({ ...e, itemData: { id: ReplaceButtonActions.Replace } }); + }, onItemClick: (e: ItemClickEvent) => this.replaceButtonAction(e), }, ...config, @@ -252,7 +256,7 @@ export default class AIDialog extends BaseDialog { protected _getCopyButtonItem(config?: ToolbarItem): ToolbarItem { return { toolbar: 'bottom', - location: 'after', + location: 'before', widget: 'dxButton', options: { stylingMode: 'outlined', @@ -443,7 +447,7 @@ export default class AIDialog extends BaseDialog { this._aiIntegration = aiIntegration; } - replaceButtonAction(event: ItemClickEvent): void { + replaceButtonAction(event: AIDialogResult['event']): void { this.hide(this._resultText, event); } @@ -465,7 +469,7 @@ export default class AIDialog extends BaseDialog { return super.show(); } - hide(resultText: string, event: ItemClickEvent): void { + hide(resultText: string, event: AIDialogResult['event']): void { this.deferred?.resolve({ resultText, event }); super.hide(); diff --git a/packages/devextreme/testing/helpers/aiDialog.js b/packages/devextreme/testing/helpers/aiDialog.js index 5b734d6815b2..73b7dc3e08d8 100644 --- a/packages/devextreme/testing/helpers/aiDialog.js +++ b/packages/devextreme/testing/helpers/aiDialog.js @@ -36,7 +36,7 @@ const createCommandsMap = (isBasicCommand) => { }; const getDropDownButton = ($container) => { - return $container.find(`.${DROP_DOWN_BUTTON_CLASS} .${BUTTON_CLASS}`).eq(0); + return $container.find(`.${DROP_DOWN_BUTTON_CLASS} .${BUTTON_CLASS}`); }; const getDropDownButtonOption = (index) => { @@ -64,13 +64,20 @@ export const showAIDialog = (instance, { isBasicCommand, config } = {}) => { }; export const clickActionButton = (insertionMode) => { + const dropDownButtons = getDropDownButton($(`.${AI_DIALOG_CLASS}`)); + + if(insertionMode === 'replace') { + dropDownButtons.eq(0).trigger(CLICK_EVENT_NAME); + + return; + } + const insertionModeToIndexMap = { - replace: 0, - insertAbove: 1, - insertBelow: 2, + insertAbove: 0, + insertBelow: 1, }; - getDropDownButton($(`.${AI_DIALOG_CLASS}`)).trigger(CLICK_EVENT_NAME); + dropDownButtons.eq(1).trigger(CLICK_EVENT_NAME); getDropDownButtonOption(insertionModeToIndexMap[insertionMode]).trigger(CLICK_EVENT_NAME); }; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js index 9833f34c9ed7..78758a4e7d84 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js @@ -184,7 +184,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(toolbarItems.length, 4, '4 toolbar items rendered'); const dropDownItem = toolbarItems.find(item => item.widget === 'dxDropDownButton'); - assert.deepEqual(dropDownItem.options.items.map(i => i.id), ['replace', 'insertAbove', 'insertBelow'], 'DropDown has correct items'); + assert.deepEqual(dropDownItem.options.items.map(i => i.id), ['insertAbove', 'insertBelow'], 'DropDown has correct items'); }); QUnit.test('Should disable buttons while loading', function(assert) { From 30eac238f5b572303703282205227b4bfeff5275 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 22 Apr 2025 17:14:55 +0300 Subject: [PATCH 05/14] HTMLEditor: Update AI Dialog spacings for all themes --- .../scss/widgets/base/_htmlEditor.scss | 3 --- .../widgets/fluent/htmlEditor/_index.scss | 13 ++++++++++ .../widgets/fluent/htmlEditor/_sizes.scss | 9 +++++++ .../widgets/generic/htmlEditor/_index.scss | 13 ++++++++++ .../widgets/generic/htmlEditor/_sizes.scss | 9 +++++++ .../widgets/material/htmlEditor/_index.scss | 25 ++++++++++++++++--- .../widgets/material/htmlEditor/_sizes.scss | 12 +++++++++ .../__internal/ui/html_editor/ui/aiDialog.ts | 22 ++++++++-------- .../htmlEditorParts/aiDialog.tests.js | 8 +++--- 9 files changed, 94 insertions(+), 20 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss b/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss index efb62c504ef8..3191af410645 100644 --- a/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss +++ b/packages/devextreme-scss/scss/widgets/base/_htmlEditor.scss @@ -354,18 +354,15 @@ $transparent-border: 1px solid transparent; .dx-aidialog-controls { display: flex; - gap: 8px; .dx-selectbox { flex: 1 0 0; - max-width: calc(50% - 4px); } } .dx-aidialog-content { display: flex; flex-direction: column; - gap: 12px; } .dx-aidialog-title { diff --git a/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_index.scss index 2da32c0eb826..41533e0c6b81 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_index.scss @@ -302,3 +302,16 @@ .dx-formdialog.dx-dropdowneditor-overlay.dx-popup-wrapper .dx-overlay-content { box-shadow: $fluent-popup-content-shadow; } + +.dx-aidialog-controls { + gap: $fluent-aidialog-selects-gap; + + .dx-selectbox { + max-width: calc(50% - $fluent-aidialog-selects-gap * 0.5); + } +} + +.dx-aidialog-content { + padding: $fluent-aidialog-content-padding; + gap: $fluent-aidialog-content-gap; +} diff --git a/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_sizes.scss index 162674e830e3..758edbf2efef 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/htmlEditor/_sizes.scss @@ -11,6 +11,9 @@ $fluent-html-editor-toolbar-menu-separator-margin: 4px !default; $fluent-html-editor-fileuploader-input-wrapper-border-radius: 8px !default; $fluent-html-editor-fileuploader-input-wrapper-border-size: 1.5px !default; $fluent-html-editor-add-image-dialog-tabs-padding: null !default; +$fluent-aidialog-selects-gap: null !default; +$fluent-aidialog-content-gap: null !default; +$fluent-aidialog-content-padding: null !default; @if $size == "default" { $fluent-toolbar-size-editor-width: 120px !default; @@ -18,6 +21,9 @@ $fluent-html-editor-add-image-dialog-tabs-padding: null !default; $fluent-html-editor-horizontal-padding: 16px !default; $fluent-html-editor-add-image-dialog-fileuploader-padding: 48px 0 24px; $fluent-html-editor-add-image-dialog-tabs-padding: 14px !default; + $fluent-aidialog-selects-gap: 8px; + $fluent-aidialog-content-gap: 16px; + $fluent-aidialog-content-padding: 16px 24px; } @else if $size == "compact" { @@ -26,6 +32,9 @@ $fluent-html-editor-add-image-dialog-tabs-padding: null !default; $fluent-html-editor-horizontal-padding: 12px !default; $fluent-html-editor-add-image-dialog-fileuploader-padding: 40px 0 18px; $fluent-html-editor-add-image-dialog-tabs-padding: 8px !default; + $fluent-aidialog-selects-gap: 4px; + $fluent-aidialog-content-gap: 12px; + $fluent-aidialog-content-padding: 12px 16px; } $fluent-htmleditor-toolbar-padding: $fluent-html-editor-horizontal-padding !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss index 7310799117ff..89feeb18ebd8 100644 --- a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss @@ -298,3 +298,16 @@ background-color: $htmleditor-cells-separator-bg; } } + +.dx-aidialog-controls { + gap: $generic-aidialog-selects-gap; + + .dx-selectbox { + max-width: calc(50% - $generic-aidialog-selects-gap * 0.5); + } +} + +.dx-aidialog-content { + padding: $generic-aidialog-content-padding; + gap: $generic-aidialog-content-gap; +} diff --git a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss index 255a51f73b48..b63a8b943913 100644 --- a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_sizes.scss @@ -5,13 +5,22 @@ $generic-toolbar-size-editor-width: null !default; $generic-html-editor-add-image-dialog-base-padding: 5px !default; $generic-html-editor-add-image-dialog-fileuploader-padding: null !default; +$generic-aidialog-selects-gap: null !default; +$generic-aidialog-content-gap: null !default; +$generic-aidialog-content-padding: null !default; @if $size == "default" { $generic-toolbar-size-editor-width: 105px !default; $generic-html-editor-add-image-dialog-fileuploader-padding: 60px 20px !default; + $generic-aidialog-selects-gap: 10px; + $generic-aidialog-content-gap: 20px; + $generic-aidialog-content-padding: 20px; } @else if $size == "compact" { $generic-toolbar-size-editor-width: 80px !default; $generic-html-editor-add-image-dialog-fileuploader-padding: 40px 20px !default; + $generic-aidialog-selects-gap: 5px; + $generic-aidialog-content-gap: 10px; + $generic-aidialog-content-padding: 10px; } diff --git a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss index d48a3b7de9a6..349dfd79c940 100644 --- a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss @@ -337,8 +337,27 @@ } } -.dx-formdialog.dx-dropdowneditor-overlay.dx-popup-wrapper { - .dx-overlay-content { - box-shadow: $material-popup-overlay-content-shadow; +.dx-aidialog { + .dx-toolbar.dx-popup-bottom { + padding: $material-aidialog-toolbar-padding; + } + + .dx-formdialog.dx-dropdowneditor-overlay.dx-popup-wrapper { + .dx-overlay-content { + box-shadow: $material-popup-overlay-content-shadow; + } + } + + .dx-aidialog-controls { + gap: $material-aidialog-selects-gap; + + .dx-selectbox { + max-width: calc(50% - $material-aidialog-selects-gap * 0.5); + } + } + + .dx-aidialog-content { + padding: $material-aidialog-content-padding; + gap: $material-aidialog-content-gap; } } diff --git a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss index 1e4cbb5cd6d4..35087df3148a 100644 --- a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_sizes.scss @@ -6,15 +6,27 @@ $material-toolbar-size-editor-width: null !default; $material-htmleditor-toolbar-padding: null !default; $material-html-editor-add-image-dialog-fileuploader-padding: null !default; +$material-aidialog-selects-gap: null !default; +$material-aidialog-content-gap: null !default; +$material-aidialog-content-padding: null !default; +$material-aidialog-toolbar-padding: null !default; @if $size == "default" { $material-toolbar-size-editor-width: 120px !default; $material-htmleditor-toolbar-padding: 16px !default; $material-html-editor-add-image-dialog-fileuploader-padding: 50px 40px; + $material-aidialog-selects-gap: 8px; + $material-aidialog-content-gap: 24px; + $material-aidialog-content-padding: 24px; + $material-aidialog-toolbar-padding: 16px 24px; } @else if $size == "compact" { $material-toolbar-size-editor-width: 90px !default; $material-htmleditor-toolbar-padding: 11px !default; $material-html-editor-add-image-dialog-fileuploader-padding: 40px 30px; + $material-aidialog-selects-gap: 8px; + $material-aidialog-content-gap: 16px; + $material-aidialog-content-padding: 16px; + $material-aidialog-toolbar-padding: 8px 16px; } diff --git a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts index d9bf6f93c1e2..e854f57f16f3 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -229,7 +229,7 @@ export default class AIDialog extends BaseDialog { protected _getReplaceButtonItem(config?: ToolbarItem): ToolbarItem { return { toolbar: 'bottom', - location: 'before', + location: 'after', widget: 'dxDropDownButton', options: { text: localizationMessage.format('dxHtmlEditor-aiReplace'), @@ -256,7 +256,7 @@ export default class AIDialog extends BaseDialog { protected _getCopyButtonItem(config?: ToolbarItem): ToolbarItem { return { toolbar: 'bottom', - location: 'before', + location: 'after', widget: 'dxButton', options: { stylingMode: 'outlined', @@ -273,7 +273,7 @@ export default class AIDialog extends BaseDialog { protected _getTryAgainButtonItem(): ToolbarItem { return { toolbar: 'bottom', - location: 'after', + location: 'before', widget: 'dxButton', options: { stylingMode: 'outlined', @@ -287,11 +287,11 @@ export default class AIDialog extends BaseDialog { protected _getGenerateButtonItem(config?: ToolbarItem): ToolbarItem { return { toolbar: 'bottom', - location: 'before', + location: 'after', widget: 'dxButton', options: { - text: localizationMessage.format('dxHtmlEditor-aiGenerate'), type: 'default', + text: localizationMessage.format('dxHtmlEditor-aiGenerate'), stylingMode: 'contained', onClick: () => this._generateAIResponse(), }, @@ -305,6 +305,8 @@ export default class AIDialog extends BaseDialog { location: 'after', widget: 'dxButton', options: { + type: 'default', + stylingMode: 'contained', text: localizationMessage.format('dxHtmlEditor-aiStop'), onClick: () => this._stopGeneration(), }, @@ -319,19 +321,19 @@ export default class AIDialog extends BaseDialog { case DialogState.Initial: case DialogState.ResultReady: items.push( - this._getReplaceButtonItem(), - this._getCopyButtonItem(), this._getTryAgainButtonItem(), + this._getCopyButtonItem(), + this._getReplaceButtonItem(), ); break; case DialogState.Asking: - items.push(this._getGenerateButtonItem(), this._getStopButtonItem({ disabled: true })); + items.push(this._getStopButtonItem({ disabled: true }), this._getGenerateButtonItem()); break; case DialogState.Generating: items.push( - this._getReplaceButtonItem({ disabled: true }), - this._getCopyButtonItem({ disabled: true }), this._getStopButtonItem(), + this._getCopyButtonItem({ disabled: true }), + this._getReplaceButtonItem({ disabled: true }), ); break; case DialogState.Error: diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js index 78758a4e7d84..a0a4f74038f9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js @@ -223,7 +223,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(resultTextAreaInstance.option('visible'), false, 'result TextArea is hidden initially'); assert.strictEqual(promptTextAreaInstance.option('readOnly'), false, 'prompt TextArea is not readOnly'); - assert.deepEqual(buttonTexts, ['Generate', 'Stop'], 'toolbar contains correct buttons for Ask AI mode'); + assert.deepEqual(buttonTexts, ['Stop', 'Generate'], 'toolbar contains correct buttons for Ask AI mode'); assert.strictEqual(generateButtonItem.disabled, undefined, 'generate button is not disabled'); assert.strictEqual(stopButtonItem.disabled, true, 'stop button is disabled'); }); @@ -248,7 +248,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(promptTextAreaInstance.option('readOnly'), true, 'prompt TextArea is readOnly'); assert.strictEqual(resultTextAreaInstance.option('visible'), true, 'result TextArea is visible'); - assert.deepEqual(buttonTexts, ['Replace', 'Copy', 'Try again'], 'Toolbar contains correct buttons after generation'); + assert.deepEqual(buttonTexts, ['Try again', 'Copy', 'Replace'], 'Toolbar contains correct buttons after generation'); assert.strictEqual(replaceButtonItem.disabled, undefined, 'replace button is not disabled'); assert.strictEqual(copyButtonItem.disabled, undefined, 'copy button is not disabled'); }); @@ -274,7 +274,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(promptTextAreaInstance.option('readOnly'), false, 'prompt TextArea is not readOnly'); assert.strictEqual(resultTextAreaInstance.option('visible'), false, 'result TextArea is hidden'); - assert.deepEqual(buttonTexts, ['Generate', 'Stop'], 'toolbar reset to Ask AI state with correct buttons'); + assert.deepEqual(buttonTexts, ['Stop', 'Generate'], 'toolbar reset to Ask AI state with correct buttons'); }); QUnit.test('Should reset fields when switching to a basic command', function(assert) { @@ -316,7 +316,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(resultTextAreaInstance.option('visible'), false, 'result TextArea is hidden'); assert.strictEqual(optionSelectBoxInstance.option('visible'), false, 'option SelectBox hidden for askAI'); - assert.deepEqual(buttonTexts, ['Generate', 'Stop'], 'toolbar contains correct buttons for Ask AI'); + assert.deepEqual(buttonTexts, ['Stop', 'Generate'], 'toolbar contains correct buttons for Ask AI'); }); }); }); From 3a0475c624da297d58152dd8d1272bb133e891d6 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 22 Apr 2025 18:18:20 +0300 Subject: [PATCH 06/14] HTMLEditor: Fix DropDownButton min width --- .../widgets/generic/htmlEditor/_index.scss | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss index 89feeb18ebd8..af8cd4f90988 100644 --- a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss @@ -299,15 +299,21 @@ } } -.dx-aidialog-controls { - gap: $generic-aidialog-selects-gap; +.dx-aidialog { + .dx-aidialog-controls { + gap: $generic-aidialog-selects-gap; - .dx-selectbox { - max-width: calc(50% - $generic-aidialog-selects-gap * 0.5); + .dx-selectbox { + max-width: calc(50% - $generic-aidialog-selects-gap * 0.5); + } } -} -.dx-aidialog-content { - padding: $generic-aidialog-content-padding; - gap: $generic-aidialog-content-gap; + .dx-aidialog-content { + padding: $generic-aidialog-content-padding; + gap: $generic-aidialog-content-gap; + } + + .dx-button { + min-width: auto; + } } From 549d183be05a9f7d677075d5370c57db241e9497 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 23 Apr 2025 12:53:53 +0300 Subject: [PATCH 07/14] HTMLEditor: AI Dialog update toolbar buttons order --- .../devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts index e854f57f16f3..54b62fce4ef7 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -33,6 +33,7 @@ const POPUP_MAX_WIDTH = 460; const REPLACE_DROPDOWN_WIDTH = 150; const TEXT_AREA_MIN_HEIGHT = 64; const TEXT_AREA_MAX_HEIGHT = 128; +const BUTTON_WIDTH = 100; enum DialogState { Initial = 'initial', @@ -290,6 +291,7 @@ export default class AIDialog extends BaseDialog { location: 'after', widget: 'dxButton', options: { + width: BUTTON_WIDTH, type: 'default', text: localizationMessage.format('dxHtmlEditor-aiGenerate'), stylingMode: 'contained', @@ -305,6 +307,7 @@ export default class AIDialog extends BaseDialog { location: 'after', widget: 'dxButton', options: { + width: BUTTON_WIDTH, type: 'default', stylingMode: 'contained', text: localizationMessage.format('dxHtmlEditor-aiStop'), @@ -327,13 +330,11 @@ export default class AIDialog extends BaseDialog { ); break; case DialogState.Asking: - items.push(this._getStopButtonItem({ disabled: true }), this._getGenerateButtonItem()); + items.push(this._getGenerateButtonItem()); break; case DialogState.Generating: items.push( this._getStopButtonItem(), - this._getCopyButtonItem({ disabled: true }), - this._getReplaceButtonItem({ disabled: true }), ); break; case DialogState.Error: From cad1381e06a22bbd658a359824b073e1aa30ab7e Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 23 Apr 2025 15:03:13 +0300 Subject: [PATCH 08/14] HTMLEditor: Update Try Again button icon --- packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts index 54b62fce4ef7..9c45c1e53c12 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -26,7 +26,7 @@ const AI_DIALOG_TITLE_TEXT_CLASS = 'dx-aidialog-title-text'; const ICON_CLASS = 'dx-icon'; const ICON_SPARKLE_CLASS = 'dx-icon-sparkle'; const COPY_BUTTON_ICON = 'copy'; -const TRY_AGAIN_BUTTON_ICON = 'redo'; +const TRY_AGAIN_BUTTON_ICON = 'restore'; const POPUP_MIN_WIDTH = 288; const POPUP_MAX_WIDTH = 460; From 32e2e7df2eb14941d591088f1025a6f772e1a637 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 23 Apr 2025 16:09:35 +0300 Subject: [PATCH 09/14] HTMLEditor: Update tests --- .../htmlEditorParts/aiDialog.tests.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js index a0a4f74038f9..a416543dad4f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/aiDialog.tests.js @@ -187,7 +187,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.deepEqual(dropDownItem.options.items.map(i => i.id), ['insertAbove', 'insertBelow'], 'DropDown has correct items'); }); - QUnit.test('Should disable buttons while loading', function(assert) { + QUnit.test('Should display only stop button while loading', function(assert) { showAIDialog(this, { config: { currentCommand: 'translate' } }); @@ -195,13 +195,9 @@ QUnit.module('AIDialog', moduleConfig, () => { this.setDialogState('generating'); const toolbarButtonItems = this.aiDialogPopup.option('toolbarItems').filter(item => ['dxButton', 'dxDropDownButton'].includes(item.widget)); - const stopButtonItem = toolbarButtonItems.find(item => item.options.text === 'Stop'); - const replaceButtonItem = toolbarButtonItems.find(item => item.options.text === 'Replace'); - const copyButtonItem = toolbarButtonItems.find(item => item.options.text === 'Copy'); + const buttonTexts = toolbarButtonItems.map(item => item.options.text); - assert.strictEqual(stopButtonItem.disabled, undefined, 'stop button is not disabled'); - assert.strictEqual(replaceButtonItem.disabled, true, 'generate button is disabled'); - assert.strictEqual(copyButtonItem.disabled, true, 'copy button not disabled'); + assert.deepEqual(buttonTexts, ['Stop'], 'toolbar contains correct buttons for Ask AI mode'); }); QUnit.module('Ask AI', () => { @@ -216,16 +212,14 @@ QUnit.module('AIDialog', moduleConfig, () => { const toolbarButtonItems = this.aiDialogPopup.option('toolbarItems').filter(item => item.widget === 'dxButton'); const generateButtonItem = toolbarButtonItems.find(item => item.options.text === 'Generate'); - const stopButtonItem = toolbarButtonItems.find(item => item.options.text === 'Stop'); const buttonTexts = toolbarButtonItems.map(item => item.options.text); assert.strictEqual(promptTextAreaInstance.option('visible'), true, 'prompt TextArea is visible'); assert.strictEqual(resultTextAreaInstance.option('visible'), false, 'result TextArea is hidden initially'); assert.strictEqual(promptTextAreaInstance.option('readOnly'), false, 'prompt TextArea is not readOnly'); - assert.deepEqual(buttonTexts, ['Stop', 'Generate'], 'toolbar contains correct buttons for Ask AI mode'); + assert.deepEqual(buttonTexts, ['Generate'], 'toolbar contains correct buttons for Ask AI mode'); assert.strictEqual(generateButtonItem.disabled, undefined, 'generate button is not disabled'); - assert.strictEqual(stopButtonItem.disabled, true, 'stop button is disabled'); }); QUnit.test('Should render correct content after generation', function(assert) { @@ -274,7 +268,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(promptTextAreaInstance.option('readOnly'), false, 'prompt TextArea is not readOnly'); assert.strictEqual(resultTextAreaInstance.option('visible'), false, 'result TextArea is hidden'); - assert.deepEqual(buttonTexts, ['Stop', 'Generate'], 'toolbar reset to Ask AI state with correct buttons'); + assert.deepEqual(buttonTexts, ['Generate'], 'toolbar reset to Ask AI state with correct buttons'); }); QUnit.test('Should reset fields when switching to a basic command', function(assert) { @@ -316,7 +310,7 @@ QUnit.module('AIDialog', moduleConfig, () => { assert.strictEqual(resultTextAreaInstance.option('visible'), false, 'result TextArea is hidden'); assert.strictEqual(optionSelectBoxInstance.option('visible'), false, 'option SelectBox hidden for askAI'); - assert.deepEqual(buttonTexts, ['Stop', 'Generate'], 'toolbar contains correct buttons for Ask AI'); + assert.deepEqual(buttonTexts, ['Generate'], 'toolbar contains correct buttons for Ask AI'); }); }); }); From b123657ab8cdac10272c5960400046806db88d85 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 23 Apr 2025 18:25:00 +0300 Subject: [PATCH 10/14] HTMLEditor: Update formdialog overlay styles & format story data --- .../stories/htmleditor/data.ts | 26 +++++++++---------- .../widgets/material/htmlEditor/_index.scss | 6 +++++ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/react-storybook/stories/htmleditor/data.ts b/apps/react-storybook/stories/htmleditor/data.ts index 38e0fa1936b5..d2ac665680ea 100644 --- a/apps/react-storybook/stories/htmleditor/data.ts +++ b/apps/react-storybook/stories/htmleditor/data.ts @@ -5,21 +5,21 @@ export const defaultToolbarItems = [ { name: 'separator' }, { name: 'undo' }, { name: 'redo' }, - ]; - - export const fullToolbarItems = [ +]; + +export const fullToolbarItems = [ { name: 'undo' }, { name: 'redo' }, { name: 'separator' }, { - name: 'size', - acceptedValues: ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'], - options: { placeholder: 'Font size' }, + name: 'size', + acceptedValues: ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'], + options: { placeholder: 'Font size' }, }, { - name: 'font', - acceptedValues: ['Arial', 'Courier New', 'Georgia', 'Impact', 'Lucida Console', 'Tahoma', 'Times New Roman', 'Verdana'], - options: { placeholder: 'Font' }, + name: 'font', + acceptedValues: ['Arial', 'Courier New', 'Georgia', 'Impact', 'Lucida Console', 'Tahoma', 'Times New Roman', 'Verdana'], + options: { placeholder: 'Font' }, }, { name: 'separator' }, { name: 'bold' }, @@ -36,9 +36,9 @@ export const defaultToolbarItems = [ { name: 'bulletList' }, { name: 'separator' }, { - name: 'header', - acceptedValues: [false, 1, 2, 3, 4, 5], - options: { placeholder: 'Header' }, + name: 'header', + acceptedValues: [false, 1, 2, 3, 4, 5], + options: { placeholder: 'Header' }, }, { name: 'separator' }, { name: 'color' }, @@ -59,4 +59,4 @@ export const defaultToolbarItems = [ { name: 'insertColumnLeft' }, { name: 'insertColumnRight' }, { name: 'deleteColumn' }, - ]; +]; diff --git a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss index 349dfd79c940..f677306608bd 100644 --- a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss @@ -337,6 +337,12 @@ } } +.dx-formdialog.dx-dropdowneditor-overlay.dx-popup-wrapper { + .dx-overlay-content { + box-shadow: $material-popup-overlay-content-shadow; + } +} + .dx-aidialog { .dx-toolbar.dx-popup-bottom { padding: $material-aidialog-toolbar-padding; From 6aee6e8716992939251e10deaa4cb115dcc0d6ff Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 23 Apr 2025 18:26:47 +0300 Subject: [PATCH 11/14] HTMLEditor: Remove treeview from dependencies --- packages/devextreme-themebuilder/tests/data/dependencies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 1df5b1ed3f32..c2c1ad739ac6 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -38,7 +38,7 @@ export const dependencies: FlatStylesDependencies = { gallery: [], toolbar: ['validation', 'button', 'loadindicator', 'loadpanel', 'scrollview', 'popup'], contextmenu: ['validation', 'button', 'loadindicator', 'textbox'], - 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', 'menu', 'dropdownbutton', 'treeview'], + 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', 'menu', 'dropdownbutton'], sortable: [], lookup: ['validation', 'button', 'loadindicator', 'textbox', 'popup', 'loadpanel', 'scrollview', 'list', 'popover'], map: [], From b6dee1e54782b68df5c341e7e7526dc332a3e0d9 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 23 Apr 2025 18:38:02 +0300 Subject: [PATCH 12/14] HTMLEditor: Add menu disabled state test --- .../htmlEditorParts/toolbarModule.tests.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js index 467649c04f33..a57c7fa9cdce 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.htmlEditor/htmlEditorParts/toolbarModule.tests.js @@ -46,6 +46,7 @@ const DROPDOWNEDITOR_ICON_CLASS = 'dx-dropdowneditor-icon'; const LIST_ITEM_CLASS = 'dx-list-item'; const POPUP_TITLE_CLASS = 'dx-popup-title'; const MENU_ITEM_CLASS = 'dx-menu-item'; +const MENU_CLASS = 'dx-menu'; const BOLD_FORMAT_CLASS = 'dx-bold-format'; const SIZE_FORMAT_CLASS = 'dx-size-format'; @@ -1578,6 +1579,17 @@ testModule('Toolbar AI menu', dialogAIModuleConfig, () => { const $menuItem = $(`.${MENU_ITEM_CLASS}`).last(); assert.strictEqual($menuItem.text(), 'Custom command', 'custom command rendered in menu'); }); + + QUnit.test('root menu item is disabled if commands list is empty', function(assert) { + this.options.items = [{ name: 'ai', commands: [] }]; + new Toolbar(this.quillMock, this.options); + + openAIToolbarMenu(this.$element); + + const menuInstance = $(`.${MENU_CLASS}`).dxMenu('instance'); + + assert.strictEqual(menuInstance.option('disabled'), true, 'menu is disabled'); + }); }); testModule('Toolbar with multiline mode', simpleModuleConfig, function() { From 7faa9a7160964fd66555e9e52a82f087c609401b Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 24 Apr 2025 10:48:12 +0300 Subject: [PATCH 13/14] HTMLEditor: Add treeview dependency --- packages/devextreme-themebuilder/tests/data/dependencies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index c2c1ad739ac6..1df5b1ed3f32 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -38,7 +38,7 @@ export const dependencies: FlatStylesDependencies = { gallery: [], toolbar: ['validation', 'button', 'loadindicator', 'loadpanel', 'scrollview', 'popup'], contextmenu: ['validation', 'button', 'loadindicator', 'textbox'], - 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', 'menu', 'dropdownbutton'], + 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', 'menu', 'dropdownbutton', 'treeview'], sortable: [], lookup: ['validation', 'button', 'loadindicator', 'textbox', 'popup', 'loadpanel', 'scrollview', 'list', 'popover'], map: [], From 56b5a4597d7e018302e7685b87b9c75a9bdbb15b Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 24 Apr 2025 10:49:34 +0300 Subject: [PATCH 14/14] HTMLEditor: Format story --- apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx index 961d9661a0b3..8fa3ef8cd1e1 100644 --- a/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx +++ b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx @@ -83,4 +83,5 @@ export const WithAI: StoryObj = { ), - } \ No newline at end of file + } + \ No newline at end of file