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..8fa3ef8cd1e1 --- /dev/null +++ b/apps/react-storybook/stories/htmleditor/HtmlEditor.stories.tsx @@ -0,0 +1,87 @@ +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', + 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..d2ac665680ea --- /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-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..af8cd4f90988 100644 --- a/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/htmlEditor/_index.scss @@ -298,3 +298,22 @@ background-color: $htmleditor-cells-separator-bg; } } + +.dx-aidialog { + .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; + } + + .dx-button { + min-width: auto; + } +} 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..f677306608bd 100644 --- a/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/htmlEditor/_index.scss @@ -342,3 +342,28 @@ 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-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: [], 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 a230a152c79c..9c45c1e53c12 100644 --- a/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts +++ b/packages/devextreme/js/__internal/ui/html_editor/ui/aiDialog.ts @@ -1,9 +1,11 @@ +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'; 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'; @@ -23,12 +25,15 @@ 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 = 'restore'; const POPUP_MIN_WIDTH = 288; 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', @@ -54,7 +59,7 @@ export interface AIDialogShowPayload { export interface AIDialogResult { resultText: string; - event: ItemClickEvent; + event: ItemClickEvent | ButtonClickEvent & ItemClickEvent['itemData']; } export default class AIDialog extends BaseDialog { @@ -225,20 +230,24 @@ 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'), 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, @@ -251,6 +260,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); @@ -263,9 +274,11 @@ export default class AIDialog extends BaseDialog { protected _getTryAgainButtonItem(): ToolbarItem { return { toolbar: 'bottom', - location: 'after', + location: 'before', widget: 'dxButton', options: { + stylingMode: 'outlined', + icon: TRY_AGAIN_BUTTON_ICON, text: localizationMessage.format('dxHtmlEditor-aiTryAgain'), onClick: () => this._retryAIRequest(), }, @@ -275,11 +288,12 @@ 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'), + width: BUTTON_WIDTH, type: 'default', + text: localizationMessage.format('dxHtmlEditor-aiGenerate'), stylingMode: 'contained', onClick: () => this._generateAIResponse(), }, @@ -293,6 +307,9 @@ export default class AIDialog extends BaseDialog { location: 'after', widget: 'dxButton', options: { + width: BUTTON_WIDTH, + type: 'default', + stylingMode: 'contained', text: localizationMessage.format('dxHtmlEditor-aiStop'), onClick: () => this._stopGeneration(), }, @@ -307,18 +324,16 @@ export default class AIDialog extends BaseDialog { case DialogState.Initial: case DialogState.ResultReady: items.push( - this._getReplaceButtonItem(), this._getTryAgainButtonItem(), this._getCopyButtonItem(), + this._getReplaceButtonItem(), ); break; case DialogState.Asking: - items.push(this._getGenerateButtonItem(), this._getStopButtonItem({ disabled: true })); + items.push(this._getGenerateButtonItem()); break; case DialogState.Generating: items.push( - this._getReplaceButtonItem({ disabled: true }), - this._getCopyButtonItem({ disabled: true }), this._getStopButtonItem(), ); break; @@ -435,7 +450,7 @@ export default class AIDialog extends BaseDialog { this._aiIntegration = aiIntegration; } - replaceButtonAction(event: ItemClickEvent): void { + replaceButtonAction(event: AIDialogResult['event']): void { this.hide(this._resultText, event); } @@ -457,7 +472,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 b63f6784f9f6..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 @@ -184,10 +184,10 @@ 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) { + 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, ['Generate', 'Stop'], '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) { @@ -248,7 +242,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, ['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 +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, ['Generate', 'Stop'], '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, ['Generate', 'Stop'], 'toolbar contains correct buttons for Ask AI'); + assert.deepEqual(buttonTexts, ['Generate'], 'toolbar contains correct buttons for Ask AI'); }); }); }); 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() {