Skip to content

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { dxElementWrapper } from '@js/core/renderer';
import type { Callback } from '@js/core/utils/callbacks';
import type { DeferredObj } from '@js/core/utils/deferred';
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
import type { StoreLoadOptions } from '@ts/data/data_source/types';
import type { SortingInfo } from '@ts/data/utils';
import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types';

import type { DataFilter } from '../filter/types';
Expand Down Expand Up @@ -154,14 +156,29 @@ export interface ColumnDataSourceParameter {
isExpanded?: boolean;
}

export type SortParameters = StoreLoadOptions['sort']
Comment thread
anna-shakhova marked this conversation as resolved.
| ColumnDataSourceParameter[]
| SortingInfo[]
| null;

export type GroupParameters = StoreLoadOptions['group']
| ColumnDataSourceParameter[]
| SortingInfo[]
| null;

export interface ColumnsDataSourceParameters {
sorting: unknown;
grouping: unknown;
sorting: SortParameters;
grouping: GroupParameters;
filtering?: DataFilter;
}

export type GroupColumn = Column & { groupIndex: number };

export type DisplayValueColumn = Pick<
Column,
'displayValueMap' | 'calculateDisplayValue' | 'type' | 'calculateGroupValue'
> & { lookup?: Pick<NonNullable<Column['lookup']>, 'calculateCellValue'> };

