Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,52 @@ import { isFunction } from '@js/core/utils/type';

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.
// Rows can be primitives (e.g. a store of numbers with the 'this' selector)
interface AggregateNode {
Comment thread
anna-shakhova marked this conversation as resolved.
items: unknown[];
aggregates?: unknown[];
}

interface Aggregator {
seed?: number | unknown[] | ((groupIndex?: number) => unknown);
step: (accumulator: unknown, value: unknown) => unknown;
finalize?: (accumulator: unknown) => unknown;
}

interface NormalizedAggregate {
selector: (data: unknown) => 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: unknown) => 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;
Expand All @@ -59,32 +65,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: unknown[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get better type from usage and docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By checking usage of _data in source code its type should be: RawItemData[] | GroupData<RawItemData>[]

However, I have chosen to type unknown instead, because of this reasons:

  1. Data items are just passed to handler: aggregate.selector(item) to extract needed value and their props are never directly read. Only the .items prop is read if data item is actually a group item: line of code

  2. aggregateCalculator.tests.ts and dataSource.tests.ts have tests in which _data has number type. Defining type of _data would also require us to update these tests. (This is doable actually, not a lot of such tests)

I'm OK with both approaches. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already typed data in calculateTotalAggregates that calls constructor of that class, the only issue is tests then, we can add ts-expect-error directive to tests, if there are too many of them, but if not better fix


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: unknown[];
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 = [];
}

Expand All @@ -102,31 +109,37 @@ export default class AggregateCalculator {
return this._totals;
}

private _aggregate(aggregates, data, container) {
private _aggregate(
aggregates: NormalizedAggregate[],
data: AggregateNode,
container: unknown[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get better type from usage and docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think unknown is the accurate type here, because custom accumulator can return any value. We would need to define a broad type: number | string | object | etc for it, but it wouldn't be useful anyways, because grid already treats summary results as unknowns.

): 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++) {
for (let j = 0; j < length; j += 1) {
this._accumulate(i, aggregates[i], container, data.items[j]);
}
}
}

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);
}
}

Expand All @@ -135,45 +148,54 @@ export default class AggregateCalculator {
}
}

private _calculateGroups(root) {
private _calculateGroups(root: AggregateNode): void {
const maxLevel = this._groupLevel;
let currentLevel = maxLevel + 1;

const seedFn = this._seed.bind(this, this._groupAggregates);
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: unknown,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we get better types from usage and docs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

results is the same variable as container in this comment: #35464 (comment)
item depends on this resolution of this comment: #35464 (comment)

): void {
const value = aggregate.selector(item);
const { aggregator } = aggregate;
const { skipEmptyValues } = aggregate;
Expand All @@ -182,15 +204,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])
Expand Down
11 changes: 8 additions & 3 deletions packages/devextreme/js/__internal/grids/data_grid/m_utils.ts
Original file line number Diff line number Diff line change
@@ -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<StoreLoadOptions, 'group' | 'filter'>,
): DataFilter {
const groups = normalizeSortingInfo(storeLoadOptions.group);

const filter: any = [];
const filter: unknown[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use here some type from our existing filter types or get one from usage and docs?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing filter types don't fit here. The group selector can be a function (e.g. calculateGroupValue), and a group key can be any value. DataFilter expects a string selector and a scalar value, so DataFilter[] fails with TS2345. The public FilterDescriptor is just any. combineFilters takes unknown[] anyway, so I'd keep unknown[] here.


for (let i = 0; i < path.length; i++) {
for (let i = 0; i < path.length; i += 1) {
filter.push([groups[i].selector, '=', path[i]]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -349,7 +350,7 @@ export const summaryDataSourceAdapterExtender = (
}

private calculateTotalAggregates(
data,
data: RawItemData[] | GroupData<RawItemData>[],
totalAggregates: Aggregate[],
groupAggregates: Aggregate[],
groupLevel: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Comment thread
Copilot marked this conversation as resolved.
Outdated

interface KeyDownArgs {
event: unknown;
Comment thread
Tucchhaa marked this conversation as resolved.
Outdated
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,
);
};
Loading
Loading