diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/etalons/T1317623-expand-columns-with-band-columns (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/etalons/T1317623-expand-columns-with-band-columns (fluent.blue.light).png new file mode 100644 index 000000000000..bdf57d5b899b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/etalons/T1317623-expand-columns-with-band-columns (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/etalons/T1317623-horizontal-scroll-with-fixed-band-columns (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/etalons/T1317623-horizontal-scroll-with-fixed-band-columns (fluent.blue.light).png new file mode 100644 index 000000000000..a2ec21a333cd Binary files /dev/null and b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/etalons/T1317623-horizontal-scroll-with-fixed-band-columns (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/visual.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/visual.ts index 306c59a6c76d..344aed1eacac 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/visual.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/fixedColumns/visual.ts @@ -314,3 +314,73 @@ test('The grid layout should be correct after unfixing a column via the context { dataField: 'State' }, ], })); + +// T1317623 +test('Expand columns headers offsets should be correct with fixed band columns and fixed command columns (T1317623)', async (t) => { + const dataGrid = new DataGrid(DATA_GRID_SELECTOR); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await t.expect(dataGrid.isReady()).ok(); + + await testScreenshot(t, takeScreenshot, 'T1317623-expand-columns-with-band-columns.png', { element: dataGrid.element }); + + await dataGrid.scrollTo(t, { x: 5000 }); + + await testScreenshot(t, takeScreenshot, 'T1317623-horizontal-scroll-with-fixed-band-columns.png', { element: dataGrid.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxDataGrid', { + dataSource: [ + { + ID: 1, + CompanyName: 'Super Mart of the West', + Address: '702 SW 8th Street', + City: 'Bentonville', + State: 'Arkansas', + Zipcode: 72716, + Phone: '(800) 555-2797', + Fax: '(800) 555-2171', + }, + { + ID: 2, + CompanyName: 'K&S Music', + Address: '1000 Nicllet Mall', + City: 'Minneapolis', + State: 'Minnesota', + Zipcode: 55403, + Phone: '(612) 304-6073', + Fax: '(612) 304-6074', + }, + ], + keyExpr: 'ID', + width: '100%', + showBorders: true, + columnWidth: 200, + columnFixing: { enabled: true }, + selection: { mode: 'multiple' }, + grouping: { autoExpandAll: true }, + masterDetail: { + enabled: true, + }, + columns: [ + { + caption: 'Company Info', + fixed: true, + fixedPosition: 'left', + columns: [ + { dataField: 'CompanyName', groupIndex: 1, showWhenGrouped: true }, + { dataField: 'Phone' }, + { dataField: 'Fax' }, + ], + }, + 'City', + { + dataField: 'State', + groupIndex: 0, + }, + 'Address', + 'Zipcode', + ], +})); diff --git a/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_column_keyboard_navigation_mixin.ts b/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_column_keyboard_navigation_mixin.ts index b814293b8540..0a04c139361f 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_column_keyboard_navigation_mixin.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_column_keyboard_navigation_mixin.ts @@ -1,6 +1,6 @@ import { isCommandKeyPressed } from '@js/common/core/events/utils'; import { isDefined } from '@js/core/utils/type'; -import type { Column } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { FocusedCellPosition } from '@ts/grids/grid_core/keyboard_navigation/const'; import { KEY_CODES } from '@ts/grids/grid_core/keyboard_navigation/const'; import type { ColumnKeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_column_keyboard_navigation_core'; diff --git a/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_group_panel_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_group_panel_keyboard_navigation.ts index b4ae24f34f2a..c5af64ed3ebc 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_group_panel_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_group_panel_keyboard_navigation.ts @@ -5,7 +5,7 @@ import { } from '@js/common/core/events/utils/index'; import $ from '@js/core/renderer'; import { hiddenFocus } from '@js/ui/shared/accessibility'; -import type { Column } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import { Direction } from '@ts/grids/grid_core/keyboard_navigation/const'; import { ColumnKeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_column_keyboard_navigation_core'; import type { Views } from '@ts/grids/grid_core/m_types'; diff --git a/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_headers_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_headers_keyboard_navigation.ts index d4d757fb7ec1..47748edce42e 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_headers_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/keyboard_navigation/m_headers_keyboard_navigation.ts @@ -1,7 +1,7 @@ import { isCommandKeyPressed } from '@js/common/core/events/utils'; import $ from '@js/core/renderer'; import { isDefined } from '@js/core/utils/type'; -import type { Column } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import { KEY_CODES } from '@ts/grids/grid_core/keyboard_navigation/const'; import type { HeadersKeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_headers_keyboard_navigation'; import { headersKeyboardNavigationModule } from '@ts/grids/grid_core/keyboard_navigation/m_headers_keyboard_navigation'; diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/utils.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/utils.ts index 88662edc0fce..a6065e3b565e 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/utils.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/utils.ts @@ -1,5 +1,5 @@ import { isDefined } from '@js/core/utils/type'; -import type { Column } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; export function getSummaryCellIndex( column: Column, diff --git a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts index c51abcaf1329..175741a76b25 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/m_adaptivity.ts @@ -19,7 +19,8 @@ import { isMaterial } from '@js/ui/themes'; import type { ResizingController } from '@ts/grids/grid_core/views/m_grid_view'; import type { ExportController } from '../../data_grid/export/m_export'; -import type { Column, ColumnsController } from '../columns_controller/m_columns_controller'; +import type { ColumnsController } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import type { ColumnsResizerViewController, DraggingHeaderViewController } from '../columns_resizing_reordering/m_columns_resizing_reordering'; import type { DataController } from '../data_controller/m_data_controller'; import type { EditingController } from '../editing/m_editing'; 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 abbaf2c42428..9ba809a2b1c8 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 @@ -11,7 +11,7 @@ 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 type { Column } from '../columns_controller/types'; 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'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts new file mode 100644 index 000000000000..3db497548eb3 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.test.ts @@ -0,0 +1,227 @@ +import { + afterEach, beforeEach, describe, expect, it, +} from '@jest/globals'; + +import { + afterTest, + beforeTest, + createDataGrid, +} from '../__tests__/__mock__/helpers/utils'; + +const dataSource = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + { id: 3, name: 'Item 3' }, +]; + +describe('Column Controller', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('Expand columns in band columns layout', () => { + it('detail expand column header should have rowspan equal to header row count when band columns are used', async () => { + const { instance } = await createDataGrid({ + dataSource, + columns: [ + { caption: 'Band column 1', columns: ['id', 'name'] }, + { dataField: 'name', caption: 'Column 3', name: 'Column3' }, + ], + masterDetail: { + enabled: true, + }, + }); + + const columnsController = instance.getController('columns'); + const rowCount = columnsController.getRowCount(); + const firstRowColumns = columnsController.getVisibleColumns(0); + + expect(rowCount).toBe(2); + + const expandColumn = firstRowColumns.find((col) => col.command === 'expand'); + expect(expandColumn).toBeDefined(); + expect(expandColumn.rowspan).toBe(2); + }); + + it('should place expand columns only in the first header row with grouped columns', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { + TestField1: 'group1', TestField2: 'group2', TestField3: 'val3', TestField4: 'val4', + }, + ], + columns: [ + { dataField: 'TestField1', caption: 'Column 1', groupIndex: 0 }, + { + caption: 'Band Column 1', + columns: [ + { dataField: 'TestField2', caption: 'Column 2', groupIndex: 1 }, + { dataField: 'TestField3', caption: 'Column 3' }, + { caption: 'Band Column 2', columns: [{ dataField: 'TestField4', caption: 'Column 4' }] }, + ], + }, + ], + }); + + const columnsController = instance.getController('columns'); + const rowCount = columnsController.getRowCount(); + + // Row 0: expand columns + Band Column 1 + const firstRowColumns = columnsController.getVisibleColumns(0); + const expandColumnsInFirstRow = firstRowColumns.filter((col) => col.command === 'expand'); + + expect(rowCount).toBe(3); + expect(firstRowColumns.length).toBe(3); + expect(expandColumnsInFirstRow.length).toBe(2); + + expandColumnsInFirstRow.forEach((col) => { + expect(col.rowspan).toBe(3); + }); + + const bandColumn = firstRowColumns.find((col) => col.caption === 'Band Column 1'); + + expect(bandColumn).toBeDefined(); + expect(bandColumn.isBand).toBe(true); + expect(bandColumn.rowspan).toBeUndefined(); + + // Row 1: Column 3 + Band Column 2 + const secondRowColumns = columnsController.getVisibleColumns(1); + const expandColumnsInSecondRow = secondRowColumns.filter((col) => col.command === 'expand'); + + expect(secondRowColumns.length).toBe(2); + expect(expandColumnsInSecondRow.length).toBe(0); + + const column3 = secondRowColumns.find((col) => col.caption === 'Column 3'); + + expect(column3).toBeDefined(); + expect(column3.rowspan).toBe(2); + + const bandColumn2 = secondRowColumns.find((col) => col.caption === 'Band Column 2'); + + expect(bandColumn2).toBeDefined(); + expect(bandColumn2.isBand).toBe(true); + expect(bandColumn2.rowspan).toBeUndefined(); + + // Row 2: Column 4 + const thirdRowColumns = columnsController.getVisibleColumns(2); + const expandColumnsInThirdRow = thirdRowColumns.filter((col) => col.command === 'expand'); + const column4 = thirdRowColumns.find((col) => col.caption === 'Column 4'); + + expect(expandColumnsInThirdRow.length).toBe(0); + expect(column4).toBeDefined(); + expect(column4.rowspan).toBeUndefined(); + }); + + it('should place expand columns only in the first header row with showWhenGrouped', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { field1: 'g1', field2: 'g2', field3: 'g3' }, + ], + columns: [{ + dataField: 'field1', + showWhenGrouped: true, + groupIndex: 0, + }, { + caption: 'band2', + columns: [{ + dataField: 'field2', + showWhenGrouped: true, + groupIndex: 1, + }, { + caption: 'band3', + columns: [{ + dataField: 'field3', + showWhenGrouped: true, + groupIndex: 2, + }], + }], + }], + }); + + const columnsController = instance.getController('columns'); + const rowCount = columnsController.getRowCount(); + + expect(rowCount).toBe(3); + + // Row 0: expand columns with rowspan=3, data columns with rowspan=3, band column + const firstRowColumns = columnsController.getVisibleColumns(0); + const expandColumnsRow0 = firstRowColumns.filter((col) => col.command === 'expand'); + + expect(expandColumnsRow0.length).toBe(3); + expandColumnsRow0.forEach((col) => { + expect(col.rowspan).toBe(3); + }); + + // showWhenGrouped data columns should be in the first row with rowspan=3 + const field1Col = firstRowColumns.find((col) => col.caption === 'Field 1' && !col.command); + expect(field1Col).toBeDefined(); + expect(field1Col.rowspan).toBe(3); + + // band2 should be in the first row without rowspan (it has children) + const band2Col = firstRowColumns.find((col) => col.caption === 'band2'); + expect(band2Col).toBeDefined(); + expect(band2Col.rowspan).toBeUndefined(); + + // Row 1: no expand columns + const secondRowColumns = columnsController.getVisibleColumns(1); + const expandColumnsRow1 = secondRowColumns.filter((col) => col.command === 'expand'); + + expect(expandColumnsRow1.length).toBe(0); + + // band3 should be in the second row + const band3Col = secondRowColumns.find((col) => col.caption === 'band3'); + expect(band3Col).toBeDefined(); + + // Row 2: no expand columns + const thirdRowColumns = columnsController.getVisibleColumns(2); + const expandColumnsRow2 = thirdRowColumns.filter((col) => col.command === 'expand'); + + expect(expandColumnsRow2.length).toBe(0); + }); + + it('should not set rowspan on expand columns when there is only one header row with grouped showWhenGrouped columns', async () => { + const { instance } = await createDataGrid({ + dataSource: [ + { + field1: 'val1', field2: 'val2', field3: 'g1', field4: 'val4', + }, + ], + columns: [ + 'field1', + 'field2', + { dataField: 'field3', showWhenGrouped: true, groupIndex: 0 }, + ], + }); + + const columnsController = instance.getController('columns'); + const rowCount = columnsController.getRowCount(); + + expect(rowCount).toBe(1); + + const visibleColumns = columnsController.getVisibleColumns(0); + const expandColumn = visibleColumns.find((col) => col.command === 'expand' || col.type === 'groupExpand'); + + expect(expandColumn).toBeDefined(); + expect(expandColumn.rowspan).toBeUndefined(); + }); + + it('should not set rowspan on expand column when there is a single header row', async () => { + const { instance } = await createDataGrid({ + dataSource, + columns: ['id', 'name'], + masterDetail: { + enabled: true, + }, + }); + + const columnsController = instance.getController('columns'); + const rowCount = columnsController.getRowCount(); + const firstRowColumns = columnsController.getVisibleColumns(0); + + const expandColumn = firstRowColumns.find((col) => col.command === 'expand'); + + expect(rowCount).toBe(1); + expect(expandColumn).toBeDefined(); + expect(expandColumn.rowspan).toBeUndefined(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts index da8c70ec4951..783e42bceee2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts @@ -3,7 +3,6 @@ import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import { DataSource } from '@js/common/data/data_source/data_source'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; -import type { ColumnBase } from '@js/common/grids'; import config from '@js/core/config'; import $ from '@js/core/renderer'; import Callbacks from '@js/core/utils/callbacks'; @@ -13,14 +12,14 @@ import { extend } from '@js/core/utils/extend'; import { each, map } from '@js/core/utils/iterator'; import { orderEach } from '@js/core/utils/object'; import { - isDefined, isFunction, isNumeric, isObject, isPlainObject, - isString, + isDefined, isFunction, isNumeric, isObject, isPlainObject, isString, } from '@js/core/utils/type'; import variableWrapper from '@js/core/utils/variable_wrapper'; import Store from '@js/data/abstract_store'; import filterUtils from '@js/ui/shared/filtering'; import errors from '@js/ui/widget/ui.errors'; import inflector from '@ts/core/utils/m_inflector'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/m_data_controller'; import type { FocusController } from '@ts/grids/grid_core/focus/m_focus'; import type { StateStoringController } from '@ts/grids/grid_core/state_storing/m_state_storing_core'; @@ -69,7 +68,8 @@ import { isSortOrderValid, mergeColumns, moveColumnToGroup, - numberToString, processBandColumns, + numberToString, + processBandColumns, processExpandColumns, resetBandColumnsCache, resetColumnsCache, @@ -82,13 +82,9 @@ import { updateSerializers, } from './m_columns_controller_utils'; -export interface Column extends ColumnBase { - parseValue: (text: string) => unknown; - index?: number; - groupIndex?: number; - type?: string; - visibleWidth?: string | number; - command?: string; +interface IndexedColumns { + positiveIndexedColumns: Record[][]; + negativeIndexedColumns: Record[]; } export class ColumnsController extends modules.Controller { @@ -297,7 +293,7 @@ export class ColumnsController extends modules.Controller { private _columnOptionChanged(args) { let columnOptionValue = {}; const column = this.getColumnByPath(args.fullName); - const columnOptionName = args.fullName.replace(COLUMN_OPTION_REGEXP, ''); + const columnOptionName = this.getColumnOptionNameByFullName(args.fullName); if (column) { if (columnOptionName) { @@ -709,9 +705,9 @@ export class ColumnsController extends modules.Controller { }); } - private _compileVisibleColumnsCore() { + private _compileVisibleColumnsCore(): Column[][] { const bandColumnsCache = this.getBandColumnsCache(); - const columns = mergeColumns(this, this._columns, this._commandColumns, true); + const columns: Column[] = mergeColumns(this, this._columns, this._commandColumns, true); processBandColumns(this, columns, bandColumnsCache); @@ -728,18 +724,18 @@ export class ColumnsController extends modules.Controller { return visibleColumns; } - private _getIndexedColumns(columns) { + private _getIndexedColumns(columns: Column[]): IndexedColumns { const rtlEnabled = this.option('rtlEnabled'); const rowCount = this.getRowCount(); const columnDigitsCount = digitsCount(columns.length); const bandColumnsCache = this.getBandColumnsCache(); - const positiveIndexedColumns: any = []; - const negativeIndexedColumns: any = []; + const positiveIndexedColumns: IndexedColumns['positiveIndexedColumns'] = []; + const negativeIndexedColumns: IndexedColumns['negativeIndexedColumns'] = []; for (let i = 0; i < rowCount; i += 1) { - negativeIndexedColumns[i] = [{}]; + negativeIndexedColumns[i] = {}; // 0 - fixed columns on the left side // 1 - not fixed columns @@ -748,26 +744,30 @@ export class ColumnsController extends modules.Controller { } columns.forEach((column) => { - let { visibleIndex } = column; - let indexedColumns; - - const parentBandColumns = getParentBandColumns(column.index, bandColumnsCache.columnParentByIndex); - + const { visibleIndex } = column; const isVisible = this._isColumnVisible(column); const isInGroupPanel = this._isColumnInGroupPanel(column); if (isVisible && !isInGroupPanel) { + const parentBandColumns = getParentBandColumns( + column.index, + bandColumnsCache.columnParentByIndex, + ); const rowIndex = parentBandColumns.length; + let targetIndex: string | number = visibleIndex ?? 'undefined'; + // eslint-disable-next-line @typescript-eslint/init-declarations + let indexedColumns: Record; - if (visibleIndex < 0) { - visibleIndex = -visibleIndex; + if (isDefined(visibleIndex) && visibleIndex < 0) { + targetIndex = -visibleIndex; indexedColumns = negativeIndexedColumns[rowIndex]; } else { column.fixed = parentBandColumns[0]?.fixed ?? column.fixed; column.fixedPosition = parentBandColumns[0]?.fixedPosition ?? column.fixedPosition; if (column.fixed && column.fixedPosition !== StickyPosition.Sticky) { - const isDefaultCommandColumn = !!column.command && !gridCoreUtils.isCustomCommandColumn(this._columns, column); + const isDefaultCommandColumn = !!column.command + && !gridCoreUtils.isCustomCommandColumn(this._columns, column); let isFixedToEnd = column.fixedPosition === 'right'; @@ -784,15 +784,16 @@ export class ColumnsController extends modules.Controller { } if (parentBandColumns.length) { - visibleIndex = numberToString(visibleIndex, columnDigitsCount); + targetIndex = numberToString(targetIndex, columnDigitsCount); for (let i = parentBandColumns.length - 1; i >= 0; i -= 1) { - visibleIndex = numberToString(parentBandColumns[i].visibleIndex, columnDigitsCount) + visibleIndex; + const { visibleIndex: parentVisibleIndex } = parentBandColumns[i]; + targetIndex = `${numberToString(parentVisibleIndex, columnDigitsCount)}${targetIndex}`; } } - indexedColumns[visibleIndex] = indexedColumns[visibleIndex] || []; - indexedColumns[visibleIndex].push(column); + indexedColumns[targetIndex] = indexedColumns[targetIndex] || []; + indexedColumns[targetIndex].push(column); } }); @@ -801,41 +802,50 @@ export class ColumnsController extends modules.Controller { }; } - private _getVisibleColumnsFromIndexed({ positiveIndexedColumns, negativeIndexedColumns }) { - const result: any = []; - - const rowCount = this.getRowCount(); - const expandColumns = mergeColumns(this, this.getExpandColumns(), this._columns); - - let rowspanGroupColumns = 0; - let rowspanExpandColumns = 0; + private _getVisibleColumnsFromIndexed({ + positiveIndexedColumns, + negativeIndexedColumns, + }: IndexedColumns): Column[][] { + const result: Column[][] = []; + const rowCount: number = this.getRowCount(); + const expandColumns: Column[] = mergeColumns(this, this.getExpandColumns(), this._columns); + // Process header rows columns for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { result.push([]); - orderEach(negativeIndexedColumns[rowIndex], (_, columns) => { - result[rowIndex].unshift.apply(result[rowIndex], columns); + orderEach(negativeIndexedColumns[rowIndex], (_: string, columns: Column[]) => { + result[rowIndex].unshift(...columns); }); + } - const firstPositiveIndexColumn = result[rowIndex].length; - const positiveIndexedRowColumns = positiveIndexedColumns[rowIndex]; + const firstExpandColumnIndex = result[0].length; - positiveIndexedRowColumns.forEach((columnsByFixing) => { - orderEach(columnsByFixing, (_, columnsByVisibleIndex) => { - result[rowIndex].push.apply(result[rowIndex], columnsByVisibleIndex); + for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { + positiveIndexedColumns[rowIndex].forEach((columnsByFixing) => { + orderEach(columnsByFixing, (_: string, columnsByVisibleIndex: Column[]) => { + result[rowIndex].push(...columnsByVisibleIndex); }); }); - - // The order of processing is important - if (rowspanExpandColumns <= rowIndex) { - rowspanExpandColumns += processExpandColumns.call(this, result[rowIndex], expandColumns, DETAIL_COMMAND_COLUMN_NAME, firstPositiveIndexColumn); - } - - if (rowspanGroupColumns <= rowIndex) { - rowspanGroupColumns += processExpandColumns.call(this, result[rowIndex], expandColumns, GROUP_COMMAND_COLUMN_NAME, firstPositiveIndexColumn); - } } + // The order of processing is important + processExpandColumns( + result[0], + expandColumns, + DETAIL_COMMAND_COLUMN_NAME, + firstExpandColumnIndex, + rowCount, + ); + processExpandColumns( + result[0], + expandColumns, + GROUP_COMMAND_COLUMN_NAME, + firstExpandColumnIndex, + rowCount, + ); + + // Process table body columns result.push(getDataColumns(result)); return result; @@ -1944,6 +1954,10 @@ export class ColumnsController extends modules.Controller { public isNeedToRenderVirtualColumns(scrollPosition: number): boolean { return false; } + + public getColumnOptionNameByFullName(fullName: string): string { + return fullName.replace(COLUMN_OPTION_REGEXP, ''); + } } export const columnsControllerModule: Module = { diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts index ed0e9dd13126..dfd9047faca7 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller_utils.ts @@ -5,13 +5,14 @@ import { equalByValue } from '@js/core/utils/common'; import { compileGetter, compileSetter } from '@js/core/utils/data'; import dateSerialization from '@js/core/utils/date_serialization'; import { extend } from '@js/core/utils/extend'; -import { each, map } from '@js/core/utils/iterator'; +import { each } from '@js/core/utils/iterator'; import { deepExtendArraySafe } from '@js/core/utils/object'; import { getDefaultAlignment } from '@js/core/utils/position'; import { isDefined, isFunction, isNumeric, isObject, isString, type, } from '@js/core/utils/type'; import variableWrapper from '@js/core/utils/variable_wrapper'; +import type { DataGridCommandColumnType } from '@js/ui/data_grid'; import errors from '@js/ui/widget/ui.errors'; import { HIDDEN_COLUMNS_WIDTH } from '../adaptivity/const'; @@ -30,8 +31,8 @@ import { USER_STATE_FIELD_NAMES_15_1, VIRTUAL_COMMAND_COLUMN_NAME, } from './const'; -import type { Column, ColumnsController } from './m_columns_controller'; -import type { ColumnIndex, DropLocationNames } from './types'; +import type { ColumnsController } from './m_columns_controller'; +import type { Column, ColumnIndex, DropLocationNames } from './types'; const warnFixedInChildColumnsOnce = (controller: ColumnsController, childColumns: any[]): void => { if (controller?._isWarnedAboutUnsupportedProperties) return; @@ -853,26 +854,22 @@ export const getFixedPosition = function (that: ColumnsController, column) { return column.fixedPosition; }; -export const processExpandColumns = function (columns, expandColumns, type, columnIndex) { - let customColumnIndex; - const rowCount = this.getRowCount(); - let rowspan = columns[columnIndex] && columns[columnIndex].rowspan; - let expandColumnsByType = expandColumns.filter((column) => column.type === type); - - columns.forEach((column, index) => { - if (column.type === type) { - customColumnIndex = index; - rowspan = columns[index + 1] ? columns[index + 1].rowspan : rowCount; - } - }); - - if (rowspan > 1) { - expandColumnsByType = map(expandColumnsByType, (expandColumn) => extend({}, expandColumn, { rowspan })); - } - expandColumnsByType.unshift.apply(expandColumnsByType, isDefined(customColumnIndex) ? [customColumnIndex, 1] : [columnIndex, 0]); - columns.splice.apply(columns, expandColumnsByType); - - return rowspan || 1; +export const processExpandColumns = ( + columns: Column[], + expandColumns: Column[], + commandType: DataGridCommandColumnType, + columnIndex: number, + rowspan: number, +): void => { + const expandColumnsByType = expandColumns + .filter((column) => column.type === commandType) + .map((column): Column => (rowspan > 1 ? { ...column, rowspan } : column)); + + const customExpandColumnIndex = columns.findIndex((column) => column.type === commandType); + const targetIndex = customExpandColumnIndex >= 0 ? customExpandColumnIndex : columnIndex; + const deleteCount = customExpandColumnIndex >= 0 ? 1 : 0; + + columns.splice(targetIndex, deleteCount, ...expandColumnsByType); }; export const digitsCount = function (number) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts index 90dd5a1e741e..d143a56c54d5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts @@ -1,3 +1,5 @@ +import type { ColumnBase } from '@js/common/grids'; + import type { COLUMN_CHOOSER_LOCATION, GROUP_LOCATION, @@ -12,3 +14,14 @@ export type ColumnIndex = number | { rowIndex: number; columnIndex: number; }; + +export interface Column extends ColumnBase { + parseValue?: (text: string) => unknown; + index?: number; + groupIndex?: number; + type?: string; + visibleWidth?: string | number; + command?: string; + rowspan?: number; + colspan?: number; +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/types.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/types.ts index e3360e331618..6324b4f842d1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/types.ts @@ -1,4 +1,4 @@ -import type { Column } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import type { Item, UserData } from '../data_controller/m_data_controller'; import type { RowKey } from '../m_types'; import type { INSERT_INDEX } from './const'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_column_keyboard_navigation_core.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_column_keyboard_navigation_core.ts index 4de2724c236e..bf4b3eb11d3d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_column_keyboard_navigation_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_column_keyboard_navigation_core.ts @@ -1,6 +1,6 @@ import { isDefined, isEmptyObject } from '@js/core/utils/type'; -import type { Column } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import { Direction } from './const'; import type { ColumnFocusDispatcher } from './m_column_focus_dispatcher'; import { KeyboardNavigationController as KeyboardNavigationControllerCore } from './m_keyboard_navigation_core'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_headers_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_headers_keyboard_navigation.ts index 0ff0e50ef9c7..855df101d56d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_headers_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_headers_keyboard_navigation.ts @@ -6,7 +6,7 @@ import $ from '@js/core/renderer'; import { getBoundingRect } from '@js/core/utils/position'; import { isDefined } from '@js/core/utils/type'; -import type { Column } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import type { Views } from '../m_types'; import { StickyPosition } from '../sticky_columns/const'; import { GridCoreStickyColumnsDom } from '../sticky_columns/dom'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts index 52cec10e8e0e..37558b0deda3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/keyboard_navigation/m_keyboard_navigation.ts @@ -31,7 +31,7 @@ import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; import { memoize } from '@ts/utils/memoize'; -import type { Column } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import { EDIT_FORM_CLASS, EDIT_MODE_BATCH, diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 4bec9c0e1d6f..6826eb307d57 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -22,7 +22,7 @@ import sharedFiltering from '@js/ui/shared/filtering'; import { isNumeric } from '@ts/core/utils/m_type'; import type { ColumnPoint } from '@ts/grids/grid_core/m_types'; -import type { Column } from './columns_controller/m_columns_controller'; +import type { Column } from './columns_controller/types'; import { isEqualSelectors, isSelectorEqualWithCallback } from './utils/index'; const BASE_LOAD_PANEL_Z_INDEX = 1000; diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index 30d0a86685fb..d7e78165bd0a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts @@ -7,7 +7,7 @@ import domAdapter from '@js/core/dom_adapter'; import $ from '@js/core/renderer'; import { compileGetter, toComparable } from '@js/core/utils/data'; -import type { Column } from '../columns_controller/m_columns_controller'; +import type { Column } from '../columns_controller/types'; import type { DataController, Filter } from '../data_controller/m_data_controller'; import type { HeaderPanel } from '../header_panel/m_header_panel'; import type { ModuleType } from '../m_types'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts index 503d34b73238..d8c8bdd6ae8e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/selection/m_selection.ts @@ -16,7 +16,8 @@ import { isDefined, isObject } from '@js/core/utils/type'; import errors from '@js/ui/widget/ui.errors'; import supportUtils from '@ts/core/utils/m_support'; import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers'; -import type { Column, ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { ContextMenuController } from '@ts/grids/grid_core/context_menu/m_context_menu'; import type { ModuleType } from '@ts/grids/grid_core/m_types'; import type { StateStoringController } from '@ts/grids/grid_core/state_storing/m_state_storing_core'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js index 448e2d2ee2a8..622149cb8e07 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsController.tests.js @@ -7787,83 +7787,6 @@ QUnit.module('Band columns', { beforeEach: setupModule, afterEach: teardownModul assert.ok(!visibleColumns[1].isBand, 'data column'); }); - QUnit.test('getVisibleColumns with rowIndex and grouped columns', function(assert) { - // arrange - this.applyOptions({ - columns: [ - { dataField: 'TestField1', caption: 'Column 1', groupIndex: 0 }, - { - caption: 'Band Column 1', columns: [ - { dataField: 'TestField2', caption: 'Column 2', groupIndex: 1 }, - { dataField: 'TestField3', caption: 'Column 3' }, - { caption: 'Band Column 2', columns: [{ dataField: 'TestField4', caption: 'Column 4' }] } - ] - } - ] - }); - - // assert - assert.ok(this.columnsController.isInitialized()); - - // act - let visibleColumns = this.columnsController.getVisibleColumns(0); - - // assert - assert.equal(visibleColumns.length, 3, 'count column'); - - // first column - assert.strictEqual(visibleColumns[0].caption, 'Column 1', 'caption of the first column of the first row'); - assert.strictEqual(visibleColumns[0].command, 'expand', 'command column'); - assert.ok(!visibleColumns[0].rowspan, 'rowspan of the first column of the first row'); - - // second column - assert.strictEqual(visibleColumns[1].caption, 'Column 2', 'caption of the second column of the first row'); - assert.strictEqual(visibleColumns[1].command, 'expand', 'command column'); - assert.ok(!visibleColumns[1].rowspan, 'rowspan of the second column of the first row'); - - // third column - assert.strictEqual(visibleColumns[2].caption, 'Band Column 1', 'caption of the second column of the first row'); - assert.equal(visibleColumns[2].colspan, 2, 'colspan of the second column of the first row'); - assert.ok(visibleColumns[2].isBand, 'band column'); - - // act - visibleColumns = this.columnsController.getVisibleColumns(1); - - // assert - assert.equal(visibleColumns.length, 4, 'count column'); - - // first column - assert.strictEqual(visibleColumns[0].caption, 'Column 1', 'caption of the first column of the second row'); - assert.strictEqual(visibleColumns[0].command, 'expand', 'command column'); - assert.equal(visibleColumns[0].rowspan, 2, 'rowspan of the first column of the second row'); - - // second column - assert.strictEqual(visibleColumns[1].caption, 'Column 2', 'caption of the second column of the second row'); - assert.strictEqual(visibleColumns[1].command, 'expand', 'command column'); - assert.equal(visibleColumns[1].rowspan, 2, 'rowspan of the second column of the second row'); - - // third column - assert.strictEqual(visibleColumns[2].caption, 'Column 3', 'caption of the third column of the second row'); - assert.equal(visibleColumns[2].rowspan, 2, 'rowspan of the third column of the second row'); - assert.ok(!visibleColumns[2].isBand, 'data column'); - - // fourth column - assert.strictEqual(visibleColumns[3].caption, 'Band Column 2', 'caption of the fourth column of the second row'); - assert.equal(visibleColumns[3].colspan, 1, 'colspan of the fourth column of the second row'); - assert.ok(visibleColumns[3].isBand, 'band column'); - - // act - visibleColumns = this.columnsController.getVisibleColumns(2); - - // assert - assert.equal(visibleColumns.length, 1, 'count column'); - - // first column - assert.strictEqual(visibleColumns[0].caption, 'Column 4', 'caption of the first column of the third row'); - assert.ok(!visibleColumns[0].rowspan, 'rowspan of the first column of the third row'); - assert.ok(!visibleColumns[0].isBand, 'data column'); - }); - QUnit.test('getVisibleColumnIndex with rowIndex', function(assert) { // arrange this.applyOptions({ @@ -7991,46 +7914,6 @@ QUnit.module('Band columns', { beforeEach: setupModule, afterEach: teardownModul assert.notOk(thirdRowColumns[0].rowspan, 'rowspan of the first column of the third row'); }); - // T895529 - QUnit.test('getVisibleColumns when there are grouped columns with showWhenGrouped', function(assert) { - // arrange - this.applyOptions({ - columns: [ - { - caption: 'Band 1', - columns: ['field1', 'field2'] - }, - { - caption: 'Band 2', - columns: [{ dataField: 'field3', showWhenGrouped: true, groupIndex: 0 }, 'field4'] - } - ] - }); - - // assert - assert.ok(this.columnsController.isInitialized()); - - // act - const visibleColumns = this.columnsController.getVisibleColumns(); - - assert.equal(visibleColumns.length, 5, 'column count in second row'); - assert.equal(visibleColumns[0].type, 'groupExpand', 'type of the first column'); - assert.equal(visibleColumns[0].colspan, undefined, 'colspan of the first column'); - assert.equal(visibleColumns[0].rowspan, undefined, 'rowspan of the first column'); - assert.equal(visibleColumns[1].caption, 'Field 1', 'caption of the second column'); - assert.equal(visibleColumns[1].colspan, undefined, 'colspan of the second column'); - assert.equal(visibleColumns[1].rowspan, undefined, 'rowspan of the second column'); - assert.equal(visibleColumns[2].caption, 'Field 2', 'caption of the third column'); - assert.equal(visibleColumns[2].colspan, undefined, 'colspan of the third column'); - assert.equal(visibleColumns[2].rowspan, undefined, 'rowspan of the third column'); - assert.equal(visibleColumns[3].caption, 'Field 3', 'caption of the fourth column'); - assert.equal(visibleColumns[3].colspan, undefined, 'colspan of the fourth column'); - assert.equal(visibleColumns[3].rowspan, undefined, 'rowspan of the fourth column'); - assert.equal(visibleColumns[4].caption, 'Field 4', 'caption of the fifth column'); - assert.equal(visibleColumns[4].colspan, undefined, 'colspan of the fifth column'); - assert.equal(visibleColumns[4].rowspan, undefined, 'rowspan of the fifth column'); - }); - QUnit.test('getFixedColumns for data columns', function(assert) { // arrange this.applyOptions({ @@ -8950,31 +8833,6 @@ QUnit.module('Band columns', { beforeEach: setupModule, afterEach: teardownModul assert.notOk(this.columnsController.isBandColumnsUsed(), 'band column is not used'); }); - // T647024 - QUnit.test('Expand column must have the right rowspan', function(assert) { - // arrange - - this.applyOptions({ - columns: [ - { caption: 'Band column 1', columns: ['Column1', 'Column2'] }, - { dataField: 'Column3', caption: 'Column 3' } - ], - masterDetail: { - enabled: true - } - }); - - // act - this.columnsController.getVisibleColumns(); - this.columnsController.resetColumnsCache(); - const visibleColumns = this.columnsController.getVisibleColumns(); - - // assert - assert.strictEqual(visibleColumns.length, 4, 'column count'); - assert.strictEqual(visibleColumns[0].command, 'expand', 'expand column'); - assert.ok(!visibleColumns[0].rowspan, 'rowspan of the expand column'); - }); - // T670211 QUnit.test('Delete band column via API', function(assert) { // arrange diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsHeadersView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsHeadersView.tests.js index 1ca0fb9cd380..fa396d44cd8f 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsHeadersView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/columnsHeadersView.tests.js @@ -2795,11 +2795,15 @@ QUnit.module('Headers with band columns', { // act const $headerCells = $testElement.find('.dx-row.dx-column-lines.dx-header-row').children(); + const $expandHeaders = $headerCells.filter('.dx-command-expand'); + const $dataColumnHeaders = $headerCells.filter(':not(.dx-command-expand)'); // assert - assert.equal($headerCells.length, 4, 'header cell count'); + assert.equal($headerCells.length, 3, 'header cell count'); + assert.equal($expandHeaders.length, 1, 'single expand header cell for 2 rows'); - $headerCells.each((_, headerCellElement) => { + assert.strictEqual($expandHeaders.eq(0).attr('rowspan'), '2', 'expand header cell has correct rowspan'); + $dataColumnHeaders.each((_, headerCellElement) => { assert.strictEqual($(headerCellElement).attr('rowspan'), undefined); }); });