Skip to content

Commit a8b5bad

Browse files
authored
CardView - Header Filter: Fix reset select state after search (DevExpress#29918)
Co-authored-by: Alyar <>
1 parent 15e22fe commit a8b5bad

File tree

6 files changed

+193
-67
lines changed

6 files changed

+193
-67
lines changed

e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,108 @@ test('The item\'s selection state should be correct if a custom data source is s
305305
],
306306
});
307307
});
308+
309+
test('The item\'s selection state should be correct after search', async (t) => {
310+
// arrange
311+
const cardView = new CardView('#container');
312+
313+
await t
314+
.click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon());
315+
316+
const headerFilterList = cardView.getHeaderFilterList();
317+
318+
// assert
319+
await t
320+
.expect(headerFilterList.getItems().count)
321+
.eql(4);
322+
323+
const firstHeaderFilterItem = headerFilterList.getItem(0);
324+
325+
// act
326+
await t
327+
.click(firstHeaderFilterItem.element);
328+
329+
// assert
330+
await t
331+
.expect(firstHeaderFilterItem.isSelected)
332+
.ok();
333+
334+
// act
335+
await t
336+
.typeText(headerFilterList.searchInput, '1');
337+
338+
// assert
339+
await t
340+
.expect(headerFilterList.getItems().count)
341+
.eql(1)
342+
.expect(headerFilterList.getItem(0).isSelected)
343+
.ok();
344+
}).before(async () => {
345+
await createWidget('dxCardView', {
346+
...baseConfig,
347+
headerFilter: {
348+
visible: true,
349+
search: {
350+
enabled: true,
351+
},
352+
},
353+
});
354+
});
355+
356+
test('The item\'s selection state should be correct after resetting the search', async (t) => {
357+
// arrange
358+
const cardView = new CardView('#container');
359+
360+
await t
361+
.click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon());
362+
363+
const headerFilterList = cardView.getHeaderFilterList();
364+
365+
// assert
366+
await t
367+
.expect(headerFilterList.getItems().count)
368+
.eql(4);
369+
370+
// act
371+
await t
372+
.typeText(headerFilterList.searchInput, '1');
373+
374+
// assert
375+
await t
376+
.expect(headerFilterList.getItems().count)
377+
.eql(1);
378+
379+
const firstHeaderFilterItem = headerFilterList.getItem(0);
380+
381+
// act
382+
await t
383+
.click(firstHeaderFilterItem.element);
384+
385+
// assert
386+
await t
387+
.expect(firstHeaderFilterItem.isSelected)
388+
.ok();
389+
390+
// act
391+
await t
392+
.click(headerFilterList.searchInput)
393+
.selectText(headerFilterList.searchInput)
394+
.pressKey('backspace');
395+
396+
// assert
397+
await t
398+
.expect(headerFilterList.getItems().count)
399+
.eql(4)
400+
.expect(headerFilterList.getItem(0).isSelected)
401+
.ok();
402+
}).before(async () => {
403+
await createWidget('dxCardView', {
404+
...baseConfig,
405+
headerFilter: {
406+
visible: true,
407+
search: {
408+
enabled: true,
409+
},
410+
},
411+
});
412+
});

packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { updateHeaderFilterItemSelectionState } from '@ts/grids/grid_core/header
2525
import gridCoreUtils from '@ts/grids/grid_core/m_utils';
2626
import type { Column } from '@ts/grids/new/grid_core/columns_controller/types';
2727

