From 38009640c02394867858fa2f27208a8782b06fcc Mon Sep 17 00:00:00 2001 From: Aliar666 Date: Mon, 19 Oct 2026 19:53:23 +0400 Subject: [PATCH 1/3] Grids: type m_header_filter_core --- .../column_state_mixin/column_state_mixin.ts | 11 +- .../grid_core/column_state_mixin/types.ts | 4 +- .../header_filter/m_header_filter.ts | 1 + .../header_filter/m_header_filter_core.ts | 335 +++++++++++------- .../grids/grid_core/header_filter/types.ts | 52 ++- .../filtering/header_filter/types.ts | 4 +- .../filtering/header_filter/utils.test.ts | 6 +- 7 files changed, 269 insertions(+), 144 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/column_state_mixin.ts b/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/column_state_mixin.ts index 7c7d421dae06..fa330c7d5cc0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/column_state_mixin.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/column_state_mixin.ts @@ -24,7 +24,7 @@ export const ColumnStateMixin = (Base: T) => cla protected _applyColumnState(options: ColumnStateOptions): dxElementWrapper | undefined { const rtlEnabled = this.option('rtlEnabled'); const columnAlignment = this._getColumnAlignment(options.column.alignment, rtlEnabled); - const parameters: IndicatorOptions = extend(true, { columnAlignment }, options); + const parameters = extend(true, { columnAlignment }, options); const isGroupPanelItem = parameters.rootElement.hasClass(GROUP_PANEL_ITEM_CLASS); const $indicatorsContainer = this._createIndicatorContainer(parameters, isGroupPanelItem); const $span = $('').addClass(this._getIndicatorClassName(options.name) ?? ''); @@ -33,9 +33,10 @@ export const ColumnStateMixin = (Base: T) => cla const indicatorAlignment = columnsController?.getHeaderContentAlignment(columnAlignment) ?? columnAlignment; - parameters.container = $indicatorsContainer; - parameters.indicator = $span; - this._renderIndicator(parameters); + this._renderIndicator(Object.assign(parameters, { + container: $indicatorsContainer, + indicator: $span, + })); $indicatorsContainer[(isGroupPanelItem || !options.showColumnLines) && indicatorAlignment === 'left' ? 'appendTo' : 'prependTo'](options.rootElement); @@ -60,7 +61,7 @@ export const ColumnStateMixin = (Base: T) => cla } private _createIndicatorContainer( - options: IndicatorOptions, + options: Omit, ignoreIndicatorAlignment: boolean, ): dxElementWrapper { let $indicatorsContainer = this._getIndicatorContainer(options.rootElement); diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/types.ts b/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/types.ts index d04ba94ed328..39a13f563ddb 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_state_mixin/types.ts @@ -27,8 +27,8 @@ export interface ColumnStateOptions { export interface IndicatorOptions extends ColumnStateOptions { columnAlignment: string; - container?: dxElementWrapper; - indicator?: dxElementWrapper; + container: dxElementWrapper; + indicator: dxElementWrapper; } export interface IndicatorRowOptions { diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index 29eee9b6c16c..487b763eaaac 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -485,6 +485,7 @@ const columnHeadersView = (Base: ModuleType) => class ColumnH const headerPanel = (Base: ModuleType) => class HeaderPanelHeaderFilterExtender extends headerFilterMixin(Base) { private _createGroupPanelItem($rootElement, groupColumn) { const that = this; + // @ts-expect-error DataGrid's method const $item = super._createGroupPanelItem.apply(that, arguments); let $headerFilterIndicator; diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts index 6caf15bd6785..c9e4bcf9d5da 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts @@ -4,6 +4,7 @@ import '@ts/ui/list/modules/selection'; import type { ChangedOptionInfo, NativeEventInfo } from '@js/common/core/events'; import messageLocalization from '@js/common/core/localization/message'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; @@ -12,13 +13,24 @@ import type dxCheckBox from '@js/ui/check_box'; import type { ValueChangedInfo } from '@js/ui/editor/editor'; import type dxList from '@js/ui/list'; import Popup from '@js/ui/popup/ui.popup'; +import type { SearchBoxMixinOptions } from '@js/ui/widget/ui.search_box_mixin'; +import type { ColumnStateOptions, IndicatorOptions } from '@ts/grids/grid_core/column_state_mixin/types'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import Modules from '@ts/grids/grid_core/modules/modules'; -import type { ModuleType } from '@ts/grids/grid_core/types'; +import type { ModuleType, OptionChanged } from '@ts/grids/grid_core/types'; +import type { ColumnsView } from '@ts/grids/grid_core/views/columns_view'; import List from '@ts/ui/list/list.edit.search'; import type { TreeViewBaseProperties } from '@ts/ui/tree_view/tree_view.base'; import TreeView from '@ts/ui/tree_view/tree_view.search'; import gridCoreUtils from '../utils'; +import type { + HeaderFilterGroup, + HeaderFilterItem, + HeaderFilterOptions, + HeaderFilterValues, + HeaderFilterViewOptions, +} from './types'; import { allowHeaderFiltering } from './utils'; type CheckBoxValueChangedEvent = NativeEventInfo & ValueChangedInfo; @@ -30,18 +42,25 @@ const HEADER_FILTER_MENU_CLASS = 'dx-header-filter-menu'; const DEFAULT_SEARCH_EXPRESSION = 'text'; const HANDLER_DECORATED_KEY = Symbol('HANDLER_DECORATED_KEY'); -function resetChildrenItemSelection(items) { - items = items || []; - for (let i = 0; i < items.length; i++) { - items[i].selected = false; - resetChildrenItemSelection(items[i].items); +function resetChildrenItemSelection(items: HeaderFilterItem[] | null | undefined): void { + if (!items) { + return; + } + + for (const item of items) { + item.selected = false; + resetChildrenItemSelection(item.items); } } -function getSelectAllCheckBox(listComponent): dxCheckBox { +function getSelectAllCheckBox( + listComponent: Pick, +): dxCheckBox | undefined { const selector = listComponent.NAME === 'dxTreeView' ? '.dx-treeview-select-all-item' : '.dx-list-select-all-checkbox'; + // @ts-expect-error dxCheckBox is added to the renderer at runtime + const selectAllCheckBox: dxCheckBox | undefined = listComponent.$element().find(selector).dxCheckBox('instance'); - return listComponent.$element().find(selector).dxCheckBox('instance'); + return selectAllCheckBox; } function getListSelectAllValueChangedHandler( @@ -94,7 +113,7 @@ function decorateListSelectAllValueChanged( function updateListSelectAllState( listComponent: dxList, - filterValues: any[], + filterValues: HeaderFilterValues | null | undefined, ): void { if (listComponent.option('searchValue')) { return; @@ -107,60 +126,74 @@ function updateListSelectAllState( } } -export function updateHeaderFilterItemSelectionState(item, filterValuesMatch, isExcludeFilter) { - if (filterValuesMatch ^ isExcludeFilter) { - item.selected = true; +export function updateHeaderFilterItemSelectionState( + item: HeaderFilterItem, + filterValuesMatch: boolean, + isExcludeFilter: boolean, +): void { + if (filterValuesMatch === isExcludeFilter) { + if (isExcludeFilter || item.selected) { + item.selected = false; + resetChildrenItemSelection(item.items); + } + + return; + } - if (isExcludeFilter && item.items) { - for (let j = 0; j < item.items.length; j++) { - if (!item.items[j].selected) { - item.selected = undefined; - break; - } + item.selected = true; + + if (isExcludeFilter && item.items) { + for (const childItem of item.items) { + if (!childItem.selected) { + item.selected = undefined; + break; } } - } else if (isExcludeFilter || item.selected) { - item.selected = false; - resetChildrenItemSelection(item.items); } } export class HeaderFilterView extends Modules.View { - private _popupContainer: any; + private _popupContainer?: Popup; - private _listComponent: any; + private _listComponent?: List | TreeView; - private getPopupContainer() { + private getPopupContainer(): Popup | undefined { return this._popupContainer; } - private getListComponent() { + private getListComponent(): List | TreeView | undefined { return this._listComponent; } - private applyHeaderFilter(options) { - const that = this; - const list = that.getListComponent(); - const searchValue = list.option('searchValue'); + private applyHeaderFilter(options: HeaderFilterViewOptions): void { + // @ts-expect-error the list is created here + const list: List | TreeView = this.getListComponent(); + const searchValue = (list as List).option('searchValue'); const selectAllCheckBox = getSelectAllCheckBox(list); const isAllSelected = !searchValue && !options.isFilterBuilder && selectAllCheckBox?.option('value'); - const filterValues = []; + const filterValues: HeaderFilterValues = []; - const fillSelectedItemKeys = function (filterValues, items, isExclude) { + const fillSelectedItemKeys = ( + selectedValues: HeaderFilterValues, + items: HeaderFilterItem[] | undefined, + isExclude: boolean, + ): void => { each(items, (_, item) => { - if (item.selected !== undefined && (!!item.selected as any) ^ isExclude) { - const node = list._getNode(item); - const hasChildren = list._hasChildren(node); - const hasChildrenWithSelection = hasChildren && item.items && item.items.some((item) => item.selected); + if (item.selected !== undefined && !!item.selected !== isExclude) { + const node = (list as TreeView)._getNode(item); + // @ts-expect-error every item of the tree has a node + const hasChildren = (list as TreeView)._hasChildren(node); + const hasChildrenWithSelection = hasChildren + && item.items?.some((childItem) => childItem.selected); if (!searchValue || !hasChildrenWithSelection) { - filterValues.push(item.value); + selectedValues.push(item.value); return; } } - if (item.items && item.items.length) { - fillSelectedItemKeys(filterValues, item.items, isExclude); + if (item.items?.length) { + fillSelectedItemKeys(selectedValues, item.items, isExclude); } }); }; @@ -171,7 +204,7 @@ export class HeaderFilterView extends Modules.View { options.filterType = 'include'; } - fillSelectedItemKeys(filterValues, list.option('items'), false); + fillSelectedItemKeys(filterValues, (list as TreeView).option('items'), false); options.filterValues = filterValues; } } else { @@ -190,43 +223,46 @@ export class HeaderFilterView extends Modules.View { options.apply(); - that.hideHeaderFilterMenu(); + this.hideHeaderFilterMenu(); } - public showHeaderFilterMenu($columnElement, options) { - const that = this; - + public showHeaderFilterMenu( + $columnElement: dxElementWrapper, + options: HeaderFilterViewOptions, + ): void { if (options) { - that._initializePopupContainer(options); + this._initializePopupContainer(options); - const popupContainer = that.getPopupContainer(); + // @ts-expect-error the popup is created here + const popupContainer: Popup = this.getPopupContainer(); - that.hideHeaderFilterMenu(); - that.updatePopup($columnElement, options); + this.hideHeaderFilterMenu(); + this.updatePopup($columnElement, options); + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget popupContainer.show(); } } - public hideHeaderFilterMenu() { + public hideHeaderFilterMenu(): void { const headerFilterMenu = this.getPopupContainer(); - headerFilterMenu && headerFilterMenu.hide(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- fire-and-forget + headerFilterMenu?.hide(); } - private updatePopup($element, options) { - const that = this; + private updatePopup($element: dxElementWrapper, options: HeaderFilterViewOptions): void { const showColumnLines = this.option('showColumnLines'); - const alignment = ((options.alignment === 'right') as any) ^ (!showColumnLines as any) ? 'left' : 'right'; + const alignment = (options.alignment === 'right') !== !showColumnLines ? 'left' : 'right'; - that._popupContainer.setAria({ - role: 'dialog', - label: messageLocalization.format('dxDataGrid-headerFilterLabel'), - }); + if (this._popupContainer) { + this._popupContainer.setAria({ + role: 'dialog', + label: messageLocalization.format('dxDataGrid-headerFilterLabel'), + }); - if (that._popupContainer) { - that._cleanPopupContent(); - that._popupContainer.option('position', { + this._cleanPopupContent(); + this._popupContainer.option('position', { my: `${alignment} top`, at: `${alignment} bottom`, of: $element, @@ -235,7 +271,10 @@ export class HeaderFilterView extends Modules.View { } } - protected _getSearchExpr(options, headerFilterOptions) { + protected _getSearchExpr( + options: HeaderFilterViewOptions, + headerFilterOptions: HeaderFilterOptions, + ): SearchBoxMixinOptions['searchExpr'] { const { lookup } = options; const { useDefaultSearchExpr } = options; @@ -246,7 +285,10 @@ export class HeaderFilterView extends Modules.View { return filterSearchExpr; } - if (useDefaultSearchExpr || isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) { + if ( + useDefaultSearchExpr + || (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) + ) { return DEFAULT_SEARCH_EXPRESSION; } @@ -255,7 +297,8 @@ export class HeaderFilterView extends Modules.View { } if (options.dataSource) { - const { group } = options.dataSource; + // @ts-expect-error only the header filter's own data source reaches this line + const { group }: { group: HeaderFilterGroup } = options.dataSource; if (Array.isArray(group) && group.length > 0) { return group[0].selector; } if (isFunction(group) && !options.remoteFiltering) { @@ -266,13 +309,12 @@ export class HeaderFilterView extends Modules.View { return options.dataField || options.selector; } - private _cleanPopupContent() { - this._popupContainer && this._popupContainer.$content().empty(); + private _cleanPopupContent(): void { + this._popupContainer?.$content()?.empty(); } - private _initializePopupContainer(options) { - const that = this; - const $element = that.element(); + private _initializePopupContainer(options: HeaderFilterViewOptions): void { + const $element = this.element(); const headerFilterOptions = this._normalizeHeaderFilterOptions(options); const { hidePopupCallback } = options; @@ -298,8 +340,8 @@ export class HeaderFilterView extends Modules.View { options: { text: headerFilterOptions.texts.ok ?? messageLocalization.format('dxDataGrid-headerFilterOK'), - onClick() { - that.applyHeaderFilter(options); + onClick: (): void => { + this.applyHeaderFilter(options); }, }, }, @@ -310,24 +352,25 @@ export class HeaderFilterView extends Modules.View { options: { text: headerFilterOptions.texts.cancel ?? messageLocalization.format('dxDataGrid-headerFilterCancel'), - onClick() { - that.hideHeaderFilterMenu(); + onClick: (): void => { + this.hideHeaderFilterMenu(); hidePopupCallback?.(); }, }, }, ], resizeEnabled: true, - onShowing(e) { + onShowing: (e): void => { e.component.$content().parent().addClass('dx-dropdowneditor-overlay'); - that._initializeListContainer(options, headerFilterOptions); - options.onShowing && options.onShowing(e); + this._initializeListContainer(options, headerFilterOptions); + options.onShowing?.(e); }, - onShown() { - that.getListComponent().focus(); + onShown: (): void => { + // @ts-expect-error the list is created here + this.getListComponent().focus(); }, onHidden: options.onHidden, - onInitialized(e) { + onInitialized(e): void { const { component } = e; // T321243 component.option('animation', component._getDefaultOptions().animation); @@ -335,28 +378,31 @@ export class HeaderFilterView extends Modules.View { tabFocusLoopEnabled: true, }; - if (!isDefined(that._popupContainer)) { + if (!isDefined(this._popupContainer)) { // @ts-expect-error the view is rendered here - that._popupContainer = that._createComponent($element, Popup, dxPopupOptions); + this._popupContainer = this._createComponent($element, Popup, dxPopupOptions); } else { - that._popupContainer.option(dxPopupOptions); + this._popupContainer.option(dxPopupOptions); } } - private _initializeListContainer(options, headerFilterOptions) { - const that = this; - const $content = that._popupContainer.$content(); - const needShowSelectAllCheckbox = !options.isFilterBuilder && headerFilterOptions.allowSelectAll; + private _initializeListContainer( + options: HeaderFilterViewOptions, + headerFilterOptions: HeaderFilterOptions, + ): void { + const $content: dxElementWrapper = $(this._popupContainer?.content()); + const needShowSelectAllCheckbox = !options.isFilterBuilder + && headerFilterOptions.allowSelectAll; const widgetOptions = { searchEnabled: headerFilterOptions.search.enabled, searchTimeout: headerFilterOptions.search.timeout, searchEditorOptions: headerFilterOptions.search.editorOptions, - searchMode: headerFilterOptions.search.mode || '', + searchMode: headerFilterOptions.search.mode ?? '', dataSource: options.dataSource, - onContentReady() { - that.renderCompleted.fire(); + onContentReady: (): void => { + this.renderCompleted.fire(); }, - itemTemplate(data, _, element) { + itemTemplate(data, _, element): dxElementWrapper { const $element = $(element); if (options.encodeHtml !== false) { return $element.text(data.text); @@ -366,8 +412,10 @@ export class HeaderFilterView extends Modules.View { }, }; - const shouldChangeSelectAllCheckBoxVisibility = (): boolean => needShowSelectAllCheckbox - && that.option('headerFilter.hideSelectAllOnSearch') !== false; + const shouldChangeSelectAllCheckBoxVisibility = (): boolean | undefined => ( + needShowSelectAllCheckbox + && this.option('headerFilter.hideSelectAllOnSearch') !== false + ); const onTreeViewOptionChanged = ( event: ChangedOptionInfo & { @@ -411,22 +459,24 @@ export class HeaderFilterView extends Modules.View { 'showCheckBoxesMode' | 'onOptionChanged' | 'keyExpr' > = { showCheckBoxesMode: needShowSelectAllCheckbox ? 'selectAll' : 'normal', - // @ts-expect-error the handler takes the internal TreeView, the option declares the public one + // @ts-expect-error the handler takes the internal TreeView, not the public one onOptionChanged: onTreeViewOptionChanged, keyExpr: 'id', }; - that._listComponent = that._createComponent( + this._listComponent = this._createComponent( $('
').appendTo($content), TreeView, + // @ts-expect-error the data source loads header filter items, an unset searchMode is '' extend(widgetOptions, treeViewOptions), ); } else { - that._listComponent = that._createComponent( + this._listComponent = this._createComponent( $('
').appendTo($content), List, + // @ts-expect-error the data source loads header filter items, an unset searchMode is '' extend(widgetOptions, { - searchExpr: that._getSearchExpr(options, headerFilterOptions), + searchExpr: this._getSearchExpr(options, headerFilterOptions), pageLoadMode: 'scrollBottom' as const, showSelectionControls: true, selectionMode: needShowSelectAllCheckbox ? 'all' as const : 'multiple' as const, @@ -437,9 +487,16 @@ export class HeaderFilterView extends Modules.View { const selectedItems = listComponent.option('selectedItems'); if (!listComponent._selectedItemsUpdating && !listComponent.option('searchValue') && !options.isFilterBuilder) { - const filterValues = options.filterValues || []; + const filterValues = options.filterValues ?? []; const isExclude = options.filterType === 'exclude'; - if (selectedItems.length === 0 && items.length && (filterValues.length <= 1 || isExclude && filterValues.length === items.length - 1)) { + if ( + selectedItems.length === 0 + && items.length + && ( + filterValues.length <= 1 + || (isExclude && filterValues.length === items.length - 1) + ) + ) { options.filterType = 'include'; options.filterValues = []; } else if (selectedItems.length === items.length) { @@ -449,20 +506,25 @@ export class HeaderFilterView extends Modules.View { } each(items, (index, item) => { - const selected: any = gridCoreUtils.getIndexByKey(item, selectedItems, null) >= 0; + const selected = gridCoreUtils.getIndexByKey(item, selectedItems, null) >= 0; const oldSelected = !!item.selected; if (oldSelected !== selected) { item.selected = selected; - options.filterValues = options.filterValues || []; - const filterValueIndex = gridCoreUtils.getIndexByKey(item.value, options.filterValues, null); + options.filterValues ??= []; + const filterValueIndex = gridCoreUtils.getIndexByKey( + item.value, + options.filterValues, + null, + ); if (filterValueIndex >= 0) { options.filterValues.splice(filterValueIndex, 1); } - const isExcludeFilterType: any = options.filterType === 'exclude'; - if (selected ^ isExcludeFilterType) { + const isExcludeFilterType = options.filterType === 'exclude'; + + if (selected !== isExcludeFilterType) { options.filterValues.push(item.value); } } @@ -473,11 +535,11 @@ export class HeaderFilterView extends Modules.View { onContentReady(e) { const { component: listComponent } = e; const items = listComponent.option('items'); - const selectedItems: any = []; + const selectedItems: HeaderFilterItem[] = []; - each(items, function () { - if (this.selected) { - selectedItems.push(this); + each(items, (_, item) => { + if (item.selected) { + selectedItems.push(item); } }); @@ -493,27 +555,33 @@ export class HeaderFilterView extends Modules.View { } } - private _normalizeHeaderFilterOptions(options) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const generalHeaderFilter = this.option('headerFilter') || {}; - const specificHeaderFilter = options.headerFilter || {}; + private _normalizeHeaderFilterOptions(options: HeaderFilterViewOptions): HeaderFilterOptions { + const generalHeaderFilter = this.option('headerFilter') ?? {}; + const specificHeaderFilter = options.headerFilter ?? {}; - const headerFilterOptions = extend(true, {}, generalHeaderFilter, specificHeaderFilter); + // @ts-expect-error search is set below, the default options provide texts + const headerFilterOptions: HeaderFilterOptions = extend( + true, + {}, + generalHeaderFilter, + specificHeaderFilter, + ); headerFilterOptions.search ??= {}; return headerFilterOptions; } - protected _renderCore() { + protected _renderCore(): void { // @ts-expect-error the view is rendered here this.element().addClass(HEADER_FILTER_MENU_CLASS); } } -// TODO Fix types of this mixin -export const headerFilterMixin = >(Base: T) => class HeaderFilterMixin extends Base { - public optionChanged(args) { +export const headerFilterMixin = >( + Base: T, +): T => class HeaderFilterMixin extends Base { + public optionChanged(args: OptionChanged): void { if (args.name === 'headerFilter') { const requireReady = this.name === 'columnHeadersView'; this._invalidate(requireReady, requireReady); @@ -523,51 +591,56 @@ export const headerFilterMixin = >(Base: T) => class H } } - protected _applyColumnState(options) { - let $headerFilterIndicator; + protected _applyColumnState(options: ColumnStateOptions): dxElementWrapper | undefined { const { rootElement } = options; const { column } = options; if (options.name === 'headerFilter') { rootElement.find(`.${HEADER_FILTER_CLASS}`).remove(); - if (allowHeaderFiltering(column)) { - $headerFilterIndicator = super._applyColumnState(options).toggleClass('dx-header-filter-empty', this._isHeaderFilterEmpty(column)); - if (!this.option('useLegacyKeyboardNavigation')) { - $headerFilterIndicator.attr('tabindex', this.option('tabindex') || 0); - } + if (!allowHeaderFiltering(column)) { + return undefined; + } - const indicatorLabel = (messageLocalization.format as any)('dxDataGrid-headerFilterIndicatorLabel', column.caption); + // @ts-expect-error the base _applyColumnState always returns the indicator + const $headerFilterIndicator: dxElementWrapper = super._applyColumnState(options); - this.setAria('label', indicatorLabel, $headerFilterIndicator); - this.setAria('haspopup', 'dialog', $headerFilterIndicator); - this.setAria('role', 'button', $headerFilterIndicator); + $headerFilterIndicator.toggleClass('dx-header-filter-empty', this._isHeaderFilterEmpty(column)); + if (!this.option('useLegacyKeyboardNavigation')) { + $headerFilterIndicator.attr('tabindex', this.option('tabindex') || 0); } + const indicatorLabel = messageLocalization.format('dxDataGrid-headerFilterIndicatorLabel', column.caption); + + this.setAria('label', indicatorLabel, $headerFilterIndicator); + this.setAria('haspopup', 'dialog', $headerFilterIndicator); + this.setAria('role', 'button', $headerFilterIndicator); + return $headerFilterIndicator; } return super._applyColumnState(options); } - private _isHeaderFilterEmpty(column) { - return !column.filterValues || !column.filterValues.length; + private _isHeaderFilterEmpty(column: Column): boolean { + return !column.filterValues?.length; } - protected _getIndicatorClassName(name) { + protected _getIndicatorClassName(name: string): string | undefined { if (name === 'headerFilter') { return HEADER_FILTER_CLASS; } return super._getIndicatorClassName(name); } - protected _renderIndicator(options) { + protected _renderIndicator(options: IndicatorOptions): void { const $container = options.container; const $indicator = options.indicator; if (options.name === 'headerFilter') { - const rtlEnabled = this.option('rtlEnabled'); - if ($container.children().length && (!rtlEnabled && options.columnAlignment === 'right' || rtlEnabled && options.columnAlignment === 'left')) { + const endAlignment = this.option('rtlEnabled') ? 'left' : 'right'; + + if ($container.children().length && options.columnAlignment === endAlignment) { $container.prepend($indicator); return; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts index 757d7edb541d..6e36e99932f3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts @@ -1,3 +1,15 @@ +import type { HorizontalAlignment } from '@js/common'; +import type { + ColumnHeaderFilter, + ColumnHeaderFilterSearchConfig, + FilterType, + HeaderFilter, + HeaderFilterTexts, +} from '@js/common/grids'; +import type { DataSourceLike } from '@js/data/data_source'; +import type { ShowingEvent } from '@js/ui/popup'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; + export type EmptyDateValue = '' | false; export type HeaderFilterGroupSelector = (data: unknown) => unknown; @@ -9,6 +21,44 @@ export interface HeaderFilterGroupDescriptor { compare?: (value1: unknown, value2: unknown) => number; } -export type HeaderFilterGroupItem = HeaderFilterGroupDescriptor | HeaderFilterGroupSelector; +export type HeaderFilterGroupItem = HeaderFilterGroupDescriptor + | (HeaderFilterGroupSelector & { selector?: never }); export type HeaderFilterGroup = HeaderFilterGroupSelector | HeaderFilterGroupItem[]; + +export interface HeaderFilterItem { + key?: unknown; + value?: unknown; + text?: string; + selected?: boolean; + items?: HeaderFilterItem[]; +} + +export type HeaderFilterValues = unknown[]; + +export type HeaderFilterOptions = HeaderFilter + & ColumnHeaderFilter + & { + search: ColumnHeaderFilterSearchConfig; + texts: HeaderFilterTexts; + }; + +export interface HeaderFilterViewOptions { + type: 'tree' | 'list'; + apply: () => void; + alignment?: HorizontalAlignment; + encodeHtml?: boolean; + isFilterBuilder?: boolean; + filterType?: FilterType; + filterValues?: HeaderFilterValues | null; + headerFilter?: Partial; + dataSource?: DataSourceLike; + lookup?: Column['lookup']; + dataField?: string; + selector?: Column['selector']; + remoteFiltering?: boolean; + useDefaultSearchExpr?: boolean; + hidePopupCallback?: () => void; + onShowing?: (e: ShowingEvent) => void; + onHidden?: () => void; +} 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 9ab996f5b255..0453b3c81b77 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,4 +1,4 @@ -import type { FilterType } from '@js/common/grids'; +import type { ColumnHeaderFilter, FilterType } from '@js/common/grids'; import type { DataSourceLike } from '@js/data/data_source'; import type { FilterValue } from '@ts/grids/new/grid_core/filtering/types'; @@ -69,7 +69,7 @@ export interface HeaderFilterBaseOptions { export interface HeaderFilterColumnOptions extends HeaderFilterBaseOptions { // Specifies the header filter's data source. - dataSource?: DataSourceLike; + dataSource?: ColumnHeaderFilter['dataSource']; // Specifies how the header filter combines values into groups. // Does not apply if you specify a custom header filter data source // TODO: Maybe we should add values for dates to this option diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts index 457056544c50..8cf04e3637d8 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import type { DataSourceLike } from '@js/data/data_source'; +import type { ColumnHeaderFilter } from '@js/common/grids'; import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; import type { HeaderFilterRootOptions, @@ -68,13 +68,13 @@ describe('HeaderFilter', () => { root: {}, col: { headerFilter: { - dataSource: { test: 'TEST_DS' } as DataSourceLike, + dataSource: { test: 'TEST_DS' } as ColumnHeaderFilter['dataSource'], }, filterValues: ['A', 'B', 'C'], }, result: { headerFilter: { - dataSource: { test: 'TEST_DS' } as DataSourceLike, + dataSource: { test: 'TEST_DS' } as ColumnHeaderFilter['dataSource'], search: {}, }, filterValues: ['A', 'B', 'C'], From 8b98d23259b8965ebcce4a4e610304e0b4b88d81 Mon Sep 17 00:00:00 2001 From: Aliar666 Date: Thu, 22 Oct 2026 01:42:49 +0400 Subject: [PATCH 2/3] Grids: type m_header_filter --- .../grids/data_grid/grouping/m_grouping.ts | 1 + .../filter_sync/filter_custom_operations.ts | 2 +- .../header_filter/__tests__/utils.test.ts | 23 +- .../header_filter/m_header_filter.ts | 400 +++++++++++------- .../grids/grid_core/header_filter/types.ts | 48 ++- .../grids/grid_core/header_filter/utils.ts | 8 +- 6 files changed, 317 insertions(+), 165 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts index 00a721961f60..17f8602911ab 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts @@ -277,6 +277,7 @@ export const GroupingHeaderPanelExtender = ( const columnIndex = column && column.index; if ($target.is(HEADER_FILTER_CLASS_SELECTOR)) { + // @ts-expect-error updateColumnIndexes sets the index of every column this._headerFilterController.showHeaderFilterMenu(columnIndex, true); } else { // @ts-expect-error diff --git a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_custom_operations.ts b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_custom_operations.ts index b78515d221a1..c5d7f7ad9122 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_custom_operations.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/filter_sync/filter_custom_operations.ts @@ -103,7 +103,6 @@ function baseOperation(grid: InternalGrid): HeaderFilterOperation { 0, grid.option('headerFilter'), ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- untyped getHeaderItemText return text; } @@ -113,6 +112,7 @@ function baseOperation(grid: InternalGrid): HeaderFilterOperation { if (!textOptions.items) { column = extend({}, column, { filterType: 'include', filterValues: values }); const dataSourceOptions = headerFilterController.getDataSource(column); + // @ts-expect-error getDataSource() returns undefined only for a grid without a data source dataSourceOptions.paginate = false; const dataSource = new DataSource(dataSourceOptions); const key = dataSource.store().key(); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/__tests__/utils.test.ts index 519eab026c7c..15a5cac8e897 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/__tests__/utils.test.ts @@ -5,9 +5,10 @@ import { jest, } from '@jest/globals'; import filterUtils from '@js/ui/shared/filtering'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import { getFormatOptions } from '../m_header_filter'; -import type { HeaderFilterGroupSelector } from '../types'; +import type { DateIntervalFormat, HeaderFilterGroupSelector } from '../types'; import { getHeaderFilterGroupParameters } from '../utils'; describe('getGroupInterval', () => { @@ -177,21 +178,21 @@ describe('getHeaderFilterGroupParameters', () => { describe('getFormatOptions', () => { it('selects the interval of the requested level for a number[]', () => { - const column = { dataType: 'number', headerFilter: { groupInterval: [100, 10] } }; + const column: Column = { dataType: 'number', headerFilter: { groupInterval: [100, 10] } }; expect(getFormatOptions(700, column, 0).groupInterval).toBe(100); expect(getFormatOptions(750, column, 1).groupInterval).toBe(10); }); it('formats a numeric bucket as a " - " range', () => { - const column = { dataType: 'number', headerFilter: { groupInterval: [100, 10] } }; + const column: Column = { dataType: 'number', headerFilter: { groupInterval: [100, 10] } }; - expect(getFormatOptions(700, column, 0).getDisplayFormat()).toBe('700 - 800'); - expect(getFormatOptions(750, column, 1).getDisplayFormat()).toBe('750 - 760'); + expect(getFormatOptions(700, column, 0).getDisplayFormat?.('')).toBe('700 - 800'); + expect(getFormatOptions(750, column, 1).getDisplayFormat?.('')).toBe('750 - 760'); }); it('selects the hierarchy level name for a string[]', () => { - const column = { dataType: 'string', headerFilter: { groupInterval: ['Country', 'State', 'City'] } }; + const column: Column = { dataType: 'string', headerFilter: { groupInterval: ['Country', 'State', 'City'] } }; expect(getFormatOptions('USA', column, 0).groupInterval).toBe('Country'); expect(getFormatOptions('Arkansas', column, 1).groupInterval).toBe('State'); @@ -199,24 +200,24 @@ describe('getFormatOptions', () => { }); it('selects the date interval and its format per level for a date column', () => { - const column = { dataType: 'date', headerFilter: { groupInterval: 'month' } }; + const column: Column = { dataType: 'date', headerFilter: { groupInterval: 'month' } }; const yearLevel = getFormatOptions(2024, column, 0); expect(yearLevel.groupInterval).toBe('year'); - expect(yearLevel.format(2024)).toBe('2024'); + expect((yearLevel.format as DateIntervalFormat)(2024)).toBe('2024'); const monthLevel = getFormatOptions(3, column, 1); expect(monthLevel.groupInterval).toBe('month'); - expect(monthLevel.format(3)).toBe('March'); + expect((monthLevel.format as DateIntervalFormat)(3)).toBe('March'); }); it('selects the date interval and its format per level for a datetime column', () => { - const column = { dataType: 'datetime', headerFilter: { groupInterval: 'hour' } }; + const column: Column = { dataType: 'datetime', headerFilter: { groupInterval: 'hour' } }; expect(getFormatOptions(2024, column, 0).groupInterval).toBe('year'); const hourLevel = getFormatOptions(14, column, 3); expect(hourLevel.groupInterval).toBe('hour'); - expect(hourLevel.format(14)).toBe('14'); + expect((hourLevel.format as DateIntervalFormat)(14)).toBe('14'); }); }); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index 487b763eaaac..ded5e1320146 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -3,17 +3,24 @@ import { name as clickEventName } from '@js/common/core/events/click'; import eventsEngine from '@js/common/core/events/core/events_engine'; import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; +import type { GroupItem } from '@js/common/data'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import dataQuery from '@js/common/data/query'; import storeHelper from '@js/common/data/store_helper'; -import { compileGetter } from '@js/core/utils/data'; +import type { HeaderFilter } from '@js/common/grids'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { getDefaultAlignment } from '@js/core/utils/position'; import { isDefined, isFunction, isObject } from '@js/core/utils/type'; import { restoreFocus, saveFocusedElementInfo } from '@js/ui/shared/accessibility'; import filterUtils from '@js/ui/shared/filtering'; +import { compileGetter } from '@ts/core/utils/data'; +import type { DataSource } from '@ts/data/data_source/data_source'; +import type { StoreLoadOptions } from '@ts/data/data_source/types'; import type { ColumnHeadersView } from '@ts/grids/grid_core/column_headers/m_column_headers'; +import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_controller/types'; import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller'; import type { DataSourceController } from '@ts/grids/grid_core/data_source/data_source_controller'; import type { @@ -21,7 +28,8 @@ import type { } from '@ts/grids/grid_core/filter/types'; import type { HeaderPanel } from '@ts/grids/grid_core/header_panel/header_panel'; import Modules from '@ts/grids/grid_core/modules/modules'; -import type { ModuleType } from '@ts/grids/grid_core/types'; +import type { FormatOptions, InternalGridOptions, ModuleType } from '@ts/grids/grid_core/types'; +import type { CellRenderOptions, ViewCellOptions } from '@ts/grids/grid_core/views/types'; import type { ColumnsController } from '../columns_controller/columns_controller'; import gridCoreUtils from '../utils'; @@ -30,19 +38,32 @@ import { HeaderFilterView, updateHeaderFilterItemSelectionState, } from './m_header_filter_core'; +import type { + DateIntervalFormat, + HeaderFilterColumn, + HeaderFilterDataSourceArgs, + HeaderFilterDataSourceOptions, + HeaderFilterGroupSelector, + HeaderFilterItem, + HeaderFilterMenuOptions, + HeaderFilterViewOptions, + NormalizeGroupItemOptions, + ProcessGroupItemsOptions, +} from './types'; import { allowHeaderFiltering, createHeaderFilterExpressions, getHeaderFilterGroupParameters, } from './utils'; -const DATE_INTERVAL_FORMATS = { +const DATE_INTERVAL_FORMATS: Record = { year(value) { return String(value); }, month(value) { return dateLocalization.getMonthNames()[value - 1]; }, + // @ts-expect-error format() returns a string for the 'quarter' format quarter(value) { return dateLocalization.format(new Date(2000, value * 3 - 1), 'quarter'); }, @@ -60,12 +81,17 @@ const DATE_INTERVAL_FORMATS = { }, }; -const getDateIntervalFormat = (intervalKey: string) => DATE_INTERVAL_FORMATS[intervalKey] ?? ((v) => String(v)); +const getDateIntervalFormat = (intervalKey: string): DateIntervalFormat => ( + DATE_INTERVAL_FORMATS[intervalKey] ?? ((v: number): string => String(v)) +); -function ungroupUTCDates(items, dateParts?, dates?) { - dateParts = dateParts || []; - dates = dates || []; +const getUTCTime: (...dateParts: number[]) => number = Date.UTC; +function ungroupUTCDates( + items: GroupItem[], + dateParts: number[] = [], + dates: (Date | null)[] = [], +): (Date | null)[] { items.forEach((item) => { if (isDefined(item.key)) { const isMonthPart = dateParts.length === 1; @@ -73,7 +99,7 @@ function ungroupUTCDates(items, dateParts?, dates?) { if (item.items) { ungroupUTCDates(item.items, dateParts, dates); } else { - const date = new Date(Date.UTC.apply(Date, dateParts)); + const date = new Date(getUTCTime(...dateParts)); dates.push(date); } dateParts.pop(); @@ -84,23 +110,28 @@ function ungroupUTCDates(items, dateParts?, dates?) { return dates; } -export function convertDataFromUTCToLocal(data, column) { +export function convertDataFromUTCToLocal(data: GroupItem[], column: Column): GroupItem[] { const dates = ungroupUTCDates(data); const query = dataQuery(dates); const group = getHeaderFilterGroupParameters({ ...column, - calculateCellValue: (date) => date, + calculateCellValue: (date: Date | null): Date | null => date, }); + // @ts-expect-error queryByOptions().toArray() is typed as unknown[] return storeHelper.queryByOptions(query, { group }).toArray(); } -export function isUTCFormat(format) { +export function isUTCFormat(format: string | null | undefined): boolean { return format?.slice(-1) === 'Z' || format?.slice(-3) === '\'Z\''; } -export const getFormatOptions = function (value, column, currentLevel) { +export const getFormatOptions = ( + value: unknown, + column: Column, + currentLevel: number, +): FormatOptions => { const groupInterval = filterUtils.getGroupInterval(column); - const result: any = gridCoreUtils.getFormatOptionsByColumn(column, 'headerFilter'); + const result = gridCoreUtils.getFormatOptionsByColumn(column, 'headerFilter'); if (groupInterval) { result.groupInterval = groupInterval[currentLevel]; @@ -108,10 +139,14 @@ export const getFormatOptions = function (value, column, currentLevel) { if (gridCoreUtils.isDateType(column.dataType)) { result.format = getDateIntervalFormat(groupInterval[currentLevel]); } else if (column.dataType === 'number') { - result.getDisplayFormat = function () { + result.getDisplayFormat = (): string => { const formatOptions = { format: column.format, target: 'headerFilter' }; const firstValueText = gridCoreUtils.formatValue(value, formatOptions); - const secondValue = value + groupInterval[currentLevel]; + // @ts-expect-error a number column groups number values + const firstValue: number = value; + // @ts-expect-error getGroupInterval() types interval names and numbers as string[] + const interval: number = groupInterval[currentLevel]; + const secondValue = firstValue + interval; const secondValueText = gridCoreUtils.formatValue(secondValue, formatOptions); return firstValueText && secondValueText ? `${firstValueText} - ${secondValueText}` : ''; @@ -131,7 +166,7 @@ export class HeaderFilterController extends Modules.ViewController { private _headerFilterView!: HeaderFilterView; - public init() { + public init(): void { this._columnsController = this.getController('columns'); this._dataController = this.getController('data'); this.dataSourceController = this.getController('dataSource'); @@ -151,119 +186,139 @@ export class HeaderFilterController extends Modules.ViewController { return createHeaderFilterExpressions(columns, excludedColumn); } - private _updateSelectedState(items, column) { + private _updateSelectedState(items: HeaderFilterItem[], column: Column): void { let i = items.length; const isExclude = column.filterType === 'exclude'; - while (i--) { + while (i > 0) { + i -= 1; const item = items[i]; if ('items' in items[i]) { + // @ts-expect-error the 'items' in check doesn't narrow the optional items this._updateSelectedState(items[i].items, column); } - updateHeaderFilterItemSelectionState(item, gridCoreUtils.getIndexByKey(items[i].value, column.filterValues, null) > -1, isExclude); + updateHeaderFilterItemSelectionState( + item, + gridCoreUtils.getIndexByKey(items[i].value, column.filterValues, null) > -1, + isExclude, + ); } } - private _normalizeGroupItem(item, currentLevel, options) { - let value; - let displayValue; + private _normalizeGroupItem( + item: unknown, + currentLevel: number, + options: NormalizeGroupItemOptions, + ): HeaderFilterItem { const { path } = options; const { valueSelector } = options; const { displaySelector } = options; const { column } = options; - - if (valueSelector && displaySelector) { - value = valueSelector(item); - displayValue = displaySelector(item); - } else { - value = item.key; - displayValue = value; - } - - if (!isObject(item)) { - item = {}; - } else { - item = extend({}, item); - } + const value: unknown = valueSelector && displaySelector + ? valueSelector(item) + // @ts-expect-error without a lookup, the item is a GroupItem + : item.key; + const displayValue = valueSelector && displaySelector ? displaySelector(item) : value; + const normalizedItem: HeaderFilterItem = isObject(item) ? extend({}, item) : {}; path.push(value); if (path.length === 1) { // NOTE: Important! Deconstructing here causes a lot of failed usage scenarios. // eslint-disable-next-line prefer-destructuring - item.value = path[0]; + normalizedItem.value = path[0]; } else { - item.value = path.join('/'); + normalizedItem.value = path.join('/'); } - item.text = this.getHeaderItemText(displayValue, column, currentLevel, options.headerFilterOptions); + normalizedItem.text = this.getHeaderItemText( + displayValue, + column, + currentLevel, + options.headerFilterOptions, + ); - return item; + return normalizedItem; } // Used in the filter/m_filter_custom_operations as public method // Used in the private API WA [T1232532] - public getHeaderItemText(displayValue, column, currentLevel, headerFilterOptions) { - let text = gridCoreUtils.formatValue(displayValue, getFormatOptions(displayValue, column, currentLevel)); + public getHeaderItemText( + displayValue: unknown, + column: Column, + currentLevel: number, + headerFilterOptions: HeaderFilter | undefined, + ): string { + let text = gridCoreUtils.formatValue( + displayValue, + getFormatOptions(displayValue, column, currentLevel), + ); if (!text) { + // @ts-expect-error the default options provide texts text = headerFilterOptions.texts.emptyValue; } return text; } - private _processGroupItems(groupItems, currentLevel, path, options) { - const that = this; - let displaySelector; - let valueSelector; + private _processGroupItems( + groupItems: unknown[], + currentLevel: number | null, + path: unknown[] | null, + options: ProcessGroupItemsOptions, + ): void { const { column } = options; const { lookup } = column; const { level } = options; - - path = path || []; - currentLevel = currentLevel || 0; - - if (lookup) { - displaySelector = compileGetter(lookup.displayExpr); - valueSelector = compileGetter(lookup.valueExpr); - } - - for (let i = 0; i < groupItems.length; i++) { - groupItems[i] = that._normalizeGroupItem(groupItems[i], currentLevel, { + const itemsPath = path ?? []; + const itemsLevel = currentLevel ?? 0; + const displaySelector: HeaderFilterGroupSelector | undefined = lookup + // @ts-expect-error compileGetter() is overloaded and cannot pick one for a union argument + ? compileGetter(lookup.displayExpr) + : undefined; + const valueSelector: HeaderFilterGroupSelector | undefined = lookup + ? compileGetter(lookup.valueExpr) + : undefined; + + for (let i = 0; i < groupItems.length; i += 1) { + const item = this._normalizeGroupItem(groupItems[i], itemsLevel, { column: options.column, headerFilterOptions: options.headerFilterOptions, displaySelector, valueSelector, - path, + path: itemsPath, }); + groupItems[i] = item; - if ('items' in groupItems[i]) { - if (currentLevel === level || !isDefined(groupItems[i].value)) { - delete groupItems[i].items; + if (item.items !== undefined) { + if (itemsLevel === level || !isDefined(item.value)) { + delete item.items; } else { - that._processGroupItems(groupItems[i].items, currentLevel + 1, path, options); + this._processGroupItems(item.items, itemsLevel + 1, itemsPath, options); } } - path.pop(); + itemsPath.pop(); } } - public getDataSource(column) { + public getDataSource(column: HeaderFilterColumn): HeaderFilterDataSourceOptions | undefined { const dataSourceAdapter = this.dataSourceController.getAdapter(); const remoteGrouping = this.dataSourceController.remoteOperations().grouping; const group = getHeaderFilterGroupParameters(column, remoteGrouping); const headerFilterDataSource = column.headerFilter?.dataSource; const headerFilterOptions = this.option('headerFilter'); let isLookup = false; - const options: any = { + const options: HeaderFilterDataSourceArgs = { component: this.component, }; - if (!dataSourceAdapter) return; + if (!dataSourceAdapter) { + return undefined; + } if (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) { options.dataSource = normalizeDataSourceOptions(headerFilterDataSource); @@ -273,7 +328,11 @@ export class HeaderFilterController extends Modules.ViewController { if (this.option('syncLookupFilterValues')) { const filter = this._dataController.getCombinedFilterWithExcludedColumn(column); - options.dataSource = gridCoreUtils.getWrappedLookupDataSource(column, dataSourceAdapter, filter); + options.dataSource = gridCoreUtils.getWrappedLookupDataSource( + column, + dataSourceAdapter, + filter, + ); } else { options.dataSource = gridCoreUtils.normalizeLookupDataSource(column.lookup); } @@ -286,24 +345,28 @@ export class HeaderFilterController extends Modules.ViewController { filter, group, useDefaultSearch: true, - load: (options) => { - // @ts-expect-error Deferred ctor. - const d = new Deferred(); + load: (loadOptions: StoreLoadOptions): DeferredObj => { + const d = Deferred(); // TODO remove in 16.1 - options.dataField = column.dataField || column.name; + loadOptions.dataField = column.dataField || column.name; - dataSourceAdapter.customLoader.load(options).done(({ data }) => { - const convertUTCDates = remoteGrouping && isUTCFormat(column.serializationFormat) && cutoffLevel > 3; + dataSourceAdapter.customLoader.load(loadOptions).done(({ data }) => { + const convertUTCDates = remoteGrouping + && isUTCFormat(column.serializationFormat) + && cutoffLevel > 3; + let groupItems = data; if (convertUTCDates) { - // @ts-expect-error queryByOptions().toArray() is typed as unknown[] - data = convertDataFromUTCToLocal(data, column); + // @ts-expect-error customLoader returns groups here, but types them as rows + groupItems = convertDataFromUTCToLocal(data, column); } - that._processGroupItems(data, null, null, { + this._processGroupItems(groupItems, null, null, { level: cutoffLevel, column, headerFilterOptions, }); - d.resolve(data); + d.resolve(groupItems); + // eslint-disable-next-line @stylistic/max-len + // eslint-disable-next-line @typescript-eslint/no-misused-promises -- fail ignores the result }).fail(d.reject); return d; @@ -312,15 +375,21 @@ export class HeaderFilterController extends Modules.ViewController { } if (isFunction(headerFilterDataSource)) { + // @ts-expect-error the public d.ts types these options narrower than the grid builds them headerFilterDataSource.call(column, options); } const origPostProcess = options.dataSource.postProcess; + // eslint-disable-next-line @typescript-eslint/no-this-alias -- postProcess uses its own this const that = this; - options.dataSource.postProcess = function (data) { + options.dataSource.postProcess = function postProcess( + this: DataSource, + data: unknown[], + ): unknown[] { let items = data; if (isLookup) { + // @ts-expect-error TS doesn't link isLookup to column.lookup; lookup items are unknown items = items.filter((item) => item[column.lookup.valueExpr] !== null); if (this.pageIndex() === 0 && !this.searchValue()) { @@ -334,7 +403,8 @@ export class HeaderFilterController extends Modules.ViewController { }); } - items = origPostProcess && origPostProcess.call(this, items) || items; + items = origPostProcess?.call(this, items) ?? items; + // @ts-expect-error postProcess gets untyped items, but they are header filter items here that._updateSelectedState(items, column); return items; }; @@ -342,56 +412,64 @@ export class HeaderFilterController extends Modules.ViewController { return options.dataSource; } - public showHeaderFilterMenu(columnIndex, isGroupPanel) { + public showHeaderFilterMenu(columnIndex: number, isGroupPanel: boolean): void { const columnsController = this._columnsController; - const column = extend(true, {}, this._columnsController.getColumns()[columnIndex]); - if (column) { - const visibleIndex = columnsController.getVisibleIndex(columnIndex); - // TODO getView - const view = isGroupPanel ? this.getView('headerPanel') : this.getView('columnHeadersView'); - // @ts-expect-error getColumnElements() is typed as possibly undefined - const $columnElement = view.getColumnElements() - // @ts-expect-error a column shown in the group panel has a groupIndex - .eq(isGroupPanel ? column.groupIndex : visibleIndex); - - this.showHeaderFilterMenuBase({ - columnElement: $columnElement, - column, - applyFilter: true, - apply() { - columnsController.columnOption(columnIndex, { - filterValues: this.filterValues, - filterType: this.filterType, - }); - }, - }); + const column: Column = extend(true, {}, this._columnsController.getColumns()[columnIndex]); + + if (!column) { + return; } + + const visibleIndex = columnsController.getVisibleIndex(columnIndex); + // TODO getView + const view = isGroupPanel ? this.getView('headerPanel') : this.getView('columnHeadersView'); + // @ts-expect-error getColumnElements() is typed as possibly undefined + const $columnElement = view.getColumnElements() + // @ts-expect-error a column shown in the group panel has a groupIndex + .eq(isGroupPanel ? column.groupIndex : visibleIndex); + + this.showHeaderFilterMenuBase({ + columnElement: $columnElement, + // @ts-expect-error createColumn initializes calculateCellValue + column, + applyFilter: true, + apply() { + columnsController.columnOption(columnIndex, { + filterValues: this.filterValues, + filterType: this.filterType, + }); + }, + }); } - public showHeaderFilterMenuBase(options) { - const that = this; + public showHeaderFilterMenuBase(options: HeaderFilterMenuOptions): void { const { column } = options; if (column) { const groupInterval = filterUtils.getGroupInterval(column); - const remoteFiltering = that.dataSourceController.remoteOperations().filtering; + const remoteFiltering = this.dataSourceController.remoteOperations().filtering; const previousOnHidden = options.onHidden; + const type: HeaderFilterViewOptions['type'] = groupInterval && groupInterval.length > 1 + ? 'tree' + : 'list'; - extend(options, column, { - type: groupInterval && groupInterval.length > 1 ? 'tree' : 'list', + const viewOptions = extend(options, column, { + type, remoteFiltering, onShowing: (e) => { const dxResizableInstance = e.component.$overlayContent().dxResizable('instance'); - dxResizableInstance && dxResizableInstance.option('onResizeEnd', (e) => { - let headerFilterByColumn = this._columnsController.columnOption(options.dataField, 'headerFilter'); + if (dxResizableInstance) { + dxResizableInstance.option('onResizeEnd', (resizeEndEvent) => { + let headerFilterByColumn = this._columnsController.columnOption(options.dataField, 'headerFilter'); - headerFilterByColumn = headerFilterByColumn || {}; - headerFilterByColumn.width = e.width; - headerFilterByColumn.height = e.height; + headerFilterByColumn ??= {}; + headerFilterByColumn.width = resizeEndEvent.width; + headerFilterByColumn.height = resizeEndEvent.height; - this._columnsController.columnOption(options.dataField, 'headerFilter', headerFilterByColumn, true); - }); + this._columnsController.columnOption(options.dataField, 'headerFilter', headerFilterByColumn, true); + }); + } }, onHidden: () => { previousOnHidden?.(); @@ -399,71 +477,90 @@ export class HeaderFilterController extends Modules.ViewController { }, }); - options.dataSource = that.getDataSource(options); + viewOptions.dataSource = this.getDataSource(viewOptions); - if (options.isFilterBuilder) { - options.dataSource.filter = null; - options.alignment = 'right'; + if (viewOptions.isFilterBuilder) { + // @ts-expect-error getDataSource() returns undefined only for a grid without a data source + viewOptions.dataSource.filter = null; + viewOptions.alignment = 'right'; } - that._headerFilterView.showHeaderFilterMenu(options.columnElement, options); + this._headerFilterView.showHeaderFilterMenu(viewOptions.columnElement, viewOptions); } } - public hideHeaderFilterMenu() { + public hideHeaderFilterMenu(): void { this._headerFilterView.hideHeaderFilterMenu(); } } -const columnHeadersView = (Base: ModuleType) => class ColumnHeadersViewHeaderFilterExtender extends headerFilterMixin(Base) { - protected _renderCellContent($cell, options) { - const that = this; - let $headerFilterIndicator; +const columnHeadersView = ( + Base: ModuleType, +): ModuleType => class ColumnHeadersViewHeaderFilterExtender + extends headerFilterMixin(Base) { + protected _renderCellContent( + $cell: dxElementWrapper, + options: ViewCellOptions, + renderOptions: CellRenderOptions, + ): void { const { column } = options; - if (!column.command && allowHeaderFiltering(column) && that.option('headerFilter.visible') && options.rowType === 'header') { - $headerFilterIndicator = that._applyColumnState({ + if (!column.command && allowHeaderFiltering(column) && this.option('headerFilter.visible') && options.rowType === 'header') { + const $headerFilterIndicator = this._applyColumnState({ name: 'headerFilter', rootElement: $cell, column, - showColumnLines: that.option('showColumnLines'), + showColumnLines: this.option('showColumnLines'), }); - $headerFilterIndicator && that._subscribeToIndicatorEvent($headerFilterIndicator, column, 'headerFilter'); + if ($headerFilterIndicator) { + this._subscribeToIndicatorEvent($headerFilterIndicator, column, 'headerFilter'); + } } - super._renderCellContent.apply(this, arguments as any); + super._renderCellContent($cell, options, renderOptions); } - private _subscribeToIndicatorEvent($indicator, column, indicatorName) { + private _subscribeToIndicatorEvent( + $indicator: dxElementWrapper, + column: Column, + indicatorName: string, + ): void { if (indicatorName === 'headerFilter') { eventsEngine.on($indicator, clickEventName, this.createAction((e) => { e.event.stopPropagation(); saveFocusedElementInfo($indicator, this); + // @ts-expect-error updateColumnIndexes sets the index of every column this._headerFilterController.showHeaderFilterMenu(column.index, false); })); } } - protected _updateIndicator($cell, column, indicatorName) { + protected _updateIndicator( + $cell: dxElementWrapper, + column: Column, + indicatorName: string, + ): dxElementWrapper | undefined { const $indicator = super._updateIndicator($cell, column, indicatorName); - $indicator && this._subscribeToIndicatorEvent($indicator, column, indicatorName); + if ($indicator) { + this._subscribeToIndicatorEvent($indicator, column, indicatorName); + } return undefined; } - private _updateHeaderFilterIndicators() { + private _updateHeaderFilterIndicators(): void { if (this.option('headerFilter.visible')) { this._updateIndicators('headerFilter'); } } - private _needUpdateFilterIndicators() { + private _needUpdateFilterIndicators(): boolean { return true; } - protected _columnOptionChanged(e) { + protected _columnOptionChanged(e: ColumnsChanges): void { const { optionNames } = e; const isFilterRowAndHeaderFilterValuesChanged = gridCoreUtils.checkChanges(optionNames, ['filterValues', 'filterValue']); const isHeaderFilterValuesAndTypeChanged = gridCoreUtils.checkChanges(optionNames, ['filterValues', 'filterType']); @@ -482,19 +579,23 @@ const columnHeadersView = (Base: ModuleType) => class ColumnH } }; -const headerPanel = (Base: ModuleType) => class HeaderPanelHeaderFilterExtender extends headerFilterMixin(Base) { - private _createGroupPanelItem($rootElement, groupColumn) { - const that = this; +const headerPanel = ( + Base: ModuleType, +): ModuleType => class HeaderPanelHeaderFilterExtender + extends headerFilterMixin(Base) { + private _createGroupPanelItem( + $rootElement: dxElementWrapper, + groupColumn: Column, + ): dxElementWrapper { // @ts-expect-error DataGrid's method - const $item = super._createGroupPanelItem.apply(that, arguments); - let $headerFilterIndicator; + const $item: dxElementWrapper = super._createGroupPanelItem($rootElement, groupColumn); - if (!groupColumn.command && allowHeaderFiltering(groupColumn) && that.option('headerFilter.visible')) { - $headerFilterIndicator = that._applyColumnState({ + if (!groupColumn.command && allowHeaderFiltering(groupColumn) && this.option('headerFilter.visible')) { + const $headerFilterIndicator = this._applyColumnState({ name: 'headerFilter', rootElement: $item, column: { - alignment: getDefaultAlignment(that.option('rtlEnabled')), + alignment: getDefaultAlignment(this.option('rtlEnabled')), filterValues: groupColumn.filterValues, allowHeaderFiltering: true, caption: groupColumn.caption, @@ -502,12 +603,15 @@ const headerPanel = (Base: ModuleType) => class HeaderPanelHeaderFi showColumnLines: true, }); - $headerFilterIndicator && eventsEngine.on($headerFilterIndicator, clickEventName, that.createAction((e) => { - const { event } = e; + if ($headerFilterIndicator) { + eventsEngine.on($headerFilterIndicator, clickEventName, this.createAction((e) => { + const { event } = e; - event.stopPropagation(); - this._headerFilterController.showHeaderFilterMenu(groupColumn.index, true); - })); + event.stopPropagation(); + // @ts-expect-error updateColumnIndexes sets the index of every column + this._headerFilterController.showHeaderFilterMenu(groupColumn.index, true); + })); + } } return $item; @@ -515,7 +619,7 @@ const headerPanel = (Base: ModuleType) => class HeaderPanelHeaderFi }; export const headerFilterModule = { - defaultOptions() { + defaultOptions(): Pick { return { syncLookupFilterValues: true, headerFilter: { diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts index 6e36e99932f3..5760e03102f2 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts @@ -6,12 +6,20 @@ import type { HeaderFilter, HeaderFilterTexts, } from '@js/common/grids'; -import type { DataSourceLike } from '@js/data/data_source'; +import type { dxElementWrapper } from '@js/core/renderer'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import type { DataSourceLike, DataSourceOptionsStub } from '@js/data/data_source'; import type { ShowingEvent } from '@js/ui/popup'; +import type { NormalizedDataSourceOptions, StoreLoadOptions } from '@ts/data/data_source/types'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type { InternalGrid } from '@ts/grids/grid_core/types'; export type EmptyDateValue = '' | false; +export type DateIntervalFormat = (value: number) => string; + +export type HeaderFilterColumn = Column & Required>; + export type HeaderFilterGroupSelector = (data: unknown) => unknown; export interface HeaderFilterGroupDescriptor { @@ -34,6 +42,42 @@ export interface HeaderFilterItem { items?: HeaderFilterItem[]; } +export interface ProcessGroupItemsOptions { + level: number; + column: Column; + headerFilterOptions?: HeaderFilter; +} + +export interface NormalizeGroupItemOptions + extends Pick { + path: unknown[]; + valueSelector?: HeaderFilterGroupSelector; + displaySelector?: HeaderFilterGroupSelector; +} + +export interface HeaderFilterMenuOptions + extends Pick< + HeaderFilterViewOptions, + 'apply' | 'onHidden' | 'isFilterBuilder' | 'dataSource' | 'dataField' + > { + columnElement: dxElementWrapper; + column?: HeaderFilterColumn; + applyFilter?: boolean; +} + +export interface HeaderFilterDataSourceOptions + extends Omit { + group?: DataSourceOptionsStub['group'] | HeaderFilterGroup; + store?: DataSourceOptionsStub['store'] | NormalizedDataSourceOptions['store']; + useDefaultSearch?: boolean; + load?: (loadOptions: StoreLoadOptions) => DeferredObj; +} + +export interface HeaderFilterDataSourceArgs { + component: InternalGrid; + dataSource?: HeaderFilterDataSourceOptions; +} + export type HeaderFilterValues = unknown[]; export type HeaderFilterOptions = HeaderFilter @@ -52,7 +96,7 @@ export interface HeaderFilterViewOptions { filterType?: FilterType; filterValues?: HeaderFilterValues | null; headerFilter?: Partial; - dataSource?: DataSourceLike; + dataSource?: DataSourceLike | HeaderFilterDataSourceOptions; lookup?: Column['lookup']; dataField?: string; selector?: Column['selector']; diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/utils.ts index 7c8098a36433..f656583fa426 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/utils.ts @@ -7,7 +7,9 @@ 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/utils'; -import type { EmptyDateValue, HeaderFilterGroup, HeaderFilterGroupItem } from './types'; +import type { + EmptyDateValue, HeaderFilterColumn, HeaderFilterGroup, HeaderFilterGroupItem, +} from './types'; export const allowHeaderFiltering = ( column: Column, @@ -94,7 +96,7 @@ const DATE_INTERVAL_SELECTORS = { }; const getIntervalSelector = function getIntervalSelector( - this: Column & Required>, + this: HeaderFilterColumn, interval: HeaderFilterGroupInterval | number, data: unknown, ): number | EmptyDateValue | null | undefined { @@ -117,7 +119,7 @@ const getIntervalSelector = function getIntervalSelector( }; export function getHeaderFilterGroupParameters( - column: Column & Required>, + column: HeaderFilterColumn, remoteGrouping?: boolean, ): HeaderFilterGroup { const dataField = column.dataField || column.name; From 19a9d7b18fb27bcf6b42ebaf290eaf16c3f6ad4f Mon Sep 17 00:00:00 2001 From: Aliar666 Date: Sat, 24 Oct 2026 02:43:19 +0400 Subject: [PATCH 3/3] Fix comments --- .../grids/data_grid/grouping/m_grouping.ts | 9 ++--- .../header_filter/m_header_filter.ts | 37 +++++++++---------- .../grids/grid_core/header_filter/types.ts | 5 ++- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts index 17f8602911ab..9bcc2666dc8d 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts @@ -274,14 +274,13 @@ export const GroupingHeaderPanelExtender = ( const $target = $(event.target); const groupColumnIndex = $target.closest(`.${CLASSES.groupPanelItem}`).index(); const column = this._columnsController.getGroupColumns()[groupColumnIndex]; - const columnIndex = column && column.index; + const columnIndex = column?.index; - if ($target.is(HEADER_FILTER_CLASS_SELECTOR)) { - // @ts-expect-error updateColumnIndexes sets the index of every column - this._headerFilterController.showHeaderFilterMenu(columnIndex, true); - } else { + if (!$target.is(HEADER_FILTER_CLASS_SELECTOR)) { // @ts-expect-error this._processGroupItemAction(columnIndex); + } else if (isDefined(columnIndex)) { + this._headerFilterController.showHeaderFilterMenu(columnIndex, true); } event.preventDefault(); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index ded5e1320146..954d9f5744f1 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -257,8 +257,7 @@ export class HeaderFilterController extends Modules.ViewController { ); if (!text) { - // @ts-expect-error the default options provide texts - text = headerFilterOptions.texts.emptyValue; + text = headerFilterOptions?.texts?.emptyValue ?? ''; } return text; @@ -275,10 +274,10 @@ export class HeaderFilterController extends Modules.ViewController { const { level } = options; const itemsPath = path ?? []; const itemsLevel = currentLevel ?? 0; - const displaySelector: HeaderFilterGroupSelector | undefined = lookup - // @ts-expect-error compileGetter() is overloaded and cannot pick one for a union argument - ? compileGetter(lookup.displayExpr) - : undefined; + const displayExpr = lookup?.displayExpr; + const displaySelector: HeaderFilterGroupSelector | undefined = isFunction(displayExpr) + ? displayExpr + : lookup && compileGetter(displayExpr); const valueSelector: HeaderFilterGroupSelector | undefined = lookup ? compileGetter(lookup.valueExpr) : undefined; @@ -365,9 +364,7 @@ export class HeaderFilterController extends Modules.ViewController { headerFilterOptions, }); d.resolve(groupItems); - // eslint-disable-next-line @stylistic/max-len - // eslint-disable-next-line @typescript-eslint/no-misused-promises -- fail ignores the result - }).fail(d.reject); + }).fail(d.reject as (...args: unknown[]) => void); return d; }, @@ -403,7 +400,7 @@ export class HeaderFilterController extends Modules.ViewController { }); } - items = origPostProcess?.call(this, items) ?? items; + items = origPostProcess?.call(this, items) || items; // @ts-expect-error postProcess gets untyped items, but they are header filter items here that._updateSelectedState(items, column); return items; @@ -416,10 +413,6 @@ export class HeaderFilterController extends Modules.ViewController { const columnsController = this._columnsController; const column: Column = extend(true, {}, this._columnsController.getColumns()[columnIndex]); - if (!column) { - return; - } - const visibleIndex = columnsController.getVisibleIndex(columnIndex); // TODO getView const view = isGroupPanel ? this.getView('headerPanel') : this.getView('columnHeadersView'); @@ -530,8 +523,10 @@ const columnHeadersView = ( eventsEngine.on($indicator, clickEventName, this.createAction((e) => { e.event.stopPropagation(); saveFocusedElementInfo($indicator, this); - // @ts-expect-error updateColumnIndexes sets the index of every column - this._headerFilterController.showHeaderFilterMenu(column.index, false); + + if (isDefined(column.index)) { + this._headerFilterController.showHeaderFilterMenu(column.index, false); + } })); } } @@ -583,11 +578,11 @@ const headerPanel = ( Base: ModuleType, ): ModuleType => class HeaderPanelHeaderFilterExtender extends headerFilterMixin(Base) { - private _createGroupPanelItem( + protected _createGroupPanelItem( $rootElement: dxElementWrapper, groupColumn: Column, ): dxElementWrapper { - // @ts-expect-error DataGrid's method + // @ts-expect-error _createGroupPanelItem is defined by DataGrid grouping const $item: dxElementWrapper = super._createGroupPanelItem($rootElement, groupColumn); if (!groupColumn.command && allowHeaderFiltering(groupColumn) && this.option('headerFilter.visible')) { @@ -608,8 +603,10 @@ const headerPanel = ( const { event } = e; event.stopPropagation(); - // @ts-expect-error updateColumnIndexes sets the index of every column - this._headerFilterController.showHeaderFilterMenu(groupColumn.index, true); + + if (isDefined(groupColumn.index)) { + this._headerFilterController.showHeaderFilterMenu(groupColumn.index, true); + } })); } } diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts index 5760e03102f2..967e5ad8d18a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts @@ -48,8 +48,9 @@ export interface ProcessGroupItemsOptions { headerFilterOptions?: HeaderFilter; } -export interface NormalizeGroupItemOptions - extends Pick { +export interface NormalizeGroupItemOptions { + column: Column; + headerFilterOptions?: HeaderFilter; path: unknown[]; valueSelector?: HeaderFilterGroupSelector; displaySelector?: HeaderFilterGroupSelector;