export interface ColumnsChanges {
changeTypes: {
sorting?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { describe, expect, it } from '@jest/globals';
import {
describe,
expect,
it,
jest,
} from '@jest/globals';
import filterUtils from '@js/ui/shared/filtering';

import gridCoreUtils from '../../m_utils';
import { getFormatOptions } from '../m_header_filter';
import type { HeaderFilterGroupSelector } from '../types';
import { getHeaderFilterGroupParameters } from '../utils';

describe('getGroupInterval', () => {
describe('headerFilter.groupInterval normalization', () => {
Expand Down Expand Up @@ -66,47 +72,107 @@ describe('getGroupInterval', () => {
});
});

type HeaderFilterColumn = Parameters<typeof getHeaderFilterGroupParameters>[0];

const asHeaderFilterColumn = (column: object): HeaderFilterColumn => column as HeaderFilterColumn;

describe('getHeaderFilterGroupParameters', () => {
it('builds one remote group parameter per level, expanding all but the last', () => {
const column = {
const column = asHeaderFilterColumn({
dataField: 'Price', dataType: 'number', headerFilter: { groupInterval: [100, 10] },
};
});

expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([
expect(getHeaderFilterGroupParameters(column, true)).toEqual([
{ selector: 'Price', groupInterval: 100, isExpanded: true },
{ selector: 'Price', groupInterval: 10, isExpanded: false },
]);
});

it('supports an arbitrary number of levels', () => {
const column = {
const column = asHeaderFilterColumn({
dataField: 'Population',
dataType: 'number',
headerFilter: { groupInterval: [100000, 10000, 1000] },
};
});

expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([
expect(getHeaderFilterGroupParameters(column, true)).toEqual([
{ selector: 'Population', groupInterval: 100000, isExpanded: true },
{ selector: 'Population', groupInterval: 10000, isExpanded: true },
{ selector: 'Population', groupInterval: 1000, isExpanded: false },
]);
});

it('builds a local interval selector per level that buckets the cell value', () => {
const column = {
const column = asHeaderFilterColumn({
dataField: 'Price',
dataType: 'number',
headerFilter: { groupInterval: [100, 10] },
calculateCellValue: (data) => data.Price,
};
calculateCellValue: (data: { Price: number }) => data.Price,
});

const params = gridCoreUtils.getHeaderFilterGroupParameters(column);
const params = getHeaderFilterGroupParameters(
column,
) as HeaderFilterGroupSelector[];

expect(params).toHaveLength(2);
// 753 -> floor(753 / 100) * 100 on the outer level, floor(753 / 10) * 10 on the inner level
expect(params[0]({ Price: 753 })).toBe(700);
expect(params[1]({ Price: 753 })).toBe(750);
});

describe('when the column has no group interval', () => {
it('should group remotely by the data field', () => {
const column = asHeaderFilterColumn({ dataField: 'Name', dataType: 'string' });

expect(getHeaderFilterGroupParameters(column, true)).toEqual([
{ selector: 'Name', isExpanded: false },
]);
});

it('should group remotely by the column name when there is no data field', () => {
const column = asHeaderFilterColumn({ name: 'nameColumn', dataType: 'string' });

expect(getHeaderFilterGroupParameters(column, true)).toEqual([
{ selector: 'nameColumn', isExpanded: false },
]);
});

it('should return a local selector that turns empty values into null', () => {
const column = asHeaderFilterColumn({
dataField: 'Name',
dataType: 'string',
calculateCellValue: (data: { Name?: unknown }) => data.Name,
});

const selector = getHeaderFilterGroupParameters(
column,
) as HeaderFilterGroupSelector;

expect(selector({ Name: 'a' })).toBe('a');
expect(selector({ Name: 0 })).toBe(0);
expect(selector({ Name: '' })).toBeNull();
expect(selector({})).toBeNull();
});

it('should pair the local selector with the bound sorting method', () => {
const sortingMethod = jest.fn((value1: number, value2: number) => value1 - value2);
const column = asHeaderFilterColumn({
dataField: 'Name',
dataType: 'string',
calculateCellValue: (data: { Name?: unknown }) => data.Name,
sortingMethod,
});

const [descriptor] = getHeaderFilterGroupParameters(column) as {
selector: HeaderFilterGroupSelector;
compare: (value1: unknown, value2: unknown) => number;
}[];

expect(descriptor.selector({ Name: '' })).toBeNull();
expect(descriptor.compare(3, 1)).toBe(2);
expect(sortingMethod.mock.contexts[0]).toBe(column);
});
});
});

describe('getFormatOptions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import {
HeaderFilterView,
updateHeaderFilterItemSelectionState,
} from './m_header_filter_core';
import { allowHeaderFiltering, createHeaderFilterExpressions } from './utils';
import {
allowHeaderFiltering,
createHeaderFilterExpressions,
getHeaderFilterGroupParameters,
} from './utils';

const DATE_INTERVAL_FORMATS = {
year(value) {
Expand Down Expand Up @@ -83,7 +87,7 @@ function ungroupUTCDates(items, dateParts?, dates?) {
export function convertDataFromUTCToLocal(data, column) {
const dates = ungroupUTCDates(data);
const query = dataQuery(dates);
const group = gridCoreUtils.getHeaderFilterGroupParameters({
const group = getHeaderFilterGroupParameters({
...column,
calculateCellValue: (date) => date,
});
Expand Down Expand Up @@ -251,7 +255,7 @@ export class HeaderFilterController extends Modules.ViewController {
public getDataSource(column) {
const dataSourceAdapter = this.dataSourceController.getAdapter();
const remoteGrouping = this.dataSourceController.remoteOperations().grouping;
const group = gridCoreUtils.getHeaderFilterGroupParameters(column, remoteGrouping);
const group = getHeaderFilterGroupParameters(column, remoteGrouping);
const headerFilterDataSource = column.headerFilter?.dataSource;
const headerFilterOptions = this.option('headerFilter');
let isLookup = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type EmptyDateValue = '' | false;

export type HeaderFilterGroupSelector = (data: unknown) => unknown;

export interface HeaderFilterGroupDescriptor {
selector: string | HeaderFilterGroupSelector | undefined;
groupInterval?: string | number;
isExpanded?: boolean;
compare?: (value1: unknown, value2: unknown) => number;
}

export type HeaderFilterGroupItem = HeaderFilterGroupDescriptor | HeaderFilterGroupSelector;

export type HeaderFilterGroup = HeaderFilterGroupSelector | HeaderFilterGroupItem[];
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { HeaderFilterGroupInterval } from '@js/common/grids';
import { each } from '@js/core/utils/iterator';
import { isDefined, isString } from '@js/core/utils/type';
import sharedFiltering from '@js/ui/shared/filtering';
import type { Column } from '@ts/grids/grid_core/columns_controller/types';
import type { DataFilter } from '@ts/grids/grid_core/filter/types';
import { combineFilters, isColumnExcluded } from '@ts/grids/grid_core/filter/utils';
import gridCoreUtils from '@ts/grids/grid_core/m_utils';

import type { EmptyDateValue, HeaderFilterGroup, HeaderFilterGroupItem } from './types';

export const allowHeaderFiltering = (
column: Column,
): boolean | undefined => column.allowHeaderFiltering ?? column.allowFiltering;
Expand Down Expand Up @@ -62,3 +68,92 @@ export const createHeaderFilterExpressions = (
&& hasHeaderFilterValues(column)
&& !isColumnExcluded(column, excludedColumn))
.map(createHeaderFilterExpression);

const DATE_INTERVAL_SELECTORS = {
year(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && value.getFullYear();
},
month(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && (value.getMonth() + 1);
},
day(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && value.getDate();
},
quarter(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && (Math.floor(value.getMonth() / 3) + 1);
},
hour(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && value.getHours();
},
minute(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && value.getMinutes();
},
second(value: Date | EmptyDateValue): number | EmptyDateValue {
return value && value.getSeconds();
},
};

const getIntervalSelector = function getIntervalSelector(
this: Column & Required<Pick<Column, 'calculateCellValue'>>,
interval: HeaderFilterGroupInterval | number,
data: unknown,
): number | EmptyDateValue | null | undefined {
const value = this.calculateCellValue(data);

if (!isDefined(value)) {
return null;
}

if (gridCoreUtils.isDateType(this.dataType) && isString(interval)) {
return DATE_INTERVAL_SELECTORS[interval](value);
}

if (this.dataType === 'number') {
const groupInterval = Number(interval);
return Math.floor(Number(value) / groupInterval) * groupInterval;
}

return undefined;
};

export function getHeaderFilterGroupParameters(
column: Column & Required<Pick<Column, 'calculateCellValue'>>,
remoteGrouping?: boolean,
): HeaderFilterGroup {
const dataField = column.dataField || column.name;
const groupInterval = sharedFiltering.getGroupInterval(column);

if (groupInterval) {
const result: HeaderFilterGroupItem[] = [];

each(groupInterval, (index, interval) => {
result.push(remoteGrouping ? {
selector: dataField,
groupInterval: interval,
isExpanded: index < groupInterval.length - 1,
// @ts-expect-error getGroupInterval() types interval names and numbers as string[]
} : getIntervalSelector.bind(column, interval));
});

return result;
}

if (remoteGrouping) {
return [{ selector: dataField, isExpanded: false }];
}

const selector = (data: unknown): unknown => {
const cellValue = column.calculateCellValue(data);

if (cellValue === undefined || cellValue === '') {
return null;
}
return cellValue;
};

if (column.sortingMethod) {
return [{ selector, compare: column.sortingMethod.bind(column) }];
}

return selector;
}
Loading
Loading