diff --git a/apps/react-storybook/stories/tree_list/TreeList.stories.tsx b/apps/react-storybook/stories/tree_list/TreeList.stories.tsx index d5f5d1e2901c..5a5cb0f1a9c9 100644 --- a/apps/react-storybook/stories/tree_list/TreeList.stories.tsx +++ b/apps/react-storybook/stories/tree_list/TreeList.stories.tsx @@ -17,6 +17,7 @@ type EditingMode = typeof editingModes[number]; interface HumanReadableProps { showSelectCheckboxes: boolean; + aiColumnEnabled: boolean; showRowLines: boolean; rtlEnabled: boolean; editingMode: EditingMode; @@ -70,6 +71,7 @@ const mergeProps = ( ): Record => { const result = { ...defaultProps }; const { + aiColumnEnabled, showSelectCheckboxes, firstColumnContentType, showRowLines, @@ -77,6 +79,7 @@ const mergeProps = ( firstColumnFixed, editingMode, } = humanReadableProps; + const hasAIColumn = result.columns.some((col) => col?.type === 'ai'); result.selection = { mode: showSelectCheckboxes ? 'multiple' : 'none' }; @@ -87,6 +90,19 @@ const mergeProps = ( fixedPosition: firstColumnFixed === 'left' ? 'left' : 'right', } + if (aiColumnEnabled) { + if (!hasAIColumn) { + result.columns.unshift({ + type: 'ai', + caption: 'Smart Column', + name: 'myAIColumn', + width: 200, + }); + } + } else if (hasAIColumn) { + result.columns = result.columns.filter((col) => col?.type !== 'ai'); + } + result.showRowLines = showRowLines; result.rtlEnabled = rtlEnabled; @@ -143,7 +159,8 @@ type Story = StoryObj; export const Overview: Story = { args: { - showSelectCheckboxes: false, + aiColumnEnabled: true, + showSelectCheckboxes: true, showRowLines: false, rtlEnabled: false, editingMode: 'none', diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__default (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__default (fluent-blue-light).png index 19661d509f44..429b18e98b53 100644 Binary files a/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__default (fluent-blue-light).png and b/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__default (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__multiple-selection (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__multiple-selection (fluent-blue-light).png new file mode 100644 index 000000000000..1f20602dff97 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/etalons/treelist__ai-column__multiple-selection (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/visual.ts b/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/visual.ts index 46a2d6eeaee0..c48ff7e0035e 100644 --- a/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/visual.ts +++ b/e2e/testcafe-devextreme/tests/common/treeList/aiColumn/visual.ts @@ -50,3 +50,48 @@ test('Default render', async (t) => { }, ], })); + +test('AI Column when multiple selection is enabled', async (t) => { + // arrange, act + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const treeList = new TreeList(TREE_LIST_SELECTOR); + + await t.expect(treeList.isReady()).ok(); + + await testScreenshot(t, takeScreenshot, 'treelist__ai-column__multiple-selection.png', { element: treeList.element }); + + // assert + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxTreeList', { + dataSource: [ + { + id: 1, parentId: 0, name: 'Name 1', value: 10, + }, + { + id: 2, parentId: 1, name: 'Name 2', value: 20, + }, + { + id: 3, parentId: 0, name: 'Name 3', value: 30, + }, + { + id: 4, parentId: 3, name: 'Name 4', value: 40, + }, + ], + keyExpr: 'id', + parentIdExpr: 'parentId', + expandedRowKeys: [3], + selection: { + mode: 'multiple', + }, + columns: [ + { + type: 'ai', + caption: 'AI Column', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.functional.ts index bf982612c524..351a4b950aac 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.functional.ts @@ -18,7 +18,7 @@ test('Column reordering should work when allowColumnReordering is true', async ( await t.expect(await headerRow.getHeaderTexts()).eql(['AI Column', 'ID', 'Name', 'Value']); // act - await t.drag(headerRow.getHeaderCell(0).element, 100, 0); + await t.drag(headerRow.getHeaderCell(0).element, 150, 0); // assert await t.expect(await headerRow.getHeaderTexts()).eql(['ID', 'AI Column', 'Name', 'Value']); @@ -53,7 +53,7 @@ test('Column reordering should not work when allowColumnReordering is false', as await t.expect(await headerRow.getHeaderTexts()).eql(['AI Column', 'ID', 'Name', 'Value']); // act - await t.drag(headerRow.getHeaderCell(0).element, 100, 0); + await t.drag(headerRow.getHeaderCell(0).element, 150, 0); // assert await t.expect(await headerRow.getHeaderTexts()).eql(['AI Column', 'ID', 'Name', 'Value']); @@ -88,7 +88,7 @@ test('Column reordering should not work when it has allowReordering set to false await t.expect(await headerRow.getHeaderTexts()).eql(['AI Column', 'ID', 'Name', 'Value']); // act - await t.drag(headerRow.getHeaderCell(0).element, 100, 0); + await t.drag(headerRow.getHeaderCell(0).element, 150, 0); // assert await t.expect(await headerRow.getHeaderTexts()).eql(['AI Column', 'ID', 'Name', 'Value']); @@ -112,42 +112,3 @@ test('Column reordering should not work when it has allowReordering set to false { dataField: 'value', caption: 'Value' }, ], })); - -test('The draggable AI column should have a caption', async (t) => { - // arrange - const dataGrid = new DataGrid(DATA_GRID_SELECTOR); - - await t.expect(dataGrid.isReady()).ok(); - - // act - await dataGrid.moveHeader(0, 100, 5, true); - - // assert - await t - .expect(dataGrid.getDraggableHeader().visible).ok() - .expect(dataGrid.getDraggableHeader().innerText).eql('AI Column'); - - // act - await dataGrid.dropHeader(0); - - // assert - await t.expect(dataGrid.getDraggableHeader().visible).notOk(); -}).before(async () => createWidget('dxDataGrid', { - dataSource: [ - { id: 1, name: 'Name 1', value: 10 }, - { id: 2, name: 'Name 2', value: 20 }, - { id: 3, name: 'Name 3', value: 30 }, - ], - allowColumnReordering: true, - columnWidth: 125, - columns: [ - { - type: 'ai', - caption: 'AI Column', - name: 'myAiColumn', - }, - { dataField: 'id', caption: 'ID' }, - { dataField: 'name', caption: 'Name' }, - { dataField: 'value', caption: 'Value' }, - ], -})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.visual.ts new file mode 100644 index 000000000000..36c2b684ab10 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnReordering.visual.ts @@ -0,0 +1,52 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; + +fixture.disablePageReloads`Ai Column.ColumnReordering.Visual` + .page(url(__dirname, '../../../container.html')); + +const DATA_GRID_SELECTOR = '#container'; + +test('The draggable AI column should display correctly', async (t) => { + // arrange + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(dataGrid.isReady()).ok(); + + await dataGrid.moveHeader(0, 100, 5, true); + + // assert + await t.expect(dataGrid.getDraggableHeader().visible).ok(); + + await takeScreenshot('datagrid__ai-column__dragging.png', dataGrid.element); + + // act + await dataGrid.dropHeader(0); + + // assert + await t + .expect(dataGrid.getDraggableHeader().visible) + .notOk() + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + allowColumnReordering: true, + columnWidth: 200, + columns: [ + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.functional.ts index 3a0c2fa8331c..e9402da82a34 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.functional.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.functional.ts @@ -20,13 +20,13 @@ const DATA_GRID_SELECTOR = '#container'; .expect(dataGrid.getHeaders().getHeaderRow(0).getHeaderCell(0).element.textContent) .eql('AI Column') .expect(dataCell.element.clientWidth) - .eql(100); + .eql(120); // act await dataGrid.resizeHeader(1, 50); // assert - await t.expect(dataCell.element.clientWidth).eql(150); + await t.expect(dataCell.element.clientWidth).eql(170); }).before(async () => createWidget('dxDataGrid', { dataSource: [ { id: 1, name: 'Name 1', value: 10 }, @@ -59,13 +59,13 @@ const DATA_GRID_SELECTOR = '#container'; .expect(dataGrid.getHeaders().getHeaderRow(0).getHeaderCell(0).element.textContent) .eql('AI Column') .expect(dataCell.element.clientWidth) - .eql(100); + .eql(120); // act await dataGrid.resizeHeader(1, 50); // assert - await t.expect(dataCell.element.clientWidth).eql(150); + await t.expect(dataCell.element.clientWidth).eql(170); }).before(async () => createWidget('dxDataGrid', { dataSource: [ { id: 1, name: 'Name 1', value: 10 }, @@ -104,13 +104,13 @@ const DATA_GRID_SELECTOR = '#container'; .expect(dataGrid.getHeaders().getHeaderRow(0).getHeaderCell(0).element.textContent) .eql('AI Column') .expect(dataCell.element.clientWidth) - .eql(100); + .eql(120); // act await dataGrid.resizeHeader(1, 50); // assert - await t.expect(dataCell.element.clientWidth).eql(100); + await t.expect(dataCell.element.clientWidth).eql(120); }).before(async () => createWidget('dxDataGrid', { dataSource: [ { id: 1, name: 'Name 1', value: 10 }, diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.visual.ts new file mode 100644 index 000000000000..f07e29ebde8b --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/columnResizing.visual.ts @@ -0,0 +1,48 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; + +fixture.disablePageReloads`Ai Column.ColumnResizing.Visual` + .page(url(__dirname, '../../../container.html')); + +const DATA_GRID_SELECTOR = '#container'; + +test('Resize AI Column when wordWrapEnabled is true', async (t) => { + // arrange + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(dataGrid.isReady()).ok(); + + await takeScreenshot('datagrid__ai-column__column-resizing(wordWrapEnabled=true)-1.png', dataGrid.element); + + // act + await dataGrid.resizeHeader(1, -150); + + await takeScreenshot('datagrid__ai-column__column-resizing(wordWrapEnabled=true)-2.png', dataGrid.element); + + // assert + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + allowColumnResizing: true, + wordWrapEnabled: true, + columnWidth: 100, + columns: [ + { + type: 'ai', + caption: 'AI Column AI Column', + width: 250, + }, + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=center_rtlEnabled=false).png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=center_rtlEnabled=false).png new file mode 100644 index 000000000000..3f5d6674db66 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=center_rtlEnabled=false).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=center_rtlEnabled=true).png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=center_rtlEnabled=true).png new file mode 100644 index 000000000000..dfbd6c70e4d5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=center_rtlEnabled=true).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=left_rtlEnabled=false).png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=left_rtlEnabled=false).png new file mode 100644 index 000000000000..aefd9ead0ce1 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=left_rtlEnabled=false).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=left_rtlEnabled=true).png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=left_rtlEnabled=true).png new file mode 100644 index 000000000000..0abc3d65ad48 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=left_rtlEnabled=true).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=right_rtlEnabled=false).png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=right_rtlEnabled=false).png new file mode 100644 index 000000000000..8a8f8e6b3748 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=right_rtlEnabled=false).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=right_rtlEnabled=true).png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=right_rtlEnabled=true).png new file mode 100644 index 000000000000..5b7a8e569d08 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column(alignment=right_rtlEnabled=true).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu-when-allowFixing-false.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu-when-allowFixing-false.png index 893d5abcbe76..95b9e4789387 100644 Binary files a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu-when-allowFixing-false.png and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu-when-allowFixing-false.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu.png index e426467d46cd..0164acee794a 100644 Binary files a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu.png and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column-and-sticky-columns__context-menu.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__column-resizing(wordWrapEnabled=true)-1.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__column-resizing(wordWrapEnabled=true)-1.png new file mode 100644 index 000000000000..4738af6b983b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__column-resizing(wordWrapEnabled=true)-1.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__column-resizing(wordWrapEnabled=true)-2.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__column-resizing(wordWrapEnabled=true)-2.png new file mode 100644 index 000000000000..2b98f812f319 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__column-resizing(wordWrapEnabled=true)-2.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__default.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__default.png index d955877f2513..aefd9ead0ce1 100644 Binary files a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__default.png and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__default.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__dragging.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__dragging.png new file mode 100644 index 000000000000..aa684e898323 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__dragging.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__focused-dropdown-button.png b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__focused-dropdown-button.png new file mode 100644 index 000000000000..ee719e6e16c5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/etalons/datagrid__ai-column__focused-dropdown-button.png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/keyboardNavigation.visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/keyboardNavigation.visual.ts new file mode 100644 index 000000000000..659fb2ff5bf5 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/keyboardNavigation.visual.ts @@ -0,0 +1,61 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; + +fixture.disablePageReloads`Ai Column.KeyboardNavigation.Visual` + .page(url(__dirname, '../../../container.html')); + +const DATA_GRID_SELECTOR = '#container'; + +test('Check keyboard navigation for AI column', async (t) => { + // arrange + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + const headerRow = dataGrid.getHeaders().getHeaderRow(0); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(dataGrid.isReady()).ok(); + + // act + await t.click(headerRow.getHeaderCell(0).element); + await t.pressKey('tab'); + + // assert + await t.expect(headerRow.getHeaderCell(1).isFocused).ok(); + + // act + await t.pressKey('tab'); + + // assert + await t.expect(headerRow.getHeaderCell(1).getAIDropDownButton().isFocused).ok(); + + await takeScreenshot('datagrid__ai-column__focused-dropdown-button.png', dataGrid.element); + + // act + await t.pressKey('tab'); + + // assert + await t + .expect(headerRow.getHeaderCell(2).isFocused) + .ok() + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + allowColumnReordering: true, + columnWidth: 200, + columns: [ + { dataField: 'id', caption: 'ID' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myAiColumn', + }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/visual.ts index f2a56209e0a2..2848135d8c56 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/visual.ts @@ -37,3 +37,39 @@ test('Default render', async (t) => { }, ], })); + +['left', 'center', 'right'].forEach((alignment: 'left' | 'center' | 'right') => { + [false, true].forEach((rtlEnabled: boolean) => { + test(`AI Column - ${alignment} alignment, RTL ${rtlEnabled}`, async (t) => { + // arrange, act + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + + await t.expect(dataGrid.isReady()).ok(); + + await takeScreenshot(`datagrid__ai-column(alignment=${alignment}_rtlEnabled=${rtlEnabled}).png`, dataGrid.element); + + // assert + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + { id: 2, name: 'Name 2', value: 20 }, + { id: 3, name: 'Name 3', value: 30 }, + ], + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + alignment, + }, + ], + rtlEnabled, + })); + }); +}); diff --git a/packages/devextreme-scss/scss/widgets/base/_gridBase.scss b/packages/devextreme-scss/scss/widgets/base/_gridBase.scss index 99ec83aab35c..f7ffd2dd94cb 100644 --- a/packages/devextreme-scss/scss/widgets/base/_gridBase.scss +++ b/packages/devextreme-scss/scss/widgets/base/_gridBase.scss @@ -272,7 +272,7 @@ .dx-#{$widget-name}-nowrap { white-space: nowrap; - .dx-header-row > td > .dx-#{$widget-name}-text-content { + .dx-header-row > td .dx-#{$widget-name}-text-content { white-space: nowrap; } } @@ -644,7 +644,7 @@ white-space: nowrap; overflow: hidden; - & > .dx-#{$widget-name}-text-content { + .dx-#{$widget-name}-text-content { display: inline-block; white-space: normal; vertical-align: top; @@ -1247,4 +1247,20 @@ bottom: 0; line-height: 0; } + + .dx-command-ai, .dx-command-ai-header-content { + position: relative; + } + + .dx-command-ai-header-content { + display: inline-block; + vertical-align: top; + } + + .dx-command-ai-header-content .dx-icon-chatsparkleoutline, .dx-command-ai-header-button { + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + } } diff --git a/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss b/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss index d2bd7e0c3d01..9afbe77a7a6d 100644 --- a/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/dataGrid/_index.scss @@ -457,3 +457,7 @@ $datagrid-text-stub-background-image-path: null !default; outline-offset: -2px; } } + +.dx-command-ai-header-button .dx-button.dx-state-focused { + outline: 2px solid $datagrid-focused-border-color; +} diff --git a/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss b/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss index 6a50c91e4f48..c31eb8ee8237 100644 --- a/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/treeList/_index.scss @@ -410,3 +410,7 @@ $treelist-row-error-color: $datagrid-row-error-color; background-color: color.change($treelist-base-color, $alpha: 0.08); } } + +.dx-command-ai-header-button .dx-button.dx-state-focused { + outline: 2px solid $treelist-focused-border-color; +} diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss index 3fb0c2873826..5c939ad8a8e8 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_index.scss @@ -704,15 +704,13 @@ $fluent-grid-base-group-panel-message-line-height: $fluent-button-text-line-heig color: $header-filter-color-empty; } - &:hover { + &:not([class*="dx-command"], .dx-editor-cell, .dx-#{$widget-name}-group-space):hover { + background-color: $datagrid-hover-bg; + .dx-#{$widget-name}-text-content { color: $datagrid-base-color; } } - - &:hover:not(.dx-command-select):not(.dx-command-expand):not(.dx-editor-cell):not(.dx-command-edit):not(.dx-#{$widget-name}-group-space) { - background-color: $datagrid-hover-bg; - } } } } @@ -1354,4 +1352,26 @@ $fluent-grid-base-group-panel-message-line-height: $fluent-button-text-line-heig opacity: 1; color: $datagrid-draggable-column-text-color; } + + .dx-command-ai-header-content { + max-width: calc(100% - ($fluent-button-height - $fluent-grid-base-cell-horizontal-padding + $fluent-grid-base-command-ai-header-button-offset)); + + .dx-#{$widget-name}-text-content { + padding-left: $fluent-grid-base-command-ai-text-content-padding; + } + + .dx-icon-chatsparkleoutline { + @include dx-icon-sizing($fluent-grid-base-command-ai-chatsparkleoutline-icon-size); + } + } + + .dx-command-ai-header-button { + left: auto; + right: $fluent-grid-base-command-ai-header-button-offset; + } + + td[style*="text-align: right"] .dx-command-ai-header-button { + left: $fluent-grid-base-command-ai-header-button-offset; + right: auto; + } } diff --git a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss index 9698c3f5b58b..b5c2c460cb4f 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/gridBase/_sizes.scss @@ -40,6 +40,9 @@ $fluent-command-edit-icon-size: null !default; $fluent-filter-panel-icon-size: null !default; $fluent-grid-base-filter-icon-size: $fluent-button-icon-size !default; +$fluent-grid-base-command-ai-chatsparkleoutline-icon-size: null !default; +$fluent-grid-base-command-ai-text-content-padding: null !default; +$fluent-grid-base-command-ai-header-button-offset: null !default; @if $size == "default" { $fluent-grid-base-cell-height: 48px !default; @@ -71,6 +74,9 @@ $fluent-grid-base-filter-icon-size: $fluent-button-icon-size !default; $fluent-grid-base-header-icon-size: 20px !default; $fluent-command-edit-icon-size: 20px !default; $fluent-filter-panel-icon-size: 20px !default; + $fluent-grid-base-command-ai-chatsparkleoutline-icon-size: 20px !default; + $fluent-grid-base-command-ai-text-content-padding: $fluent-grid-base-command-ai-chatsparkleoutline-icon-size + 8px !default; + $fluent-grid-base-command-ai-header-button-offset: 8px !default; } @else if $size == "compact" { @@ -103,6 +109,9 @@ $fluent-grid-base-filter-icon-size: $fluent-button-icon-size !default; $fluent-grid-base-header-icon-size: 16px !default; $fluent-command-edit-icon-size: 16px !default; $fluent-filter-panel-icon-size: 16px !default; + $fluent-grid-base-command-ai-chatsparkleoutline-icon-size: 16px !default; + $fluent-grid-base-command-ai-text-content-padding: $fluent-grid-base-command-ai-chatsparkleoutline-icon-size + 6px !default; + $fluent-grid-base-command-ai-header-button-offset: 6px !default; } $fluent-grid-base-header-cell-font-size: $fluent-grid-base-cell-font-size !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss index a0552b104a91..420d9791ad39 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/_index.scss @@ -6,6 +6,7 @@ @use "../sizes" as *; @use "../../base/icons" as *; @use "../../base/checkBox/mixins" as *; +@use "../../base/button/mixins" as *; @use "../dropDownEditor" as *; @use "../textEditor" as *; @use "../common/mixins" as *; @@ -1136,4 +1137,40 @@ $generic-grid-base-cell-input-height: round($generic-base-line-height * $generic } } } + + .dx-command-ai-header-content { + max-width: calc(100% - ($generic-grid-base-command-ai-header-button-size - $generic-grid-base-cell-padding)); + + .dx-#{$widget-name}-text-content { + padding-left: $generic-grid-base-command-ai-text-content-padding; + } + + .dx-icon-chatsparkleoutline { + @include dx-icon-sizing($generic-grid-base-command-ai-chatsparkleoutline-icon-size); + } + } + + .dx-command-ai-header-button { + left: auto; + right: 0; + } + + td[style*="text-align: right"] .dx-command-ai-header-button { + left: 0; + right: auto; + } + + .dx-command-ai .dx-command-ai-header-button .dx-button { + @include dx-button-sizing( + $generic-grid-base-command-ai-header-button-padding, + $generic-grid-base-command-ai-header-button-padding, + $generic-grid-base-command-ai-header-button-padding, + $generic-base-icon-size, + 0 + ); + + &.dx-state-focused { + outline-offset: -2px; + } + } } diff --git a/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss index 8e06e74fad3c..a80075fbf4ce 100644 --- a/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/gridBase/_sizes.scss @@ -23,6 +23,10 @@ $generic-command-edit-text-margin: null !default; $generic-grid-base-nodata-font-size: null !default; $generic-grid-base-checkbox-padding-bottom: null !default; $generic-grid-base-treeview-select-all-item-offset: null !default; +$generic-grid-base-command-ai-chatsparkleoutline-icon-size: null !default; +$generic-grid-base-command-ai-text-content-padding: null !default; +$generic-grid-base-command-ai-header-button-padding: null !default; +$generic-grid-base-command-ai-header-button-size: null !default; $datagrid-text-link-disabled-opacity: $base-disabled-opacity; $datagrid-icon-link-disabled-opacity: 0.6; @@ -48,6 +52,10 @@ $datagrid-icon-link-disabled-opacity: 0.6; $generic-grid-base-nodata-font-size: 17px !default; $generic-grid-base-checkbox-padding-bottom: 2px; $generic-grid-base-treeview-select-all-item-offset: 34px; + $generic-grid-base-command-ai-chatsparkleoutline-icon-size: 18px !default; + $generic-grid-base-command-ai-text-content-padding: $generic-grid-base-command-ai-chatsparkleoutline-icon-size + 8px !default; + $generic-grid-base-command-ai-header-button-padding: 6px !default; + $generic-grid-base-command-ai-header-button-size: 32px !default; } @else if $size == "compact" { @@ -71,6 +79,10 @@ $datagrid-icon-link-disabled-opacity: 0.6; $generic-grid-base-nodata-font-size: 14px !default; $generic-grid-base-checkbox-padding-bottom: 0; $generic-grid-base-treeview-select-all-item-offset: 28px; + $generic-grid-base-command-ai-chatsparkleoutline-icon-size: 14px !default; + $generic-grid-base-command-ai-text-content-padding: $generic-grid-base-command-ai-chatsparkleoutline-icon-size + 6px !default; + $generic-grid-base-command-ai-header-button-padding: 5px !default; + $generic-grid-base-command-ai-header-button-size: 26px !default; } $generic-grid-base-column-chooser-paddings: 0 $generic-grid-base-column-chooser-padding $generic-grid-base-column-chooser-padding $generic-grid-base-column-chooser-padding !default; diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss index a7b3d152572e..cc54ad7f2fc3 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_index.scss @@ -675,7 +675,9 @@ $material-grid-base-group-panel-message-line-height: $material-button-text-line- color: $datagrid-base-color; } - &:hover { + &:not([class*="dx-command"], .dx-editor-cell, .dx-#{$widget-name}-group-space):hover { + background-color: $datagrid-hover-bg; + .dx-#{$widget-name}-text-content { color: $datagrid-base-color; } @@ -684,10 +686,6 @@ $material-grid-base-group-panel-message-line-height: $material-button-text-line- color: $datagrid-columnchooser-hover-icon-color; } } - - &:hover:not(.dx-command-select):not(.dx-command-expand):not(.dx-editor-cell):not(.dx-command-edit):not(.dx-#{$widget-name}-group-space) { - background-color: $datagrid-hover-bg; - } } } } @@ -1330,4 +1328,30 @@ $material-grid-base-group-panel-message-line-height: $material-button-text-line- opacity: 1; color: $datagrid-draggable-column-text-color; } + + .dx-command-ai-header-content { + max-width: calc(100% - ($material-button-height - $material-grid-base-cell-horizontal-padding + $material-grid-base-command-ai-header-button-offset)); + + .dx-#{$widget-name}-text-content { + padding-left: $material-grid-base-command-ai-text-content-padding; + } + + .dx-icon-chatsparkleoutline { + @include dx-icon-sizing($material-grid-base-command-ai-chatsparkleoutline-icon-size); + } + } + + .dx-command-ai-header-button { + left: auto; + right: $material-grid-base-command-ai-header-button-offset; + } + + td[style*="text-align: right"] .dx-command-ai-header-button { + left: $material-grid-base-command-ai-header-button-offset; + right: auto; + } + + .dx-command-ai .dx-command-ai-header-button .dx-button { + @include dx-button-onlyicon-sizing(); + } } diff --git a/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss index b51ee30e8eea..0cb85dfc91db 100644 --- a/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/gridBase/_sizes.scss @@ -23,6 +23,9 @@ $material-command-edit-icon-size: null !default; $material-filter-row-between-editor-padding: null !default; $material-command-column-additional-right-margin: null !default; $material-grid-base-treeview-select-all-item-offset: null !default; +$material-grid-base-command-ai-chatsparkleoutline-icon-size: null !default; +$material-grid-base-command-ai-text-content-padding: null !default; +$material-grid-base-command-ai-header-button-offset: null !default; $material-grid-base-header-line-height: 16px !default; $material-grid-base-header-icon-size: 15px !default; @@ -57,6 +60,9 @@ $material-command-edit-text-margin: 2px !default; $material-command-column-additional-right-margin: 0 !default; $material-grid-base-treeview-select-all-item-offset: 43px !default; + $material-grid-base-command-ai-chatsparkleoutline-icon-size: 24px !default; + $material-grid-base-command-ai-text-content-padding: $material-grid-base-command-ai-chatsparkleoutline-icon-size + 8px !default; + $material-grid-base-command-ai-header-button-offset: 8px !default; } @else if $size == "compact" { @@ -86,6 +92,9 @@ $material-command-edit-text-margin: 2px !default; $material-command-column-additional-right-margin: 2px !default; $material-grid-base-treeview-select-all-item-offset: 35px !default; + $material-grid-base-command-ai-chatsparkleoutline-icon-size: 18px !default; + $material-grid-base-command-ai-text-content-padding: $material-grid-base-command-ai-chatsparkleoutline-icon-size + 6px !default; + $material-grid-base-command-ai-header-button-offset: 6px !default; } $material-grid-base-header-panel-padding: 0 $material-grid-base-cell-horizontal-padding !default; diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 04ee2525a9fc..8c0e8f2848df 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -63,5 +63,5 @@ export const dependencies: FlatStylesDependencies = { scheduler: ['validation', 'button', 'popup', 'loadindicator', 'loadpanel', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'buttongroup', 'radiogroup', 'textarea', 'tagbox', 'switch', 'dropdownbutton', 'popover', 'tooltip', 'toolbar'], filemanager: ['toast', 'validation', 'button', 'loadindicator', 'textbox', 'contextmenu', 'checkbox', 'treeview', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'numberbox', 'list', 'selectbox', 'calendar', 'box', 'datebox', 'multiview', 'tabs', 'tabpanel', 'responsivebox', 'form', 'menu', 'filterbuilder', 'buttongroup', 'dropdownbutton', 'sortable', 'datagrid', 'drawer', 'progressbar', 'fileuploader', 'textarea'], diagram: ['loadindicator', 'validation', 'button', 'loadpanel', 'scrollview', 'popup', 'toolbar', 'textbox', 'contextmenu', 'list', 'checkbox', 'selectbox', 'numberbox', 'colorbox', 'popover', 'accordion', 'tooltip', 'multiview', 'tabs', 'tabpanel', 'progressbar', 'fileuploader'], - gantt: ['loadindicator', 'loadpanel', 'validation', 'button', 'popup', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'toast', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist', 'progressbar', 'textarea'], + gantt: ['loadindicator', 'loadpanel', 'validation', 'button', 'popup', 'multiview', 'tabs', 'tabpanel', 'box', 'responsivebox', 'textbox', 'toast', 'numberbox', 'checkbox', 'calendar', 'scrollview', 'list', 'selectbox', 'datebox', 'form', 'tagbox', 'radiogroup', 'popover', 'actionsheet', 'toolbar', 'contextmenu', 'treeview', 'menu', 'filterbuilder', 'sortable', 'treelist', 'progressbar', 'textarea', 'buttongroup', 'dropdownbutton'], }; diff --git a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_column.ts b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_column.ts index 7cb4214ecce7..a2f81b2ecd78 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_column.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/module_not_extended/ai_column.ts @@ -1,5 +1,5 @@ import { AIColumnController } from '@ts/grids/grid_core/ai_column/m_ai_column_controller'; -import { AIColumnView } from '@ts/grids/grid_core/ai_column/m_ai_column_view'; +import { AIColumnView, columnHeadersViewExtender } from '@ts/grids/grid_core/ai_column/m_ai_column_view'; import gridCore from '../m_core'; @@ -10,4 +10,9 @@ gridCore.registerModule('aiColumn', { views: { aiColumnView: AIColumnView, }, + extenders: { + views: { + columnHeadersView: columnHeadersViewExtender, + }, + }, }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/ai_column.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/ai_column.integration.test.ts index 7b28dd1b3308..8728acd46ccf 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/ai_column.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/ai_column.integration.test.ts @@ -10,6 +10,8 @@ import errors from '@js/ui/widget/ui.errors'; import { AIIntegration } from '@ts/core/ai_integration/core/ai_integration'; import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; +import { CLASSES } from './const'; + const SELECTORS = { gridContainer: '#gridContainer', }; @@ -235,6 +237,78 @@ describe('Options', () => { expect(headerCellTemplate).toHaveBeenCalledTimes(1); expect(headerCell.querySelectorAll('.template-class').length).toBe(1); expect(headerCell.textContent).toBe('Template'); + expect(headerCell.querySelector(`.${CLASSES.aiColumnHeaderContent}`)).toBeNull(); + expect(headerCell.querySelector(`.${CLASSES.aiChatSparkleOutlineIcon}`)).toBeNull(); + expect(headerCell.querySelector(`.${CLASSES.aiColumnHeaderButton}`)).not.toBeNull(); + }); + }); + + describe('when headerCellTemplate isn\'t set', () => { + it('should render icon, text and button by default', async () => { + const { component } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + ], + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + }, + ], + }); + + const headerCell = component.getHeaderCell(3); + const aiColumnHeaderContent = headerCell.querySelector(`.${CLASSES.aiColumnHeaderContent}`); + const aiColumnHeaderText = aiColumnHeaderContent?.querySelector('.dx-datagrid-text-content'); + + expect(aiColumnHeaderContent).not.toBeNull(); + expect(aiColumnHeaderContent?.querySelector(`.${CLASSES.aiChatSparkleOutlineIcon}`)).not.toBeNull(); + expect(aiColumnHeaderText).not.toBeNull(); + expect(aiColumnHeaderText?.textContent).toBe('AI Column'); + expect(headerCell.querySelector(`.${CLASSES.aiColumnHeaderButton}`)).not.toBeNull(); + }); + }); + + describe('when headerCellTemplate is dynamically updated', () => { + it('should replace default header template', async () => { + const headerCellTemplate = jest.fn((container: HTMLElement) => { + const span = document.createElement('span'); + + span.className = 'my-template-class'; + span.textContent = 'Test'; + container.append(span); + }); + + const { component } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + ], + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + }, + ], + }); + + component.apiColumnOption('myColumn', 'headerCellTemplate', headerCellTemplate); + + const headerCellUpdated = component.getHeaderCell(3); + + expect(headerCellTemplate).toHaveBeenCalledTimes(1); + expect(headerCellUpdated.querySelector('.my-template-class')).not.toBeNull(); + expect(headerCellUpdated.textContent).toBe('Test'); + expect(headerCellUpdated.querySelector(`.${CLASSES.aiColumnHeaderContent}`)).toBeNull(); + expect(headerCellUpdated.querySelector(`.${CLASSES.aiChatSparkleOutlineIcon}`)).toBeNull(); + expect(headerCellUpdated.querySelector(`.${CLASSES.aiColumnHeaderButton}`)).not.toBeNull(); }); }); @@ -298,6 +372,39 @@ describe('Options', () => { .toEqual(['ID', 'AI Column', 'Name', 'Value']); }); }); + + describe('when column.ai.showHeaderMenu is set to false', () => { + it('should not render header button', async () => { + const { component } = await createDataGrid({ + dataSource: [ + { id: 1, name: 'Name 1', value: 10 }, + ], + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + type: 'ai', + caption: 'AI Column', + name: 'myColumn', + ai: { + showHeaderMenu: false, + }, + }, + ], + }); + + const headerCell = component.getHeaderCell(3); + const aiColumnHeaderContent = headerCell.querySelector(`.${CLASSES.aiColumnHeaderContent}`); + const aiColumnHeaderText = aiColumnHeaderContent?.querySelector('.dx-datagrid-text-content'); + + expect(aiColumnHeaderContent).not.toBeNull(); + expect(aiColumnHeaderContent?.querySelector(`.${CLASSES.aiChatSparkleOutlineIcon}`)).not.toBeNull(); + expect(aiColumnHeaderText).not.toBeNull(); + expect(aiColumnHeaderText?.textContent).toBe('AI Column'); + expect(headerCell.querySelector(`.${CLASSES.aiColumnHeaderButton}`)).toBeNull(); + }); + }); }); describe('columnOption', () => { diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/const.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/const.ts index 752f9ec62b87..3e49cbeec8f6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/const.ts @@ -1,5 +1,10 @@ export const AI_COLUMN_NAME = 'ai'; +export const AI_CHAT_SPARKLE_OUTLINE = 'chatsparkleoutline'; + export const CLASSES = { aiColumn: 'dx-command-ai', + aiColumnHeaderContent: 'dx-command-ai-header-content', + aiColumnHeaderButton: 'dx-command-ai-header-button', + aiChatSparkleOutlineIcon: 'dx-icon-chatsparkleoutline', }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/dom.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/dom.ts new file mode 100644 index 000000000000..16f33df73dd8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/dom.ts @@ -0,0 +1,11 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { getImageContainer } from '@js/core/utils/icon'; + +import { AI_CHAT_SPARKLE_OUTLINE, CLASSES } from './const'; + +export const createChatSparkleOutlineIcon = (): dxElementWrapper => getImageContainer( + AI_CHAT_SPARKLE_OUTLINE, +) as dxElementWrapper; + +export const createAIHeaderContainer = (): dxElementWrapper => $('
').addClass(CLASSES.aiColumnHeaderContent); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.test.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.test.ts index f232c0a07ce5..0b42f9d443af 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.test.ts @@ -119,6 +119,7 @@ describe('AIColumnView', () => { command: AI_COLUMN_NAME, cssClass: 'dx-command-ai', fixed: false, + minWidth: 120, }); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.ts index ef168039d88e..bb7ebcd7d117 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/m_ai_column_view.ts @@ -1,15 +1,27 @@ +/* eslint-disable max-classes-per-file */ +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import type { Properties as DropDownProperties } from '@js/ui/drop_down_button'; +import DropDownButton from '@js/ui/drop_down_button'; import domAdapter from '@ts/core/m_dom_adapter'; +import type { ColumnHeadersView } from '../column_headers/m_column_headers'; import type { Column, ColumnsController } from '../columns_controller/m_columns_controller'; import { getColumnHeaderCellSelector } from '../columns_controller/m_columns_controller_utils'; import { View } from '../m_modules'; +import type { ModuleType } from '../m_types'; import { AIPromptEditor } from './ai_prompt_editor/ai_prompt_editor'; import type { AIPromptEditorOptions } from './ai_prompt_editor/types'; -import { AI_COLUMN_NAME } from './const'; +import { AI_COLUMN_NAME, CLASSES } from './const'; +import { createAIHeaderContainer, createChatSparkleOutlineIcon } from './dom'; import type { AIColumnController } from './m_ai_column_controller'; import { - getAICommandColumnOptions, isAIColumnAutoMode, isEditorOptions, isPopupOptions, + getAICommandColumnDefaultOptions, + isAIColumnAutoMode, + isAIColumnHeader, + isEditorOptions, + isHeaderDropDownButtonVisible, + isPopupOptions, isPromptOption, isRefreshOption, } from './utils'; @@ -22,7 +34,7 @@ export class AIColumnView extends View { private promptEditorInstance!: AIPromptEditor; private addAICommandColumn(): void { - this.columnsController.addCommandColumn(getAICommandColumnOptions()); + this.columnsController.addCommandColumn(getAICommandColumnDefaultOptions()); } private getAIPromptEditorConfig( @@ -164,3 +176,68 @@ export class AIColumnView extends View { return this.promptEditorInstance; } } + +export const columnHeadersViewExtender = (Base: ModuleType) => class AIColumnHeadersViewExtender extends Base { + private getDropDownButtonConfig(): DropDownProperties { + return { + showArrowIcon: false, + icon: 'overflow', + stylingMode: 'text', + }; + } + + private renderHeaderDropDownButton($container: dxElementWrapper): void { + const $dropDownButton = $('
') + .addClass(CLASSES.aiColumnHeaderButton) + .appendTo($container); + + this._createComponent($dropDownButton, DropDownButton, this.getDropDownButtonConfig()); + } + + private renderAIHeader($container: dxElementWrapper, column: Column): void { + const $iconElement = createChatSparkleOutlineIcon(); + const $aiHeaderContainer = createAIHeaderContainer(); + const $cellContent = this.createCellContent($container, column); + + $cellContent.text(column.caption ?? ''); + $aiHeaderContainer + .append($iconElement) + .append($cellContent) + .appendTo($container); + } + + protected getHeaderDefaultTemplate($container: dxElementWrapper, options): void { + if (isAIColumnHeader(options.column, options.rowType)) { + this.renderAIHeader($container, options.column); + return; + } + + super.getHeaderDefaultTemplate($container, options); + } + + protected _processTemplate(template, options) { + const renderingTemplate = super._processTemplate(template, options); + const needToRenderHeaderDropDownButton = isAIColumnHeader(options.column, options.rowType) + && isHeaderDropDownButtonVisible(options.column); + + if (renderingTemplate && needToRenderHeaderDropDownButton) { + return { + render: (options) => { + renderingTemplate.render(options); + this.renderHeaderDropDownButton($(options.container)); + }, + }; + } + + return renderingTemplate; + } + + public renderDragCellContent($dragContainer: dxElementWrapper, column: Column): void { + if (column.type === AI_COLUMN_NAME) { + this.renderAIHeader($dragContainer, column); + return; + } + + super.renderDragCellContent($dragContainer, column); + } +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts index aaa6bf8d0b8a..743b5bff0fea 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/ai_column/utils.ts @@ -4,11 +4,12 @@ import type { Column } from '../columns_controller/m_columns_controller'; import type { Item, UserData } from '../data_controller/m_data_controller'; import { AI_COLUMN_NAME, CLASSES } from './const'; -export const getAICommandColumnOptions = (): unknown => ({ +export const getAICommandColumnDefaultOptions = (): unknown => ({ type: AI_COLUMN_NAME, command: AI_COLUMN_NAME, cssClass: CLASSES.aiColumn, fixed: false, + minWidth: 120, }); export const getDataFromRowItems = (items: Item[]): UserData[] => items @@ -59,3 +60,12 @@ export const isRefreshOption = (optionName: string, value: unknown): boolean => const valueKeys = Object.keys(value as Record); return valueKeys.some((key) => refreshOptionNames.includes(key)); }; + +export const isAIColumnHeader = ( + column: Column, + rowType: string, +): boolean => rowType === 'header' && column.type === AI_COLUMN_NAME; + +export const isHeaderDropDownButtonVisible = ( + column: Column, +): boolean => column?.ai?.showHeaderMenu !== false; diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts b/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts new file mode 100644 index 000000000000..5bc102d854f5 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_headers/const.ts @@ -0,0 +1,3 @@ +export const CLASSES = { + cellContent: 'text-content', +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts b/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts index b661b989c507..499b2acc3bd1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_headers/m_column_headers.ts @@ -12,12 +12,13 @@ import { ColumnContextMenuMixin } from '@ts/grids/grid_core/context_menu/m_colum import type { HeaderFilterController } from '@ts/grids/grid_core/header_filter/m_header_filter'; import type { HeaderPanel } from '@ts/grids/grid_core/header_panel/m_header_panel'; +import type { Column } from '../columns_controller/m_columns_controller'; import { CLASSES as REORDERING_CLASSES } from '../columns_resizing_reordering/const'; import type { HeadersKeyboardNavigationController } from '../keyboard_navigation/m_headers_keyboard_navigation'; import { registerKeyboardAction } from '../m_accessibility'; import { ColumnsView } from '../views/m_columns_view'; +import { CLASSES } from './const'; -const CELL_CONTENT_CLASS = 'text-content'; const HEADERS_CLASS = 'headers'; const NOWRAP_CLASS = 'nowrap'; const ROW_CLASS_SELECTOR = '.dx-row'; @@ -36,41 +37,6 @@ const HEADER_FILTER_INDICATOR_CLASS = 'dx-header-filter-indicator'; const MULTI_ROW_HEADER_CLASS = 'dx-header-multi-row'; const LINK = 'dx-link'; -const createCellContent = function (that, $cell, options) { - const $cellContent = $('
').addClass(that.addWidgetPrefix(CELL_CONTENT_CLASS)); - - that.setAria('role', 'presentation', $cellContent); - - addCssClassesToCellContent(that, $cell, options.column, $cellContent); - const showColumnLines = that.option('showColumnLines'); - // TODO getController - const contentAlignment = that.getController('columns').getHeaderContentAlignment(options.column.alignment); - - return $cellContent[showColumnLines || contentAlignment === 'right' ? 'appendTo' : 'prependTo']($cell); -}; - -function addCssClassesToCellContent(that, $cell, column, $cellContent?) { - const $indicatorElements = that._getIndicatorElements($cell, true); - const $visibleIndicatorElements = that._getIndicatorElements($cell); - const indicatorCount = $indicatorElements?.length; - const columnAlignment = that._getColumnAlignment(column.alignment); - - const sortIndicatorClassName = `.${that._getIndicatorClassName('sort')}`; - const sortIndexIndicatorClassName = `.${that._getIndicatorClassName('sortIndex')}`; - - const $sortIndicator = $visibleIndicatorElements.filter(sortIndicatorClassName); - const $sortIndexIndicator = $visibleIndicatorElements.children().filter(sortIndexIndicatorClassName); - - $cellContent = $cellContent || $cell.children(`.${that.addWidgetPrefix(CELL_CONTENT_CLASS)}`); - - $cellContent - .toggleClass(TEXT_CONTENT_ALIGNMENT_CLASS_PREFIX + columnAlignment, indicatorCount > 0) - .toggleClass(TEXT_CONTENT_ALIGNMENT_CLASS_PREFIX + (columnAlignment === 'left' ? 'right' : 'left'), indicatorCount > 0 && column.alignment === 'center') - .toggleClass(SORT_INDICATOR_CLASS, !!$sortIndicator.length) - .toggleClass(SORT_INDEX_INDICATOR_CLASS, !!$sortIndexIndicator.length) - .toggleClass(HEADER_FILTER_INDICATOR_CLASS, !!$visibleIndicatorElements.filter(`.${that._getIndicatorClassName('headerFilter')}`).length); -} - export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { private _isGroupingChanged: any; @@ -84,6 +50,56 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { protected _headerFilterController!: HeaderFilterController; + private addCssClassesToCellContent( + $cell: dxElementWrapper, + column: Column, + $cellContent?: dxElementWrapper, + ): void { + const $indicatorElements = this._getIndicatorElements($cell, true); + const $visibleIndicatorElements = this._getIndicatorElements($cell); + const indicatorCount = $indicatorElements?.length; + const columnAlignment = this._getColumnAlignment(column.alignment ?? ''); + + const sortIndicatorClassName = `.${this._getIndicatorClassName('sort')}`; + const sortIndexIndicatorClassName = `.${this._getIndicatorClassName('sortIndex')}`; + + const $sortIndicator = $visibleIndicatorElements.filter(sortIndicatorClassName); + const $sortIndexIndicator = $visibleIndicatorElements + .children() + .filter(sortIndexIndicatorClassName); + const $content = $cellContent + ?? $cell.children(`.${this.addWidgetPrefix(CLASSES.cellContent)}`); + + $content + .toggleClass(TEXT_CONTENT_ALIGNMENT_CLASS_PREFIX + columnAlignment, indicatorCount > 0) + .toggleClass(TEXT_CONTENT_ALIGNMENT_CLASS_PREFIX + (columnAlignment === 'left' ? 'right' : 'left'), indicatorCount > 0 && column.alignment === 'center') + .toggleClass(SORT_INDICATOR_CLASS, !!$sortIndicator.length) + .toggleClass(SORT_INDEX_INDICATOR_CLASS, !!$sortIndexIndicator.length) + .toggleClass(HEADER_FILTER_INDICATOR_CLASS, !!$visibleIndicatorElements.filter(`.${this._getIndicatorClassName('headerFilter')}`).length); + } + + protected createCellContent( + $cell: dxElementWrapper, + column: Column, + ): dxElementWrapper { + const $cellContent = $('
').addClass(this.addWidgetPrefix(CLASSES.cellContent)); + + this.setAria('role', 'presentation', $cellContent); + + this.addCssClassesToCellContent($cell, column, $cellContent); + const showColumnLines = this.option('showColumnLines'); + // TODO getController + const contentAlignment = this.getController('columns').getHeaderContentAlignment(column.alignment); + + if (showColumnLines || contentAlignment === 'right') { + $cellContent.appendTo($cell); + } else { + $cellContent.prependTo($cell); + } + + return $cellContent; + } + public init(): void { super.init(); this._headerPanelView = this.getView('headerPanel'); @@ -111,23 +127,20 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { return this.option('useLegacyKeyboardNavigation'); } - private _getDefaultTemplate(column) { - const that = this; - - return function ($container, options) { - const { caption } = column; - const needCellContent = !column.command || (caption && column.command !== 'expand'); + protected getHeaderDefaultTemplate($container, options) { + const { column } = options; + const { caption } = column; + const needCellContent = !column.command || (caption && column.command !== 'expand'); - if (column.command === 'empty') { - that._renderEmptyMessage($container, options); - } else if (needCellContent) { - const $content = createCellContent(that, $container, options); + if (column.command === 'empty') { + this._renderEmptyMessage($container, options); + } else if (needCellContent) { + const $content = this.createCellContent($container, column); - $content.text(caption); - } else if (column.command) { - $container.html(' '); - } - }; + $content.text(caption); + } else if (column.command) { + $container.html(' '); + } } private _renderEmptyMessage($container, options) { @@ -138,7 +151,7 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { return; } - const $cellContent = createCellContent(this, $container, options); + const $cellContent = this.createCellContent($container, options.column); const needSplit = textEmpty.includes('{0}'); if (needSplit) { @@ -179,7 +192,10 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { } private _getHeaderTemplate(column) { - return column.headerCellTemplate || { allowRenderToDetachedContainer: true, render: this._getDefaultTemplate(column) }; + return column.headerCellTemplate || { + allowRenderToDetachedContainer: true, + render: this.getHeaderDefaultTemplate.bind(this), + }; } protected _processTemplate(template, options) { @@ -191,7 +207,7 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { if (options.rowType === 'header' && renderingTemplate && column.headerCellTemplate && !column.command) { resultTemplate = { render(options) { - const $content = createCellContent(that, options.container, options.model); + const $content = that.createCellContent(options.container, options.model); renderingTemplate.render(extend({}, options, { container: $content })); }, }; @@ -411,7 +427,7 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { .clone() .addClass(VISIBILITY_HIDDEN_CLASS) .css('float', '') - .insertBefore($cell.children(`.${this.addWidgetPrefix(CELL_CONTENT_CLASS)}`)); + .insertBefore($cell.children(`.${this.addWidgetPrefix(CLASSES.cellContent)}`)); } } @@ -433,7 +449,7 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { this._alignCaptionByCenter($cell); } - addCssClassesToCellContent(this, $cell, column); + this.addCssClassesToCellContent($cell, column); return $indicatorElement; } @@ -672,6 +688,10 @@ export class ColumnHeadersView extends ColumnContextMenuMixin(ColumnsView) { public getKeyboardNavigationController() { return this._headersKeyboardNavigation; } + + public renderDragCellContent($dragContainer: dxElementWrapper, column: Column): void { + $dragContainer.text(column.caption ?? ''); + } } export const columnHeadersModule = { diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/m_column_state_mixin.ts b/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/m_column_state_mixin.ts index 3cd58991113d..795a5dc819ee 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/m_column_state_mixin.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/m_column_state_mixin.ts @@ -1,3 +1,4 @@ +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; import { getDefaultAlignment } from '@js/core/utils/position'; @@ -48,8 +49,11 @@ export const ColumnStateMixin = ColumnStateMix // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _getIndicatorClassName(name: string): string {} - private _getColumnAlignment(alignment, rtlEnabled) { - rtlEnabled = rtlEnabled || this.option('rtlEnabled'); + protected _getColumnAlignment( + alignment: string, + rtl = false, + ): string { + const rtlEnabled = rtl || this.option('rtlEnabled'); return alignment && alignment !== 'center' ? alignment : getDefaultAlignment(rtlEnabled); } @@ -71,10 +75,14 @@ export const ColumnStateMixin = ColumnStateMix return $cell && $cell.find(`.${COLUMN_INDICATORS_CLASS}`); } - private _getIndicatorElements($cell) { + protected _getIndicatorElements( + $cell: dxElementWrapper, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + returnAll = false, + ): dxElementWrapper { const $indicatorContainer = this._getIndicatorContainer($cell); - return $indicatorContainer && $indicatorContainer.children(); + return $indicatorContainer?.children(); } /** diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts index f0b01690618e..93cfe15e1c0e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/m_columns_resizing_reordering.ts @@ -33,6 +33,7 @@ import modules from '../m_modules'; import gridCoreUtils from '../m_utils'; import type { PagerView } from '../pager/m_pager'; import { CLASSES } from './const'; +import type { DraggingPanel } from './types'; const COLUMNS_SEPARATOR_CLASS = 'columns-separator'; const COLUMNS_SEPARATOR_TRANSPARENT = 'columns-separator-transparent'; @@ -407,6 +408,39 @@ export class DraggingHeaderView extends modules.View { private _testPointsByColumns: any; /// #ENDDEBUG + private getSourceDraggingPanel(): DraggingPanel { + const { + sourceLocation, + draggingPanels, + }: { + sourceLocation: string; + draggingPanels: DraggingPanel[]; + } = this._dragOptions; + + return draggingPanels + .find( + (draggingPanel: DraggingPanel) => draggingPanel.getName() === sourceLocation, + ) as DraggingPanel; + } + + private updateDragElement(): void { + const { columnElement, sourceColumn } = this._dragOptions; + const sourceDraggingPanel = this.getSourceDraggingPanel(); + const dragElement = this.element(); + + dragElement + .empty() + .css({ + textAlign: columnElement?.css('textAlign'), + height: columnElement && getHeight(columnElement), + width: columnElement && getWidth(columnElement), + whiteSpace: columnElement?.css('whiteSpace'), + }) + .addClass(this.addWidgetPrefix(HEADERS_DRAG_ACTION_CLASS)); + + sourceDraggingPanel.renderDragCellContent(dragElement, sourceColumn); + } + public init() { super.init(); @@ -490,39 +524,30 @@ export class DraggingHeaderView extends modules.View { } public dragHeader(options) { - const that = this; const { columnElement } = options; - that._isDragging = true; - that._dragOptions = options; - that._dropOptions = { + this._isDragging = true; + this._dragOptions = options; + this._dropOptions = { sourceIndex: options.index, - sourceColumnIndex: that._getVisibleIndexObject(options.rowIndex, options.columnIndex), - sourceColumnElement: options.columnElement, + sourceColumnIndex: this._getVisibleIndexObject(options.rowIndex, options.columnIndex), + sourceColumnElement: columnElement, sourceLocation: options.sourceLocation, }; const document = domAdapter.getDocument(); // eslint-disable-next-line spellcheck/spell-checker - that._onSelectStart = document.onselectstart; + this._onSelectStart = document.onselectstart; // eslint-disable-next-line spellcheck/spell-checker document.onselectstart = function () { return false; }; - that._controller.drag(that._dropOptions); - - that.element().css({ - textAlign: columnElement?.css('textAlign'), - height: columnElement && getHeight(columnElement), - width: columnElement && getWidth(columnElement), - whiteSpace: columnElement?.css('whiteSpace'), - }) - .addClass(that.addWidgetPrefix(HEADERS_DRAG_ACTION_CLASS)) - .text(options.sourceColumn.caption); + this._controller.drag(this._dropOptions); + this.updateDragElement(); - that.element().appendTo(swatchContainer.getSwatchContainer(columnElement)); + this.element().appendTo(swatchContainer.getSwatchContainer(columnElement)); } public moveHeader(args) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/types.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/types.ts new file mode 100644 index 000000000000..2085f2bd48e7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_resizing_reordering/types.ts @@ -0,0 +1,5 @@ +import type { ColumnChooserView } from '../column_chooser/m_column_chooser'; +import type { ColumnHeadersView } from '../column_headers/m_column_headers'; +import type { HeaderPanel } from '../header_panel/m_header_panel'; + +export type DraggingPanel = ColumnHeadersView | ColumnChooserView | HeaderPanel; diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts index 7811a04749a7..fb317c2fe834 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_columns_view.ts @@ -34,7 +34,7 @@ import { ColumnStateMixin } from '@ts/grids/grid_core/column_state_mixin/m_colum import type { EditorFactory } from '@ts/grids/grid_core/editor_factory/m_editor_factory'; import type { SelectionController } from '@ts/grids/grid_core/selection/m_selection'; -import type { ColumnsController } from '../columns_controller/m_columns_controller'; +import type { Column, ColumnsController } from '../columns_controller/m_columns_controller'; import type { DataController } from '../data_controller/m_data_controller'; import modules from '../m_modules'; import gridCoreUtils from '../m_utils'; @@ -1476,4 +1476,8 @@ export class ColumnsView extends ColumnStateMixin(modules.View) { public isDisposed() { return this.component?._disposed; } + + public renderDragCellContent($dragContainer: dxElementWrapper, column: Column): void { + $dragContainer.text(column.caption ?? ''); + } } diff --git a/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_column.ts b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_column.ts index 7cb4214ecce7..a2f81b2ecd78 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_column.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/module_not_extended/ai_column.ts @@ -1,5 +1,5 @@ import { AIColumnController } from '@ts/grids/grid_core/ai_column/m_ai_column_controller'; -import { AIColumnView } from '@ts/grids/grid_core/ai_column/m_ai_column_view'; +import { AIColumnView, columnHeadersViewExtender } from '@ts/grids/grid_core/ai_column/m_ai_column_view'; import gridCore from '../m_core'; @@ -10,4 +10,9 @@ gridCore.registerModule('aiColumn', { views: { aiColumnView: AIColumnView, }, + extenders: { + views: { + columnHeadersView: columnHeadersViewExtender, + }, + }, }); diff --git a/packages/devextreme/testing/helpers/gridBaseMocks.js b/packages/devextreme/testing/helpers/gridBaseMocks.js index 71ee48e932ca..f2aef4cd8ad4 100644 --- a/packages/devextreme/testing/helpers/gridBaseMocks.js +++ b/packages/devextreme/testing/helpers/gridBaseMocks.js @@ -932,8 +932,11 @@ module.exports = function($, gridCore, columnResizingReordering, domUtils, commo getScrollTop: function() { return options.scrollTop; - } + }, + renderDragCellContent: function($dragContainer, column) { + $dragContainer.text(column.caption); + } }; }; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js index a6de16239514..a79626098f86 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsResizingReorderingModule.tests.js @@ -3687,7 +3687,9 @@ QUnit.module('Headers reordering', { }), sourceColumn: { caption: 'TestDrag' - } + }, + sourceLocation: 'headers', + draggingPanels: this.draggingPanels, }); const $dragHeader = $('.dx-datagrid-drag-header'); @@ -3720,7 +3722,9 @@ QUnit.module('Headers reordering', { }), sourceColumn: { caption: 'TestDrag' - } + }, + sourceLocation: 'headers', + draggingPanels: this.draggingPanels, }); const $dragHeader = $('.dx-datagrid-drag-header'); @@ -4788,7 +4792,8 @@ QUnit.module('Headers reordering', { sourceLocation: 'headers', sourceColumn: { caption: 'TestDrag' - } + }, + draggingPanels: this.draggingPanels, }); draggingHeader.moveHeader({ event: { @@ -4826,7 +4831,9 @@ QUnit.module('Headers reordering', { sourceColumn: { command: 'edit', type: 'buttons' - } + }, + sourceLocation: 'headers', + draggingPanels: this.draggingPanels, }); const $dragHeader = $('.dx-datagrid-drag-header'); diff --git a/packages/testcafe-models/dataGrid/headers/cell.ts b/packages/testcafe-models/dataGrid/headers/cell.ts index 219732c5ded8..29cd130be8ae 100644 --- a/packages/testcafe-models/dataGrid/headers/cell.ts +++ b/packages/testcafe-models/dataGrid/headers/cell.ts @@ -1,6 +1,7 @@ import { ClientFunction, Selector } from 'testcafe'; import FocusableElement from '../../internal/focusable'; import Widget from '../../internal/widget'; +import DropDownButton from '../../dropDownButton'; type StickyPosition = 'left' | 'right' | 'sticky'; @@ -12,6 +13,7 @@ const CLASS = { sticky: 'dx-datagrid-sticky-column', stickyLeft: 'dx-datagrid-sticky-column-left', stickyRight: 'dx-datagrid-sticky-column-right', + aiHeaderButton: 'dx-command-ai-header-button', }; const getStickyClassNames = (position: StickyPosition | undefined): string[] => { @@ -63,4 +65,8 @@ export default class HeaderCell { getEditor(): FocusableElement { return new FocusableElement(this.element.find('.dx-texteditor-input, .dx-checkbox')); } + + getAIDropDownButton(): DropDownButton { + return new DropDownButton(this.element.find(`.${CLASS.aiHeaderButton}`)); + } } diff --git a/packages/testcafe-models/dropDownButton.ts b/packages/testcafe-models/dropDownButton.ts index d91f723f8f13..a85a4a4927cc 100644 --- a/packages/testcafe-models/dropDownButton.ts +++ b/packages/testcafe-models/dropDownButton.ts @@ -5,7 +5,16 @@ const ATTR = { popupId: 'aria-owns', }; +const CLASS = { + focused: 'dx-state-focused', + buttonGroup: 'dx-buttongroup', +}; + export default class DropDownButton extends DropDownList { + public get isFocused(): Promise { + return this.element.find(`.${CLASS.buttonGroup}`).hasClass(CLASS.focused); + } + // eslint-disable-next-line class-methods-use-this getName(): WidgetName { return 'dxDropDownButton'; }