From e531475a36f5948758f16604d74f415cfa898d38 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 30 Sep 2026 01:52:43 +0400 Subject: [PATCH 1/2] Grids: convert the pivot grid LocalStore into an ES6 class and remove the Class.inherit leftovers in grid_core modules, the pivot data source and QUnit tests --- .../__internal/grids/grid_core/m_modules.ts | 9 +- .../js/__internal/grids/grid_core/m_types.ts | 6 +- .../pivot_grid/data_source/m_data_source.ts | 3 +- .../pivot_grid/local_store/m_local_store.ts | 953 +++++++++--------- .../grid_core.modules.tests.js | 94 +- .../dataController.tests.js | 34 +- 6 files changed, 499 insertions(+), 600 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts b/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts index 525c4ef01e90..3366e3c4445d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts @@ -389,13 +389,8 @@ function getExtendedTypes( Object.entries(moduleExtenders) .forEach(([name, extender]) => { const currentType = types[name]; - if (currentType) { - if (isFunction(extender)) { - extendTypes[name] = extender(currentType); - } else { - const classType = currentType as { inherit: (type: unknown) => unknown }; - extendTypes[name] = classType.inherit(extender); - } + if (currentType && isFunction(extender)) { + extendTypes[name] = extender(currentType); } }); return extendTypes; 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 242bdcc824c6..99a50e66c3ce 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -274,13 +274,11 @@ type ViewTypes = { export type ModuleType = (new (...args: any[]) => T); type ControllersExtender = { - [P in keyof Controllers]: ((Base: ModuleType) => ModuleType) - | Record; + [P in keyof Controllers]: (Base: ModuleType) => ModuleType; }; type ViewsExtender = { - [P in keyof Views]: ((Base: ModuleType) => ModuleType) - | Record; + [P in keyof Views]: (Base: ModuleType) => ModuleType; }; export interface Module { diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts index ef1a61eaa54e..b497c9d0fa9a 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts @@ -1,5 +1,4 @@ import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; -import Class from '@js/core/class'; import { EventsStrategy } from '@js/core/events_strategy'; import { normalizeIndexes } from '@js/core/utils/array'; import { Deferred, when } from '@js/core/utils/deferred'; @@ -368,7 +367,7 @@ class PivotGridDataSource { || (storeOptions instanceof Store) || Array.isArray(storeOptions)) { store = this.createLocalOrRemoteStore(dataSourceOptions, notifyProgress); - } else if (storeOptions instanceof Class || storeOptions instanceof xmlaStore.XmlaStore) { + } else if (isFunction(storeOptions.load) && isFunction(storeOptions.getFields)) { store = storeOptions; } } diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts index 4ee6f3179b67..20d2c6f16b1f 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts @@ -4,7 +4,6 @@ import { DataSource } from '@js/common/data/data_source/data_source'; import dataQuery from '@js/common/data/query'; // eslint-disable-next-line import/extensions import { aggregators } from '@js/common/data/utils'; -import Class from '@js/core/class'; import { noop } from '@js/core/utils/common'; import { compileGetter, toComparable } from '@js/core/utils/data'; import dateSerialization from '@js/core/utils/date_serialization'; @@ -22,570 +21,568 @@ import { const PATH_DELIMETER = '/./'; -const LocalStore = Class.inherit((function () { - const DATE_INTERVAL_SELECTORS = { - year(date) { - return date && date.getFullYear(); - }, - quarter(date) { - return date && (Math.floor(date.getMonth() / 3) + 1); - }, - month(date) { - return date && (date.getMonth() + 1); - }, - day(date) { - return date && date.getDate(); - }, - dayOfWeek(date) { - return date && date.getDay(); - }, +const DATE_INTERVAL_SELECTORS = { + year(date) { + return date && date.getFullYear(); + }, + quarter(date) { + return date && (Math.floor(date.getMonth() / 3) + 1); + }, + month(date) { + return date && (date.getMonth() + 1); + }, + day(date) { + return date && date.getDate(); + }, + dayOfWeek(date) { + return date && date.getDay(); + }, +}; + +function getDataSelector(dataField) { + return dataField.indexOf('.') !== -1 ? compileGetter(dataField) : function (data) { return data[dataField]; }; +} + +function getDateValue(dataSelector) { + return function (data) { + let value = dataSelector(data); + if (value && !(value instanceof Date)) { + value = dateSerialization.deserializeDate(value); + } + return value; }; +} + +function prepareFields(fields) { + each(fields || [], (_, field) => { + let fieldSelector; + let intervalSelector; + const { dataField } = field; + let groupInterval; + const { levels } = field; + let dataSelector; + + if (!field.selector) { + if (!dataField) { + dataSelector = function (data) { return data; }; + } else { + dataSelector = getDataSelector(dataField); + } - function getDataSelector(dataField) { - return dataField.indexOf('.') !== -1 ? compileGetter(dataField) : function (data) { return data[dataField]; }; - } - - function getDateValue(dataSelector) { - return function (data) { - let value = dataSelector(data); - if (value && !(value instanceof Date)) { - value = dateSerialization.deserializeDate(value); + if (levels) { + prepareFields(levels); } - return value; - }; - } - function prepareFields(fields) { - each(fields || [], (_, field) => { - let fieldSelector; - let intervalSelector; - const { dataField } = field; - let groupInterval; - const { levels } = field; - let dataSelector; - - if (!field.selector) { - if (!dataField) { - dataSelector = function (data) { return data; }; - } else { - dataSelector = getDataSelector(dataField); - } + if (field.dataType === 'date') { + intervalSelector = DATE_INTERVAL_SELECTORS[field.groupInterval]; + const valueSelector = getDateValue(dataSelector); + + fieldSelector = function (data) { + const value = valueSelector(data); + return intervalSelector ? intervalSelector(value) : value; + }; + } else if (field.dataType === 'number') { + groupInterval = isNumeric(field.groupInterval) + && field.groupInterval > 0 + && field.groupInterval; + + fieldSelector = function (data) { + let value = dataSelector(data); + if (isString(value)) { + value = Number(value); + } + return groupInterval ? Math.floor(value / groupInterval) * groupInterval : value; + }; + } else { + fieldSelector = dataSelector; + } - if (levels) { - prepareFields(levels); - } + setDefaultFieldValueFormatting(field); - if (field.dataType === 'date') { - intervalSelector = DATE_INTERVAL_SELECTORS[field.groupInterval]; - const valueSelector = getDateValue(dataSelector); - - fieldSelector = function (data) { - const value = valueSelector(data); - return intervalSelector ? intervalSelector(value) : value; - }; - } else if (field.dataType === 'number') { - groupInterval = isNumeric(field.groupInterval) - && field.groupInterval > 0 - && field.groupInterval; - - fieldSelector = function (data) { - let value = dataSelector(data); - if (isString(value)) { - value = Number(value); - } - return groupInterval ? Math.floor(value / groupInterval) * groupInterval : value; - }; - } else { - fieldSelector = dataSelector; - } + setFieldProperty(field, 'selector', fieldSelector); + } + }); +} - setDefaultFieldValueFormatting(field); +const addHierarchyItem = function (value, hierarchyItems, pathHash, childrenHash) { + let hierarchyItem = childrenHash[pathHash]; - setFieldProperty(field, 'selector', fieldSelector); - } - }); + if (!hierarchyItem) { + hierarchyItem = { + value, + // eslint-disable-next-line no-plusplus + index: childrenHash.length++, + }; + childrenHash[pathHash] = hierarchyItem; + hierarchyItems.push(hierarchyItem); } + return hierarchyItem; +}; - const addHierarchyItem = function (value, hierarchyItems, pathHash, childrenHash) { - let hierarchyItem = childrenHash[pathHash]; - - if (!hierarchyItem) { - hierarchyItem = { - value, - // eslint-disable-next-line no-plusplus - index: childrenHash.length++, - }; - childrenHash[pathHash] = hierarchyItem; - hierarchyItems.push(hierarchyItem); - } - return hierarchyItem; - }; - - function fillHierarchyItemIndexesCore(indexes, options, children, expandIndex, pathHash?) { - const dimension = options.dimensions[expandIndex]; - const { expandedPathsHash } = options; - let dimensionValue; - let hierarchyItem; +function fillHierarchyItemIndexesCore(indexes, options, children, expandIndex, pathHash?) { + const dimension = options.dimensions[expandIndex]; + const { expandedPathsHash } = options; + let dimensionValue; + let hierarchyItem; - if (dimension) { - dimensionValue = dimension.selector(options.data); - pathHash = pathHash !== undefined ? pathHash + PATH_DELIMETER + dimensionValue : `${dimensionValue}`; + if (dimension) { + dimensionValue = dimension.selector(options.data); + pathHash = pathHash !== undefined ? pathHash + PATH_DELIMETER + dimensionValue : `${dimensionValue}`; - hierarchyItem = addHierarchyItem(dimensionValue, children, pathHash, options.childrenHash); + hierarchyItem = addHierarchyItem(dimensionValue, children, pathHash, options.childrenHash); - indexes.push(hierarchyItem.index); + indexes.push(hierarchyItem.index); - if (expandedPathsHash && expandedPathsHash[pathHash] || dimension.expanded) { - if (!hierarchyItem.children) { - hierarchyItem.children = []; - } - fillHierarchyItemIndexesCore( - indexes, - options, - hierarchyItem.children, - expandIndex + 1, - pathHash, - ); + if (expandedPathsHash && expandedPathsHash[pathHash] || dimension.expanded) { + if (!hierarchyItem.children) { + hierarchyItem.children = []; } + fillHierarchyItemIndexesCore( + indexes, + options, + hierarchyItem.children, + expandIndex + 1, + pathHash, + ); } } +} + +function generateHierarchyItems(data, loadOptions, headers, headerName) { + const result = [0]; + const expandIndex = loadOptions.headerName === headerName ? loadOptions.path.length : 0; + const expandedPaths = headerName === 'rows' ? loadOptions.rowExpandedPaths : loadOptions.columnExpandedPaths; + const options = { + data, + childrenHash: headers[`${headerName}Hash`], + dimensions: loadOptions[headerName], + expandedPathsHash: loadOptions.headerName !== headerName + && expandedPaths + && expandedPaths.hash, + }; - function generateHierarchyItems(data, loadOptions, headers, headerName) { - const result = [0]; - const expandIndex = loadOptions.headerName === headerName ? loadOptions.path.length : 0; - const expandedPaths = headerName === 'rows' ? loadOptions.rowExpandedPaths : loadOptions.columnExpandedPaths; - const options = { - data, - childrenHash: headers[`${headerName}Hash`], - dimensions: loadOptions[headerName], - expandedPathsHash: loadOptions.headerName !== headerName - && expandedPaths - && expandedPaths.hash, - }; - - fillHierarchyItemIndexesCore(result, options, headers[headerName], expandIndex); - return result; + fillHierarchyItemIndexesCore(result, options, headers[headerName], expandIndex); + return result; +} + +function generateAggregationCells(data, cells, headers, options) { + const cellSet: any = []; + let x; + let y; + let rowIndex; + let columnIndex; + + const rowIndexes = generateHierarchyItems(data, options, headers, 'rows'); + const columnIndexes = generateHierarchyItems(data, options, headers, 'columns'); + + for (y = 0; y < rowIndexes.length; y += 1) { + rowIndex = rowIndexes[y]; + cells[rowIndex] = cells[rowIndex] || []; + for (x = 0; x < columnIndexes.length; x += 1) { + columnIndex = columnIndexes[x]; + cellSet.push(cells[rowIndex][columnIndex] = cells[rowIndex][columnIndex] || []); + } } - function generateAggregationCells(data, cells, headers, options) { - const cellSet: any = []; - let x; - let y; - let rowIndex; - let columnIndex; - - const rowIndexes = generateHierarchyItems(data, options, headers, 'rows'); - const columnIndexes = generateHierarchyItems(data, options, headers, 'columns'); - - for (y = 0; y < rowIndexes.length; y += 1) { - rowIndex = rowIndexes[y]; - cells[rowIndex] = cells[rowIndex] || []; - for (x = 0; x < columnIndexes.length; x += 1) { - columnIndex = columnIndexes[x]; - cellSet.push(cells[rowIndex][columnIndex] = cells[rowIndex][columnIndex] || []); - } - } + return cellSet; +} - return cellSet; +function fillHashExpandedPath(expandedPaths) { + if (expandedPaths) { + const hash = expandedPaths.hash = {}; + expandedPaths.forEach((path) => { + const pathValue = path.map((value) => `${value}`).join(PATH_DELIMETER); + hash[pathValue] = true; + }); } - - function fillHashExpandedPath(expandedPaths) { - if (expandedPaths) { - const hash = expandedPaths.hash = {}; - expandedPaths.forEach((path) => { - const pathValue = path.map((value) => `${value}`).join(PATH_DELIMETER); - hash[pathValue] = true; - }); - } +} + +function prepareLoadOption(options) { + options.rows = options.rows || []; + options.columns = options.columns || []; + options.filters = options.filters || []; + + fillHashExpandedPath(options.columnExpandedPaths); + fillHashExpandedPath(options.rowExpandedPaths); + + prepareFields(options.columns); + prepareFields(options.rows); + prepareFields(options.values); + prepareFields(options.filters); +} + +function getAggregator(field) { + if (field.summaryType === 'custom') { + field.calculateCustomSummary = field.calculateCustomSummary || noop; + + return { + seed() { + const options = { + summaryProcess: 'start', + totalValue: undefined, + }; + field.calculateCustomSummary(options); + return options; + }, + step(options, value) { + options.summaryProcess = 'calculate'; + options.value = value; + field.calculateCustomSummary(options); + return options; + }, + finalize(options) { + options.summaryProcess = 'finalize'; + delete options.value; + field.calculateCustomSummary(options); + return options.totalValue; + }, + }; } - function prepareLoadOption(options) { - options.rows = options.rows || []; - options.columns = options.columns || []; - options.filters = options.filters || []; + return aggregators[field.summaryType] || aggregators.count; +} - fillHashExpandedPath(options.columnExpandedPaths); - fillHashExpandedPath(options.rowExpandedPaths); +function aggregationStep(measures, aggregationCells, data) { + for (let aggregatorIndex = 0; aggregatorIndex < measures.length; aggregatorIndex += 1) { + const cellField = measures[aggregatorIndex]; + const cellValue = cellField.selector(data); - prepareFields(options.columns); - prepareFields(options.rows); - prepareFields(options.values); - prepareFields(options.filters); - } + const aggregator = getAggregator(cellField); + const isAggregatorSeedFunction = typeof aggregator.seed === 'function'; - function getAggregator(field) { - if (field.summaryType === 'custom') { - field.calculateCustomSummary = field.calculateCustomSummary || noop; - - return { - seed() { - const options = { - summaryProcess: 'start', - totalValue: undefined, - }; - field.calculateCustomSummary(options); - return options; - }, - step(options, value) { - options.summaryProcess = 'calculate'; - options.value = value; - field.calculateCustomSummary(options); - return options; - }, - finalize(options) { - options.summaryProcess = 'finalize'; - delete options.value; - field.calculateCustomSummary(options); - return options.totalValue; - }, - }; + for (let cellSetIndex = 0; cellSetIndex < aggregationCells.length; cellSetIndex += 1) { + const cell = aggregationCells[cellSetIndex]; + if (cell.length <= aggregatorIndex) { + cell[aggregatorIndex] = isAggregatorSeedFunction ? aggregator.seed() : aggregator.seed; + } + if (cell[aggregatorIndex] === undefined) { + cell[aggregatorIndex] = cellValue; + } else if (isDefined(cellValue)) { + cell[aggregatorIndex] = aggregator.step(cell[aggregatorIndex], cellValue); + } } - - return aggregators[field.summaryType] || aggregators.count; } +} + +function aggregationFinalize(measures, cells) { + each(measures, (aggregatorIndex, cellField) => { + const aggregator = getAggregator(cellField); + if (aggregator.finalize) { + each(cells, (_, row) => { + each(row, (_, cell) => { + if (cell && cell[aggregatorIndex] !== undefined) { + cell[aggregatorIndex] = aggregator.finalize(cell[aggregatorIndex]); + } + }); + }); + } + }); +} - function aggregationStep(measures, aggregationCells, data) { - for (let aggregatorIndex = 0; aggregatorIndex < measures.length; aggregatorIndex += 1) { - const cellField = measures[aggregatorIndex]; - const cellValue = cellField.selector(data); +function areValuesEqual(filterValue, fieldValue) { + let valueOfFilter = filterValue && filterValue.valueOf(); + let valueOfField = fieldValue && fieldValue.valueOf(); - const aggregator = getAggregator(cellField); - const isAggregatorSeedFunction = typeof aggregator.seed === 'function'; + if (Array.isArray(filterValue)) { + fieldValue = fieldValue || []; - for (let cellSetIndex = 0; cellSetIndex < aggregationCells.length; cellSetIndex += 1) { - const cell = aggregationCells[cellSetIndex]; - if (cell.length <= aggregatorIndex) { - cell[aggregatorIndex] = isAggregatorSeedFunction ? aggregator.seed() : aggregator.seed; - } - if (cell[aggregatorIndex] === undefined) { - cell[aggregatorIndex] = cellValue; - } else if (isDefined(cellValue)) { - cell[aggregatorIndex] = aggregator.step(cell[aggregatorIndex], cellValue); - } + for (let i = 0; i < filterValue.length; i += 1) { + valueOfFilter = filterValue[i] && filterValue[i].valueOf(); + valueOfField = fieldValue[i] && fieldValue[i].valueOf(); + + if (valueOfFilter !== valueOfField) { + return false; } } + return true; } - - function aggregationFinalize(measures, cells) { - each(measures, (aggregatorIndex, cellField) => { - const aggregator = getAggregator(cellField); - if (aggregator.finalize) { - each(cells, (_, row) => { - each(row, (_, cell) => { - if (cell && cell[aggregatorIndex] !== undefined) { - cell[aggregatorIndex] = aggregator.finalize(cell[aggregatorIndex]); - } - }); - }); + return valueOfFilter === valueOfField; +} + +function getGroupValue(levels, data) { + const value: any = []; + each(levels, (_, field) => { + value.push(field.selector(data)); + }); + return value; +} + +function createDimensionFilters(dimension) { + const filters: any = []; + each(dimension, (_, field) => { + const filterValues = field.filterValues || []; + const { groupName } = field; + + if (groupName && isNumeric(field.groupIndex)) { + return; + } + const filter = function (dataItem) { + const value = field.levels + ? getGroupValue(field.levels, dataItem) + : field.selector(dataItem); + let result = false; + for (let i = 0; i < filterValues.length; i += 1) { + if (areValuesEqual(filterValues[i], value)) { + result = true; + break; + } } - }); - } - - function areValuesEqual(filterValue, fieldValue) { - let valueOfFilter = filterValue && filterValue.valueOf(); - let valueOfField = fieldValue && fieldValue.valueOf(); - - if (Array.isArray(filterValue)) { - fieldValue = fieldValue || []; - - for (let i = 0; i < filterValue.length; i += 1) { - valueOfFilter = filterValue[i] && filterValue[i].valueOf(); - valueOfField = fieldValue[i] && fieldValue[i].valueOf(); + return field.filterType === 'exclude' ? !result : result; + }; - if (valueOfFilter !== valueOfField) { + filterValues.length && filters.push(filter); + }); + return filters; +} + +function createFilter(options) { + const filters = createDimensionFilters(options.rows) + .concat(createDimensionFilters(options.columns)) + .concat(createDimensionFilters(options.filters)); + const expandedDimensions = options[options.headerName]; + const { path } = options; + + if (expandedDimensions) { + filters.push((dataItem) => { + let expandValue; + for (let i = 0; i < path.length; i += 1) { + expandValue = expandedDimensions[i].selector(dataItem); + if (toComparable(expandValue, true) !== toComparable(path[i], true)) { return false; } } return true; - } - return valueOfFilter === valueOfField; - } - - function getGroupValue(levels, data) { - const value: any = []; - each(levels, (_, field) => { - value.push(field.selector(data)); }); - return value; } - function createDimensionFilters(dimension) { - const filters: any = []; - each(dimension, (_, field) => { - const filterValues = field.filterValues || []; - const { groupName } = field; - - if (groupName && isNumeric(field.groupIndex)) { - return; + return function (dataItem) { + for (let i = 0; i < filters.length; i += 1) { + if (!filters[i](dataItem)) { + return false; } - const filter = function (dataItem) { - const value = field.levels - ? getGroupValue(field.levels, dataItem) - : field.selector(dataItem); - let result = false; - for (let i = 0; i < filterValues.length; i += 1) { - if (areValuesEqual(filterValues[i], value)) { - result = true; - break; - } + } + return true; + }; +} + +function loadCore(items, options, notifyProgress) { + const headers = { + columns: [], + rows: [], + columnsHash: { length: 1 }, + rowsHash: { length: 1 }, + }; + const values = []; + let aggregationCells; + let data; + // @ts-expect-error + const d = new Deferred(); + let i = 0; + + const filter = createFilter(options); + + function processData() { + const t = new Date(); + const startIndex = i; + + for (; i < items.length; i += 1) { + if (i > startIndex && i % 10000 === 0) { + if ((new Date() as any) - (t as any) >= 300) { + notifyProgress(i / items.length); + setTimeout(processData, 0); + + return; } - return field.filterType === 'exclude' ? !result : result; - }; + } + data = items[i]; + if (filter(data)) { + aggregationCells = generateAggregationCells(data, values, headers, options); + aggregationStep(options.values, aggregationCells, data); + } + } - filterValues.length && filters.push(filter); + aggregationFinalize(options.values, values); + notifyProgress(1); + d.resolve({ + rows: headers.rows, + columns: headers.columns, + values, + grandTotalRowIndex: 0, + grandTotalColumnIndex: 0, }); - return filters; } - function createFilter(options) { - const filters = createDimensionFilters(options.rows) - .concat(createDimensionFilters(options.columns)) - .concat(createDimensionFilters(options.filters)); - const expandedDimensions = options[options.headerName]; - const { path } = options; - - if (expandedDimensions) { - filters.push((dataItem) => { - let expandValue; - for (let i = 0; i < path.length; i += 1) { - expandValue = expandedDimensions[i].selector(dataItem); - if (toComparable(expandValue, true) !== toComparable(path[i], true)) { - return false; - } - } - return true; - }); - } + processData(); - return function (dataItem) { - for (let i = 0; i < filters.length; i += 1) { - if (!filters[i](dataItem)) { - return false; - } - } - return true; - }; - } - - function loadCore(items, options, notifyProgress) { - const headers = { - columns: [], - rows: [], - columnsHash: { length: 1 }, - rowsHash: { length: 1 }, - }; - const values = []; - let aggregationCells; - let data; - // @ts-expect-error - const d = new Deferred(); - let i = 0; + return d; +} - const filter = createFilter(options); +function filterDataSource(dataSource, fieldSelectors) { + let filter = dataSource.filter(); - function processData() { - const t = new Date(); - const startIndex = i; + if (dataSource.store() instanceof CustomStore && filter) { + filter = processFilter(filter, fieldSelectors); + return dataQuery(dataSource.items()).filter(filter).toArray(); + } - for (; i < items.length; i += 1) { - if (i > startIndex && i % 10000 === 0) { - if ((new Date() as any) - (t as any) >= 300) { - notifyProgress(i / items.length); - setTimeout(processData, 0); + return dataSource.items(); +} - return; - } - } - data = items[i]; - if (filter(data)) { - aggregationCells = generateAggregationCells(data, values, headers, options); - aggregationStep(options.values, aggregationCells, data); - } - } +function loadDataSource(dataSource, fieldSelectors, reload?) { + // @ts-expect-error + const d = new Deferred(); - aggregationFinalize(options.values, values); - notifyProgress(1); - d.resolve({ - rows: headers.rows, - columns: headers.columns, - values, - grandTotalRowIndex: 0, - grandTotalColumnIndex: 0, - }); + const customizeStoreLoadOptionsHandler = function (options) { + if (dataSource.store() instanceof ArrayStore) { + options.storeLoadOptions.filter = processFilter( + options.storeLoadOptions.filter, + fieldSelectors, + ); } + }; - processData(); + dataSource.on('customizeStoreLoadOptions', customizeStoreLoadOptionsHandler); - return d; + if (!dataSource.isLoaded() || reload) { + const loadDeferred = reload ? dataSource.load() : dataSource.reload(); + when(loadDeferred).done(() => { + loadDataSource(dataSource, fieldSelectors).done(() => { + d.resolve(filterDataSource(dataSource, fieldSelectors)); + }).fail(d.reject); + }).fail(d.reject); + } else { + d.resolve(filterDataSource(dataSource, fieldSelectors)); } - - function filterDataSource(dataSource, fieldSelectors) { - let filter = dataSource.filter(); - - if (dataSource.store() instanceof CustomStore && filter) { - filter = processFilter(filter, fieldSelectors); - return dataQuery(dataSource.items()).filter(filter).toArray(); + return d.always(() => { + dataSource.off('customizeStoreLoadOptions', customizeStoreLoadOptionsHandler); + }); +} + +function fillSelectorsByFields(selectors, fields) { + fields.forEach((field) => { + if (field.dataField && field.dataType === 'date') { + const valueSelector = getDateValue(getDataSelector(field.dataField)); + selectors[field.dataField] = function (data) { return valueSelector(data); }; } + }); +} - return dataSource.items(); - } +function getFieldSelectors(options) { + const selectors = {}; - function loadDataSource(dataSource, fieldSelectors, reload?) { - // @ts-expect-error - const d = new Deferred(); - - const customizeStoreLoadOptionsHandler = function (options) { - if (dataSource.store() instanceof ArrayStore) { - options.storeLoadOptions.filter = processFilter( - options.storeLoadOptions.filter, - fieldSelectors, - ); - } - }; + if (Array.isArray(options)) { + fillSelectorsByFields(selectors, options); + } else if (options) { + ['rows', 'columns', 'filters'].forEach((area) => { + options[area] && fillSelectorsByFields(selectors, options[area]); + }); + } + return selectors; +} - dataSource.on('customizeStoreLoadOptions', customizeStoreLoadOptionsHandler); +function processFilter(filter, fieldSelectors) { + if (!Array.isArray(filter)) { + return filter; + } - if (!dataSource.isLoaded() || reload) { - const loadDeferred = reload ? dataSource.load() : dataSource.reload(); - when(loadDeferred).done(() => { - loadDataSource(dataSource, fieldSelectors).done(() => { - d.resolve(filterDataSource(dataSource, fieldSelectors)); - }).fail(d.reject); - }).fail(d.reject); - } else { - d.resolve(filterDataSource(dataSource, fieldSelectors)); - } - return d.always(() => { - dataSource.off('customizeStoreLoadOptions', customizeStoreLoadOptionsHandler); - }); + filter = filter.slice(0); + if (isString(filter[0]) && (filter[1] instanceof Date || filter[2] instanceof Date)) { + filter[0] = fieldSelectors[filter[0]]; } - function fillSelectorsByFields(selectors, fields) { - fields.forEach((field) => { - if (field.dataField && field.dataType === 'date') { - const valueSelector = getDateValue(getDataSelector(field.dataField)); - selectors[field.dataField] = function (data) { return valueSelector(data); }; - } - }); + for (let i = 0; i < filter.length; i += 1) { + filter[i] = processFilter(filter[i], fieldSelectors); } + return filter; +} - function getFieldSelectors(options) { - const selectors = {}; +class LocalStore { + _progressChanged: any; - if (Array.isArray(options)) { - fillSelectorsByFields(selectors, options); - } else if (options) { - ['rows', 'columns', 'filters'].forEach((area) => { - options[area] && fillSelectorsByFields(selectors, options[area]); - }); - } - return selectors; + _dataSource: any; + + constructor(options) { + this._progressChanged = options.onProgressChanged || noop; + this._dataSource = new DataSource(options); + this._dataSource.paginate(false); } - function processFilter(filter, fieldSelectors) { - if (!Array.isArray(filter)) { - return filter; - } + getFields(fields) { + // @ts-expect-error + const d = new Deferred(); - filter = filter.slice(0); - if (isString(filter[0]) && (filter[1] instanceof Date || filter[2] instanceof Date)) { - filter[0] = fieldSelectors[filter[0]]; - } + loadDataSource(this._dataSource, getFieldSelectors(fields)).done((data) => { + d.resolve(discoverObjectFields(data, fields)); + }).fail(d.reject); - for (let i = 0; i < filter.length; i += 1) { - filter[i] = processFilter(filter[i], fieldSelectors); - } - return filter; + return d; } - return { - ctor(options) { - this._progressChanged = options.onProgressChanged || noop; - this._dataSource = new DataSource(options); - this._dataSource.paginate(false); - }, - - getFields(fields) { - const that: any = this; - const dataSource = that._dataSource; - // @ts-expect-error - const d = new Deferred(); - - loadDataSource(dataSource, getFieldSelectors(fields)).done((data) => { - d.resolve(discoverObjectFields(data, fields)); - }).fail(d.reject); + key() { + return this._dataSource.key(); + } + + load(options) { + // @ts-expect-error + const d = new Deferred(); - return d; - }, + prepareLoadOption(options); - key() { - return this._dataSource.key(); - }, + loadDataSource(this._dataSource, getFieldSelectors(options), options.reload).done((data) => { + when(loadCore(data, options, this._progressChanged)).done(d.resolve); + }).fail(d.reject); - load(options) { - const that: any = this; - const dataSource = that._dataSource; - // @ts-expect-error - const d = new Deferred(); + return d; + } - prepareLoadOption(options); + filter(...args: unknown[]) { + return this._dataSource.filter(...args); + } - loadDataSource(dataSource, getFieldSelectors(options), options.reload).done((data) => { - when(loadCore(data, options, that._progressChanged)).done(d.resolve); - }).fail(d.reject); + supportPaging() { + return false; + } - return d; - }, - - filter() { - const dataSource = this._dataSource; - - return dataSource.filter.apply(dataSource, arguments); - }, - - supportPaging() { - return false; - }, - - getDrillDownItems(loadOptions, params) { - loadOptions = loadOptions || {}; - params = params || {}; - prepareLoadOption(loadOptions); - - const drillDownItems: any = []; - const items = this._dataSource.items(); - let item; - const { maxRowCount } = params; - const { customColumns } = params; - const filter = createFilter(loadOptions); - const pathFilter = createFilter({ - rows: getFiltersByPath(loadOptions.rows, params.rowPath), - columns: getFiltersByPath(loadOptions.columns, params.columnPath), - filters: [], - }); + getDrillDownItems(loadOptions, params) { + loadOptions = loadOptions || {}; + params = params || {}; + prepareLoadOption(loadOptions); + + const drillDownItems: any = []; + const items = this._dataSource.items(); + let item; + const { maxRowCount } = params; + const { customColumns } = params; + const filter = createFilter(loadOptions); + const pathFilter = createFilter({ + rows: getFiltersByPath(loadOptions.rows, params.rowPath), + columns: getFiltersByPath(loadOptions.columns, params.columnPath), + filters: [], + }); - for (let i = 0; i < items.length; i += 1) { - if (pathFilter(items[i]) && filter(items[i])) { - if (customColumns) { - item = {}; - for (let j = 0; j < customColumns.length; j += 1) { - item[customColumns[j]] = items[i][customColumns[j]]; - } - } else { - item = items[i]; + for (let i = 0; i < items.length; i += 1) { + if (pathFilter(items[i]) && filter(items[i])) { + if (customColumns) { + item = {}; + for (let j = 0; j < customColumns.length; j += 1) { + item[customColumns[j]] = items[i][customColumns[j]]; } - - drillDownItems.push(item); - } - if (maxRowCount > 0 && drillDownItems.length === maxRowCount) { - break; + } else { + item = items[i]; } + + drillDownItems.push(item); + } + if (maxRowCount > 0 && drillDownItems.length === maxRowCount) { + break; } + } - return drillDownItems; - }, - }; -})()).include(storeDrillDownMixin); + return drillDownItems; + } +} + +Object.assign(LocalStore.prototype, storeDrillDownMixin); export default { LocalStore }; export { LocalStore }; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/grid_core.modules.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/grid_core.modules.tests.js index 493df33c530b..bb80626f671b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/grid_core.modules.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/grid_core.modules.tests.js @@ -1,37 +1,6 @@ import modules from '__internal/grids/grid_core/m_modules'; QUnit.module('Modules used class', {}, () => { - /* const RootController = modules.Controller.inherit( - { - method: function(param) { - RootController.calls.push({ instance: this, param }); - return param; - }, - additionalMethod: function(param) { - RootController.calls.push({ instance: this, param }); - return 'additionalMethod'; - }, - publicMethods: function() { - return ['method']; - } - } - ); - const InheritedController = RootController.inherit({ - method: function(param) { - this.callBase(param); - RootController.calls.push({ instance: this, param: param + 'override' }); - return param; - }, - newMethod: function(param) { - RootController.calls.push({ instance: this, param }); - return param; - }, - publicMethods: function() { - return []; - } - }); - const WrongInheritance = Class.inherit({}); - */ class RootController extends modules.Controller { static calls = []; static constructors = []; @@ -325,12 +294,6 @@ QUnit.module('Modules used class', {}, () => { QUnit.test('Extend extender method', function(assert) { // arrange - /* const ExtendedExtender = { - extenderMethod: function() { - RootController.calls.push({ instance: this, name: 'extenderMethod', type: 'extenderOfExtender' }); - this.callBase(); - } - };*/ const applyExtendedExtender = (Base) => class ExtendedExtender extends Base { extenderMethod() { @@ -364,18 +327,12 @@ QUnit.module('Modules used class', {}, () => { assert.deepEqual( RootController.calls[1].type, 'extender', - 'check type class.inherit' + 'check type extender' ); }); QUnit.test('sinon.spy should work', function(assert) { // arrange - /* const ExtendedExtender = { - extenderMethod: function() { - RootController.calls.push({ instance: this, name: 'extenderMethod', type: 'extenderOfExtender' }); - this.callBase(); - } - };*/ const applyExtendedExtender = (Base) => class ExtendedExtender extends Base { extenderMethod() { @@ -418,20 +375,6 @@ QUnit.module('Modules used class', {}, () => { } }; }; - /* const classInheritExtender = { - ctor: function(component) { - this.callBase(component); - RootController.constructors.push('class.inherit'); - } - } - - modules.registerModule('class.inherit', { - extenders: { - controllers: { - 'root-controller': classInheritExtender - } - } - });*/ modules.registerModule('extenderOfExtender', { extenders: { controllers: { @@ -466,14 +409,6 @@ QUnit.module('Modules used class', {}, () => { 'Use extender method in other extender', function(assert) { // arrange - /* - const ExtendedExtender = { - methodUsedExtender: function() { - RootController.calls.push({ instance: this, name: 'methodUsedExtender', type: 'extenderOfExtender' }); - this.extenderMethod(); - } - }; - */ const applyExtendedExtender = (Base) => class ExtendedExtender extends Base { methodUsedExtender() { @@ -518,30 +453,13 @@ QUnit.module('Modules used class', {}, () => { assert.deepEqual( RootController.calls[1].type, 'extender', - 'check type class.inherit' + 'check type extender' ); } ); QUnit.test('override several extender', function(assert) { // arrange - /* - const Extender1 = { - extenderMethod1: function() { - RootController.calls.push({ name: 'extenderMethod1', type: 'Extender1' }); - } - }; - const ExtendedExtender = { - extenderMethod: function() { - RootController.calls.push({ name: 'extenderMethod', type: 'extenderOfExtender' }); - this.callBase(); - }, - extenderMethod1: function() { - RootController.calls.push({ name: 'extenderMethod1', type: 'extenderOfExtender' }); - this.callBase(); - } - }; - */ const applyExtender1 = (Base) => class Extender1 extends Base { extenderMethod1() { @@ -616,12 +534,6 @@ QUnit.module('Modules used class', {}, () => { 'Check view use same approach as controller', function(assert) { // arrange - /* const ExtendedExtender = { - extenderMethod: function() { - RootView.calls.push({ instance: this, name: 'extenderMethod', type: 'extenderOfExtender' }); - this.callBase(); - } - };*/ class RootView extends modules.View { static calls = []; extenderMethod() { @@ -681,7 +593,7 @@ QUnit.module('Modules used class', {}, () => { name: 'extenderMethod', type: 'RootView', }, - 'check type class.inherit' + 'check type extender' ); } ); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/dataController.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/dataController.tests.js index 07f2dbf508e2..f08c9c516935 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/dataController.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.pivotGrid/dataController.tests.js @@ -1,5 +1,4 @@ import $ from 'jquery'; -import Class from 'core/class'; import { DataController } from '__internal/grids/pivot_grid/data_controller/m_data_controller'; import virtualScrolling from '__internal/grids/grid_core/virtual_scrolling/m_virtual_scrolling_core'; import stateStoring from '__internal/grids/grid_core/state_storing/state_storing_controller_core'; @@ -6092,18 +6091,18 @@ QUnit.module('Remote paging', { const that = this; this.loadArgs = []; - const MockStore = Class.inherit({ - ctor: function(options) { + class MockStore { + constructor(options) { this._rowCount = options.rowCount; this._columnCount = options.columnCount; - }, - getFields: function() { + } + getFields() { return $.Deferred().resolve([]); - }, - supportPaging: function() { + } + supportPaging() { return true; - }, - load: function(loadOptions) { + } + load(loadOptions) { that.loadArgs.push(loadOptions); const rowCount = loadOptions.rows.length ? this._rowCount : 0; const columnCount = loadOptions.columns.length ? this._columnCount : 0; @@ -6116,7 +6115,7 @@ QUnit.module('Remote paging', { grandTotalColumnIndex: 0 }); } - }); + } this.component = { option: function(name) { @@ -6711,15 +6710,14 @@ QUnit.module('Remote paging', { QUnit.test('load with CustomStore', function(assert) { const that = this; - const CustomPivotStore = Class.inherit({ - ctor: function() {}, - getFields: function() { + class CustomPivotStore { + getFields() { return $.Deferred().resolve([]).promise(); - }, - supportPaging: function() { + } + supportPaging() { return true; - }, - load: function(loadOptions) { + } + load(loadOptions) { that.loadArgs.push(loadOptions); return $.Deferred().resolve({ rows: [ @@ -6732,7 +6730,7 @@ QUnit.module('Remote paging', { grandTotalColumnIndex: 0 }).promise(); } - }); + } const dataController = that.setup({ paginate: true, From 5a654aafd76f572b0072a5cbe5cb11491a431058 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Wed, 30 Sep 2026 17:37:44 +0400 Subject: [PATCH 2/2] Grids: keep the strict extender types and let a non-function extender fail loudly --- .../grids/grid_core/adaptivity/adaptivity_module.ts | 1 + .../js/__internal/grids/grid_core/m_modules.ts | 11 +++++++---- .../js/__internal/grids/grid_core/m_types.ts | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/adaptivity_module.ts b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/adaptivity_module.ts index bd1bfa020293..c53dca6fafb8 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/adaptivity/adaptivity_module.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/adaptivity/adaptivity_module.ts @@ -29,6 +29,7 @@ export const adaptivityModule: Module = { draggingHeader: adaptivityDraggingHeaderViewControllerExtender, editing: adaptivityEditingViewControllerExtender, resizing: adaptivityResizingViewControllerExtender, + // @ts-expect-error the extender assumes the master detail extension of DataController data: adaptivityDataControllerExtender, editorFactory: adaptivityEditorFactoryViewControllerExtender, columns: adaptivityColumnsControllerExtender, diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts b/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts index 3366e3c4445d..46d658a9bcbe 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_modules.ts @@ -15,6 +15,7 @@ import errors from '@js/ui/widget/ui.errors'; import type { Controllers, GridPropertyType, InternalGrid, InternalGridOptions, Module, + ModuleType, OptionChanged, Views, } from './m_types'; @@ -381,15 +382,17 @@ export class View extends ModuleItem { const MODULES_ORDER_MAX_INDEX = 1000000; +type ModuleTypeExtender = (Base: ModuleType) => ModuleType; + function getExtendedTypes( - types: Record, - moduleExtenders: Record = {}, -): Record { + types: Record>, + moduleExtenders: Record = {}, +): Record> { const extendTypes = { }; Object.entries(moduleExtenders) .forEach(([name, extender]) => { const currentType = types[name]; - if (currentType && isFunction(extender)) { + if (currentType && extender) { extendTypes[name] = extender(currentType); } }); 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 99a50e66c3ce..e0613863ca0a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -274,11 +274,11 @@ type ViewTypes = { export type ModuleType = (new (...args: any[]) => T); type ControllersExtender = { - [P in keyof Controllers]: (Base: ModuleType) => ModuleType; + [P in keyof Controllers]: (Base: ModuleType) => ModuleType; }; type ViewsExtender = { - [P in keyof Views]: (Base: ModuleType) => ModuleType; + [P in keyof Views]: (Base: ModuleType) => ModuleType; }; export interface Module {