From 5b4f1b358085658425b3ab75a7e14f633836d767 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:16:39 +0200 Subject: [PATCH 1/2] Grids: remove stateStoring self extension --- .../js/__internal/core/m_devices.ts | 2 +- .../js/__internal/core/utils/m_storage.ts | 4 +- .../grid_core/columns_controller/const.ts | 9 +- .../m_columns_controller_utils.ts | 2 +- .../grid_core/columns_controller/types.ts | 6 + .../grids/grid_core/data_controller/types.ts | 2 +- .../js/__internal/grids/grid_core/m_types.ts | 1 + .../grids/grid_core/selection/m_selection.ts | 1 - .../state_storing_data_controller.ts | 8 +- .../state_storing/m_state_storing.ts | 185 ----------------- .../m_state_storing_controller.ts | 151 ++++++++------ .../state_storing/state_storing_controller.ts | 195 ++++++++++++++++++ .../state_storing/state_storing_module.ts | 12 +- .../grids/grid_core/state_storing/types.ts | 24 +++ .../grids/tree_list/state_storing.ts | 21 +- 15 files changed, 347 insertions(+), 276 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts create mode 100644 packages/devextreme/js/__internal/grids/grid_core/state_storing/types.ts diff --git a/packages/devextreme/js/__internal/core/m_devices.ts b/packages/devextreme/js/__internal/core/m_devices.ts index ede859993d55..1e449dc5652b 100644 --- a/packages/devextreme/js/__internal/core/m_devices.ts +++ b/packages/devextreme/js/__internal/core/m_devices.ts @@ -319,7 +319,7 @@ class Devices { return; } - const deviceOrName = sessionStorage.getItem('dx-force-device'); + const deviceOrName = sessionStorage.getItem('dx-force-device') ?? ''; try { return JSON.parse(deviceOrName); diff --git a/packages/devextreme/js/__internal/core/utils/m_storage.ts b/packages/devextreme/js/__internal/core/utils/m_storage.ts index 2a775524398f..b520f27c6a3c 100644 --- a/packages/devextreme/js/__internal/core/utils/m_storage.ts +++ b/packages/devextreme/js/__internal/core/utils/m_storage.ts @@ -2,8 +2,8 @@ import { getWindow } from '@js/core/utils/window'; const window = getWindow(); -const getSessionStorage = function () { - let sessionStorage; +const getSessionStorage = function (): Storage | undefined { + let sessionStorage: Storage | undefined; try { sessionStorage = window.sessionStorage; diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts index f7c24e8b9aa2..0de8786f40d0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/const.ts @@ -1,5 +1,10 @@ -export const USER_STATE_FIELD_NAMES_15_1 = ['filterValues', 'filterType', 'fixed', 'fixedPosition']; -export const USER_STATE_FIELD_NAMES = ['visibleIndex', 'dataField', 'name', 'dataType', 'width', 'visible', 'sortOrder', 'lastSortOrder', 'sortIndex', 'groupIndex', 'filterValue', 'bufferedFilterValue', 'selectedFilterOperation', 'bufferedSelectedFilterOperation', 'added'].concat(USER_STATE_FIELD_NAMES_15_1); +export const USER_STATE_FIELD_NAMES_15_1 = ['filterValues', 'filterType', 'fixed', 'fixedPosition'] as const; +export const USER_STATE_FIELD_NAMES = [ + 'visibleIndex', 'dataField', 'name', 'dataType', 'width', 'visible', 'sortOrder', + 'lastSortOrder', 'sortIndex', 'groupIndex', 'filterValue', 'bufferedFilterValue', + 'selectedFilterOperation', 'bufferedSelectedFilterOperation', 'added', + ...USER_STATE_FIELD_NAMES_15_1, +] as const; // eslint-disable-next-line @stylistic/max-len export const IGNORE_COLUMN_OPTION_NAMES = { visibleWidth: true, bestFitWidth: true, bufferedFilterValue: true }; export const COMMAND_EXPAND_CLASS = 'dx-command-expand'; 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 da98089c0960..d3dcd37385d2 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 @@ -519,7 +519,7 @@ export const applyUserState = function (that: ColumnsController) { if (fieldName === 'dataType') { column[fieldName] = column[fieldName] || userStateColumn[fieldName]; - } else if (USER_STATE_FIELD_NAMES_15_1.includes(fieldName)) { + } else if ((USER_STATE_FIELD_NAMES_15_1 as readonly string[]).includes(fieldName)) { if (fieldName in userStateColumn) { column[fieldName] = userStateColumn[fieldName]; } 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 7307a9f03be5..618e886f4056 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 @@ -13,6 +13,8 @@ export type ColumnIndex = number | { export type FilterField = Omit & { filterOperations?: string[] | null }; +export type AddedColumn = string | (Column & { columns?: (Column | string)[] }); + export interface Column extends ColumnBase { parseValue?: (text: string) => unknown; index?: number; @@ -25,6 +27,10 @@ export interface Column extends ColumnBase { command?: string; rowspan?: number; colspan?: number; + lastSortOrder?: ColumnBase['sortOrder']; + bufferedFilterValue?: ColumnBase['filterValue']; + bufferedSelectedFilterOperation?: ColumnBase['selectedFilterOperation']; + added?: AddedColumn; } export interface ColumnsChanges { diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts index 0b6a7b193ced..fae3b4d2e376 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/types.ts @@ -22,7 +22,7 @@ export interface UserState { searchText: string | undefined; pageIndex: number; pageSize: number; - expandedRowKeys?: unknown; + expandedRowKeys?: unknown[]; } export interface ItemProcessingOptions { diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 2f94bd160ce7..ebcdb7eb8463 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -108,6 +108,7 @@ type TemporarlyOptionsTakenFromDataGrid = Pick; type TemporarlyOptionsTakenFromTreeList = Pick; + isStateLoading: () => boolean; } export const stateStoringDataControllerExtender = ( @@ -53,9 +54,10 @@ export const stateStoringDataControllerExtender = ( this.stateLoaded.fire(); deferred.resolve(); }) - .fail((error: Error) => { + // NOTE: DeferredObj types fail() with the resolved value, not the rejection reason + .fail((error: unknown) => { this.stateLoaded.fire(); - this.loadErrorHandler(error ?? 'Unknown error'); + this.loadErrorHandler((error as Error | undefined) ?? 'Unknown error'); deferred.reject(); }); }); @@ -75,7 +77,7 @@ export const stateStoringDataControllerExtender = ( return super.isLoading() || this._stateStoringController.isLoading(); } - private isStateLoading(): boolean { + public isStateLoading(): boolean { return isDefined(this._restoreStateTimeoutID); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts index c722f7a0cb2a..7b2ba1fb256f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing.ts @@ -1,95 +1,10 @@ /* eslint-disable max-classes-per-file */ -import { equalByValue, getKeyHash } from '@js/core/utils/common'; -import { extend } from '@js/core/utils/extend'; -import { isDefined } from '@js/core/utils/type'; - import type { ColumnsController } from '../columns_controller/m_columns_controller'; import type { DataController } from '../data_controller/data_controller'; import type { ModuleType } from '../m_types'; import type { SelectionController } from '../selection/m_selection'; import type { RowsView } from '../views/m_rows_view'; import type { StateStoringDataControllerExtension } from './extenders/state_storing_data_controller'; -import type { StateStoringController } from './m_state_storing_controller'; - -const getDataState = (that) => { - // TODO getView - const pagerView = that.getView('pagerView'); - // TODO getController - const dataController = that.getController('data'); - const state = { - allowedPageSizes: pagerView ? pagerView.getPageSizes() : undefined, - filterPanel: { filterEnabled: that.option('filterPanel.filterEnabled') }, - filterValue: that.option('filterValue'), - focusedRowKey: that.option('focusedRowEnabled') ? that.option('focusedRowKey') : undefined, - }; - - return extend(state, dataController.getUserState()); -}; - -// TODO move processLoadState to target modules (data, columns, pagerView) -const processLoadState = (that) => { - // TODO getController - const columnsController = that.getController('columns'); - const selectionController = that.getController('selection'); - const exportController = that.getController('export'); - const dataController = that.getController('data'); - - if (columnsController) { - columnsController.columnsChanged.add(() => { - that.updateState({ - columns: columnsController.getUserState(), - }); - }); - } - - if (selectionController) { - selectionController.selectionChanged.add((e) => { - that.updateState({ - selectedRowKeys: e.selectedRowKeys, - selectionFilter: e.selectionFilter, - }); - }); - } - - if (dataController) { - that._initialPageSize = that.option('paging.pageSize'); - that._initialFilterValue = that.option('filterValue'); - - dataController.changed.add(() => { - const state = getDataState(that); - - that.updateState(state); - }); - } - - if (exportController) { - exportController.selectionOnlyChanged.add(() => { - that.updateState({ - exportSelectionOnly: exportController.selectionOnly(), - }); - }); - } -}; - -const getFilterValue = (that, state) => { - // TODO: getController - const filterSyncController = that.getController('filterSync'); - if (!filterSyncController) { - return null; - } - - if (state.filterValue !== undefined) { - return state.filterValue; - } - - const filterValueFromColumns = filterSyncController.getFilterValueFromColumns?.(state.columns); - if (filterValueFromColumns?.length > 0) { - return filterValueFromColumns; - } - - const columns = that.getController('columns').getColumns(); - return that._initialFilterValue ?? filterSyncController.getFilterValueFromColumns(columns); -}; export const rowsView = (Base: ModuleType) => class StateStoringRowsViewExtender extends Base { protected _dataController!: DataController & StateStoringDataControllerExtension; @@ -110,106 +25,6 @@ export const rowsView = (Base: ModuleType) => class StateStoringRowsVi } }; -export const stateStoring = (Base: ModuleType) => class StateStoringExtender extends Base { - private readonly _initialPageSize: any; - - public init() { - // @ts-expect-error - super.init.apply(this, arguments); - processLoadState(this); - - return this; - } - - public isLoading() { - // @ts-expect-error - return super.isLoading() || this.getDataController().isStateLoading(); - } - - protected state(state?) { - // @ts-expect-error - const result = super.state.apply(this, arguments); - - if (state !== undefined) { - this.applyState(extend(true, {}, state)); - } - - return result; - } - - private updateState(state) { - if (this.isEnabled()) { - const oldState = this.state(); - const newState = extend({}, oldState, state); - const oldStateHash = getKeyHash(oldState); - const newStateHash = getKeyHash(newState); - - if (!equalByValue(oldStateHash, newStateHash)) { - state = extend(true, {}, state); - extend(this._state, state); - - this.save(); - } - } else { - extend(this._state, state); - } - } - - /** - * @extended: TreeList's state_storing - */ - protected applyState(state) { - const { allowedPageSizes } = state; - const { searchText } = state; - const { selectedRowKeys } = state; - const { selectionFilter } = state; - const scrollingMode = this.option('scrolling.mode'); - const isVirtualScrollingMode = scrollingMode === 'virtual' || scrollingMode === 'infinite'; - const showPageSizeSelector = this.option('pager.visible') === true && this.option('pager.showPageSizeSelector'); - // TODO getView - const hasHeight = this.getView('rowsView')?.hasHeight(); - - this.component.beginUpdate(); - - if (this.getColumnsController()) { - this.getColumnsController().setUserState(state.columns); - } - - if (this.getExportController()) { - this.getExportController().selectionOnly(state.exportSelectionOnly); - } - - if (!this.option('selection.deferred')) { - this.option('selectedRowKeys', selectedRowKeys || []); - } - - // @ts-expect-error - this.option('selectionFilter', selectionFilter); - - if (allowedPageSizes && this.option('pager.allowedPageSizes') === 'auto') { - this.option('pager')!.allowedPageSizes = allowedPageSizes; - } - - if (this.option('focusedRowEnabled')) { - this.option('focusedRowIndex', -1); - this.option('focusedRowKey', state.focusedRowKey ?? null); - } - - this.component.endUpdate(); - - this.option('searchPanel.text', searchText || ''); - - this.option('filterValue', getFilterValue(this, state)); - - this.option('filterPanel.filterEnabled', state.filterPanel ? state.filterPanel.filterEnabled : true); - - this.option('paging.pageIndex', (!isVirtualScrollingMode || hasHeight) && state.pageIndex || 0); - this.option('paging.pageSize', (!isVirtualScrollingMode || showPageSizeSelector) && isDefined(state.pageSize) ? state.pageSize : this._initialPageSize); - - this.getDataController() && this.getDataController().reset(); - } -}; - export const columns = (Base: ModuleType) => class StateStoringColumnsExtender extends Base { protected _shouldReturnVisibleColumns() { // @ts-expect-error diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts index a7b9d0dc6b80..b4c5615061f2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts @@ -1,36 +1,45 @@ import eventsEngine from '@js/common/core/events/core/events_engine'; -// @ts-expect-error -import { fromPromise } from '@js/core/utils/deferred'; +import type { StateStoring } from '@js/common/grids'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { sessionStorage } from '@js/core/utils/storage'; import { isDefined, isEmptyObject, isPlainObject } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; import errors from '@js/ui/widget/ui.errors'; +import { fromPromise } from '@ts/core/utils/m_deferred'; import type { ExportController } from '@ts/grids/data_grid/export/m_export'; import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import modules from '@ts/grids/grid_core/m_modules'; +import type { OptionChanged } from '@ts/grids/grid_core/m_types'; -import modules from '../m_modules'; +import type { PersistentState } from './types'; const DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/; -const parseDates = function (state) { - if (!state) return; +const parseDates = (state): void => { + if (!state) { + return; + } + each(state, (key, value) => { if (isPlainObject(value) || Array.isArray(value)) { parseDates(value); } else if (typeof value === 'string') { - const date = DATE_REGEX.exec(value); - if (date) { - state[key] = new Date(Date.UTC(+date[1], +date[2] - 1, +date[3], +date[4], +date[5], +date[6])); + const dateParts = DATE_REGEX.exec(value); + if (dateParts) { + const [, year, month, day, hours, minutes, seconds] = dateParts; + state[key] = new Date(Date.UTC(+year, +month - 1, +day, +hours, +minutes, +seconds)); } } }); }; -const getStorage = function (options) { - const storage = options.type === 'sessionStorage' ? sessionStorage() : getWindow().localStorage; +const getStorage = (options: StateStoring): Storage | undefined => { + const storage = options.type === 'sessionStorage' + ? sessionStorage() + : getWindow().localStorage; if (!storage) { throw new Error('E1007'); @@ -39,24 +48,25 @@ const getStorage = function (options) { return storage; }; -const getUniqueStorageKey = function (options) { - return isDefined(options.storageKey) ? options.storageKey : 'storage'; -}; +const getUniqueStorageKey = ( + options: { storageKey?: string }, +): string => (isDefined(options.storageKey) ? options.storageKey : 'storage'); -export class StateStoringController extends modules.ViewController { - protected _state: any; +export class StateStoringController< + TState extends object = PersistentState, +> extends modules.ViewController { + protected _state!: TState; - private _isLoaded: any; + private _isLoaded!: boolean; private _isLoading!: boolean; - private _windowUnloadHandler: any; + private _windowUnloadHandler!: () => void; - private _savingTimeoutID: any; + private _savingTimeoutID?: ReturnType; // TODO getController - // NOTE: sometimes fields empty in the runtime - // getter here is a temporary solution + // NOTE: sometimes fields empty in the runtime, getter here is a temporary solution protected getDataController(): DataController { return this.getController('data'); } @@ -69,12 +79,12 @@ export class StateStoringController extends modules.ViewController { return this.getController('columns'); } - public init() { - this._state = {}; + public init(): this { + this._state = {} as TState; this._isLoaded = false; this._isLoading = false; - this._windowUnloadHandler = () => { + this._windowUnloadHandler = (): void => { if (this._savingTimeoutID !== undefined) { this._saveState(this.state()); } @@ -85,13 +95,11 @@ export class StateStoringController extends modules.ViewController { return this; // needed by pivotGrid mocks } - public optionChanged(args) { - const that = this; - + public optionChanged(args: OptionChanged): void { switch (args.name) { case 'stateStoring': - if (that.isEnabled() && !that.isLoading()) { - that.load(); + if (this.isEnabled() && !this.isLoading()) { + this.load(); } args.handled = true; @@ -101,47 +109,61 @@ export class StateStoringController extends modules.ViewController { } } - public dispose() { + public dispose(): void { clearTimeout(this._savingTimeoutID); eventsEngine.off(getWindow(), 'visibilitychange', this._windowUnloadHandler); } - private _loadState() { - const options = this.option('stateStoring')!; + private _loadState(): TState | PromiseLike | undefined { + const options = this.option('stateStoring'); - if (options.type === 'custom') { - return options.customLoad && options.customLoad(); + if (!options) { + return undefined; } + + if (options.type === 'custom' && options.customLoad) { + return options.customLoad(); + } + try { - return JSON.parse(getStorage(options).getItem(getUniqueStorageKey(options))); - } catch (e: any) { - errors.log('W1022', 'State storing', e.message); + const storedState = getStorage(options)?.getItem(getUniqueStorageKey(options)); + + return storedState ? JSON.parse(storedState) as TState : undefined; + } catch (e: unknown) { + errors.log('W1022', 'State storing', (e as Error).message); + + return undefined; } } - private _saveState(state) { - const options = this.option('stateStoring')!; + private _saveState(state: TState): void { + const options = this.option('stateStoring'); + + if (!options) { + return; + } if (options.type === 'custom') { - options.customSave && options.customSave(state); + options.customSave?.(state); return; } + try { - getStorage(options).setItem(getUniqueStorageKey(options), JSON.stringify(state)); - } catch (e: any) { - errors.log(e.message); + getStorage(options)?.setItem(getUniqueStorageKey(options), JSON.stringify(state)); + } catch (e: unknown) { + errors.log((e as Error).message); } } - public publicMethods() { + public publicMethods(): string[] { return ['state']; } - public isEnabled() { - return this.option('stateStoring.enabled'); + public isEnabled(): boolean { + return !!this.option('stateStoring.enabled'); } - public isLoaded() { + public isLoaded(): boolean { return this._isLoaded; } @@ -149,9 +171,10 @@ export class StateStoringController extends modules.ViewController { return this._isLoading; } - public load() { + public load(): DeferredObj { this._isLoading = true; - const loadResult = fromPromise(this._loadState()); + const loadResult: DeferredObj = fromPromise(this._loadState()); + loadResult.always(() => { this._isLoaded = true; this._isLoading = false; @@ -160,27 +183,31 @@ export class StateStoringController extends modules.ViewController { this.state(state); } }); + return loadResult; } - protected state(state?) { - const that = this; - - if (!arguments.length) { - return extend(true, {}, that._state); + protected state(): TState; + protected state(state: TState | undefined): void; + protected state(...args: [state?: TState]): TState | void { + if (!args.length) { + return extend(true, {}, this._state) as TState; } - that._state = extend({}, state); - parseDates(that._state); + + this._state = extend({}, args[0]) as TState; + parseDates(this._state); + + return undefined; } - protected save() { - const that = this; + protected save(): void { + clearTimeout(this._savingTimeoutID); - clearTimeout(that._savingTimeoutID); - that._savingTimeoutID = setTimeout(() => { - that._saveState(that.state()); - that._savingTimeoutID = undefined; - }, that.option('stateStoring.savingTimeout')); + // eslint-disable-next-line no-restricted-globals + this._savingTimeoutID = setTimeout(() => { + this._saveState(this.state()); + this._savingTimeoutID = undefined; + }, this.option('stateStoring.savingTimeout')); } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts new file mode 100644 index 000000000000..9e2180b34b14 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts @@ -0,0 +1,195 @@ +import { equalByValue, getKeyHash } from '@js/core/utils/common'; +import { isDefined } from '@js/core/utils/type'; +import { extend } from '@ts/core/utils/m_extend'; +import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; +import type { InternalGridOptions } from '@ts/grids/grid_core/m_types'; + +import type { StateStoringDataControllerExtension } from './extenders/state_storing_data_controller'; +import { StateStoringController } from './m_state_storing_controller'; +import type { GridState } from './types'; + +const getDataState = (that): GridState => { + // TODO getView + const pagerView = that.getView('pagerView'); + // TODO getController + const dataController = that.getController('data'); + const state = { + allowedPageSizes: pagerView ? pagerView.getPageSizes() : undefined, + filterPanel: { filterEnabled: that.option('filterPanel.filterEnabled') }, + filterValue: that.option('filterValue'), + focusedRowKey: that.option('focusedRowEnabled') ? that.option('focusedRowKey') : undefined, + }; + + return extend(state, dataController.getUserState()) as GridState; +}; + +// TODO move processLoadState to target modules (data, columns, pagerView) +const processLoadState = (that): void => { + // TODO getController + const columnsController = that.getController('columns'); + const selectionController = that.getController('selection'); + const exportController = that.getController('export'); + const dataController = that.getController('data'); + + if (columnsController) { + columnsController.columnsChanged.add(() => { + that.updateState({ + columns: columnsController.getUserState(), + }); + }); + } + + if (selectionController) { + selectionController.selectionChanged.add((e) => { + that.updateState({ + selectedRowKeys: e.selectedRowKeys, + selectionFilter: e.selectionFilter, + }); + }); + } + + if (dataController) { + that._initialPageSize = that.option('paging.pageSize'); + that._initialFilterValue = that.option('filterValue'); + + dataController.changed.add(() => { + const state = getDataState(that); + + that.updateState(state); + }); + } + + if (exportController) { + exportController.selectionOnlyChanged.add(() => { + that.updateState({ + exportSelectionOnly: exportController.selectionOnly(), + }); + }); + } +}; + +const getFilterValue = (that, state: GridState): InternalGridOptions['filterValue'] => { + // TODO: getController + const filterSyncController = that.getController('filterSync'); + if (!filterSyncController) { + return null; + } + + if (state.filterValue !== undefined) { + return state.filterValue; + } + + const filterValueFromColumns = filterSyncController.getFilterValueFromColumns?.(state.columns); + if (filterValueFromColumns?.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return filterValueFromColumns; + } + + const columns = that.getController('columns').getColumns(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return that._initialFilterValue ?? filterSyncController.getFilterValueFromColumns(columns); +}; + +export class GridStateStoringController extends StateStoringController { + private readonly _initialPageSize?: number; + + public init(): this { + super.init(); + processLoadState(this); + + return this; + } + + public isLoading(): boolean { + const dataController = this.getController('data') as DataController & StateStoringDataControllerExtension; + + return super.isLoading() || dataController.isStateLoading(); + } + + protected state(): GridState; + protected state(state: GridState | undefined): void; + protected state(...args: [state?: GridState]): GridState | void { + if (!args.length) { + return super.state(); + } + + const [state] = args; + super.state(state); + + if (state !== undefined) { + this.applyState(extend(true, {}, state)); + } + + return undefined; + } + + private updateState(state: GridState): void { + if (this.isEnabled()) { + const oldState = this.state(); + const newState = { + ...oldState, + ...state, + }; + const oldStateHash = getKeyHash(oldState); + const newStateHash = getKeyHash(newState); + + if (!equalByValue(oldStateHash, newStateHash)) { + // eslint-disable-next-line no-param-reassign + state = extend(true, {}, state); + extend(this._state, state); + + this.save(); + } + } else { + extend(this._state, state); + } + } + + /** + * @extended: TreeList's state_storing + */ + protected applyState(state: GridState): void { + const { scrolling, pager } = this.option(); + + const isVirtualScrollingMode = scrolling?.mode === 'virtual' || scrolling?.mode === 'infinite'; + const showPageSizeSelector = pager?.visible === true && !!pager.showPageSizeSelector; + // TODO getView + const hasHeight = this.getView('rowsView')?.hasHeight(); + const allowsPageIndexRestore = !isVirtualScrollingMode || hasHeight; + const allowsPageSizeRestore = !isVirtualScrollingMode || showPageSizeSelector; + const canRestorePageIndex = allowsPageIndexRestore && isDefined(state.pageIndex); + const canRestorePageSize = allowsPageSizeRestore && isDefined(state.pageSize); + + this.component.beginUpdate(); + + this.getColumnsController()?.setUserState(state.columns); + this.getExportController()?.selectionOnly(state.exportSelectionOnly); + + if (!this.option('selection.deferred')) { + this.option('selectedRowKeys', state.selectedRowKeys ?? []); + } + + this.option('selectionFilter', state.selectionFilter); + + if (state.allowedPageSizes && pager?.allowedPageSizes === 'auto') { + pager.allowedPageSizes = state.allowedPageSizes; + } + + if (this.option('focusedRowEnabled')) { + this.option('focusedRowIndex', -1); + this.option('focusedRowKey', state.focusedRowKey ?? null); + } + + this.component.endUpdate(); + + this.option('searchPanel.text', state.searchText ?? ''); + + this.option('filterValue', getFilterValue(this, state)); + this.option('filterPanel.filterEnabled', state.filterPanel ? state.filterPanel.filterEnabled : true); + + this.option('paging.pageIndex', canRestorePageIndex ? state.pageIndex : 0); + this.option('paging.pageSize', canRestorePageSize ? state.pageSize : this._initialPageSize); + + this.getDataController()?.reset(); + } +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_module.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_module.ts index d03b47138c2b..3f7d3511b005 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_module.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_module.ts @@ -1,13 +1,8 @@ import type { StateStoring } from '@js/common/grids'; import { stateStoringDataControllerExtender } from './extenders/state_storing_data_controller'; -import { - columns, - rowsView, - selection, - stateStoring, -} from './m_state_storing'; -import { StateStoringController } from './m_state_storing_controller'; +import { columns, rowsView, selection } from './m_state_storing'; +import { GridStateStoringController } from './state_storing_controller'; export const stateStoringModule = { defaultOptions(): { stateStoring: StateStoring } { @@ -23,14 +18,13 @@ export const stateStoringModule = { }; }, controllers: { - stateStoring: StateStoringController, + stateStoring: GridStateStoringController, }, extenders: { views: { rowsView, }, controllers: { - stateStoring, columns, data: stateStoringDataControllerExtender, selection, diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/types.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/types.ts new file mode 100644 index 000000000000..337d2136754b --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/types.ts @@ -0,0 +1,24 @@ +import type { PagerPageSize } from '@js/common/grids'; + +import type { USER_STATE_FIELD_NAMES } from '../columns_controller/const'; +import type { Column } from '../columns_controller/types'; +import type { UserState } from '../data_controller/types'; +import type { InternalGridOptions } from '../m_types'; + +export type PersistentState = Record; + +export type ColumnUserState = Pick; + +/** + * State persisted by DataGrid and TreeList. + */ +export interface GridState extends Partial { + columns?: ColumnUserState[]; + selectedRowKeys?: unknown[]; + selectionFilter?: InternalGridOptions['selectionFilter']; + exportSelectionOnly?: boolean; + allowedPageSizes?: (number | PagerPageSize)[]; + filterPanel?: { filterEnabled?: boolean }; + filterValue?: InternalGridOptions['filterValue']; + focusedRowKey?: unknown; +} diff --git a/packages/devextreme/js/__internal/grids/tree_list/state_storing.ts b/packages/devextreme/js/__internal/grids/tree_list/state_storing.ts index 4b237e20db1d..2047c177172b 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/state_storing.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/state_storing.ts @@ -1,24 +1,24 @@ -// eslint-disable-next-line max-classes-per-file +/* eslint-disable max-classes-per-file */ import type { UserState } from '@ts/grids/grid_core/data_controller/types'; import type { StateStoringDataControllerExtension, } from '@ts/grids/grid_core/state_storing/extenders/state_storing_data_controller'; +import { + GridStateStoringController, +} from '@ts/grids/grid_core/state_storing/state_storing_controller'; import { stateStoringModule } from '@ts/grids/grid_core/state_storing/state_storing_module'; +import type { GridState } from '@ts/grids/grid_core/state_storing/types'; import type { DataController } from '../grid_core/data_controller/data_controller'; import type { ModuleType } from '../grid_core/m_types'; -import type { StateStoringController } from '../grid_core/state_storing/m_state_storing_controller'; import treeListCore from './m_core'; -const stateStoring = ( - Base: ModuleType, - // eslint-disable-next-line @stylistic/max-len -): ModuleType => class TreeListStateStoringExtender extends stateStoringModule.extenders.controllers.stateStoring(Base) { - protected applyState(state): void { +class TreeListStateStoringController extends GridStateStoringController { + protected applyState(state: GridState): void { super.applyState(state); this.option('expandedRowKeys', state.expandedRowKeys ? state.expandedRowKeys.slice() : []); } -}; +} const data = ( Base: ModuleType, @@ -39,11 +39,14 @@ const data = ( treeListCore.registerModule('stateStoring', { ...stateStoringModule, + controllers: { + ...stateStoringModule.controllers, + stateStoring: TreeListStateStoringController, + }, extenders: { ...stateStoringModule.extenders, controllers: { ...stateStoringModule.extenders.controllers, - stateStoring, data, }, }, From 210382f5b3745316cc9ebad3f0dbcc87a7e9da5a Mon Sep 17 00:00:00 2001 From: "anna.shakhova" <68295572+anna-shakhova@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:58:43 +0200 Subject: [PATCH 2/2] copilot review fix --- packages/devextreme/js/__internal/core/m_devices.ts | 6 +++++- .../grid_core/state_storing/m_state_storing_controller.ts | 6 +++--- .../grid_core/state_storing/state_storing_controller.ts | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/core/m_devices.ts b/packages/devextreme/js/__internal/core/m_devices.ts index 1e449dc5652b..a4d00c42092a 100644 --- a/packages/devextreme/js/__internal/core/m_devices.ts +++ b/packages/devextreme/js/__internal/core/m_devices.ts @@ -319,7 +319,11 @@ class Devices { return; } - const deviceOrName = sessionStorage.getItem('dx-force-device') ?? ''; + const deviceOrName = sessionStorage.getItem('dx-force-device'); + + if (deviceOrName === null) { + return null; + } try { return JSON.parse(deviceOrName); diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts index b4c5615061f2..84fa3f9ad57b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/m_state_storing_controller.ts @@ -121,14 +121,14 @@ export class StateStoringController< return undefined; } - if (options.type === 'custom' && options.customLoad) { - return options.customLoad(); + if (options.type === 'custom') { + return options.customLoad?.(); } try { const storedState = getStorage(options)?.getItem(getUniqueStorageKey(options)); - return storedState ? JSON.parse(storedState) as TState : undefined; + return isDefined(storedState) ? JSON.parse(storedState) as TState : undefined; } catch (e: unknown) { errors.log('W1022', 'State storing', (e as Error).message); diff --git a/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts index 9e2180b34b14..e5eaf8dee013 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/state_storing/state_storing_controller.ts @@ -91,7 +91,11 @@ const getFilterValue = (that, state: GridState): InternalGridOptions['filterValu }; export class GridStateStoringController extends StateStoringController { - private readonly _initialPageSize?: number; + // eslint-disable-next-line @typescript-eslint/prefer-readonly + private _initialPageSize?: number; + + // eslint-disable-next-line @typescript-eslint/prefer-readonly + private _initialFilterValue?: InternalGridOptions['filterValue']; public init(): this { super.init();