= {
+ 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() {