diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts index b97bd803f515..18ecf44bcd20 100644 --- a/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts @@ -305,3 +305,108 @@ test('The item\'s selection state should be correct if a custom data source is s ], }); }); + +test('The item\'s selection state should be correct after search', async (t) => { + // arrange + const cardView = new CardView('#container'); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()); + + const headerFilterList = cardView.getHeaderFilterList(); + + // assert + await t + .expect(headerFilterList.getItems().count) + .eql(4); + + const firstHeaderFilterItem = headerFilterList.getItem(0); + + // act + await t + .click(firstHeaderFilterItem.element); + + // assert + await t + .expect(firstHeaderFilterItem.isSelected) + .ok(); + + // act + await t + .typeText(headerFilterList.searchInput, '1'); + + // assert + await t + .expect(headerFilterList.getItems().count) + .eql(1) + .expect(headerFilterList.getItem(0).isSelected) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + headerFilter: { + visible: true, + search: { + enabled: true, + }, + }, + }); +}); + +test('The item\'s selection state should be correct after resetting the search', async (t) => { + // arrange + const cardView = new CardView('#container'); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()); + + const headerFilterList = cardView.getHeaderFilterList(); + + // assert + await t + .expect(headerFilterList.getItems().count) + .eql(4); + + // act + await t + .typeText(headerFilterList.searchInput, '1'); + + // assert + await t + .expect(headerFilterList.getItems().count) + .eql(1); + + const firstHeaderFilterItem = headerFilterList.getItem(0); + + // act + await t + .click(firstHeaderFilterItem.element); + + // assert + await t + .expect(firstHeaderFilterItem.isSelected) + .ok(); + + // act + await t + .click(headerFilterList.searchInput) + .selectText(headerFilterList.searchInput) + .pressKey('backspace'); + + // assert + await t + .expect(headerFilterList.getItems().count) + .eql(4) + .expect(headerFilterList.getItem(0).isSelected) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + headerFilter: { + visible: true, + search: { + enabled: true, + }, + }, + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts index 4c4a320507c3..0ee706f25c41 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts @@ -25,6 +25,8 @@ import { updateHeaderFilterItemSelectionState } from '@ts/grids/grid_core/header import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { HeaderFilterListType } from './types'; + export const getHeaderItemText = ( displayValue, column, @@ -137,10 +139,12 @@ const _processGroupItems = ( export const getDataSourceOptions = ( storeLoadAdapter, - column, + popupOptions, headerFilterOptions, filter, ) => { + const { column } = popupOptions; + if (!storeLoadAdapter) { return undefined; } @@ -198,16 +202,20 @@ export const getDataSourceOptions = ( let items = data; items = origPostProcess?.call(this, items) || items; - _updateSelectedState(items, column); + _updateSelectedState(items, { + ...column, + filterType: popupOptions.filterType, + filterValues: popupOptions.filterValues, + }); return items; }; return options.dataSource; }; -export const getFilterType = ( +export const getHeaderFilterListType = ( column: Column, -): 'tree' | 'list' => { +): HeaderFilterListType => { const groupInterval = filteringUtils.getGroupInterval(column); return groupInterval && groupInterval.length > 1 ? 'tree' : 'list'; }; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts index 4d02ba589292..ffb09894c801 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/types.ts @@ -1,12 +1,28 @@ +import type { FilterType } from '@js/common/grids'; import type { DataSourceLike } from '@js/data/data_source'; +import type { Column } from '../../columns_controller/types'; + export type HeaderFilterSearchMode = 'contains' | 'startswith' | 'equals'; export type HeaderFilterType = 'include' | 'exclude'; +export type HeaderFilterListType = 'tree' | 'list'; + +export interface PopupOptions { + type: HeaderFilterListType; + column: Column; + headerFilter: HeaderFilterColumnOptions; + dataSource?: DataSourceLike; + isFilterBuilder?: boolean; + filterType?: FilterType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + filterValues?: any[]; + apply: () => void; + hidePopupCallback: () => void; +} export type PopupState = { element: Element; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options: Record; + options: PopupOptions; } | null; export interface HeaderFilterTextOptions { diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx index d6d31d0f713b..34a7f3fdae7d 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view.test.tsx @@ -114,7 +114,7 @@ describe('HeaderFilter', () => { { popupState } as any, ); - popupState.value = { element: {} as any, options: expectedOptions }; + popupState.value = { element: {} as any, options: expectedOptions as any }; expect(oldHeaderFilterMock.showHeaderFilterMenu) .toHaveBeenCalledTimes(1); @@ -130,7 +130,7 @@ describe('HeaderFilter', () => { { popupState } as any, ); - popupState.value = { element: {} as any, options: {} }; + popupState.value = { element: {} as any, options: {} as any }; popupState.value = null; expect(oldHeaderFilterMock.showHeaderFilterMenu) diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.test.ts index 9e0c2bfbfe6b..9e454ab01349 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.test.ts @@ -156,8 +156,8 @@ describe('HeaderFilter', () => { const state = viewController.popupState.peek(); - expect(typeof state?.options.dataSource.load).toBe('function'); - expect(typeof state?.options.dataSource.postProcess).toBe('function'); + expect(typeof (state?.options.dataSource as any).load).toBe('function'); + expect(typeof (state?.options.dataSource as any).postProcess).toBe('function'); }); // NOTE: Unfortunately, we cannot test perfectly local group functions here @@ -195,8 +195,8 @@ describe('HeaderFilter', () => { const state = viewController.popupState.peek(); - expect(state?.options.dataSource.group).toBeTruthy(); - expect(checkFn(state?.options.dataSource.group)).toBeTruthy(); + expect((state?.options.dataSource as any).group).toBeTruthy(); + expect(checkFn((state?.options.dataSource as any).group)).toBeTruthy(); }); }); }); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts index 28c979db0e01..af4b9458e14a 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/view_controller.ts @@ -12,8 +12,8 @@ import { OptionsController } from '../../options_controller/options_controller'; import { FilterController } from '../filter_controller'; import type { AppliedFilters } from '../types'; import { getAppliedFilterExpressions } from '../utils'; -import { getDataSourceOptions, getFilterType } from './legacy_header_filter'; -import type { PopupState } from './types'; +import { getDataSourceOptions, getHeaderFilterListType } from './legacy_header_filter'; +import type { PopupOptions, PopupState } from './types'; import { getColumnIdentifier } from './utils'; export class HeaderFilterViewController { @@ -45,70 +45,67 @@ export class HeaderFilterViewController { const rootDataSource = this.dataController.getStoreLoadAdapter(); const rootHeaderFilterOptions = this.options.oneWay('headerFilter').peek(); const filterExpression = this.getFilterExpressionWithoutCurrentColumn(column); + const type = getHeaderFilterListType(column); + const { columnsController } = this; + const applyFilter = (filterValues, filterType): void => { + if (customApply) { + customApply(filterValues); + } else { + columnsController.updateColumns( + (columns) => { + const index = getColumnIndexByName(columns, column.name); + const newColumns = [...columns]; + + newColumns[index] = { + ...newColumns[index], + headerFilter: { + ...newColumns[index].headerFilter, + }, + // NOTE: Copy array because of mutations in legacy code + filterValues: Array.isArray(filterValues) + ? [...filterValues] + : filterValues, + filterType, + }; + return newColumns; + }, + ); + } + + onFilterCloseCallback?.(); + }; + const popupOptions: PopupOptions = { + type, + column: { ...column }, + isFilterBuilder, + headerFilter: { ...column.headerFilter }, + filterType: column.filterType, + // NOTE: Copy array because of mutations in legacy code + filterValues: Array.isArray(column.filterValues) + ? [...column.filterValues] + : column.filterValues, + apply(): void { + applyFilter(this.filterValues, this.filterType); + }, + hidePopupCallback: (): void => { + this.popupStateInternal.value = null; + onFilterCloseCallback?.(); + }, + }; - const filterDataSourceOptions = getDataSourceOptions( + popupOptions.dataSource = getDataSourceOptions( rootDataSource, - { - ...column, - filterType: column.filterType, - filterValues: column.filterValues, - }, + popupOptions, // NOTE: Only text used from root options { texts: rootHeaderFilterOptions.texts, }, - filterExpression, ); - const type = getFilterType(column); - const { columnsController } = this; - this.popupStateInternal.value = { element, - options: { - type, - isFilterBuilder, - headerFilter: { ...column.headerFilter }, - dataSource: filterDataSourceOptions, - filterType: column.filterType, - // NOTE: Copy array because of mutations in legacy code - filterValues: Array.isArray(column.filterValues) - - ? [...column.filterValues] - : column.filterValues, - apply(): void { - if (customApply) { - customApply(this.filterValues); - } else { - columnsController.updateColumns( - (columns) => { - const index = getColumnIndexByName(columns, column.name); - const newColumns = [...columns]; - - newColumns[index] = { - ...newColumns[index], - headerFilter: { - ...newColumns[index].headerFilter, - }, - // NOTE: Copy array because of mutations in legacy code - filterValues: Array.isArray(this.filterValues) - ? [...this.filterValues] - : this.filterValues, - filterType: this.filterType, - }; - return newColumns; - }, - ); - } - - onFilterCloseCallback?.(); - }, - hidePopupCallback: (): void => { - this.popupStateInternal.value = null; - onFilterCloseCallback?.(); - }, - }, + options: popupOptions, }; }