Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const getFilterSuccessMessage = async (
filterValue: FilterExprArray,
): Promise<string> => {
try {
const customOperations = component.getController('filterSync').getCustomFilterOperations();
const customOperations = component.getController('filterBuilder').getCustomFilterOperations();
const filterText: string = await when(
component.getView('filterPanelView').getFilterText(filterValue, customOperations),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from '@jest/globals';
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';
import {
afterTest,
beforeTest,
createDataGrid,
toPlainFilter,
} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';

const DATA = [
{ id: 1, name: 'Alex' },
{ id: 2, name: 'Dan' },
];

const GRID_OPTIONS: DataGridProperties = {
dataSource: DATA,
filterPanel: { visible: true },
filterSyncEnabled: true,
columns: ['name'],
};

// A column filter is only visible to the column sources while `filterValue` stays empty,
// and the sync keeps those two in step. Setting the filter with the sync suppressed is
// the one way to reach that state.
const createGridWithFilterRowValue = async (): Promise<DataGridInstance> => {
const { instance } = await createDataGrid(GRID_OPTIONS);

instance.getController('filterSync').withColumnOptionsSync(() => {
instance.columnOption('name', 'filterValue', 'Alex');
});

return instance;
};

describe('FilterController.suspendColumnSources', () => {
beforeEach(beforeTest);
afterEach(afterTest);

describe('when a callback runs', () => {
it('should skip the column sources and resume them afterwards', async () => {
const instance = await createGridWithFilterRowValue();
const filterController = instance.getController('filter');

filterController.suspendColumnSources(() => {
expect(filterController.getAdditionalFilter()).toBeUndefined();
});

expect(toPlainFilter(filterController.getAdditionalFilter())).toEqual(['name', 'contains', 'Alex']);
});

it('should return the callback result', async () => {
const instance = await createGridWithFilterRowValue();
const filterController = instance.getController('filter');

expect(filterController.suspendColumnSources(() => 'result')).toBe('result');
});
});

describe('when a nested call completes', () => {
it('should keep the column sources suspended for the outer call', async () => {
const instance = await createGridWithFilterRowValue();
const filterController = instance.getController('filter');

filterController.suspendColumnSources(() => {
filterController.suspendColumnSources(() => undefined);

expect(filterController.getAdditionalFilter()).toBeUndefined();
});

expect(toPlainFilter(filterController.getAdditionalFilter())).toEqual(['name', 'contains', 'Alex']);
});
});

describe('when the callback throws', () => {
it('should rethrow the error', async () => {
const instance = await createGridWithFilterRowValue();
const filterController = instance.getController('filter');

expect(() => filterController.suspendColumnSources(() => {
throw new Error('callback failed');
})).toThrow('callback failed');
});

it('should resume the column sources', async () => {
const instance = await createGridWithFilterRowValue();
const filterController = instance.getController('filter');

expect(() => filterController.suspendColumnSources(() => {
throw new Error('callback failed');
})).toThrow('callback failed');

expect(toPlainFilter(filterController.getAdditionalFilter())).toEqual(['name', 'contains', 'Alex']);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SOURCE_ORDER = ['applyFilter', 'headerFilter', 'searchPanel', 'filterBuilder'] as const;
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { LangParams } from '@js/common/data';
import config from '@js/core/config';
import { extend } from '@js/core/utils/extend';
import { isFunction, isString } from '@js/core/utils/type';
import { isDefined, isFunction, isString } from '@js/core/utils/type';
import type { Column } from '@ts/grids/grid_core/columns_controller/types';
import type { DataFilter } from '@ts/grids/grid_core/filter/types';
import modules from '@ts/grids/grid_core/m_modules';
import type { Controllers } from '@ts/grids/grid_core/m_types';

import { SOURCE_ORDER } from './const';
import type { FilterSourceContext } from './types';
import { combineFilters } from './utils';

type TaggedFilter = unknown[] & {
columnIndex?: number;
filterValue?: unknown;
Expand All @@ -18,27 +22,43 @@ export class FilterController extends modules.Controller {

protected dataSourceController!: Controllers['dataSource'];

private columnSourcesSuspended = false;

public init(): void {
this.columnsController = this.getController('columns');
this.dataSourceController = this.getController('dataSource');
}

public isFilterSyncActive(): boolean | undefined {
public isFilterSyncActive(): boolean {
const filterSyncEnabled = this.option('filterSyncEnabled');

return filterSyncEnabled === 'auto' ? this.option('filterPanel.visible') : filterSyncEnabled;
return filterSyncEnabled === 'auto' ? !!this.option('filterPanel.visible') : !!filterSyncEnabled;
}

protected getLangParams(): LangParams | undefined {
return this.dataSourceController.getDataSource()?.loadOptions?.()?.langParams;
public suspendColumnSources<T>(callback: () => T): T {
const wasSuspended = this.columnSourcesSuspended;

this.columnSourcesSuspended = true;

try {
return callback();
} finally {
this.columnSourcesSuspended = wasSuspended;
}
}

/**
* @extended: filter_row, filter_sync, header_filter, search
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getAdditionalFilter(excludedColumn?: Column | null): DataFilter {
return null;
const context = this.createSourceContext(excludedColumn);

return SOURCE_ORDER.reduce<DataFilter>((filter, sourceName) => {
const source = this.getController(sourceName);

if (!source?.isFilterSourceActive(context)) {
return filter;
}

return combineFilters([filter, ...source.getFilterExpressions(context)]);
}, null);
}

public normalizeFilterSelectors(
Expand All @@ -50,6 +70,23 @@ export class FilterController extends modules.Controller {
return this.normalizeNode(filter, remoteFiltering, columnIndex, filterValue) as DataFilter;
}

private createSourceContext(excludedColumn?: Column | null): FilterSourceContext {
const filterSyncActive = this.isFilterSyncActive();

return {
langParams: this.getLangParams(),
excludedColumn: excludedColumn ?? null,
filterSyncActive,
columnSourcesActive: !filterSyncActive
|| (!isDefined(this.option('filterValue')) && !this.columnSourcesSuspended),
columnsController: this.columnsController,
};
}

private getLangParams(): LangParams | undefined {
return this.dataSourceController?.getDataSource()?.loadOptions?.()?.langParams;
}

private normalizeNode(
node: unknown,
remoteFiltering: boolean | undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { LangParams } from '@js/common/data';
import type { SearchOperation } from '@js/common/data.types';
import type { ScalarFilterValue } from '@js/common/grids';
import type { Column } from '@ts/grids/grid_core/columns_controller/types';
import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types';
import type { Controllers } from '@ts/grids/grid_core/m_types';

import type { SOURCE_ORDER } from './const';

export type FilterCombiner = 'and' | 'or';

Expand Down Expand Up @@ -47,3 +52,13 @@ export type FilterValueExpression = FilterValueCondition
| [FilterValueExpression, ...(FilterCombiner | FilterValueExpression)[]];

export type FilterValue = FilterValueExpression | null | undefined;

export interface FilterSourceContext {
readonly langParams: LangParams | undefined;
readonly excludedColumn: Column | null;
readonly filterSyncActive: boolean;
readonly columnSourcesActive: boolean;
readonly columnsController: Controllers['columns'];
}

export type FilterSourceName = typeof SOURCE_ORDER[number];
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from '@jest/globals';
import type { CustomOperation } from '@js/ui/filter_builder';
import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';
import {
afterTest,
beforeTest,
createDataGrid,
} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';

const DATA = [
{ id: 1, name: 'Alex', age: 15 },
{ id: 2, name: 'Dan', age: 20 },
];

const CUSTOM_OPERATION: CustomOperation = {
name: 'isEven',
caption: 'Is even',
dataTypes: ['number'],
hasValue: false,
calculateFilterExpression: () => [['age', '%', 2], '=', 0],
};

const createGrid = (
customOperations?: CustomOperation[],
): Promise<{ instance: DataGridInstance }> => createDataGrid({
dataSource: DATA,
columns: ['name', 'age'],
filterPanel: { visible: true },
filterBuilder: customOperations ? { customOperations } : {},
});

const getOperationNames = (operations: CustomOperation[]): (string | undefined)[] => operations
.map((operation) => operation.name);

describe('FilterBuilderController.getCustomFilterOperations', () => {
beforeEach(beforeTest);
afterEach(afterTest);

describe('when no custom operation is specified', () => {
it('should return the built-in operations only', async () => {
const { instance } = await createGrid();

const operations = instance.getController('filterBuilder').getCustomFilterOperations();

expect(getOperationNames(operations)).toEqual(['anyof', 'noneof']);
});
});

describe('when filterBuilder.customOperations is specified', () => {
it('should add them after the built-in operations', async () => {
const { instance } = await createGrid([CUSTOM_OPERATION]);

const operations = instance.getController('filterBuilder').getCustomFilterOperations();

expect(getOperationNames(operations)).toEqual(['anyof', 'noneof', 'isEven']);
});
});

describe('when the method is called on the component', () => {
it('should be available as a public method', async () => {
const { instance } = await createGrid([CUSTOM_OPERATION]);

const operations = (instance as unknown as {
getCustomFilterOperations: () => CustomOperation[];
}).getCustomFilterOperations();

expect(getOperationNames(operations)).toEqual(['anyof', 'noneof', 'isEven']);
});
});
});
Loading
Loading