From 394da245289d3c4787208c0e99cbf2f476793ee6 Mon Sep 17 00:00:00 2001 From: Raushen Date: Tue, 13 Jan 2026 22:43:21 +0200 Subject: [PATCH 01/14] Ad test --- .../__mock__/model/column_chooser.ts | 43 ++++++ .../__tests__/__mock__/model/grid_core.ts | 5 + .../column_chooser.integration.test.ts | 143 ++++++++++++++++++ .../ui/__tests__/__mock__/model/checkbox.ts | 15 ++ .../ui/__tests__/__mock__/model/textbox.ts | 13 ++ .../ui/__tests__/__mock__/model/tree_view.ts | 52 +++++++ 6 files changed, 271 insertions(+) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/column_chooser.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts create mode 100644 packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts create mode 100644 packages/devextreme/js/__internal/ui/__tests__/__mock__/model/textbox.ts create mode 100644 packages/devextreme/js/__internal/ui/__tests__/__mock__/model/tree_view.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/column_chooser.ts new file mode 100644 index 000000000000..c2d7657a7083 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/column_chooser.ts @@ -0,0 +1,43 @@ +import { TreeViewModel } from '@ts/ui/__tests__/__mock__/model/tree_view'; + +const CLASSES = { + columnChooser: 'dx-datagrid-column-chooser', + columnChooserList: 'dx-datagrid-column-chooser-list', + popupWrapper: 'dx-popup-wrapper', +}; + +export class ColumnChooserModel { + constructor(protected readonly root: HTMLElement) {} + + private getPopupWrapper(): HTMLElement | null { + return document.body.querySelector(`.${CLASSES.popupWrapper}.${CLASSES.columnChooser}`); + } + + private getOverlay(): HTMLElement | null { + const wrapper = this.getPopupWrapper(); + return wrapper?.querySelector('.dx-overlay-content') ?? null; + } + + private getTreeView(): TreeViewModel | null { + const overlay = this.getOverlay(); + if (!overlay) return null; + + const treeViewElement = overlay.querySelector(`.${CLASSES.columnChooserList}`) as HTMLElement; + return treeViewElement ? new TreeViewModel(treeViewElement) : null; + } + + public isVisible(): boolean { + return this.getOverlay() !== null; + } + + public searchColumn(text: string): void { + const treeView = this.getTreeView(); + treeView?.setSearchValue(text); + } + + public toggleColumn(columnText: string): void { + const treeView = this.getTreeView(); + const checkBox = treeView?.getCheckboxByText(columnText); + checkBox?.toggle(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts index b949ac1d3ee0..0d93490374ae 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/__mock__/model/grid_core.ts @@ -10,6 +10,7 @@ import { AIPromptEditorModel } from './ai_prompt_editor'; import { AIHeaderCellModel } from './cell/ai_header_cell'; import { DataCellModel } from './cell/data_cell'; import { HeaderCellModel } from './cell/header_cell'; +import { ColumnChooserModel } from './column_chooser'; import { EditFormModel } from './edit_form'; import { DataRowModel } from './row/data_row'; @@ -134,5 +135,9 @@ export abstract class GridCoreModel { return new EditFormModel(this.root.querySelector(`.${this.addWidgetPrefix(SELECTORS.editForm)}`)); } + public getColumnChooser(): ColumnChooserModel { + return new ColumnChooserModel(this.root); + } + public abstract getInstance(): TInstance; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts new file mode 100644 index 000000000000..1e65189f93ee --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts @@ -0,0 +1,143 @@ +import { + afterEach, beforeEach, describe, expect, it, jest, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import DataGrid from '@js/ui/data_grid'; +import errors from '@js/ui/widget/ui.errors'; +import { DataGridModel } from '@ts/grids/data_grid/__tests__/__mock__/model/data_grid'; + +const SELECTORS = { + gridContainer: '#gridContainer', +}; + +const GRID_CONTAINER_ID = 'gridContainer'; + +const createDataGrid = async ( + options: DataGridProperties = {}, +): Promise<{ + $container: dxElementWrapper; + component: DataGridModel; + instance: DataGrid; +}> => new Promise((resolve) => { + const $container = $('
') + .attr('id', GRID_CONTAINER_ID) + .appendTo(document.body); + + const dataGridOptions: DataGridProperties = { + keyExpr: 'id', + ...options, + }; + + const instance = new DataGrid($container.get(0) as HTMLDivElement, dataGridOptions); + const component = new DataGridModel($container.get(0) as HTMLElement); + + jest.runAllTimers(); + + resolve({ + $container, + component, + instance, + }); +}); + +const beforeTest = (): void => { + jest.useFakeTimers(); + jest.spyOn(errors, 'log').mockImplementation(jest.fn()); + jest.spyOn(errors, 'Error').mockImplementation(() => ({})); +}; + +const afterTest = (): void => { + const $container = $(SELECTORS.gridContainer); + const dataGrid = ($container as any).dxDataGrid('instance') as DataGrid; + + dataGrid.dispose(); + $container.remove(); + jest.clearAllMocks(); + jest.useRealTimers(); +}; + +describe('Bugs', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + describe('T1311329 - DataGrid - Column chooser hides a banded column on using search and recursive selection', () => { + it('should not hide banded column when using search', async () => { + const { instance, component } = await createDataGrid({ + dataSource: [ + { + id: 1, + name: 'Name 1', + value: 10, + phone: 'Banded 1', + email: 'Banded 2', + skype: 'Banded 3', + }, + ], + columnChooser: { + enabled: true, + search: { + enabled: true, + }, + mode: 'select', + selection: { + recursive: true, + selectByClick: true, + allowSelectAll: true, + }, + }, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + caption: 'Contacts', + columns: [ + { + dataField: 'phone', + visible: false, + }, + { + dataField: 'email', + }, + { + dataField: 'skype', + }, + ], + }, + ], + }); + + let visibleColumnsLevel0 = instance.getVisibleColumns(0); + let visibleColumnsLevel1 = instance.getVisibleColumns(1); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined(); + expect(visibleColumnsLevel0.find((col) => col.dataField === 'name')).toBeDefined(); + + instance.showColumnChooser(); + jest.runAllTimers(); + + const columnChooser = component.getColumnChooser(); + expect(columnChooser.isVisible()).toBe(true); + + columnChooser.searchColumn('n'); + jest.runAllTimers(); + + columnChooser.toggleColumn('Name'); + jest.runAllTimers(); + + visibleColumnsLevel0 = instance.getVisibleColumns(0); + visibleColumnsLevel1 = instance.getVisibleColumns(1); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined(); + expect(visibleColumnsLevel0.find((col) => col.dataField === 'name')).toBeUndefined(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts new file mode 100644 index 000000000000..4fe8057ef277 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts @@ -0,0 +1,15 @@ +import CheckBox from '@js/ui/check_box'; + +export class CheckBoxModel { + constructor(protected readonly root: HTMLElement) {} + + public getInstance(): CheckBox { + return CheckBox.getInstance(this.root) as CheckBox; + } + + public toggle(): void { + const instance = this.getInstance(); + const currentValue = instance.option('value'); + instance.option('value', !currentValue); + } +} diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/textbox.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/textbox.ts new file mode 100644 index 000000000000..5f52c040b63b --- /dev/null +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/textbox.ts @@ -0,0 +1,13 @@ +import TextBox from '@js/ui/text_box'; + +export class TextBoxModel { + constructor(protected readonly root: HTMLElement) {} + + public getInstance(): TextBox { + return TextBox.getInstance(this.root) as TextBox; + } + + public setValue(value: string): void { + this.getInstance()?.option('value', value); + } +} diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/tree_view.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/tree_view.ts new file mode 100644 index 000000000000..a3bc104ee225 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/tree_view.ts @@ -0,0 +1,52 @@ +import TreeView from '@js/ui/tree_view'; + +import { CheckBoxModel } from './checkbox'; +import { TextBoxModel } from './textbox'; + +const CLASSES = { + treeView: 'dx-treeview', + searchBox: 'dx-treeview-search', + node: 'dx-treeview-node', + item: 'dx-treeview-item', + checkbox: 'dx-checkbox', +}; + +export class TreeViewModel { + constructor(protected readonly root: HTMLElement) {} + + public getInstance(): TreeView { + return TreeView.getInstance(this.root) as TreeView; + } + + private getSearchBox(): TextBoxModel { + return new TextBoxModel(this.root?.querySelector(`.${CLASSES.searchBox}`) as HTMLElement); + } + + public setSearchValue(value: string): void { + const searchBox = this.getSearchBox(); + searchBox.setValue(value); + } + + private getNodes(): NodeListOf | null { + return this.root?.querySelectorAll(`.${CLASSES.node}`) ?? null; + } + + private getNodeByText(text: string): HTMLElement | null { + const nodes = this.getNodes(); + if (!nodes) return null; + + const foundNode = Array.from(nodes).find((node) => { + const itemElement = node.querySelector(`.${CLASSES.item}`); + const nodeText = itemElement?.textContent; + return nodeText?.includes(text); + }) ?? null; + + return foundNode; + } + + public getCheckboxByText(text: string): CheckBoxModel | null { + const node = this.getNodeByText(text); + const checkboxElement = node?.querySelector(`.${CLASSES.checkbox}`) as HTMLElement; + return checkboxElement ? new CheckBoxModel(checkboxElement) : null; + } +} From b9a58ce802220897dd9c353698456e849146117a Mon Sep 17 00:00:00 2001 From: Raushen Date: Tue, 13 Jan 2026 23:23:58 +0200 Subject: [PATCH 02/14] T1311329 --- .../column_chooser/m_column_chooser.ts | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 96bfc370b10a..4fd9766091a1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -288,6 +288,30 @@ export class ColumnChooserView extends ColumnsView { }; } + private getBandColumnVisibility(columnIndex: number): boolean { + const childColumns = this._columnsController.getChildrenByBandColumn(columnIndex, true); + return !!childColumns.some((column) => !!column.visible); + } + + private getColumnVisibility(columnIndex: number, isNodeSelected: boolean): boolean { + const column = this._columnsController.columnOption(columnIndex); + + if (!column?.hasColumns) { + return isNodeSelected; + } + + return this.getBandColumnVisibility(columnIndex); + } + + private updateColumnVisibility(nodes): void { + nodes.forEach((node) => { + const columnIndex = node.itemData.id; + const isVisible = this.getColumnVisibility(columnIndex, node.selected); + + this._columnsController.columnOption(columnIndex, 'visible', isVisible); + }); + } + private _prepareSelectModeConfig() { const that = this; const selectionOptions = this.option('columnChooser.selection') ?? {}; @@ -312,14 +336,6 @@ export class ColumnChooserView extends ColumnsView { .forEach((node) => e.component.selectItem(node.key)); }; - const updateColumnVisibility = (nodes) => { - nodes.forEach((node) => { - const columnIndex = node.itemData.id; - const isVisible = node.selected !== false; - that._columnsController.columnOption(columnIndex, 'visible', isVisible); - }); - }; - let isUpdatingSelection = false; const selectionChangedHandler = (e) => { @@ -340,7 +356,7 @@ export class ColumnChooserView extends ColumnsView { that.component.beginUpdate(); this._isUpdatingColumnVisibility = true; - updateColumnVisibility(nodes); + this.updateColumnVisibility(nodes); that.component.endUpdate(); this._isUpdatingColumnVisibility = false; From f3f1b96c56d176719c7cfeb351ac67d06467f445 Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 14 Jan 2026 00:41:24 +0200 Subject: [PATCH 03/14] Fix click on band column --- .../column_chooser.integration.test.ts | 83 +++++++++++++++++++ .../column_chooser/m_column_chooser.ts | 14 +++- .../ui/__tests__/__mock__/model/checkbox.ts | 4 +- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts index 1e65189f93ee..ee9c60600e0b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts @@ -139,5 +139,88 @@ describe('Bugs', () => { expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined(); expect(visibleColumnsLevel0.find((col) => col.dataField === 'name')).toBeUndefined(); }); + + it('should hide banded column by click', async () => { + const { instance, component } = await createDataGrid({ + dataSource: [ + { + id: 1, + name: 'Name 1', + value: 10, + phone: 'Banded 1', + email: 'Banded 2', + skype: 'Banded 3', + }, + ], + columnChooser: { + enabled: true, + search: { + enabled: true, + }, + mode: 'select', + selection: { + recursive: true, + selectByClick: true, + allowSelectAll: true, + }, + }, + columns: [ + { dataField: 'id', caption: 'ID' }, + { dataField: 'name', caption: 'Name' }, + { dataField: 'value', caption: 'Value' }, + { + caption: 'Contacts', + columns: [ + { + dataField: 'phone', + visible: false, + }, + { + dataField: 'email', + }, + { + dataField: 'skype', + }, + ], + }, + ], + }); + + let visibleColumnsLevel0 = instance.getVisibleColumns(0); + let visibleColumnsLevel1 = instance.getVisibleColumns(1); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined(); + + instance.showColumnChooser(); + jest.runAllTimers(); + + const columnChooser = component.getColumnChooser(); + expect(columnChooser.isVisible()).toBe(true); + + columnChooser.toggleColumn('Contacts'); + jest.runAllTimers(); + + visibleColumnsLevel0 = instance.getVisibleColumns(0); + visibleColumnsLevel1 = instance.getVisibleColumns(1); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeDefined(); + + columnChooser.toggleColumn('Contacts'); + jest.runAllTimers(); + + visibleColumnsLevel0 = instance.getVisibleColumns(0); + visibleColumnsLevel1 = instance.getVisibleColumns(1); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'Contacts')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'phone')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'email')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'skype')).toBeUndefined(); + }); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 4fd9766091a1..173a1d59fdaf 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -303,8 +303,20 @@ export class ColumnChooserView extends ColumnsView { return this.getBandColumnVisibility(columnIndex); } + private sortNodesByBandColumns(nodes): any[] { + return [...nodes].sort((a, b) => { + const columnA = this._columnsController.columnOption(a.itemData.id); + const columnB = this._columnsController.columnOption(b.itemData.id); + const isBandA = columnA?.hasColumns ? 1 : 0; + const isBandB = columnB?.hasColumns ? 1 : 0; + return isBandA - isBandB; + }); + } + private updateColumnVisibility(nodes): void { - nodes.forEach((node) => { + const sortedNodes = this.sortNodesByBandColumns(nodes); + + sortedNodes.forEach((node) => { const columnIndex = node.itemData.id; const isVisible = this.getColumnVisibility(columnIndex, node.selected); diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts index 4fe8057ef277..edc859b76028 100644 --- a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/checkbox.ts @@ -8,8 +8,6 @@ export class CheckBoxModel { } public toggle(): void { - const instance = this.getInstance(); - const currentValue = instance.option('value'); - instance.option('value', !currentValue); + this.root.click(); } } From 91950c4acb8a3be7f47daeaa2dcd9ccf82969aa4 Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 14 Jan 2026 00:46:16 +0200 Subject: [PATCH 04/14] Add comment --- .../grids/grid_core/column_chooser/m_column_chooser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 173a1d59fdaf..2d9287673730 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -309,6 +309,7 @@ export class ColumnChooserView extends ColumnsView { const columnB = this._columnsController.columnOption(b.itemData.id); const isBandA = columnA?.hasColumns ? 1 : 0; const isBandB = columnB?.hasColumns ? 1 : 0; + // Band columns should be updated after regular columns return isBandA - isBandB; }); } From 092b536ccc61e3084329fa0d8ce11d644b6402ca Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 14 Jan 2026 11:58:57 +0200 Subject: [PATCH 05/14] Fix test --- .../grids/grid_core/column_chooser/m_column_chooser.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 2d9287673730..356ff3e34e7f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -295,12 +295,14 @@ export class ColumnChooserView extends ColumnsView { private getColumnVisibility(columnIndex: number, isNodeSelected: boolean): boolean { const column = this._columnsController.columnOption(columnIndex); + const selectionOptions = this.option('columnChooser.selection'); + const recursive = selectionOptions?.recursive; - if (!column?.hasColumns) { - return isNodeSelected; + if (recursive && column?.hasColumns) { + return this.getBandColumnVisibility(columnIndex); } - return this.getBandColumnVisibility(columnIndex); + return isNodeSelected; } private sortNodesByBandColumns(nodes): any[] { From 78217d3b4883597c55f6fdae0ae829c16234e2a5 Mon Sep 17 00:00:00 2001 From: Raushen Date: Wed, 14 Jan 2026 12:17:08 +0200 Subject: [PATCH 06/14] Add types --- .../column_chooser/m_column_chooser.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 356ff3e34e7f..f529caf76551 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -9,6 +9,7 @@ import { isDefined } from '@js/core/utils/type'; import Button from '@js/ui/button'; import type { Properties as PopupProperties } from '@js/ui/popup'; import Popup from '@js/ui/popup/ui.popup'; +import type { Item } from '@js/ui/tree_view'; import TreeView from '@js/ui/tree_view'; import type { RowsView } from '@ts/grids/grid_core/views/m_rows_view'; @@ -31,6 +32,12 @@ const COLUMN_CHOOSER_ITEM_CLASS = 'dx-column-chooser-item'; const COLUMN_OPTIONS_USED_IN_ITEMS = ['showInColumnChooser', 'caption', 'allowHiding', 'visible', 'cssClass', 'ownerBand']; +type Node = Item & { + itemData: { + id: number; + }; +}; + const processItems = function (that: ColumnChooserView, chooserColumns) { const items: any = []; const isSelectMode = that.isSelectMode(); @@ -293,7 +300,10 @@ export class ColumnChooserView extends ColumnsView { return !!childColumns.some((column) => !!column.visible); } - private getColumnVisibility(columnIndex: number, isNodeSelected: boolean): boolean { + private getColumnVisibility( + columnIndex: number, + isNodeSelected: boolean | undefined, + ): boolean | undefined { const column = this._columnsController.columnOption(columnIndex); const selectionOptions = this.option('columnChooser.selection'); const recursive = selectionOptions?.recursive; @@ -305,7 +315,7 @@ export class ColumnChooserView extends ColumnsView { return isNodeSelected; } - private sortNodesByBandColumns(nodes): any[] { + private sortNodesByBandColumns(nodes: Node[]): Node[] { return [...nodes].sort((a, b) => { const columnA = this._columnsController.columnOption(a.itemData.id); const columnB = this._columnsController.columnOption(b.itemData.id); @@ -316,7 +326,7 @@ export class ColumnChooserView extends ColumnsView { }); } - private updateColumnVisibility(nodes): void { + private updateColumnVisibility(nodes: Node[]): void { const sortedNodes = this.sortNodesByBandColumns(nodes); sortedNodes.forEach((node) => { @@ -331,7 +341,7 @@ export class ColumnChooserView extends ColumnsView { const that = this; const selectionOptions = this.option('columnChooser.selection') ?? {}; - const getFlatNodes = (nodes) => { + const getFlatNodes = (nodes): any[] => { const addNodesToArray = (nodes, flatNodesArray) => nodes.reduce((result, node) => { result.push(node); From cccfac0652d1d258aab2800d1cb885eebe81f123 Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 15 Jan 2026 04:21:21 +0200 Subject: [PATCH 07/14] Add three band level test --- .../column_chooser.integration.test.ts | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts index ee9c60600e0b..3ac149c22b5e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts @@ -63,7 +63,7 @@ describe('Bugs', () => { afterEach(afterTest); describe('T1311329 - DataGrid - Column chooser hides a banded column on using search and recursive selection', () => { - it('should not hide banded column when using search', async () => { + it('should not hide banded column when using search (two levels)', async () => { const { instance, component } = await createDataGrid({ dataSource: [ { @@ -140,6 +140,88 @@ describe('Bugs', () => { expect(visibleColumnsLevel0.find((col) => col.dataField === 'name')).toBeUndefined(); }); + it('should not hide banded column when using search (three levels)', async () => { + const { instance, component } = await createDataGrid({ + dataSource: [], + columnChooser: { + enabled: true, + search: { + enabled: true, + }, + mode: 'select', + selection: { + recursive: true, + selectByClick: true, + allowSelectAll: true, + }, + }, + columns: [ + { + caption: 'band_level1', + columns: [ + { + caption: 'band_level2', + columns: [ + { + dataField: 'data1_level3', + visible: false, + }, + { + dataField: 'data2_level3', + }, + ], + }, + { + dataField: 'data1_level2', + }, + { + dataField: 'data2_level2', + }, + ], + }, + { + dataField: 'data1_level1', + }, + ], + }); + + let visibleColumnsLevel0 = instance.getVisibleColumns(0); + let visibleColumnsLevel1 = instance.getVisibleColumns(1); + let visibleColumnsLevel2 = instance.getVisibleColumns(2); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'band_level1')).toBeDefined(); + expect(visibleColumnsLevel0.find((col) => col.dataField === 'data1_level1')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'data1_level2')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'data2_level2')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.caption === 'band_level2')).toBeDefined(); + expect(visibleColumnsLevel2.find((col) => col.dataField === 'data1_level3')).toBeUndefined(); + expect(visibleColumnsLevel2.find((col) => col.dataField === 'data2_level3')).toBeDefined(); + + instance.showColumnChooser(); + jest.runAllTimers(); + + const columnChooser = component.getColumnChooser(); + expect(columnChooser.isVisible()).toBe(true); + + columnChooser.searchColumn('1'); + jest.runAllTimers(); + + columnChooser.toggleColumn('Data 1 level 1'); + jest.runAllTimers(); + + visibleColumnsLevel0 = instance.getVisibleColumns(0); + visibleColumnsLevel1 = instance.getVisibleColumns(1); + visibleColumnsLevel2 = instance.getVisibleColumns(2); + + expect(visibleColumnsLevel0.find((col) => col.caption === 'band_level1')).toBeDefined(); + expect(visibleColumnsLevel0.find((col) => col.dataField === 'data1_level1')).toBeUndefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'data1_level2')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.dataField === 'data2_level2')).toBeDefined(); + expect(visibleColumnsLevel1.find((col) => col.caption === 'band_level2')).toBeDefined(); + expect(visibleColumnsLevel2.find((col) => col.dataField === 'data1_level3')).toBeUndefined(); + expect(visibleColumnsLevel2.find((col) => col.dataField === 'data2_level3')).toBeDefined(); + }); + it('should hide banded column by click', async () => { const { instance, component } = await createDataGrid({ dataSource: [ From d2c10005c5d7437225f0d04fc0617ffae61d08bf Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 15 Jan 2026 04:24:48 +0200 Subject: [PATCH 08/14] Refactoring --- .../column_chooser/m_column_chooser.ts | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index f529caf76551..fefb69060000 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -32,10 +32,12 @@ const COLUMN_CHOOSER_ITEM_CLASS = 'dx-column-chooser-item'; const COLUMN_OPTIONS_USED_IN_ITEMS = ['showInColumnChooser', 'caption', 'allowHiding', 'visible', 'cssClass', 'ownerBand']; -type Node = Item & { +type NodeInternal = Item & { itemData: { id: number; }; + level: number; + children: NodeInternal[]; }; const processItems = function (that: ColumnChooserView, chooserColumns) { @@ -315,44 +317,37 @@ export class ColumnChooserView extends ColumnsView { return isNodeSelected; } - private sortNodesByBandColumns(nodes: Node[]): Node[] { - return [...nodes].sort((a, b) => { - const columnA = this._columnsController.columnOption(a.itemData.id); - const columnB = this._columnsController.columnOption(b.itemData.id); - const isBandA = columnA?.hasColumns ? 1 : 0; - const isBandB = columnB?.hasColumns ? 1 : 0; - // Band columns should be updated after regular columns - return isBandA - isBandB; - }); - } - - private updateColumnVisibility(nodes: Node[]): void { - const sortedNodes = this.sortNodesByBandColumns(nodes); - - sortedNodes.forEach((node) => { - const columnIndex = node.itemData.id; - const isVisible = this.getColumnVisibility(columnIndex, node.selected); + private updateColumnVisibility(nodes: NodeInternal[]): void { + nodes + .sort((a, b) => b.level - a.level) + .forEach((node) => { + const columnIndex = node.itemData.id; + const isVisible = this.getColumnVisibility(columnIndex, node.selected); - this._columnsController.columnOption(columnIndex, 'visible', isVisible); - }); + this._columnsController.columnOption(columnIndex, 'visible', isVisible); + }); } private _prepareSelectModeConfig() { - const that = this; const selectionOptions = this.option('columnChooser.selection') ?? {}; - const getFlatNodes = (nodes): any[] => { - const addNodesToArray = (nodes, flatNodesArray) => nodes.reduce((result, node) => { + const getFlatNodes = (nodes): NodeInternal[] => { + const addNodesToArray = ( + sourceNodes: NodeInternal[], + flatNodesArray: NodeInternal[], + level: number, + ): NodeInternal[] => sourceNodes.reduce((result, node) => { + node.level = level; result.push(node); if (node.children.length) { - addNodesToArray(node.children, result); + addNodesToArray(node.children, result, level + 1); } return result; }, flatNodesArray); - return addNodesToArray(nodes, []); + return addNodesToArray(nodes, [], 0); }; const updateSelection = (e, nodes) => { @@ -378,12 +373,12 @@ export class ColumnChooserView extends ColumnsView { e.component.endUpdate(); isUpdatingSelection = false; - that.component.beginUpdate(); + this.component.beginUpdate(); this._isUpdatingColumnVisibility = true; this.updateColumnVisibility(nodes); - that.component.endUpdate(); + this.component.endUpdate(); this._isUpdatingColumnVisibility = false; }; From c602c336d7a979b7c6b81550a8d255284d53468f Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 15 Jan 2026 05:00:50 +0200 Subject: [PATCH 09/14] Refactoring --- .../column_chooser/m_column_chooser.ts | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index fefb69060000..18330e313269 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -36,10 +36,14 @@ type NodeInternal = Item & { itemData: { id: number; }; - level: number; children: NodeInternal[]; }; +interface NodeLevelPair { + node: NodeInternal; + level: number; +} + const processItems = function (that: ColumnChooserView, chooserColumns) { const items: any = []; const isSelectMode = that.isSelectMode(); @@ -318,37 +322,39 @@ export class ColumnChooserView extends ColumnsView { } private updateColumnVisibility(nodes: NodeInternal[]): void { - nodes - .sort((a, b) => b.level - a.level) - .forEach((node) => { - const columnIndex = node.itemData.id; - const isVisible = this.getColumnVisibility(columnIndex, node.selected); + nodes.forEach((node) => { + const columnIndex = node.itemData.id; + const isVisible = this.getColumnVisibility(columnIndex, node.selected); - this._columnsController.columnOption(columnIndex, 'visible', isVisible); - }); + this._columnsController.columnOption(columnIndex, 'visible', isVisible); + }); } - private _prepareSelectModeConfig() { - const selectionOptions = this.option('columnChooser.selection') ?? {}; + private getSortedFlatNodes(nodes): NodeInternal[] { + const getNodeLevelPairs = ( + sourceNodes: NodeInternal[], + flatNodesArray: NodeLevelPair[], + level: number, + ): NodeLevelPair[] => sourceNodes.reduce((result, node) => { + result.push({ node, level }); - const getFlatNodes = (nodes): NodeInternal[] => { - const addNodesToArray = ( - sourceNodes: NodeInternal[], - flatNodesArray: NodeInternal[], - level: number, - ): NodeInternal[] => sourceNodes.reduce((result, node) => { - node.level = level; - result.push(node); - - if (node.children.length) { - addNodesToArray(node.children, result, level + 1); - } + if (node.children.length) { + getNodeLevelPairs(node.children, result, level + 1); + } - return result; - }, flatNodesArray); + return result; + }, flatNodesArray); - return addNodesToArray(nodes, [], 0); - }; + const flatNodes = getNodeLevelPairs(nodes, [], 0) + // Band columns should be updated after regular columns + .sort((a, b) => b.level - a.level) + .map((pair) => pair.node); + + return flatNodes; + } + + private _prepareSelectModeConfig() { + const selectionOptions = this.option('columnChooser.selection') ?? {}; const updateSelection = (e, nodes) => { nodes @@ -363,7 +369,7 @@ export class ColumnChooserView extends ColumnsView { return; } - const nodes = getFlatNodes(e.component.getNodes()); + const nodes = this.getSortedFlatNodes(e.component.getNodes()); e.component.beginUpdate(); isUpdatingSelection = true; From 2c4d33659da1679ed9251fa0d444e95bc385cc2d Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 15 Jan 2026 05:21:01 +0200 Subject: [PATCH 10/14] Rename method --- .../grids/grid_core/column_chooser/m_column_chooser.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 18330e313269..0ed677627afc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -331,7 +331,7 @@ export class ColumnChooserView extends ColumnsView { } private getSortedFlatNodes(nodes): NodeInternal[] { - const getNodeLevelPairs = ( + const getNodeLevelPairsRecursive = ( sourceNodes: NodeInternal[], flatNodesArray: NodeLevelPair[], level: number, @@ -339,13 +339,13 @@ export class ColumnChooserView extends ColumnsView { result.push({ node, level }); if (node.children.length) { - getNodeLevelPairs(node.children, result, level + 1); + getNodeLevelPairsRecursive(node.children, result, level + 1); } return result; }, flatNodesArray); - const flatNodes = getNodeLevelPairs(nodes, [], 0) + const flatNodes = getNodeLevelPairsRecursive(nodes, [], 0) // Band columns should be updated after regular columns .sort((a, b) => b.level - a.level) .map((pair) => pair.node); From bf10849d7bad65b5bdcfa159a6f19b56e600307c Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 15 Jan 2026 09:50:33 +0200 Subject: [PATCH 11/14] Test commit --- .../column_chooser/__tests__/column_chooser.integration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts index 3ac149c22b5e..3b2dd3da8f7e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/__tests__/column_chooser.integration.test.ts @@ -267,7 +267,6 @@ describe('Bugs', () => { }, ], }); - let visibleColumnsLevel0 = instance.getVisibleColumns(0); let visibleColumnsLevel1 = instance.getVisibleColumns(1); From c5feb57ac415e43a107a47c7b1cf86ede8142ff2 Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 16 Jan 2026 10:31:51 +0400 Subject: [PATCH 12/14] simplify the logic and rename private methods --- .../column_chooser/m_column_chooser.ts | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 0ed677627afc..c044cbc6bd42 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -39,11 +39,6 @@ type NodeInternal = Item & { children: NodeInternal[]; }; -interface NodeLevelPair { - node: NodeInternal; - level: number; -} - const processItems = function (that: ColumnChooserView, chooserColumns) { const items: any = []; const isSelectMode = that.isSelectMode(); @@ -301,12 +296,12 @@ export class ColumnChooserView extends ColumnsView { }; } - private getBandColumnVisibility(columnIndex: number): boolean { + private _getBandColumnVisibility(columnIndex: number): boolean { const childColumns = this._columnsController.getChildrenByBandColumn(columnIndex, true); return !!childColumns.some((column) => !!column.visible); } - private getColumnVisibility( + private _getColumnVisibility( columnIndex: number, isNodeSelected: boolean | undefined, ): boolean | undefined { @@ -315,42 +310,37 @@ export class ColumnChooserView extends ColumnsView { const recursive = selectionOptions?.recursive; if (recursive && column?.hasColumns) { - return this.getBandColumnVisibility(columnIndex); + return this._getBandColumnVisibility(columnIndex); } return isNodeSelected; } - private updateColumnVisibility(nodes: NodeInternal[]): void { + private _updateColumnVisibility(nodes: NodeInternal[]): void { nodes.forEach((node) => { const columnIndex = node.itemData.id; - const isVisible = this.getColumnVisibility(columnIndex, node.selected); + const isVisible = this._getColumnVisibility(columnIndex, node.selected); this._columnsController.columnOption(columnIndex, 'visible', isVisible); }); } - private getSortedFlatNodes(nodes): NodeInternal[] { + private _getSortedFlatNodes(nodes): NodeInternal[] { const getNodeLevelPairsRecursive = ( sourceNodes: NodeInternal[], - flatNodesArray: NodeLevelPair[], - level: number, - ): NodeLevelPair[] => sourceNodes.reduce((result, node) => { - result.push({ node, level }); + flatNodesArray: NodeInternal[], + ): NodeInternal[] => sourceNodes.reduce((result, node) => { + result.push(node); if (node.children.length) { - getNodeLevelPairsRecursive(node.children, result, level + 1); + getNodeLevelPairsRecursive(node.children, result); } return result; }, flatNodesArray); - const flatNodes = getNodeLevelPairsRecursive(nodes, [], 0) - // Band columns should be updated after regular columns - .sort((a, b) => b.level - a.level) - .map((pair) => pair.node); - - return flatNodes; + // Band columns should be updated after regular columns + return getNodeLevelPairsRecursive(nodes, []).reverse(); } private _prepareSelectModeConfig() { @@ -369,7 +359,7 @@ export class ColumnChooserView extends ColumnsView { return; } - const nodes = this.getSortedFlatNodes(e.component.getNodes()); + const nodes = this._getSortedFlatNodes(e.component.getNodes()); e.component.beginUpdate(); isUpdatingSelection = true; @@ -382,7 +372,7 @@ export class ColumnChooserView extends ColumnsView { this.component.beginUpdate(); this._isUpdatingColumnVisibility = true; - this.updateColumnVisibility(nodes); + this._updateColumnVisibility(nodes); this.component.endUpdate(); this._isUpdatingColumnVisibility = false; From 9ab1dca4e3e63282478a1f18a622e7105264c5da Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 16 Jan 2026 10:39:28 +0400 Subject: [PATCH 13/14] fix type warning --- .../grids/grid_core/column_chooser/m_column_chooser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index c044cbc6bd42..ad20ad949699 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -325,7 +325,7 @@ export class ColumnChooserView extends ColumnsView { }); } - private _getSortedFlatNodes(nodes): NodeInternal[] { + private _getSortedFlatNodes(nodes: NodeInternal[]): NodeInternal[] { const getNodeLevelPairsRecursive = ( sourceNodes: NodeInternal[], flatNodesArray: NodeInternal[], From 3bb66d215c7edda88e6aa7b530fa31e43ea50a5b Mon Sep 17 00:00:00 2001 From: Danil Mirgaev Date: Fri, 16 Jan 2026 22:15:57 +0400 Subject: [PATCH 14/14] rename methods --- .../grids/grid_core/column_chooser/m_column_chooser.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index ad20ad949699..5151f03522d7 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -325,22 +325,22 @@ export class ColumnChooserView extends ColumnsView { }); } - private _getSortedFlatNodes(nodes: NodeInternal[]): NodeInternal[] { - const getNodeLevelPairsRecursive = ( + private _getOrderedFlatNodes(nodes: NodeInternal[]): NodeInternal[] { + const getFlattenedNodesRecursive = ( sourceNodes: NodeInternal[], flatNodesArray: NodeInternal[], ): NodeInternal[] => sourceNodes.reduce((result, node) => { result.push(node); if (node.children.length) { - getNodeLevelPairsRecursive(node.children, result); + getFlattenedNodesRecursive(node.children, result); } return result; }, flatNodesArray); // Band columns should be updated after regular columns - return getNodeLevelPairsRecursive(nodes, []).reverse(); + return getFlattenedNodesRecursive(nodes, []).reverse(); } private _prepareSelectModeConfig() { @@ -359,7 +359,7 @@ export class ColumnChooserView extends ColumnsView { return; } - const nodes = this._getSortedFlatNodes(e.component.getNodes()); + const nodes = this._getOrderedFlatNodes(e.component.getNodes()); e.component.beginUpdate(); isUpdatingSelection = true;