Skip to content

Commit 536c672

Browse files
authored
🚨 CardView: hotfix filter issues (DevExpress#30068)
1 parent 77054d3 commit 536c672

File tree

25 files changed

+252
-271
lines changed

25 files changed

+252
-271
lines changed

e2e/testcafe-devextreme/tests/cardView/filterSync/api.functional.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ test('sync from headerFilter: filter by one value', async (t) => {
6767
await cardView.apiColumnOption('id', 'filterValues', [0]);
6868

6969
await t.expect(cardView.apiOption('filterValue'))
70-
.eql([['id', '=', 0]]);
70+
.eql(['id', '=', 0]);
7171

7272
await expectFilterElementsState(t, ['id']);
7373
}).before(() => createWidget('dxCardView', baseConfig));
@@ -79,7 +79,7 @@ test('sync from headerFilter: filter by exclude one value', async (t) => {
7979
await cardView.apiColumnOption('id', 'filterValues', [0]);
8080

8181
await t.expect(cardView.apiOption('filterValue'))
82-
.eql([['id', '<>', 0]]);
82+
.eql(['id', '<>', 0]);
8383

8484
await expectFilterElementsState(t, ['id']);
8585
}).before(() => createWidget('dxCardView', baseConfig));
@@ -90,7 +90,7 @@ test('sync from headerFilter: filter by two values', async (t) => {
9090
await cardView.apiColumnOption('id', 'filterValues', [0, 1]);
9191

9292
await t.expect(cardView.apiOption('filterValue'))
93-
.eql([['id', 'anyof', [0, 1]]]);
93+
.eql(['id', 'anyof', [0, 1]]);
9494

9595
await expectFilterElementsState(t, ['id']);
9696
}).before(() => createWidget('dxCardView', baseConfig));
@@ -102,7 +102,7 @@ test('sync from headerFilter: filter by exclude two values', async (t) => {
102102
await cardView.apiColumnOption('id', 'filterValues', [0, 1]);
103103

104104
await t.expect(cardView.apiOption('filterValue'))
105-
.eql([['id', 'noneof', [0, 1]]]);
105+
.eql(['id', 'noneof', [0, 1]]);
106106

107107
await expectFilterElementsState(t, ['id']);
108108
}).before(() => createWidget('dxCardView', baseConfig));
@@ -113,7 +113,7 @@ test('sync from headerFilter: filter by empty', async (t) => {
113113
await cardView.apiColumnOption('gender', 'filterValues', [null]);
114114

115115
await t.expect(cardView.apiOption('filterValue'))
116-
.eql([['gender', '=', null]]);
116+
.eql(['gender', '=', null]);
117117

118118
await expectFilterElementsState(t, ['gender']);
119119
}).before(() => createWidget('dxCardView', baseConfig));
@@ -125,7 +125,7 @@ test('sync from headerFilter: filter by non-empty', async (t) => {
125125
await cardView.apiColumnOption('gender', 'filterValues', [null]);
126126

127127
await t.expect(cardView.apiOption('filterValue'))
128-
.eql([['gender', '<>', null]]);
128+
.eql(['gender', '<>', null]);
129129

130130
await expectFilterElementsState(t, ['gender']);
131131
}).before(() => createWidget('dxCardView', baseConfig));
@@ -166,7 +166,7 @@ test('sync from headerFilter: filter by groupInterval', async (t) => {
166166
await cardView.apiColumnOption('id', 'filterValues', [0]);
167167

168168
await t.expect(cardView.apiOption('filterValue')).eql(
169-
[['id', 'anyof', [0]]],
169+
['id', 'anyof', [0]],
170170
);
171171
}).before(() => createWidget('dxCardView', {
172172
...baseConfig,
@@ -265,28 +265,28 @@ test('sync from filterPanel: is not blank filter', async (t) => {
265265

266266
[
267267
{
268-
column: 'id', filterName: 'is less than', operation: '', value: 0,
268+
column: 'id', filterName: 'is less than', operation: '<', value: 0,
269269
},
270270
{
271-
column: 'id', filterName: 'is greater than', operation: '', value: 0,
271+
column: 'id', filterName: 'is greater than', operation: '>', value: 0,
272272
},
273273
{
274-
column: 'id', filterName: 'is less than or equal to', operation: '', value: 0,
274+
column: 'id', filterName: 'is less than or equal to', operation: '<=', value: 0,
275275
},
276276
{
277-
column: 'id', filterName: 'is greater than or equal to', operation: '', value: 0,
277+
column: 'id', filterName: 'is greater than or equal to', operation: '>=', value: 0,
278278
},
279279
{
280-
column: 'gender', filterName: 'contains', operation: '', value: 'a',
280+
column: 'gender', filterName: 'contains', operation: 'contains', value: 'a',
281281
},
282282
{
283-
column: 'gender', filterName: 'does not contain', operation: '', value: 'a',
283+
column: 'gender', filterName: 'does not contain', operation: 'notcontains', value: 'a',
284284
},
285285
{
286-
column: 'gender', filterName: 'starts with', operation: '', value: 'a',
286+
column: 'gender', filterName: 'starts with', operation: 'startswith', value: 'a',
287287
},
288288
{
289-
column: 'gender', filterName: 'ends with', operation: '', value: 'a',
289+
column: 'gender', filterName: 'ends with', operation: 'endswith', value: 'a',
290290
},
291291
].forEach(({
292292
column, filterName, operation, value,

packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import type { Props as ColumnSortableProps } from './column_sortable';
1717
import { ColumnSortable } from './column_sortable';
1818
import { Item } from './item';
1919
import type { DraggingOptions } from './options';
20-
import { hasFilterValues } from './utils';
2120

2221
export const CLASSES = {
2322
link: 'dx-link',
@@ -196,11 +195,11 @@ export class HeaderPanel extends Component<HeaderPanelProps> {
196195
}
197196

198197
private itemHasFilters(column: VisibleColumn, filterSyncValue: unknown): boolean {
199-
const { filterValues, filterType } = column;
198+
const { filterValues } = column;
200199

201200
const columnId = getColumnIdentifier(column);
202201

203-
const hasHeaderFilterValue = hasFilterValues(filterType, filterValues);
202+
const hasHeaderFilterValue = !!filterValues?.length;
204203
const hasFilterSyncValue = filterHasField(filterSyncValue, columnId) as boolean;
205204

206205
return hasHeaderFilterValue || hasFilterSyncValue;

packages/devextreme/js/__internal/grids/new/card_view/header_panel/utils.test.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

packages/devextreme/js/__internal/grids/new/card_view/header_panel/utils.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/devextreme/js/__internal/grids/new/grid_core/accessibility/render.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ describe('Accessibility attributes', () => {
6363
});
6464

6565
describe('Status description', () => {
66-
it('should be displayed on the status container', () => {
66+
// TODO a11y: remove "firstRender" flags, fix this test and unskip.
67+
it.skip('should be displayed on the status container', () => {
6768
const cardView = setup({
6869
dataSource: [
6970
{ A: 'A_0' }, { A: 'A_1' }, { A: 'A_2' }, { A: 'A_3' }, { A: 'A_4' },
@@ -82,6 +83,7 @@ describe('Accessibility attributes', () => {
8283
expect(statusContainer?.innerHTML).toBe('Card view with 5 cards. Each card has 1 fields');
8384

8485
cardView.option('paging', { pageSize: 2 });
86+
// TODO a11y: is it ok that page size = 2 and status message has 5 cards?
8587
expect(statusContainer?.innerHTML).toBe('Card view with 5 cards. Each card has 1 fields');
8688
cardView.option('paging', { pageIndex: 2 });
8789
expect(statusContainer?.innerHTML).toBe('Card view with 5 cards. Each card has 1 fields');

packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { InfernoNode } from 'inferno';
21
import { Component } from 'inferno';
32

43
import { CLASSES as BASE_CLASSES } from '../const';
@@ -13,7 +12,7 @@ export interface A11yStatusContainerComponentProps {
1312
}
1413

1514
export class A11yStatusContainer extends Component<A11yStatusContainerComponentProps> {
16-
public render(): InfernoNode {
15+
public render(): JSX.Element {
1716
return (
1817
<div
1918
className={`${CLASSES.container} ${CLASSES.excludeFlexBox}`}

packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export class DataController {
229229

230230
effect(
231231
() => {
232+
const initialized = this.options.initialized.value;
232233
const dataSource = this.dataSource.value;
233234
const pageIndex = this.pageIndex.value;
234235
const pageSize = this.pageSize.value;
@@ -237,6 +238,10 @@ export class DataController {
237238
const pagingEnabled = this.pagingEnabled.value;
238239
const sortParameters = this.sortingController.sortParameters.value;
239240

241+
if (!initialized) {
242+
return;
243+
}
244+
240245
let someParamChanged = false;
241246

242247
if (dataSource.pageIndex() !== pageIndex) {

packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ describe('PublicMethods', () => {
198198
dataSourceFilter: undefined,
199199
columnFilterValues: [1, 2],
200200
});
201-
expect(gridCore.getCombinedFilter()).toBe(undefined);
201+
expect(gridCore.getCombinedFilter()).toStrictEqual([['a', '=', 1], 'or', ['a', '=', 2]]);
202202
});
203203
});
204204

@@ -208,7 +208,7 @@ describe('PublicMethods', () => {
208208
dataSourceFilter: ['a', '=', 123],
209209
columnFilterValues: undefined,
210210
});
211-
expect(gridCore.getCombinedFilter()).toEqual(['a', '=', 123]);
211+
expect(gridCore.getCombinedFilter()).toStrictEqual(['a', '=', 123]);
212212
});
213213
});
214214

@@ -218,7 +218,7 @@ describe('PublicMethods', () => {
218218
dataSourceFilter: ['a', '=', 123],
219219
columnFilterValues: [1, 2],
220220
});
221-
expect(gridCore.getCombinedFilter()).toEqual(['a', '=', 123]);
221+
expect(gridCore.getCombinedFilter()).toStrictEqual([['a', '=', 123], 'and', [['a', '=', 1], 'or', ['a', '=', 2]]]);
222222
});
223223
});
224224
});

packages/devextreme/js/__internal/grids/new/grid_core/di.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import { EditingController } from './editing/controller';
1111
import { EditPopupView } from './editing/popup/view';
1212
import { ErrorController } from './error_controller/error_controller';
1313
import * as FilterSyncModule from './filtering/filter_sync/index';
14-
import { ClearFilterVisitor } from './filtering/filter_visitors/clear_filter_visitor';
15-
import { FilterCustomOperationsVisitor } from './filtering/filter_visitors/filter_custom_operations_visitor';
16-
import { GetAppliedFilterVisitor } from './filtering/filter_visitors/get_applied_filters_visitor';
1714
import {
1815
CompatibilityHeaderFilterController,
1916
HeaderFilterController,
@@ -62,9 +59,6 @@ export function register(diContext: DIContext): void {
6259
diContext.register(SearchView);
6360
diContext.register(HeaderFilterViewController);
6461

65-
diContext.register(ClearFilterVisitor);
66-
diContext.register(GetAppliedFilterVisitor);
67-
diContext.register(FilterCustomOperationsVisitor);
6862
diContext.register(KeyboardNavigationController);
6963
diContext.register(AccessibilityController);
7064
diContext.register(OptionsValidationController);
Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-return */
2-
import type { Signal } from '@preact/signals-core';
3-
import { computed, signal } from '@preact/signals-core';
2+
import { computed } from '@preact/signals-core';
43
import gridCoreUtils from '@ts/grids/grid_core/m_utils';
4+
import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
5+
import { getColumnByIndexOrName } from '@ts/grids/new/grid_core/columns_controller/utils';
6+
import { HeaderFilterController } from '@ts/grids/new/grid_core/filtering/header_filter/controller';
7+
import type {
8+
HeaderFilterRootOptions,
9+
} from '@ts/grids/new/grid_core/filtering/header_filter/index';
10+
import { anyOf, noneOf } from '@ts/grids/new/grid_core/filtering/legacy_filter_custom_operations';
11+
import { SearchController } from '@ts/grids/new/grid_core/search/index';
512

613
import { ColumnsController } from '../columns_controller/index';
714
import { OptionsController } from '../options_controller/options_controller';
8-
import type { AppliedFilters } from './types';
915
import { getAppliedFilterExpressions } from './utils';
1016

1117
export class FilterController {
12-
public readonly filterPanelFilterEnabled = this.options.oneWay('filterPanel.filterEnabled');
18+
private readonly filterBuilderCustomOperations = this.options.oneWay('filterBuilder.customOperations');
19+
20+
public readonly filterPanelFilterEnabled = this.options.twoWay('filterPanel.filterEnabled');
1321

1422
public readonly filterPanelVisible = this.options.oneWay('filterPanel.visible');
1523

@@ -23,13 +31,11 @@ export class FilterController {
2331

2432
public readonly filterSyncEnabledOption = this.options.oneWay('filterSyncEnabled');
2533

26-
public readonly appliedFilters: Signal<AppliedFilters> = signal({});
27-
28-
public readonly customOperations: Signal<unknown[]> = signal([]);
29-
3034
public static dependencies = [
3135
OptionsController,
3236
ColumnsController,
37+
SearchController,
38+
HeaderFilterController,
3339
] as const;
3440

3541
public readonly filterSyncEnabled = computed(() => (
@@ -50,37 +56,56 @@ export class FilterController {
5056
: null
5157
));
5258

53-
private readonly appliedFilterExpressions = computed(
54-
() => {
55-
const isCustomOperationsCreated = this.customOperations.value.length > 0;
56-
57-
if (!isCustomOperationsCreated) {
58-
return [];
59-
}
59+
public readonly appliedFilters = computed(() => ({
60+
filterPanel: this.filterPanelValue.value,
61+
headerFilter: this.headerFilterController.composedHeaderFilter.value,
62+
search: this.searchController.searchFilter.value,
63+
}));
64+
65+
public readonly customOperations = computed(() => {
66+
const config = {
67+
columnOption: (columnName: string): Column | undefined => {
68+
const columns = this.columnsController.columns.peek();
69+
70+
return getColumnByIndexOrName(columns, columnName);
71+
},
72+
/*
73+
Note: Root headerFilter options are used because the legacy code handles retrieving
74+
options for specific columns on its own
75+
*/
76+
getHeaderFilterOptions: (): HeaderFilterRootOptions => this.options.oneWay('headerFilter').peek(),
77+
getHeaderFilterController: (): unknown => this.headerFilterCompatibilityController,
78+
};
79+
80+
const builtInCustomOperation = [
81+
anyOf(config),
82+
noneOf(config),
83+
];
84+
85+
return builtInCustomOperation
86+
.concat(this.filterBuilderCustomOperations.value)
87+
.filter((o) => o) as unknown[];
88+
});
6089

61-
return getAppliedFilterExpressions(
90+
public readonly displayFilter = computed(
91+
() => {
92+
const appliedFilterExpressions = getAppliedFilterExpressions(
6293
this.appliedFilters.value,
6394
this.columnsController.filterableColumns.value,
6495
this.customOperations.value,
6596
this.filterSyncEnabled.value,
6697
);
98+
99+
return gridCoreUtils.combineFilters(appliedFilterExpressions) ?? null;
67100
},
68101
);
69102

70-
public readonly displayFilter = computed(
71-
() => gridCoreUtils.combineFilters(
72-
this.appliedFilterExpressions.value,
73-
) ?? null,
74-
);
103+
public headerFilterCompatibilityController: unknown = null;
75104

76105
constructor(
77106
private readonly options: OptionsController,
78107
private readonly columnsController: ColumnsController,
79-
) { }
80-
81-
public clearFilterCallback = (): void => {};
82-
83-
public clearFilter(): void {
84-
this.clearFilterCallback();
85-
}
108+
private readonly searchController: SearchController,
109+
private readonly headerFilterController: HeaderFilterController,
110+
) {}
86111
}

0 commit comments

Comments
 (0)