diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_aggregate_calculator.ts b/packages/devextreme/js/__internal/grids/data_grid/m_aggregate_calculator.ts index 042e0f944680..b1ac476f8caf 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_aggregate_calculator.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_aggregate_calculator.ts @@ -2,49 +2,56 @@ import { errors } from '@js/common/data/errors'; import { aggregators } from '@js/common/data/utils'; import { compileGetter } from '@js/core/utils/data'; import { isFunction } from '@js/core/utils/type'; +import type { GroupData } from '@js/ui/data_grid'; +import type { RawItemData } from '../grid_core/data_source_adapter/types'; import type { Aggregate } from './summary/types'; -function depthFirstSearch(i, depth, root, callback) { +// NOTE: only the deepest groups hold rows in items; the groups above hold groups +interface AggregateNode { + items: (AggregateNode | RawItemData)[]; + aggregates?: unknown[]; +} + +interface Aggregator { + seed?: number | unknown[] | ((groupIndex?: number) => unknown); + step: (accumulator: unknown, value: unknown) => unknown; + finalize?: (accumulator: unknown) => unknown; +} + +interface NormalizedAggregate { + selector: (data: RawItemData) => unknown; + aggregator: Aggregator; + skipEmptyValues: boolean | undefined; +} + +const depthFirstSearch = ( + i: number, + depth: number, + root: AggregateNode, + callback: (node: AggregateNode) => void, +): void => { let j = 0; if (i < depth) { - for (; j < root.items.length; j++) { - depthFirstSearch(i + 1, depth, root.items[j], callback); + for (; j < root.items.length; j += 1) { + // NOTE: above the given depth, items are groups + depthFirstSearch(i + 1, depth, root.items[j] as AggregateNode, callback); } } if (i === depth) { callback(root); } -} +}; -// NOTE: https://github.com/jquery/jquery/blame/master/src/core.js#L392 -function map(array, callback) { - let i; +const isEmpty = (x: unknown): boolean => Number.isNaN(x) || (x === '') || (x === null) || (x === undefined); - if ('map' in array) { - return array.map(callback); - } +const isCount = (aggregator: Aggregator): boolean => aggregator === aggregators.count; - const result = new Array(array.length); - // eslint-disable-next-line guard-for-in - for (i in array) { - result[i] = callback(array[i], i); - } +const normalizeAggregate = (aggregate: Aggregate): NormalizedAggregate => { + // @ts-expect-error badly typed compileGetter + const selector = compileGetter(aggregate.selector) as (data: RawItemData) => unknown; - return result; -} - -function isEmpty(x) { - return (x !== x) || (x === '') || (x === null) || (x === undefined); -} - -function isCount(aggregator) { - return aggregator === aggregators.count; -} - -function normalizeAggregate(aggregate) { - const selector = compileGetter(aggregate.selector); const skipEmptyValues = 'skipEmptyValues' in aggregate ? aggregate.skipEmptyValues : true; @@ -59,32 +66,33 @@ function normalizeAggregate(aggregate) { return { selector, + // @ts-expect-error aggregators[name] is untyped; local aggregates always have one aggregator, skipEmptyValues, }; -} +}; export default class AggregateCalculator { - private readonly _data: any; + private readonly _data: RawItemData[] | GroupData[]; private readonly _groupLevel: number; - private readonly _totalAggregates: Aggregate[]; + private readonly _totalAggregates: NormalizedAggregate[]; - private readonly _groupAggregates: Aggregate[]; + private readonly _groupAggregates: NormalizedAggregate[]; private _totals: unknown[]; constructor(options: { - data: any; + data: RawItemData[] | GroupData[]; groupLevel: number; totalAggregates: Aggregate[]; groupAggregates: Aggregate[]; }) { this._data = options.data; this._groupLevel = options.groupLevel; - this._totalAggregates = map(options.totalAggregates, normalizeAggregate); - this._groupAggregates = map(options.groupAggregates, normalizeAggregate); + this._totalAggregates = options.totalAggregates.map(normalizeAggregate); + this._groupAggregates = options.groupAggregates.map(normalizeAggregate); this._totals = []; } @@ -102,31 +110,38 @@ export default class AggregateCalculator { return this._totals; } - private _aggregate(aggregates, data, container) { + private _aggregate( + aggregates: NormalizedAggregate[], + data: AggregateNode, + container: unknown[], + ): void { const length = data.items ? data.items.length : 0; - for (let i = 0; i < aggregates.length; i++) { + for (let i = 0; i < aggregates.length; i += 1) { if (isCount(aggregates[i].aggregator)) { - container[i] = (container[i] || 0) + length; + container[i] = (container[i] as number | undefined ?? 0) + length; + // eslint-disable-next-line no-continue continue; } - for (let j = 0; j < length; j++) { - this._accumulate(i, aggregates[i], container, data.items[j]); + for (let j = 0; j < length; j += 1) { + // NOTE: at the group level, items are rows + this._accumulate(i, aggregates[i], container, data.items[j] as RawItemData); } } } - private _calculateTotals(level, data) { + private _calculateTotals(level: number, root: AggregateNode): void { if (level === 0) { this._totals = this._seed(this._totalAggregates); } if (level === this._groupLevel) { - this._aggregate(this._totalAggregates, data, this._totals); + this._aggregate(this._totalAggregates, root, this._totals); } else { - for (let i = 0; i < data.items.length; i++) { - this._calculateTotals(level + 1, data.items[i]); + for (const item of root.items) { + // NOTE: above the group level, items are groups + this._calculateTotals(level + 1, item as AggregateNode); } } @@ -135,7 +150,7 @@ export default class AggregateCalculator { } } - private _calculateGroups(root) { + private _calculateGroups(root: AggregateNode): void { const maxLevel = this._groupLevel; let currentLevel = maxLevel + 1; @@ -143,37 +158,46 @@ export default class AggregateCalculator { const stepFn = this._aggregate.bind(this, this._groupAggregates); const finalizeFn = this._finalize.bind(this, this._groupAggregates); - function aggregator(node) { - node.aggregates = seedFn(currentLevel - 1); + const aggregator = (node: AggregateNode): void => { + const aggregates = seedFn(currentLevel - 1); + node.aggregates = aggregates; if (currentLevel === maxLevel) { - stepFn(node, node.aggregates); + stepFn(node, aggregates); } else { depthFirstSearch(currentLevel, maxLevel, node, (innerNode) => { - stepFn(innerNode, node.aggregates); + stepFn(innerNode, aggregates); }); } - node.aggregates = finalizeFn(node.aggregates); - } + node.aggregates = finalizeFn(aggregates); + }; - while (--currentLevel > 0) { + currentLevel -= 1; + while (currentLevel > 0) { depthFirstSearch(0, currentLevel, root, aggregator); + currentLevel -= 1; } } - private _seed(aggregates, groupIndex?) { - return map(aggregates, (aggregate) => { + private _seed(aggregates: NormalizedAggregate[], groupIndex?: number): unknown[] { + return aggregates.map((aggregate) => { const { aggregator } = aggregate; - const seed = 'seed' in aggregator - ? isFunction(aggregator.seed) ? aggregator.seed(groupIndex) : aggregator.seed - : NaN; - return seed; + if (!('seed' in aggregator)) { + return NaN; + } + + return isFunction(aggregator.seed) ? aggregator.seed(groupIndex) : aggregator.seed; }); } - private _accumulate(aggregateIndex, aggregate, results, item) { + private _accumulate( + aggregateIndex: number, + aggregate: NormalizedAggregate, + results: unknown[], + item: RawItemData, + ): void { const value = aggregate.selector(item); const { aggregator } = aggregate; const { skipEmptyValues } = aggregate; @@ -182,15 +206,15 @@ export default class AggregateCalculator { return; } - if (results[aggregateIndex] !== results[aggregateIndex]) { + if (Number.isNaN(results[aggregateIndex])) { results[aggregateIndex] = value; } else { results[aggregateIndex] = aggregator.step(results[aggregateIndex], value); } } - private _finalize(aggregates, results) { - return map(aggregates, (aggregate, index) => { + private _finalize(aggregates: NormalizedAggregate[], results: unknown[]): unknown[] { + return aggregates.map((aggregate, index) => { const fin = aggregate.aggregator.finalize; return fin ? fin(results[index]) diff --git a/packages/devextreme/js/__internal/grids/data_grid/m_utils.ts b/packages/devextreme/js/__internal/grids/data_grid/m_utils.ts index 2b6a0ac40c71..2ed7b2ee207a 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/m_utils.ts @@ -1,15 +1,20 @@ import { normalizeSortingInfo } from '@js/common/data/utils'; import { isDefined } from '@ts/core/utils/m_type'; +import type { StoreLoadOptions } from '@ts/data/data_source/types'; +import type { DataFilter } from '@ts/grids/grid_core/filter/types'; import { combineFilters } from '@ts/grids/grid_core/filter/utils'; import type { Column } from './types'; -export function createGroupFilter(path, storeLoadOptions) { +export function createGroupFilter( + path: unknown[], + storeLoadOptions: Pick, +): DataFilter { const groups = normalizeSortingInfo(storeLoadOptions.group); - const filter: any = []; + const filter: unknown[] = []; - for (let i = 0; i < path.length; i++) { + for (let i = 0; i < path.length; i += 1) { filter.push([groups[i].selector, '=', path[i]]); } diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts index 9f48b8704354..741bc082426b 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts @@ -6,10 +6,11 @@ import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; import { isDefined, isPlainObject } from '@js/core/utils/type'; +import type { GroupData } from '@js/ui/data_grid'; import type { DataSource } from '@ts/data/data_source/data_source'; import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; -import type { RemoteOperationsOptions } from '@ts/grids/grid_core/data_source_adapter/types'; +import type { RawItemData, RemoteOperationsOptions } from '@ts/grids/grid_core/data_source_adapter/types'; import type { EditingControllerRequired, ModuleType } from '@ts/grids/grid_core/m_types'; import { ColumnsView } from '@ts/grids/grid_core/views/m_columns_view'; @@ -349,7 +350,7 @@ export const summaryDataSourceAdapterExtender = ( } private calculateTotalAggregates( - data, + data: RawItemData[] | GroupData[], totalAggregates: Aggregate[], groupAggregates: Aggregate[], groupLevel: number, diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/types.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/types.ts index adfce3a0a63c..1f9224c26e27 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/types.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/types.ts @@ -44,7 +44,7 @@ export interface SortInfo { export type SortByGroups = (SortInfo[] | undefined)[]; export interface CustomAggregator { - seed: (groupIndex: number) => unknown; + seed: (groupIndex?: number) => unknown; step: (totalValue: unknown, value: unknown) => unknown; finalize: (totalValue: unknown) => unknown; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_accessibility.ts b/packages/devextreme/js/__internal/grids/grid_core/m_accessibility.ts index cc02d7b70231..5ca3a9535cf3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_accessibility.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_accessibility.ts @@ -1,20 +1,46 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import type { DxEvent } from '@js/events'; import * as accessibility from '@js/ui/shared/accessibility'; -export const registerKeyboardAction = function (viewName, instance, $element, selector, action) { +import type { View } from './m_modules'; + +interface KeyDownArgs { + event: unknown; + handled: boolean; +} + +export const registerKeyboardAction = ( + viewName: string, + instance: View, + $element: dxElementWrapper, + selector: string | undefined, + action: (args: { event: DxEvent }) => void, +): void => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - let executeKeyDown = (args) => {}; + let executeKeyDown = (args: KeyDownArgs): void => {}; + const keyboardController = instance.getController('keyboardNavigation'); - if (instance.option('useLegacyKeyboardNavigation') || (keyboardController && !keyboardController.isKeyboardEnabled())) { + if ( + instance.option('useLegacyKeyboardNavigation') + || (keyboardController && !keyboardController.isKeyboardEnabled()) + ) { return; } if (viewName === 'filterPanel') { - executeKeyDown = (args) => { + executeKeyDown = (args: KeyDownArgs): void => { instance.executeAction('onKeyDown', args); }; instance.createAction('onKeyDown'); } - accessibility.registerKeyboardAction(viewName, instance, $element, selector, action, executeKeyDown); + accessibility.registerKeyboardAction( + viewName, + instance, + $element, + selector, + action, + executeKeyDown, + ); }; diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_export.ts b/packages/devextreme/js/__internal/grids/grid_core/m_export.ts index 53d9222d65d2..09f857325c9d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_export.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_export.ts @@ -1,22 +1,39 @@ import { extend } from '@js/core/utils/extend'; -function prepareItems(items, emptyCell) { - const defaultSetter = (value) => (!value ? 1 : value); - const cloneItem = (item) => extend({}, item, emptyCell); +interface ExportItem { + colspan?: number; + rowspan?: number; +} + +type PreparedItem = T & { + colspan: number; + rowspan: number; +}; + +function prepareItems( + items: T[][], + emptyCell?: Partial, +): PreparedItem[][] { + const defaultSetter = (value: number | undefined): number => (!value ? 1 : value); + const cloneItem = (item: PreparedItem): PreparedItem => ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- extend isn't typed + extend({}, item, emptyCell) + ); - const resultItems: any[] = []; + const resultItems: PreparedItem[][] = []; - const cols = (items[0] || []).reduce((sum, item) => sum + defaultSetter(item.colspan), 0); + const cols = (items[0] ?? []).reduce((sum, item) => sum + defaultSetter(item.colspan), 0); - const getItem = ((items) => { + const getItem = ((rows: T[][]) => { let rowIndex = 0; let cellIndex = 0; - return () => { - const row = items[rowIndex] || []; - const item = row[cellIndex++]; + return (): PreparedItem | undefined => { + const row = rows[rowIndex] ?? []; + const item = row[cellIndex] as T | undefined; + cellIndex += 1; if (cellIndex >= row.length) { - rowIndex++; + rowIndex += 1; cellIndex = 0; } if (item) { @@ -24,20 +41,21 @@ function prepareItems(items, emptyCell) { item.rowspan = defaultSetter(item.rowspan); } - return item; + return item as PreparedItem | undefined; }; })(items); - const addItem = (rowIndex, cellIndex, item) => { - const row = resultItems[rowIndex] = resultItems[rowIndex] || []; + const addItem = (rowIndex: number, cellIndex: number, item: PreparedItem): void => { + resultItems[rowIndex] = resultItems[rowIndex] ?? []; + const row = resultItems[rowIndex]; row[cellIndex] = item; if (item.colspan > 1 || item.rowspan > 1) { const clone = cloneItem(item); - for (let c = 1; c < item.colspan; c++) { + for (let c = 1; c < item.colspan; c += 1) { addItem(rowIndex, cellIndex + c, clone); } - for (let r = 1; r < item.rowspan; r++) { - for (let c = 0; c < item.colspan; c++) { + for (let r = 1; r < item.rowspan; r += 1) { + for (let c = 0; c < item.colspan; c += 1) { addItem(rowIndex + r, cellIndex + c, clone); } } @@ -48,20 +66,19 @@ function prepareItems(items, emptyCell) { let rowIndex = 0; while (item) { - for (let cellIndex = 0; cellIndex < cols; cellIndex++) { + for (let cellIndex = 0; cellIndex < cols; cellIndex += 1) { if (!item) { break; } - if (resultItems[rowIndex] && resultItems[rowIndex][cellIndex]) { - continue; - } - addItem(rowIndex, cellIndex, item); + if (!resultItems[rowIndex]?.[cellIndex]) { + addItem(rowIndex, cellIndex, item); - cellIndex += item.colspan - 1; + cellIndex += item.colspan - 1; - item = getItem(); + item = getItem(); + } } - rowIndex++; + rowIndex += 1; } return resultItems; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/aggregateCalculator.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/aggregateCalculator.tests.js index edd3ca9a138f..9380ed280e5b 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/aggregateCalculator.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/aggregateCalculator.tests.js @@ -43,11 +43,11 @@ function createHierarchicalData() { items: [ { key: '1.1', - items: [4, 6] + items: [{ value: 4 }, { value: 6 }] }, { key: '1.2', - items: [5, 4] + items: [{ value: 5 }, { value: 4 }] } ] }, @@ -56,7 +56,7 @@ function createHierarchicalData() { items: [ { key: '2.1', - items: [2, 3] + items: [{ value: 2 }, { value: 3 }] } ] } @@ -65,14 +65,14 @@ function createHierarchicalData() { QUnit.test('total aggregates for plain list', function(assert) { const calculator = new AggregateCalculator({ - data: [1, 2, 3], + data: [{ value: 1 }, { value: 2 }, { value: 3 }], totalAggregates: [ { aggregator: 'count' }, - { aggregator: 'sum', selector: 'this' }, - { aggregator: 'max', selector: 'this' }, - { aggregator: 'min', selector: 'this' }, - { aggregator: 'avg', selector: 'this' }, - { aggregator: customAggregator, selector: 'this' } + { aggregator: 'sum', selector: 'value' }, + { aggregator: 'max', selector: 'value' }, + { aggregator: 'min', selector: 'value' }, + { aggregator: 'avg', selector: 'value' }, + { aggregator: customAggregator, selector: 'value' } ], groupAggregates: [], groupLevel: 0 @@ -87,7 +87,7 @@ QUnit.test('total aggregates for grouped list', function(assert) { const calculator = new AggregateCalculator({ totalAggregates: [ { aggregator: 'count' }, - { aggregator: 'avg', selector: 'this' } + { aggregator: 'avg', selector: 'value' } ], groupAggregates: [], data: createHierarchicalData(), @@ -106,12 +106,12 @@ QUnit.test('group aggregates', function(assert) { totalAggregates: [], groupAggregates: [ { aggregator: 'count' }, - { aggregator: 'sum', selector: 'this' }, - { aggregator: 'max', selector: 'this' }, - { aggregator: 'min', selector: 'this' }, - { aggregator: 'avg', selector: 'this' }, - { aggregator: customAggregator, selector: 'this' }, - { aggregator: customAggregatorForSecondGroup, selector: 'this' } + { aggregator: 'sum', selector: 'value' }, + { aggregator: 'max', selector: 'value' }, + { aggregator: 'min', selector: 'value' }, + { aggregator: 'avg', selector: 'value' }, + { aggregator: customAggregator, selector: 'value' }, + { aggregator: customAggregatorForSecondGroup, selector: 'value' } ], groupLevel: 2 }); @@ -165,7 +165,7 @@ QUnit.test('exception in case of incorrect aggregator name', function(assert) { assert.throws( function() { new AggregateCalculator({ - data: [1], + data: [{ value: 1 }], totalAggregates: [{ aggregator: 'Avg' }], groupAggregates: [], groupLevel: 0 @@ -183,10 +183,10 @@ QUnit.test('total aggregates for empty list', function(assert) { data: [], totalAggregates: [ { aggregator: 'count' }, - { aggregator: 'sum', selector: 'this' }, - { aggregator: 'max', selector: 'this' }, - { aggregator: 'min', selector: 'this' }, - { aggregator: 'avg', selector: 'this' } + { aggregator: 'sum', selector: 'value' }, + { aggregator: 'max', selector: 'value' }, + { aggregator: 'min', selector: 'value' }, + { aggregator: 'avg', selector: 'value' } ], groupAggregates: [], groupLevel: 0 @@ -208,10 +208,10 @@ QUnit.test('group aggregates for empty list', function(assert) { totalAggregates: [], groupAggregates: [ { aggregator: 'count' }, - { aggregator: 'sum', selector: 'this' }, - { aggregator: 'max', selector: 'this' }, - { aggregator: 'min', selector: 'this' }, - { aggregator: 'avg', selector: 'this' } + { aggregator: 'sum', selector: 'value' }, + { aggregator: 'max', selector: 'value' }, + { aggregator: 'min', selector: 'value' }, + { aggregator: 'avg', selector: 'value' } ], groupLevel: 1 }); @@ -360,8 +360,8 @@ QUnit.test('global variables (see T353923)', function(assert) { data: data, totalAggregates: [], groupAggregates: [ - { aggregator: customAggregator, selector: 'this' }, - { aggregator: customAggregatorWithGlobal, selector: 'this' } + { aggregator: customAggregator, selector: 'value' }, + { aggregator: customAggregatorWithGlobal, selector: 'value' } ], groupLevel: 2 });