From b82ea8090957457e3ef3365eacf0894e51824dac Mon Sep 17 00:00:00 2001 From: Mark Allen Ramirez Date: Tue, 19 May 2026 17:25:31 +0800 Subject: [PATCH 1/5] spike --- .../columns_controller/columns_controller.ts | 37 ++++++++++++++++++- .../new/grid_core/columns_controller/const.ts | 6 +++ .../options_controller_base.ts | 17 +++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts index 6f6876098824..c6c5fc868c26 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts @@ -1,3 +1,5 @@ +import { equalByValue } from '@js/core/utils/common'; +import { getPathParts } from '@js/core/utils/data'; import type { ReadonlySignal, Signal } from '@ts/core/state_manager/index'; import { computed, effect, signal } from '@ts/core/state_manager/index'; import type { DataObject } from '@ts/grids/new/grid_core/data_controller/types'; @@ -6,7 +8,9 @@ import { isColumnFilterable, mergeColumnHeaderFilterOptions } from '@ts/grids/ne import type { OptionWithChanges } from '@ts/grids/new/grid_core/options_controller/types'; import { OptionsController } from '../options_controller/options_controller'; +import { getTreeNodeByPath } from '../utils/tree/index'; import { updateColumnSettings } from './columns_settings/index'; +import { IGNORE_COLUMN_OPTION_NAMES } from './const'; import type { ColumnProperties, ColumnSettings, PreNormalizedColumn } from './options'; import type { Column, ColumnsConfigurationFromData, VisibleColumn } from './types'; import { @@ -121,14 +125,16 @@ export class ColumnsController { } public columnOption( - { name }: Column, + column: Column, // TODO: Fix type -> option may be path with dots in runtime // E.g: 'columnOption('A', 'headerFilter.search.enabled', true) option: TProp, value: ColumnSettings[TProp], ): void { + const { name } = column; const settings = this.columnsSettings.peek(); const columnIdx = getColumnIndexByName(settings, name); + const prevValue = getTreeNodeByPath(column, getPathParts(option)); this.columnsSettings.value = columnOptionUpdate( settings, @@ -136,12 +142,41 @@ export class ColumnsController { option, value, ); + + this.fireOptionChanged(columnIdx, option, value, prevValue); } public updateColumns(func: (columns: PreNormalizedColumn[]) => PreNormalizedColumn[]): void { + const prevColumns = this.columns.peek(); + let newColumnSettings = func(this.columnsSettings.peek()); newColumnSettings = normalizeColumnsVisibleIndexes(newColumnSettings); this.columnsSettings.value = newColumnSettings; + + const newColumns = this.columns.peek(); + newColumns.forEach((newColumn, columnIdx) => { + const prevColumn = prevColumns[columnIdx]; + const options = new Set([...Object.keys(prevColumn), ...Object.keys(newColumn)]); + for (const option of options) { + if (!IGNORE_COLUMN_OPTION_NAMES[option]) { + const prevValue = prevColumn[option]; + const newValue = newColumn[option]; + this.fireOptionChanged(columnIdx, option, newValue, prevValue); + } + } + }); + } + + private fireOptionChanged( + columnIndex: number, + optionName: string, + newValue: unknown, + prevValue: unknown, + ): void { + if (!equalByValue(prevValue, newValue, { maxDepth: 5 })) { + const fullOptionPath = `columns[${columnIndex}].${optionName}`; + this.options.notifyColumnOptionChanged(fullOptionPath, newValue, prevValue); + } } public setColumnOptionsFromDataItem( diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts new file mode 100644 index 000000000000..3f42777ddec4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts @@ -0,0 +1,6 @@ +export const IGNORE_COLUMN_OPTION_NAMES = { + visibleWidth: true, + bestFitWidth: true, + bufferedFilterValue: true, + selector: true, +}; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts index 0b41813464a1..509b3943a06b 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts @@ -47,6 +47,8 @@ export class OptionsController< private isControlledMode = false; + private _skipProcessingColumnsChange: string | false = false; + private readonly internalOptions: Signal>; // @ts-expect-error Component type doesn't have fields from widget.ts @@ -76,6 +78,10 @@ export class OptionsController< private onOptionChangedHandler(optionChanges: ChangedOptionInfo): void { const { fullName } = optionChanges; + if (this._skipProcessingColumnsChange === fullName) { + return; + } + this.updateIsControlledMode(); this.updateInternalOptionsState(fullName, optionChanges); } @@ -221,4 +227,15 @@ export class OptionsController< ); }); } + + public notifyColumnOptionChanged( + fullOptionPath: string, + newValue: unknown, + prevValue: unknown, + ): void { + this._skipProcessingColumnsChange = fullOptionPath; + // @ts-expect-error + this.component._notifyOptionChanged(fullOptionPath, newValue, prevValue); + this._skipProcessingColumnsChange = false; + } } From df28aaa9d9d50478ed5c2a4815065ac17a877888 Mon Sep 17 00:00:00 2001 From: Mark Allen Ramirez Date: Tue, 19 May 2026 18:29:31 +0800 Subject: [PATCH 2/5] spike fix --- .../columns_controller/columns_controller.ts | 17 ++++++++++------- .../new/grid_core/columns_controller/const.ts | 3 --- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts index c6c5fc868c26..1f15857289af 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.ts @@ -15,6 +15,7 @@ import type { ColumnProperties, ColumnSettings, PreNormalizedColumn } from './op import type { Column, ColumnsConfigurationFromData, VisibleColumn } from './types'; import { columnOptionUpdate, + getColumnByIndexOrName, getColumnIndexByName, getColumnOptionsFromDataItem, normalizeColumns, @@ -155,13 +156,15 @@ export class ColumnsController { const newColumns = this.columns.peek(); newColumns.forEach((newColumn, columnIdx) => { - const prevColumn = prevColumns[columnIdx]; - const options = new Set([...Object.keys(prevColumn), ...Object.keys(newColumn)]); - for (const option of options) { - if (!IGNORE_COLUMN_OPTION_NAMES[option]) { - const prevValue = prevColumn[option]; - const newValue = newColumn[option]; - this.fireOptionChanged(columnIdx, option, newValue, prevValue); + const prevColumn = getColumnByIndexOrName(prevColumns, newColumn.name); + if (prevColumn) { + const options = new Set([...Object.keys(prevColumn), ...Object.keys(newColumn)]); + for (const option of options) { + if (!IGNORE_COLUMN_OPTION_NAMES[option]) { + const prevValue = prevColumn[option]; + const newValue = newColumn[option]; + this.fireOptionChanged(columnIdx, option, newValue, prevValue); + } } } }); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts index 3f42777ddec4..862389a7ea76 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/const.ts @@ -1,6 +1,3 @@ export const IGNORE_COLUMN_OPTION_NAMES = { - visibleWidth: true, - bestFitWidth: true, - bufferedFilterValue: true, selector: true, }; From ccda9d7aeda13d2c933306b22fa0c2b3351bbb03 Mon Sep 17 00:00:00 2001 From: Mark Allen Ramirez Date: Wed, 20 May 2026 15:28:55 +0800 Subject: [PATCH 3/5] test --- .../columns_controller.test.ts | 86 ++++++++++++++++++- .../options_controller_base.test.ts | 18 ++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts index 6e96902aeda7..f7c883355355 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from '@jest/globals'; +import { + describe, expect, it, jest, +} from '@jest/globals'; import config from '@js/core/config'; import { DataController } from '../data_controller'; @@ -471,4 +473,86 @@ describe('ColumnsController', () => { expect(resultThird.sortIndex).toBeUndefined(); }); }); + + describe('onOptionChanged', () => { + it('should be called when a column option changes', () => { + const onOptionChanged = jest.fn(); + const { columnsController } = setup({ + columns: ['a', 'b'], + onOptionChanged, + }); + + const [col] = columnsController.columns.peek(); + columnsController.columnOption(col, 'visible', false); + + expect(onOptionChanged).toHaveBeenCalledTimes(1); + expect(onOptionChanged).toHaveBeenCalledWith( + expect.objectContaining({ + fullName: 'columns[0].visible', + name: 'columns', + previousValue: true, + value: false, + }), + ); + }); + + it('should not be called when value is unchanged', () => { + const onOptionChanged = jest.fn(); + const { columnsController } = setup({ + columns: ['a', 'b'], + onOptionChanged, + }); + + const [col] = columnsController.columns.peek(); + columnsController.columnOption(col, 'visible', true); + + expect(onOptionChanged).not.toHaveBeenCalled(); + }); + + it('should be called for each changed column in updateColumns', () => { + const onOptionChanged = jest.fn(); + const { columnsController } = setup({ + columns: ['a', 'b', 'c'], + onOptionChanged, + }); + + let sortIndex = 0; + columnsController.updateColumns((columns) => columns.map((col, idx) => { + if (idx === 1) { + return col; + } + + sortIndex += 1; + + return { + ...col, + sortOrder: 'asc', + sortIndex, + }; + })); + + const optionChangeCalls = onOptionChanged.mock.calls; + expect(optionChangeCalls).toHaveLength(4); + expect(optionChangeCalls[0][0]).toMatchObject({ + fullName: 'columns[0].sortOrder', + name: 'columns', + value: 'asc', + }); + expect(optionChangeCalls[1][0]).toMatchObject({ + fullName: 'columns[0].sortIndex', + name: 'columns', + value: 0, + }); + expect(optionChangeCalls[2][0]).toMatchObject({ + fullName: 'columns[2].sortOrder', + name: 'columns', + value: 'asc', + }); + expect(optionChangeCalls[3][0]).toMatchObject({ + fullName: 'columns[2].sortIndex', + name: 'columns', + value: 1, + }); + }); + }); }); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.test.ts index 2ea093a68cb9..64e919251133 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.test.ts @@ -828,3 +828,21 @@ describe('oneWayWithChanges', () => { }); }); }); + +describe('notifyColumnOptionChanged', () => { + it('should not update the internal state', () => { + const publicOptions = { columns: [{ visible: true }] }; + const { optionsController } = setup<{ columns?: { visible?: boolean }[] }>(publicOptions, {}); + + const columnVisible = optionsController.oneWay('columns[0].visible'); + const columnVisibleWithChanges = optionsController.oneWayWithChanges('columns[0].visible'); + + optionsController.notifyColumnOptionChanged('columns[0].visible', false, true); + + expect(columnVisible.peek()).toBe(true); + expect(columnVisibleWithChanges.peek()).toStrictEqual({ + changes: null, + value: true, + }); + }); +}); From 4a7807a9719174cac4634a6398f049f9bfde2791 Mon Sep 17 00:00:00 2001 From: Mark Allen Ramirez Date: Wed, 20 May 2026 15:34:11 +0800 Subject: [PATCH 4/5] side fix --- .../__internal/grids/new/card_view/header_panel/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/controller.ts b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/controller.ts index ff18039eea29..06b2361dc335 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/controller.ts +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/controller.ts @@ -47,7 +47,7 @@ export class HeaderPanelController { const columnsCount = this.columnsController.columns.peek().length; this.columnsController.columnOption(column, 'visible', true); - this.columnsController.columnOption(column, 'visibleIndex', columnsCount); + this.columnsController.columnOption(column, 'visibleIndex', columnsCount - 1); return; } From cf33ea1204c5236586433f89ce17c035a95d517c Mon Sep 17 00:00:00 2001 From: Mark Allen Ramirez Date: Wed, 20 May 2026 15:47:29 +0800 Subject: [PATCH 5/5] test fix --- .../new/grid_core/columns_controller/columns_controller.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts index f7c883355355..0b1513431e58 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/columns_controller.test.ts @@ -516,7 +516,7 @@ describe('ColumnsController', () => { onOptionChanged, }); - let sortIndex = 0; + let sortIndex = -1; columnsController.updateColumns((columns) => columns.map((col, idx) => { if (idx === 1) { return col;