28+
import type { HeaderFilterListType } from './types';
29+
2830
export const getHeaderItemText = (
2931
displayValue,
3032
column,
@@ -137,10 +139,12 @@ const _processGroupItems = (
137139

138140
export const getDataSourceOptions = (
139141
storeLoadAdapter,
140-
column,
142+
popupOptions,
141143
headerFilterOptions,
142144
filter,
143145
) => {
146+
const { column } = popupOptions;
147+
144148
if (!storeLoadAdapter) {
145149
return undefined;
146150
}
@@ -198,16 +202,20 @@ export const getDataSourceOptions = (
198202
let items = data;
199203

200204
items = origPostProcess?.call(this, items) || items;
201-
_updateSelectedState(items, column);
205+
_updateSelectedState(items, {
206+
...column,
207+
filterType: popupOptions.filterType,
208+
filterValues: popupOptions.filterValues,
209+
});
202210
return items;
203211
};
204212

205213
return options.dataSource;
206214
};
207215

208-
export const getFilterType = (
216+
export const getHeaderFilterListType = (
209217
column: Column,
210-
): 'tree' | 'list' => {
218+
): HeaderFilterListType => {
211219
const groupInterval = filteringUtils.getGroupInterval(column);
212220
return groupInterval && groupInterval.length > 1 ? 'tree' : 'list';
213221
};

packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1+
import type { FilterType } from '@js/common/grids';
12
import type { DataSourceLike } from '@js/data/data_source';
23

4+
import type { Column } from '../../columns_controller/types';
5+
36
export type HeaderFilterSearchMode = 'contains' | 'startswith' | 'equals';
47
export type HeaderFilterType = 'include' | 'exclude';
8+
export type HeaderFilterListType = 'tree' | 'list';
9+
10+
export interface PopupOptions {
11+
type: HeaderFilterListType;
12+
column: Column;
13+
headerFilter: HeaderFilterColumnOptions;
14+
dataSource?: DataSourceLike<unknown>;
15+
isFilterBuilder?: boolean;
16+
filterType?: FilterType;
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
filterValues?: any[];
19+
apply: () => void;
20+
hidePopupCallback: () => void;
21+
}
522

623
export type PopupState = {
724
element: Element;
8-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9-
options: Record<string, any>;
25+
options: PopupOptions;
1026
} | null;
1127

1228
export interface HeaderFilterTextOptions {

packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('HeaderFilter', () => {
114114
{ popupState } as any,
115115
);
116116

117-
popupState.value = { element: {} as any, options: expectedOptions };
117+
popupState.value = { element: {} as any, options: expectedOptions as any };
118118

119119
expect(oldHeaderFilterMock.showHeaderFilterMenu)
120120
.toHaveBeenCalledTimes(1);
@@ -130,7 +130,7 @@ describe('HeaderFilter', () => {
130130
{ popupState } as any,
131131
);
132132

133-
popupState.value = { element: {} as any, options: {} };
133+
popupState.value = { element: {} as any, options: {} as any };
134134
popupState.value = null;
135135

136136
expect(oldHeaderFilterMock.showHeaderFilterMenu)

packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ describe('HeaderFilter', () => {
156156

157157
const state = viewController.popupState.peek();
158158

159-
expect(typeof state?.options.dataSource.load).toBe('function');
160-
expect(typeof state?.options.dataSource.postProcess).toBe('function');
159+
expect(typeof (state?.options.dataSource as any).load).toBe('function');
160+
expect(typeof (state?.options.dataSource as any).postProcess).toBe('function');
161161
});
162162

163163
// NOTE: Unfortunately, we cannot test perfectly local group functions here
@@ -195,8 +195,8 @@ describe('HeaderFilter', () => {
195195

196196
const state = viewController.popupState.peek();
197197

198-
expect(state?.options.dataSource.group).toBeTruthy();
199-
expect(checkFn(state?.options.dataSource.group)).toBeTruthy();
198+
expect((state?.options.dataSource as any).group).toBeTruthy();
199+
expect(checkFn((state?.options.dataSource as any).group)).toBeTruthy();
200200
});
201201
});
202202
});

packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { OptionsController } from '../../options_controller/options_controller';
1212
import { FilterController } from '../filter_controller';
1313
import type { AppliedFilters } from '../types';
1414
import { getAppliedFilterExpressions } from '../utils';
15-
import { getDataSourceOptions, getFilterType } from './legacy_header_filter';
16-
import type { PopupState } from './types';
15+
import { getDataSourceOptions, getHeaderFilterListType } from './legacy_header_filter';
16+
import type { PopupOptions, PopupState } from './types';
1717
import { getColumnIdentifier } from './utils';
1818

1919
export class HeaderFilterViewController {
@@ -45,70 +45,67 @@ export class HeaderFilterViewController {
4545
const rootDataSource = this.dataController.getStoreLoadAdapter();
4646
const rootHeaderFilterOptions = this.options.oneWay('headerFilter').peek();
4747
const filterExpression = this.getFilterExpressionWithoutCurrentColumn(column);
48+
const type = getHeaderFilterListType(column);
49+
const { columnsController } = this;
50+
const applyFilter = (filterValues, filterType): void => {
51+
if (customApply) {
52+
customApply(filterValues);
53+
} else {
54+
columnsController.updateColumns(
55+
(columns) => {
56+
const index = getColumnIndexByName(columns, column.name);
57+
const newColumns = [...columns];
58+
59+
newColumns[index] = {
60+
...newColumns[index],
61+
headerFilter: {
62+
...newColumns[index].headerFilter,
63+
},
64+
// NOTE: Copy array because of mutations in legacy code
65+
filterValues: Array.isArray(filterValues)
66+
? [...filterValues]
67+
: filterValues,
68+
filterType,
69+
};
70+
return newColumns;
71+
},
72+
);
73+
}
74+
75+
onFilterCloseCallback?.();
76+
};
77+
const popupOptions: PopupOptions = {
78+
type,
79+
column: { ...column },
80+
isFilterBuilder,
81+
headerFilter: { ...column.headerFilter },
82+
filterType: column.filterType,
83+
// NOTE: Copy array because of mutations in legacy code
84+
filterValues: Array.isArray(column.filterValues)
85+
? [...column.filterValues]
86+
: column.filterValues,
87+
apply(): void {
88+
applyFilter(this.filterValues, this.filterType);
89+
},
90+
hidePopupCallback: (): void => {
91+
this.popupStateInternal.value = null;
92+
onFilterCloseCallback?.();
93+
},
94+
};
4895

49-
const filterDataSourceOptions = getDataSourceOptions(
96+
popupOptions.dataSource = getDataSourceOptions(
5097
rootDataSource,
51-
{
52-
...column,
53-
filterType: column.filterType,
54-
filterValues: column.filterValues,
55-
},
98+
popupOptions,
5699
// NOTE: Only text used from root options
57100
{
58101
texts: rootHeaderFilterOptions.texts,
59102
},
60-
61103
filterExpression,
62104
);
63105

64-
const type = getFilterType(column);
65-
const { columnsController } = this;
66-
67106
this.popupStateInternal.value = {
68107
element,
69-
options: {
70-
type,
71-
isFilterBuilder,
72-
headerFilter: { ...column.headerFilter },
73-
dataSource: filterDataSourceOptions,
74-
filterType: column.filterType,
75-
// NOTE: Copy array because of mutations in legacy code
76-
filterValues: Array.isArray(column.filterValues)
77-
78-
? [...column.filterValues]
79-
: column.filterValues,
80-
apply(): void {
81-
if (customApply) {
82-
customApply(this.filterValues);
83-
} else {
84-
columnsController.updateColumns(
85-
(columns) => {
86-
const index = getColumnIndexByName(columns, column.name);
87-
const newColumns = [...columns];
88-
89-
newColumns[index] = {
90-
...newColumns[index],
91-
headerFilter: {
92-
...newColumns[index].headerFilter,
93-
},
94-
// NOTE: Copy array because of mutations in legacy code
95-
filterValues: Array.isArray(this.filterValues)
96-
? [...this.filterValues]
97-
: this.filterValues,
98-
filterType: this.filterType,
99-
};
100-
return newColumns;
101-
},
102-
);
103-
}
104-
105-
onFilterCloseCallback?.();
106-
},
107-
hidePopupCallback: (): void => {
108-
this.popupStateInternal.value = null;
109-
onFilterCloseCallback?.();
110-
},
111-
},
108+
options: popupOptions,
112109
};
113110
}
114111

0 commit comments

Comments
 (0)