From 40293f7740a7e07edf51e37ba8bfe1e72b4dc8e2 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 6 Oct 2026 23:07:24 +0400 Subject: [PATCH 01/11] Grids: Type part 1 of grid_core m_utils --- .../js/__internal/grids/grid_core/m_utils.ts | 182 +++++++++++------- .../js/__internal/grids/grid_core/types.ts | 34 +++- .../__internal/grids/grid_core/views/utils.ts | 2 +- 3 files changed, 147 insertions(+), 71 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 74bb46bdbc62..a2dc8a52a767 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -2,9 +2,13 @@ import eventsEngine from '@js/common/core/events/core/events_engine'; import dateLocalization from '@js/common/core/localization/date'; +import type { GroupDescriptor } from '@js/common/data'; import DataSource from '@js/common/data/data_source'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import { normalizeSortingInfo as normalizeSortingInfoUtility } from '@js/common/data/utils'; +import type { HeaderFilterGroupInterval } from '@js/common/grids'; +import { data as elementData } from '@js/core/element_data'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { equalByValue } from '@js/core/utils/common'; import { Deferred, when } from '@js/core/utils/deferred'; @@ -22,8 +26,16 @@ import LoadPanel from '@js/ui/load_panel'; import sharedFiltering from '@js/ui/shared/filtering'; import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; -import type { Column } from '@ts/grids/grid_core/columns_controller/types'; -import type { ColumnPoint, SelectionRange } from '@ts/grids/grid_core/types'; +import type { SortingInfo } from '@ts/data/utils'; +import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_controller/types'; +import type { + ColumnPoint, + FormatOptions, + LoadPanelPosition, + SelectionRange, + SummaryTextItem, + WidgetElementData, +} from '@ts/grids/grid_core/types'; import { AI_COLUMN_NAME } from './ai_column/const'; import type DataSourceAdapter from './data_source_adapter/data_source_adapter'; @@ -43,25 +55,25 @@ const LEGACY_SCROLLING_MODE = 'scrolling.legacyMode'; const SCROLLING_MODE_OPTION = 'scrolling.mode'; const ROW_RENDERING_MODE_OPTION = 'scrolling.rowRenderingMode'; const DATE_INTERVAL_SELECTORS = { - year(value) { + year(value: Date): number { return value && value.getFullYear(); }, - month(value) { + month(value: Date): number { return value && (value.getMonth() + 1); }, - day(value) { + day(value: Date): number { return value && value.getDate(); }, - quarter(value) { + quarter(value: Date): number { return value && (Math.floor(value.getMonth() / 3) + 1); }, - hour(value) { + hour(value: Date): number { return value && value.getHours(); }, - minute(value) { + minute(value: Date): number { return value && value.getMinutes(); }, - second(value) { + second(value: Date): number { return value && value.getSeconds(); }, }; @@ -72,19 +84,24 @@ export function isDateType(dataType: string | undefined): boolean { return dataType === 'date' || dataType === 'datetime'; } -const getIntervalSelector = function () { - const data = arguments[1]; +const getIntervalSelector = function getIntervalSelector( + this: Column & Required>, + interval: HeaderFilterGroupInterval | number, + data: unknown, +): number | null | undefined { const value = this.calculateCellValue(data); if (!isDefined(value)) { return null; } if (isDateType(this.dataType)) { - const nameIntervalSelector = arguments[0]; + const nameIntervalSelector = interval as HeaderFilterGroupInterval; return DATE_INTERVAL_SELECTORS[nameIntervalSelector](value); } if (this.dataType === 'number') { - const groupInterval = arguments[0]; + const groupInterval = interval as number; return Math.floor(Number(value) / groupInterval) * groupInterval; } + + return undefined; }; const getGlobalFormat = (dataType: string): Format | undefined => { @@ -104,27 +121,31 @@ const getGlobalFormat = (dataType: string): Format | undefined => { : globalFormat; }; -const setEmptyText = function ($container) { +const setEmptyText = ($container: dxElementWrapper): void => { $container.get(0).textContent = '\u00A0'; }; -const normalizeSortingInfo = function (sort) { - sort = sort || []; - const result = normalizeSortingInfoUtility(sort); +const normalizeSortingInfo = (sort: unknown): SortingInfo[] => { + const sortItems = sort || []; + const result = normalizeSortingInfoUtility(sortItems); - for (let i = 0; i < sort.length; i++) { - if (sort && sort[i] && sort[i].isExpanded !== undefined) { - result[i].isExpanded = sort[i].isExpanded; - } - if (sort && sort[i] && sort[i].groupInterval !== undefined) { - result[i].groupInterval = sort[i].groupInterval; + if (Array.isArray(sortItems)) { + for (let i = 0; i < sortItems.length; i += 1) { + if (sortItems?.[i]?.isExpanded !== undefined) { + result[i].isExpanded = sortItems[i].isExpanded; + } + if (sortItems?.[i]?.groupInterval !== undefined) { + result[i].groupInterval = sortItems[i].groupInterval; + } } } return result; }; -const formatValue = function (value, options) { - const valueText = formatHelper.format(value, options.format) || (value && value.toString()) || ''; +const formatValue = (value: unknown, options: FormatOptions): string => { + // @ts-expect-error typings of format() and of this || chain expect a primitive value + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- the value can be of any type + const valueText: string = formatHelper.format(value, options.format) || (value && value.toString()) || ''; const formatObject = { value, valueText: options.getDisplayFormat ? options.getDisplayFormat(valueText) : valueText, @@ -132,27 +153,36 @@ const formatValue = function (value, options) { groupInterval: options.groupInterval, }; - return options.customizeText ? options.customizeText.call(options, formatObject) : formatObject.valueText; + return options.customizeText + ? options.customizeText.call(options, formatObject) + : formatObject.valueText; }; -const getSummaryText = function (summaryItem, summaryTexts) { +const getSummaryText = ( + summaryItem: SummaryTextItem, + summaryTexts: Record, +): string => { const displayFormat = summaryItem.displayFormat || (summaryItem.columnCaption && summaryTexts[`${summaryItem.summaryType}OtherColumn`]) || summaryTexts[summaryItem.summaryType]; return formatValue(summaryItem.value, { format: summaryItem.valueFormat, getDisplayFormat(valueText) { - return displayFormat ? format(displayFormat, valueText, summaryItem.columnCaption) : valueText; + return displayFormat + ? format(displayFormat, valueText, summaryItem.columnCaption) + : valueText; }, customizeText: summaryItem.customizeText, }); }; -const getWidgetInstance = function ($element) { - const editorData = $element?.data(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- callers use their editor's API +const getWidgetInstance = ($element: dxElementWrapper | undefined): any => { + const element = $element?.get(0); + const editorData: WidgetElementData | undefined = element && elementData(element); const dxComponents = editorData?.dxComponents; const widgetName = dxComponents?.[0]; - return widgetName && editorData[widgetName]; + return widgetName && editorData?.[widgetName]; }; const createPoint = (options: T): ColumnPoint => ({ @@ -162,7 +192,11 @@ const createPoint = (options: T): ColumnPoint => ({ y: options.y, }); -const addPointIfNeed = (points: ColumnPoint[], pointProps: T, pointCreated: (point: T) => boolean): void => { +const addPointIfNeed = ( + points: ColumnPoint[], + pointProps: T, + pointCreated: (point: T) => boolean, +): void => { let notCreatePoint = false; if (pointCreated) { @@ -184,16 +218,16 @@ const getColumnWidths = (columns: Column[]): number[] => columns return isNumeric(width) ? parseFloat(width as string) : DEFAULT_COLUMN_WIDTH; }); -function normalizeGroupingLoadOptions(group) { - if (!Array.isArray(group)) { - group = [group]; - } +function normalizeGroupingLoadOptions( + group: GroupDescriptor | GroupDescriptor[], +): GroupDescriptor[] { + const groups = Array.isArray(group) ? group : [group]; - return group.map((item, i) => { + return groups.map((item, i) => { if (isString(item)) { return { selector: item, - isExpanded: i < group.length - 1, + isExpanded: i < groups.length - 1, }; } @@ -202,18 +236,18 @@ function normalizeGroupingLoadOptions(group) { } export default { - renderNoDataText($element) { - const that = this; - $element = $element || this.element(); + renderNoDataText($container?: dxElementWrapper): void { + const $element = $container || this.element(); if (!$element) { return; } - const noDataClass = that.addWidgetPrefix(NO_DATA_CLASS); + const noDataClass = this.addWidgetPrefix(NO_DATA_CLASS); let noDataElement = $element.find(`.${noDataClass}`).last(); const isVisible = this._dataController.isEmpty(); - const isDefaultLoading = this._dataController.isLoading() && !this._dataController.isCustomLoading?.(); + const isDefaultLoading = this._dataController.isLoading() + && !this._dataController.isCustomLoading?.(); if (!noDataElement.length) { noDataElement = $('') @@ -227,19 +261,22 @@ export default { if (isVisible && !isDefaultLoading) { noDataElement .removeClass('dx-hidden') - .text(that._getNoDataText()); + .text(this._getNoDataText()); } else { noDataElement .addClass('dx-hidden'); } }, - renderLoadPanel($element, $container, isLocalStore) { - const that = this; - let loadPanelOptions; - - that._loadPanel && that._loadPanel.$element().remove(); - loadPanelOptions = that.option('loadPanel'); + renderLoadPanel( + $element: dxElementWrapper, + $container: dxElementWrapper, + isLocalStore?: boolean, + ): void { + if (this._loadPanel) { + this._loadPanel.$element().remove(); + } + let loadPanelOptions = this.option('loadPanel'); if (loadPanelOptions && (loadPanelOptions.enabled === 'auto' ? !isLocalStore : loadPanelOptions.enabled)) { loadPanelOptions = extend({ @@ -249,13 +286,13 @@ export default { zIndex: BASE_LOAD_PANEL_Z_INDEX, }, loadPanelOptions); - that._loadPanel = that._createComponent($('
').appendTo($container), LoadPanel, loadPanelOptions); + this._loadPanel = this._createComponent($('
').appendTo($container), LoadPanel, loadPanelOptions); } else { - that._loadPanel = null; + this._loadPanel = null; } }, - calculateLoadPanelPosition($element) { + calculateLoadPanelPosition($element: dxElementWrapper | undefined): LoadPanelPosition { const $window = $(getWindow()); if (getHeight($element) > getHeight($window)) { return { @@ -267,13 +304,13 @@ export default { return { of: $element }; }, - getIndexByKey(key, items, keyName?) { + getIndexByKey(key: unknown, items: unknown, keyName?: string | string[] | null): number { let index = -1; if (key !== undefined && Array.isArray(items)) { - keyName = arguments.length <= 2 ? 'key' : keyName; - for (let i = 0; i < items.length; i++) { - const item = isDefined(keyName) ? items[i][keyName] : items[i]; + const keyField = arguments.length <= 2 ? 'key' : keyName; + for (let i = 0; i < items.length; i += 1) { + const item = isDefined(keyField) ? items[i][String(keyField)] : items[i]; if (equalByValue(key, item)) { index = i; @@ -285,12 +322,12 @@ export default { return index; }, - checkChanges(changes, changeNames) { + checkChanges(changes: ColumnsChanges['optionNames'], changeNames: string[]): number | boolean { let changesWithChangeNamesCount = 0; - for (let i = 0; i < changeNames.length; i++) { - if (changes[changeNames[i]]) { - changesWithChangeNamesCount++; + for (const changeName of changeNames) { + if (changes[changeName]) { + changesWithChangeNamesCount += 1; } } @@ -299,7 +336,10 @@ export default { formatValue, - getFormatOptionsByColumn(column, target) { + getFormatOptionsByColumn( + column: Column & Pick, + target: string, + ): FormatOptions { return { format: column.format, getDisplayFormat: column.getDisplayFormat, @@ -310,11 +350,13 @@ export default { }; }, - getDisplayValue(column, value, data, rowType?) { - if (column.displayValueMap && column.displayValueMap[value] !== undefined) { - return column.displayValueMap[value]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- callers use it as their own type + getDisplayValue(column: Column, value: unknown, data: unknown, rowType?: string): any { + if (column.displayValueMap?.[String(value)] !== undefined) { + return column.displayValueMap[String(value)]; } if (column.calculateDisplayValue && data && rowType !== 'group') { + // @ts-expect-error after columnOption it can still be a string, and the call throws return column.calculateDisplayValue(data); } @@ -323,18 +365,22 @@ export default { && (rowType !== 'group' || (!column.calculateGroupValue && !column.calculateDisplayValue)); if (isCalculatedFromLookup) { + // @ts-expect-error the lookup is checked above and gets calculateCellValue on init return column.lookup.calculateCellValue(value); } return value; }, - getGroupRowSummaryText(summaryItems, summaryTexts) { + getGroupRowSummaryText( + summaryItems: SummaryTextItem[], + summaryTexts: Record | undefined, + ): string { let result = '('; - for (let i = 0; i < summaryItems.length; i++) { + for (let i = 0; i < summaryItems.length; i += 1) { const summaryItem = summaryItems[i]; - result += (i > 0 ? ', ' : '') + getSummaryText(summaryItem, summaryTexts); + result += (i > 0 ? ', ' : '') + getSummaryText(summaryItem, summaryTexts ?? {}); } // eslint-disable-next-line no-return-assign return result += ')'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/types.ts b/packages/devextreme/js/__internal/grids/grid_core/types.ts index 7db81a500ce8..618f3e45cf86 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/types.ts @@ -1,12 +1,17 @@ /* eslint-disable spellcheck/spell-checker */ import type { - GridBase, GridBaseOptions, SelectionBase, + ColumnCustomizeTextArg, GridBase, GridBaseOptions, SelectionBase, } from '@js/common/grids'; import type { PropertyType } from '@js/core'; import type { Component } from '@js/core/component'; import type { dxElementWrapper } from '@js/core/renderer'; import type { DeferredObj } from '@js/core/utils/deferred'; -import type { Properties as DataGridOptions, Scrolling as DataGridScrolling } from '@js/ui/data_grid'; +import type { Format } from '@js/localization'; +import type { + Properties as DataGridOptions, + Scrolling as DataGridScrolling, + SummaryTotalItem, +} from '@js/ui/data_grid'; import type { Properties as TreeListdOptions, Scrolling as TreeListScrolling } from '@js/ui/tree_list'; import type Widget from '@js/ui/widget/ui.widget'; import type { IntegrationOptions } from '@ts/core/template_manager'; @@ -33,6 +38,31 @@ export interface SelectionRange { selectionEnd: number; } +export interface FormatOptions { + format?: Format; + getDisplayFormat?: (valueText: string) => string; + customizeText?: (cellInfo: ColumnCustomizeTextArg & { valueText: string }) => string; + target?: string; + groupInterval?: string | number; + trueText?: string; + falseText?: string; +} + +/** @architectureLeak data_grid/summary: core renders summary texts in group rows */ +export interface SummaryTextItem extends Pick { + summaryType: string; + value?: unknown; + columnCaption?: string; +} + +export interface LoadPanelPosition { + of: dxElementWrapper | undefined; + boundary?: dxElementWrapper; + collision?: 'fit'; +} + +export type WidgetElementData = Record & { dxComponents?: string[] }; + type OptionsMethod = (() => TOptions) & ((options: TOptions) => void) & ( diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/utils.ts b/packages/devextreme/js/__internal/grids/grid_core/views/utils.ts index 558ffbcafddc..dc16289586f6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/utils.ts @@ -11,7 +11,7 @@ export const getCellText = ( displayValue: unknown, ): string => ( columnHasValue(column) - ? gridCoreUtils.formatValue(displayValue, column) as string + ? gridCoreUtils.formatValue(displayValue, column) : '' ); From dd9de4c8acc77f80e2eed5dcb42107ca4bd636c1 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 8 Oct 2026 00:29:02 +0400 Subject: [PATCH 02/11] Grids: Type part 2 of grid_core m_utils --- .../header_filter/__tests__/utils.test.ts | 84 +++++- .../js/__internal/grids/grid_core/m_utils.ts | 246 ++++++++++++------ .../grids/grid_core/modules/modules.ts | 2 +- .../js/__internal/grids/grid_core/types.ts | 51 ++++ 4 files changed, 294 insertions(+), 89 deletions(-) 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 8beb0d1be88f..93470bc8c7b5 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 @@ -1,5 +1,11 @@ -import { describe, expect, it } from '@jest/globals'; +import { + describe, + expect, + it, + jest, +} from '@jest/globals'; import filterUtils from '@js/ui/shared/filtering'; +import type { HeaderFilterGroupSelector } from '@ts/grids/grid_core/types'; import gridCoreUtils from '../../m_utils'; import { getFormatOptions } from '../m_header_filter'; @@ -66,11 +72,15 @@ describe('getGroupInterval', () => { }); }); +type HeaderFilterColumn = Parameters[0]; + +const asHeaderFilterColumn = (column: object): HeaderFilterColumn => column as HeaderFilterColumn; + describe('getHeaderFilterGroupParameters', () => { it('builds one remote group parameter per level, expanding all but the last', () => { - const column = { + const column = asHeaderFilterColumn({ dataField: 'Price', dataType: 'number', headerFilter: { groupInterval: [100, 10] }, - }; + }); expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ { selector: 'Price', groupInterval: 100, isExpanded: true }, @@ -79,11 +89,11 @@ describe('getHeaderFilterGroupParameters', () => { }); it('supports an arbitrary number of levels', () => { - const column = { + const column = asHeaderFilterColumn({ dataField: 'Population', dataType: 'number', headerFilter: { groupInterval: [100000, 10000, 1000] }, - }; + }); expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ { selector: 'Population', groupInterval: 100000, isExpanded: true }, @@ -93,20 +103,76 @@ describe('getHeaderFilterGroupParameters', () => { }); it('builds a local interval selector per level that buckets the cell value', () => { - const column = { + const column = asHeaderFilterColumn({ dataField: 'Price', dataType: 'number', headerFilter: { groupInterval: [100, 10] }, - calculateCellValue: (data) => data.Price, - }; + calculateCellValue: (data: { Price: number }) => data.Price, + }); - const params = gridCoreUtils.getHeaderFilterGroupParameters(column); + const params = gridCoreUtils.getHeaderFilterGroupParameters( + column, + ) as HeaderFilterGroupSelector[]; expect(params).toHaveLength(2); // 753 -> floor(753 / 100) * 100 on the outer level, floor(753 / 10) * 10 on the inner level expect(params[0]({ Price: 753 })).toBe(700); expect(params[1]({ Price: 753 })).toBe(750); }); + + describe('when the column has no group interval', () => { + it('should group remotely by the data field', () => { + const column = asHeaderFilterColumn({ dataField: 'Name', dataType: 'string' }); + + expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ + { selector: 'Name', isExpanded: false }, + ]); + }); + + it('should group remotely by the column name when there is no data field', () => { + const column = asHeaderFilterColumn({ name: 'nameColumn', dataType: 'string' }); + + expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ + { selector: 'nameColumn', isExpanded: false }, + ]); + }); + + it('should return a local selector that turns empty values into null', () => { + const column = asHeaderFilterColumn({ + dataField: 'Name', + dataType: 'string', + calculateCellValue: (data: { Name?: unknown }) => data.Name, + }); + + const selector = gridCoreUtils.getHeaderFilterGroupParameters( + column, + ) as HeaderFilterGroupSelector; + + expect(selector({ Name: 'a' })).toBe('a'); + expect(selector({ Name: 0 })).toBe(0); + expect(selector({ Name: '' })).toBeNull(); + expect(selector({})).toBeNull(); + }); + + it('should pair the local selector with the bound sorting method', () => { + const sortingMethod = jest.fn((value1: number, value2: number) => value1 - value2); + const column = asHeaderFilterColumn({ + dataField: 'Name', + dataType: 'string', + calculateCellValue: (data: { Name?: unknown }) => data.Name, + sortingMethod, + }); + + const [descriptor] = gridCoreUtils.getHeaderFilterGroupParameters(column) as { + selector: HeaderFilterGroupSelector; + compare: (value1: unknown, value2: unknown) => number; + }[]; + + expect(descriptor.selector({ Name: '' })).toBeNull(); + expect(descriptor.compare(3, 1)).toBe(2); + expect(sortingMethod.mock.contexts[0]).toBe(column); + }); + }); }); describe('getFormatOptions', () => { diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index a2dc8a52a767..634dfd626ebc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -10,7 +10,9 @@ import type { HeaderFilterGroupInterval } from '@js/common/grids'; import { data as elementData } from '@js/core/element_data'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; +import type { BrowserInfo } from '@js/core/utils/browser'; import { equalByValue } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { each } from '@js/core/utils/iterator'; @@ -26,20 +28,33 @@ import LoadPanel from '@js/ui/load_panel'; import sharedFiltering from '@js/ui/shared/filtering'; import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; +import type { NormalizedDataSourceOptions, StoreLoadOptions } from '@ts/data/data_source/types'; import type { SortingInfo } from '@ts/data/utils'; import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnPoint, + ColumnPointProps, + ExpandCellTemplate, + ExpandCellTemplateOptions, FormatOptions, + HeaderFilterGroup, + HeaderFilterGroupItem, LoadPanelPosition, + LookupDataSource, + OptionsReader, SelectionRange, SummaryTextItem, + TextSelectionElement, WidgetElementData, + WrappedLookupDataSource, } from '@ts/grids/grid_core/types'; import { AI_COLUMN_NAME } from './ai_column/const'; import type DataSourceAdapter from './data_source_adapter/data_source_adapter'; +import type { RawItemData } from './data_source_adapter/types'; +import type { DataFilter } from './filter/types'; import { combineFilters } from './filter/utils'; +import type { ModuleItem } from './modules/modules'; import { isEqualSelectors, isSelectorEqualWithCallback } from './utils/index'; const BASE_LOAD_PANEL_Z_INDEX = 1000; @@ -390,7 +405,7 @@ export default { normalizeSortingInfo, - getFormatByDataType(dataType) { + getFormatByDataType(dataType: string | undefined): Format | undefined { switch (dataType) { case 'date': return getGlobalFormat('date') || 'shortDate'; @@ -401,12 +416,16 @@ export default { } }, - getHeaderFilterGroupParameters(column, remoteGrouping?) { - let result: any = []; + getHeaderFilterGroupParameters( + column: Column & Required>, + remoteGrouping?: boolean, + ): HeaderFilterGroup { const dataField = column.dataField || column.name; const groupInterval = sharedFiltering.getGroupInterval(column); if (groupInterval) { + const result: HeaderFilterGroupItem[] = []; + each(groupInterval, (index, interval) => { result.push(remoteGrouping ? { selector: dataField, @@ -420,56 +439,75 @@ export default { } if (remoteGrouping) { - result = [{ selector: dataField, isExpanded: false }]; - } else { - result = function (data) { - let result = column.calculateCellValue(data); + return [{ selector: dataField, isExpanded: false }]; + } - if (result === undefined || result === '') { - result = null; - } - return result; - }; + const selector = (data: unknown): unknown => { + let value = column.calculateCellValue(data); - if (column.sortingMethod) { - result = [{ selector: result, compare: column.sortingMethod.bind(column) }]; + if (value === undefined || value === '') { + value = null; } + return value; + }; + + if (column.sortingMethod) { + return [{ selector, compare: column.sortingMethod.bind(column) }]; } - return result; + return selector; }, - equalSortParameters(sortParameters1, sortParameters2, ignoreIsExpanded?) { - sortParameters1 = normalizeSortingInfo(sortParameters1); - sortParameters2 = normalizeSortingInfo(sortParameters2); + equalSortParameters( + sortParameters1: unknown, + sortParameters2: unknown, + ignoreIsExpanded?: boolean, + ): boolean { + const sortInfo1 = normalizeSortingInfo(sortParameters1); + const sortInfo2 = normalizeSortingInfo(sortParameters2); - if (Array.isArray(sortParameters1) && Array.isArray(sortParameters2)) { - if (sortParameters1.length !== sortParameters2.length) { + if (Array.isArray(sortInfo1) && Array.isArray(sortInfo2)) { + if (sortInfo1.length !== sortInfo2.length) { return false; } - for (let i = 0; i < sortParameters1.length; i++) { - if (!isEqualSelectors(sortParameters1[i].selector, sortParameters2[i].selector) || sortParameters1[i].desc !== sortParameters2[i].desc || sortParameters1[i].groupInterval !== sortParameters2[i].groupInterval || (!ignoreIsExpanded && Boolean(sortParameters1[i].isExpanded) !== Boolean(sortParameters2[i].isExpanded))) { + for (let i = 0; i < sortInfo1.length; i += 1) { + if ( + !isEqualSelectors(sortInfo1[i].selector, sortInfo2[i].selector) + || sortInfo1[i].desc !== sortInfo2[i].desc + || sortInfo1[i].groupInterval !== sortInfo2[i].groupInterval + || (!ignoreIsExpanded + && Boolean(sortInfo1[i].isExpanded) !== Boolean(sortInfo2[i].isExpanded)) + ) { return false; } } return true; } - return (!sortParameters1 || !sortParameters1.length) === (!sortParameters2 || !sortParameters2.length); + return !sortInfo1?.length === !sortInfo2?.length; }, - getPointsByColumns(items, pointCreated: (point: T) => boolean, isVertical = false, startColumnIndex = 0, needToCheckPrevPoint = false): ColumnPoint[] { + getPointsByColumns( + items: dxElementWrapper, + pointCreated: (point: ColumnPointProps) => boolean, + isVertical = false, + startColumnIndex = 0, + needToCheckPrevPoint = false, + ): ColumnPoint[] { const result: ColumnPoint[] = []; const cellsLength: number = items.length; - let $item; + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the loop + let $item: dxElementWrapper | undefined; let offset: { left: number; top: number } = { left: 0, top: 0 }; let itemRect: { width: number; height: number } = { width: 0, height: 0 }; let columnIndex = startColumnIndex; - let rtlEnabled; + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the loop + let rtlEnabled: boolean | undefined; - for (let i = 0; i <= cellsLength; i++) { + for (let i = 0; i <= cellsLength; i += 1) { if (i < cellsLength) { $item = items.eq(i); + // @ts-expect-error offset() of a wrapper with an element is always defined offset = $item.offset(); itemRect = getBoundingRect($item.get(0)); rtlEnabled = $item.css('direction') === 'rtl'; @@ -478,7 +516,7 @@ export default { const offsetRight = offset.left + itemRect.width; const offsetBottom = offset.top + itemRect.height; - const pointProps: any = { + const pointProps: ColumnPointProps = { index: columnIndex, columnIndex, item: $item?.get(0), @@ -487,6 +525,7 @@ export default { }; if (!isVertical && i > 0) { + // @ts-expect-error offset() of a wrapper with an element is always defined const prevItemOffset: { left: number; top: number } = items.eq(i - 1).offset(); const { width: prevItemWidth }: { width: number } = getBoundingRect(items[i - 1]); const prevItemOffsetX = rtlEnabled @@ -498,12 +537,13 @@ export default { } if (needToCheckPrevPoint && Math.round(prevItemOffsetX) !== Math.round(pointProps.x)) { - const prevPointProps: any = { + const prevPointProps: ColumnPointProps = { ...pointProps, item: items[i - 1], x: prevItemOffsetX, }; + // eslint-disable-next-line max-depth -- inside a loop; flattening needs continue if (rtlEnabled) { pointProps.isRightBoundary = true; prevPointProps.isLeftBoundary = true; @@ -517,18 +557,22 @@ export default { } addPointIfNeed(result, pointProps, pointCreated); - columnIndex++; + columnIndex += 1; } return result; }, - getExpandCellTemplate() { + getExpandCellTemplate(): ExpandCellTemplate { return { allowRenderToDetachedContainer: true, - render(container, options) { + render(container: dxElementWrapper, options: ExpandCellTemplateOptions): void { const $container = $(container); - if (isDefined(options.value) && !(options.data && options.data.isContinuation) && !options.row.isNewRow) { + if ( + isDefined(options.value) + && !options.data?.isContinuation + && !options.row.isNewRow + ) { const rowsView = options.component.getView('rowsView'); $container .addClass(DATAGRID_EXPAND_CLASS) @@ -538,7 +582,11 @@ export default { .addClass(options.value ? DATAGRID_GROUP_OPENED_CLASS : DATAGRID_GROUP_CLOSED_CLASS) .appendTo($container); - rowsView.setAria('label', options.value ? rowsView.localize('dxDataGrid-ariaCollapse') : rowsView.localize('dxDataGrid-ariaExpand'), $container); + rowsView.setAria( + 'label', + options.value ? rowsView.localize('dxDataGrid-ariaCollapse') : rowsView.localize('dxDataGrid-ariaExpand'), + $container, + ); } else { setEmptyText($container); } @@ -550,12 +598,16 @@ export default { isDateType, - getSelectionRange(focusedElement): SelectionRange { + getSelectionRange(focusedElement: TextSelectionElement | null | undefined): SelectionRange { try { if (focusedElement) { return { - selectionStart: isNumeric(focusedElement.selectionStart) ? focusedElement.selectionStart : -1, - selectionEnd: isNumeric(focusedElement.selectionEnd) ? focusedElement.selectionEnd : -1, + selectionStart: isNumeric(focusedElement.selectionStart) + ? focusedElement.selectionStart as number + : -1, + selectionEnd: isNumeric(focusedElement.selectionEnd) + ? focusedElement.selectionEnd as number + : -1, }; } } catch (e) { /* empty */ } @@ -566,23 +618,33 @@ export default { }; }, - setSelectionRange(focusedElement, selectionRange: SelectionRange): void { + setSelectionRange( + focusedElement: TextSelectionElement | null | undefined, + selectionRange: SelectionRange, + ): void { try { - if (focusedElement && focusedElement.setSelectionRange && selectionRange.selectionStart >= 0 && selectionRange.selectionEnd >= 0) { - focusedElement.setSelectionRange(selectionRange.selectionStart, selectionRange.selectionEnd); + if ( + focusedElement?.setSelectionRange + && selectionRange.selectionStart >= 0 + && selectionRange.selectionEnd >= 0 + ) { + focusedElement.setSelectionRange( + selectionRange.selectionStart, + selectionRange.selectionEnd, + ); } } catch (e) { /* empty */ } }, - focusAndSelectElement(component, $element) { + focusAndSelectElement(component: ModuleItem, $element: dxElementWrapper): void { const isFocused = $element.is(':focus'); // @ts-expect-error eventsEngine.trigger($element, 'focus'); const isSelectTextOnEditingStart = component.option('editing.selectTextOnEditStart'); - const element = $element.get(0); if (!isFocused && isSelectTextOnEditingStart && $element.is('.dx-texteditor-input') && !$element.is('[readonly]')) { + const element = $element.get(0) as HTMLInputElement | HTMLTextAreaElement; const editor = getWidgetInstance($element.closest('.dx-texteditor')); when(editor && editor._loadItemDeferred).done(() => { @@ -593,11 +655,13 @@ export default { getWidgetInstance, - getLastResizableColumnIndex(columns, resultWidths?) { - const hasResizableColumns = columns.some((column) => column && !column.command && !column.fixed && column.allowResizing !== false); - let lastColumnIndex; + getLastResizableColumnIndex(columns: Column[], resultWidths?: (number | string)[]): number { + const hasResizableColumns = columns.some( + (column) => column && !column.command && !column.fixed && column.allowResizing !== false, + ); + let lastColumnIndex = columns.length - 1; - for (lastColumnIndex = columns.length - 1; columns[lastColumnIndex]; lastColumnIndex--) { + for (; columns[lastColumnIndex]; lastColumnIndex -= 1) { const column = columns[lastColumnIndex]; const width = resultWidths && resultWidths[lastColumnIndex]; const allowResizing = !hasResizableColumns || column.allowResizing !== false; @@ -610,8 +674,11 @@ export default { return lastColumnIndex; }, - isElementInCurrentGrid(controller, $element) { - if ($element && $element.length) { + isElementInCurrentGrid( + controller: ModuleItem, + $element: dxElementWrapper | null | undefined, + ): boolean { + if ($element?.length) { const $grid = $element.closest(`.${controller.getWidgetContainerClass()}`).parent(); return $grid.is(controller.component.$element()); @@ -619,7 +686,7 @@ export default { return false; }, - isVirtualRowRendering(that) { + isVirtualRowRendering(that: OptionsReader): boolean { const rowRenderingMode = that.option(ROW_RENDERING_MODE_OPTION); const isVirtualMode = that.option(SCROLLING_MODE_OPTION) === SCROLLING_MODE_VIRTUAL; const isAppendMode = that.option(SCROLLING_MODE_OPTION) === SCROLLING_MODE_INFINITE; @@ -631,17 +698,17 @@ export default { return rowRenderingMode === SCROLLING_MODE_VIRTUAL; }, - getPixelRatio(window) { + getPixelRatio(window: Window): number { return window.devicePixelRatio || 1; }, /// #DEBUG - _setPixelRatioFn(value) { + _setPixelRatioFn(value: (window: Window) => number): void { this.getPixelRatio = value; }, /// #ENDDEBUG - getContentHeightLimit(browser) { + getContentHeightLimit(browser: BrowserInfo): number { if (browser.mozilla) { return 8000000; } @@ -649,25 +716,37 @@ export default { return 15000000 / this.getPixelRatio(getWindow()); }, - normalizeLookupDataSource(lookup) { - let lookupDataSourceOptions; + normalizeLookupDataSource(lookup: NonNullable): NormalizedDataSourceOptions { + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the if-else + let lookupDataSourceOptions: typeof lookup.items | typeof lookup.dataSource; if (lookup.items) { lookupDataSourceOptions = lookup.items; } else { lookupDataSourceOptions = lookup.dataSource; - if (isFunction(lookupDataSourceOptions) && !variableWrapper.isWrapped(lookupDataSourceOptions)) { + if ( + isFunction(lookupDataSourceOptions) + && !variableWrapper.isWrapped(lookupDataSourceOptions) + ) { lookupDataSourceOptions = lookupDataSourceOptions({}); } } return normalizeDataSourceOptions(lookupDataSourceOptions); }, - getWrappedLookupDataSource(column, dataSourceAdapter: DataSourceAdapter | null | undefined, filter) { - if (!dataSourceAdapter) { + getWrappedLookupDataSource( + column: Column, + dataSourceAdapter: DataSourceAdapter | null | undefined, + filter: DataFilter, + ): LookupDataSource { + const { lookup } = column; + + if (!dataSourceAdapter || !lookup) { return []; } - const lookupDataSourceOptions = this.normalizeLookupDataSource(column.lookup); + const lookupDataSourceOptions: NormalizedDataSourceOptions = this.normalizeLookupDataSource( + lookup, + ); if (column.calculateCellValue !== column.defaultCalculateCellValue) { return lookupDataSourceOptions; @@ -676,29 +755,33 @@ export default { const hasGroupPaging = dataSourceAdapter.remoteOperations().groupPaging; const hasLookupOptimization = column.displayField && isString(column.displayField); - let cachedUniqueRelevantItems; - let previousTake; - let previousSkip; + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in a closure + let cachedUniqueRelevantItems: RawItemData[] | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in a closure + let previousTake: number | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in a closure + let previousSkip: number | undefined; - const sliceItems = (items, loadOptions) => { + const sliceItems = (items: RawItemData[], loadOptions: StoreLoadOptions): RawItemData[] => { const start = loadOptions.skip ?? 0; const end = loadOptions.take ? start + loadOptions.take : items.length; return items.slice(start, end); }; - const loadUniqueRelevantItems = (loadOptions) => { + const loadUniqueRelevantItems = ( + loadOptions: StoreLoadOptions, + ): DeferredObj => { const group = normalizeGroupingLoadOptions( + // @ts-expect-error a bound lookup column has dataField, and displayField is checked above hasLookupOptimization ? [column.dataField, column.displayField] : column.dataField, ); // @ts-expect-error const d = new Deferred(); - const canUseCache = cachedUniqueRelevantItems && ( - !hasGroupPaging - || (loadOptions.skip === previousSkip && loadOptions.take === previousTake) - ); + const isSamePage = !hasGroupPaging + || (loadOptions.skip === previousSkip && loadOptions.take === previousTake); - if (canUseCache) { + if (cachedUniqueRelevantItems && isSamePage) { d.resolve(sliceItems(cachedUniqueRelevantItems, loadOptions)); } else { previousSkip = loadOptions.skip; @@ -714,13 +797,14 @@ export default { }).fail(d.fail); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- untyped new Deferred() return d; }; - const lookupDataSource = { + const lookupDataSource: WrappedLookupDataSource = { ...lookupDataSourceOptions, __dataGridSourceFilter: filter, - load: (loadOptions) => { + load: (loadOptions: StoreLoadOptions): DeferredObj => { // @ts-expect-error const d = new Deferred(); loadUniqueRelevantItems(loadOptions).done((items) => { @@ -729,9 +813,9 @@ export default { return; } - const filter = combineFilters( + const keysFilter = combineFilters( items.flatMap((data) => data.key).map((key) => [ - column.lookup.valueExpr, key, + lookup.valueExpr, key, ]), 'or', ); @@ -739,7 +823,7 @@ export default { const newDataSource = new DataSource({ ...lookupDataSourceOptions, ...loadOptions, - filter: combineFilters([filter, loadOptions.filter], 'and'), + filter: combineFilters([keysFilter, loadOptions.filter], 'and'), paginate: false, // pagination is included to filter }); @@ -748,13 +832,14 @@ export default { .done(d.resolve) .fail(d.fail); }).fail(d.fail); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- untyped new Deferred() return d; }, - key: column.lookup.valueExpr, - byKey(key) { - const d = Deferred(); + key: lookup.valueExpr, + byKey(key: unknown): Promise { + const d = Deferred(); this.load({ - filter: [column.lookup.valueExpr, '=', key], + filter: [lookup.valueExpr, '=', key], }).done((arr) => { d.resolve(arr[0]); }); @@ -766,7 +851,10 @@ export default { return lookupDataSource; }, - getComponentBorderWidth(that, $rowsViewElement) { + getComponentBorderWidth( + that: ModuleItem, + $rowsViewElement: dxElementWrapper | undefined, + ): number { const borderWidth = that.option('showBorders') ? Math.ceil(getOuterWidth($rowsViewElement) - getInnerWidth($rowsViewElement)) : 0; @@ -774,7 +862,7 @@ export default { return borderWidth; }, - isCustomCommandColumn(columns, commandColumn): boolean { + isCustomCommandColumn(columns: Column[], commandColumn: Column): boolean { const customCommandColumns = columns .filter((column) => column.type === commandColumn.type); diff --git a/packages/devextreme/js/__internal/grids/grid_core/modules/modules.ts b/packages/devextreme/js/__internal/grids/grid_core/modules/modules.ts index 44822c8bcc78..2babb00d367e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/modules/modules.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/modules/modules.ts @@ -130,7 +130,7 @@ export class ModuleItem { return this.component._setOptionWithoutOptionChange(name, value); } - protected localize(name: string): string { + public localize(name: string): string { const optionCache = this.component._optionCache; if (optionCache) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/types.ts b/packages/devextreme/js/__internal/grids/grid_core/types.ts index 618f3e45cf86..61c78e6a3293 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/types.ts @@ -16,6 +16,7 @@ import type { Properties as TreeListdOptions, Scrolling as TreeListScrolling } f import type Widget from '@js/ui/widget/ui.widget'; import type { IntegrationOptions } from '@ts/core/template_manager'; import type { ActionConfig } from '@ts/core/widget/component'; +import type { NormalizedDataSourceOptions, StoreLoadOptions } from '@ts/data/data_source/types'; import type { FilterValue } from '@ts/grids/grid_core/filter/types'; import type { ModuleItem } from '@ts/grids/grid_core/modules/modules'; @@ -63,6 +64,56 @@ export interface LoadPanelPosition { export type WidgetElementData = Record & { dxComponents?: string[] }; +export interface ColumnPointProps extends ColumnPoint { + item: Element | undefined; + isLeftBoundary?: boolean; + isRightBoundary?: boolean; +} + +export type HeaderFilterGroupSelector = (data: unknown) => unknown; + +export interface HeaderFilterGroupDescriptor { + selector: string | HeaderFilterGroupSelector | undefined; + groupInterval?: string | number; + isExpanded?: boolean; + compare?: (value1: unknown, value2: unknown) => number; +} + +export type HeaderFilterGroupItem = HeaderFilterGroupDescriptor | HeaderFilterGroupSelector; + +export type HeaderFilterGroup = HeaderFilterGroupSelector | HeaderFilterGroupItem[]; + +export interface ExpandCellTemplateOptions { + value?: unknown; + data?: { isContinuation?: boolean }; + row: { isNewRow?: boolean }; + component: InternalGrid; +} + +export interface ExpandCellTemplate { + allowRenderToDetachedContainer: boolean; + render: (container: dxElementWrapper, options: ExpandCellTemplateOptions) => void; +} + +export type TextSelectionElement = Element & { + selectionStart?: number | null; + selectionEnd?: number | null; + setSelectionRange?: (start: number, end: number) => void; +}; + +export interface OptionsReader { + option: (name: string) => unknown; +} + +export type WrappedLookupDataSource = NormalizedDataSourceOptions & { + __dataGridSourceFilter: unknown; + load: (loadOptions: StoreLoadOptions) => DeferredObj; + key: string | undefined; + byKey: (key: unknown) => Promise; +}; + +export type LookupDataSource = never[] | NormalizedDataSourceOptions | WrappedLookupDataSource; + type OptionsMethod = (() => TOptions) & ((options: TOptions) => void) & ( From 2849cf777fad55ac6f149b03d61e157fefb8401d Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 8 Oct 2026 00:30:13 +0400 Subject: [PATCH 03/11] Grids: Refactor part 2 of grid_core m_utils --- .../grids/grid_core/__tests__/utils.test.ts | 508 ++++++++++++++++++ .../js/__internal/grids/grid_core/m_utils.ts | 80 ++- 2 files changed, 546 insertions(+), 42 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts new file mode 100644 index 000000000000..0ff8e21e163c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts @@ -0,0 +1,508 @@ +import { + afterEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { Deferred } from '@js/core/utils/deferred'; +import { variableWrapper } from '@ts/core/utils/m_variable_wrapper'; +import type { NormalizedDataSourceOptions } from '@ts/data/data_source/types'; +import type { Column } from '@ts/grids/grid_core/columns_controller/types'; +import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; +import type { DataFilter } from '@ts/grids/grid_core/filter/types'; +import type { WrappedLookupDataSource } from '@ts/grids/grid_core/types'; + +import gridCoreUtils from '../m_utils'; + +interface CellRect { + left: number; + top: number; + width: number; + height: number; +} + +interface CreatedPoint { + index: number; + columnIndex: number; + x: number; + y: number; + item?: Element; + isLeftBoundary?: boolean; + isRightBoundary?: boolean; +} + +interface DoneCallbackHolder { + done: (callback: (value: T) => void) => unknown; +} + +const CELLS_CLASS = 'test-cells'; + +const LTR_CELLS: CellRect[] = [ + { + left: 0, top: 10, width: 50, height: 20, + }, + { + left: 50, top: 10, width: 60, height: 20, + }, + { + left: 110, top: 10, width: 40, height: 20, + }, +]; + +const RTL_CELLS: CellRect[] = [ + { + left: 100, top: 10, width: 50, height: 20, + }, + { + left: 40, top: 10, width: 60, height: 20, + }, + { + left: 0, top: 10, width: 40, height: 20, + }, +]; + +const LOOKUP_ITEMS = [ + { id: 1, text: 'One' }, + { id: 2, text: 'Two' }, + { id: 3, text: 'Three' }, +]; + +const createCells = (rects: CellRect[], direction = 'ltr'): dxElementWrapper => { + const $container = $('
').addClass(CELLS_CLASS).appendTo(document.body); + + rects.forEach(({ + left, top, width, height, + }) => { + const cell = $('
').css('direction', direction).appendTo($container).get(0) as HTMLElement; + const rect = { + left, + top, + width, + height, + x: left, + y: top, + right: left + width, + bottom: top + height, + toJSON: (): void => {}, + } as DOMRect; + + cell.getBoundingClientRect = (): DOMRect => rect; + cell.getClientRects = (): DOMRectList => [rect] as unknown as DOMRectList; + }); + + return $container.children(); +}; + +const notCreated = (): boolean => false; + +const createPointCollector = (): { + points: CreatedPoint[]; + pointCreated: (point: CreatedPoint) => boolean; +} => { + const points: CreatedPoint[] = []; + + return { + points, + pointCreated: (point: CreatedPoint): boolean => { + points.push({ ...point }); + return false; + }, + }; +}; + +const toPromise = (deferred: DoneCallbackHolder): Promise => new Promise((resolve) => { + deferred.done(resolve); +}); + +const calculateCellValue = (data: { categoryId?: number }): unknown => data.categoryId; + +const createLookupColumn = (overrides: Record = {}): Column => ({ + dataField: 'categoryId', + calculateCellValue, + defaultCalculateCellValue: calculateCellValue, + lookup: { dataSource: LOOKUP_ITEMS, valueExpr: 'id', displayExpr: 'text' }, + ...overrides, +}) as unknown as Column; + +const createDataSourceAdapter = ( + groupItems: { key: unknown }[], + groupPaging = false, +): { + dataSourceAdapter: DataSourceAdapter; + customLoad: jest.Mock<(options: unknown) => unknown>; +} => { + const customLoad = jest.fn<(options: unknown) => unknown>( + () => Deferred().resolve({ data: groupItems }), + ); + const dataSourceAdapter = { + remoteOperations: () => ({ groupPaging }), + customLoader: { load: customLoad }, + } as unknown as DataSourceAdapter; + + return { dataSourceAdapter, customLoad }; +}; + +describe('getPointsByColumns', () => { + afterEach(() => { + $(`.${CELLS_CLASS}`).remove(); + }); + + describe('when the columns are in a row', () => { + it('should create a point at the left edge of each column and at the right edge of the last one', () => { + const points = gridCoreUtils.getPointsByColumns(createCells(LTR_CELLS), notCreated); + + expect(points).toEqual([ + { + index: 0, columnIndex: 0, x: 0, y: 10, + }, + { + index: 1, columnIndex: 1, x: 50, y: 10, + }, + { + index: 2, columnIndex: 2, x: 110, y: 10, + }, + { + index: 3, columnIndex: 3, x: 150, y: 10, + }, + ]); + }); + + it('should pass the column element of each point to pointCreated', () => { + const cells = createCells(LTR_CELLS); + const { points, pointCreated } = createPointCollector(); + + gridCoreUtils.getPointsByColumns(cells, pointCreated); + + expect(points.map((point) => point.item)).toEqual([ + cells.get(0), cells.get(1), cells.get(2), cells.get(2), + ]); + expect(points[3].item).toBe(cells.get(2)); + }); + + it('should start the indexes from startColumnIndex', () => { + const points = gridCoreUtils.getPointsByColumns( + createCells(LTR_CELLS), + notCreated, + false, + 2, + ); + + expect(points.map((point) => [point.index, point.columnIndex])).toEqual([ + [2, 2], [3, 3], [4, 4], [5, 5], + ]); + }); + + it('should skip the points for which pointCreated returns true', () => { + const points = gridCoreUtils.getPointsByColumns( + createCells(LTR_CELLS), + (point: CreatedPoint) => point.index === 1, + ); + + expect(points.map((point) => point.index)).toEqual([0, 2, 3]); + }); + + it('should use the top of the previous column when it is higher', () => { + const points = gridCoreUtils.getPointsByColumns(createCells([ + { + left: 0, top: 10, width: 50, height: 20, + }, + { + left: 50, top: 15, width: 60, height: 20, + }, + ]), notCreated); + + expect(points.map((point) => point.y)).toEqual([10, 10, 15]); + }); + }); + + describe('when the grid is in RTL mode', () => { + it('should create a point at the right edge of each column and at the left edge of the last one', () => { + const points = gridCoreUtils.getPointsByColumns(createCells(RTL_CELLS, 'rtl'), notCreated); + + expect(points.map((point) => point.x)).toEqual([150, 100, 40, 0]); + }); + }); + + describe('when the points are vertical', () => { + it('should create a point at the top of each row and at the bottom of the last one', () => { + const points = gridCoreUtils.getPointsByColumns(createCells([ + { + left: 0, top: 0, width: 50, height: 20, + }, + { + left: 0, top: 20, width: 50, height: 20, + }, + { + left: 0, top: 40, width: 50, height: 20, + }, + ]), notCreated, true); + + expect(points.map((point) => [point.x, point.y])).toEqual([ + [0, 0], [0, 20], [0, 40], [0, 60], + ]); + }); + }); + + describe('when there is a gap between columns and needToCheckPrevPoint is set', () => { + it('should add a right boundary point after the previous column and mark the next point as a left boundary', () => { + const cells = createCells([ + { + left: 0, top: 10, width: 50, height: 20, + }, + { + left: 80, top: 10, width: 50, height: 20, + }, + ]); + const { points, pointCreated } = createPointCollector(); + + const result = gridCoreUtils.getPointsByColumns(cells, pointCreated, false, 0, true); + + expect(result.map((point) => [point.index, point.x])).toEqual([ + [0, 0], [1, 50], [1, 80], [2, 130], + ]); + const boundaries = points.map( + (point) => [point.x, point.isLeftBoundary, point.isRightBoundary], + ); + + expect(boundaries).toEqual([ + [0, undefined, undefined], + [50, undefined, true], + [80, true, undefined], + [130, undefined, undefined], + ]); + expect(points[1].item).toBe(cells.get(0)); + expect(points[2].item).toBe(cells.get(1)); + expect(points[3].item).toBe(cells.get(1)); + }); + + it('should swap the boundaries in RTL mode', () => { + const cells = createCells([ + { + left: 80, top: 10, width: 50, height: 20, + }, + { + left: 0, top: 10, width: 50, height: 20, + }, + ], 'rtl'); + const { points, pointCreated } = createPointCollector(); + + gridCoreUtils.getPointsByColumns(cells, pointCreated, false, 0, true); + + const boundaries = points.map( + (point) => [point.x, point.isLeftBoundary, point.isRightBoundary], + ); + + expect(boundaries).toEqual([ + [130, undefined, undefined], + [80, true, undefined], + [50, undefined, true], + [0, undefined, undefined], + ]); + }); + }); + + describe('when there are no columns', () => { + it('should create a single point without an element at the origin', () => { + const { points, pointCreated } = createPointCollector(); + + const result = gridCoreUtils.getPointsByColumns(createCells([]), pointCreated); + + expect(result).toEqual([{ + index: 0, columnIndex: 0, x: 0, y: 0, + }]); + expect(points[0].item).toBeUndefined(); + }); + }); +}); + +describe('normalizeLookupDataSource', () => { + describe('when the lookup has items', () => { + it('should use the items instead of the data source', async () => { + const options = gridCoreUtils.normalizeLookupDataSource({ + items: LOOKUP_ITEMS, + dataSource: [{ id: 9, text: 'Nine' }], + }); + + await expect(toPromise(options.store.load())).resolves.toEqual(LOOKUP_ITEMS); + }); + }); + + describe('when the lookup has a data source array', () => { + it('should use the array as the store data', async () => { + const options = gridCoreUtils.normalizeLookupDataSource({ dataSource: LOOKUP_ITEMS }); + + await expect(toPromise(options.store.load())).resolves.toEqual(LOOKUP_ITEMS); + }); + }); + + describe('when the data source is a function', () => { + it('should call it with an empty object and use its result', async () => { + const dataSource = jest.fn<(options: object) => typeof LOOKUP_ITEMS>(() => LOOKUP_ITEMS); + + const options = gridCoreUtils.normalizeLookupDataSource({ dataSource }); + + expect(dataSource).toHaveBeenCalledWith({}); + await expect(toPromise(options.store.load())).resolves.toEqual(LOOKUP_ITEMS); + }); + }); + + describe('when the data source function is a wrapped variable', () => { + afterEach(() => { + variableWrapper.resetInjection(); + }); + + it('should not call it', () => { + const dataSource = jest.fn<(options: object) => typeof LOOKUP_ITEMS>(() => LOOKUP_ITEMS); + variableWrapper.inject({ isWrapped: (value: unknown) => value === dataSource }); + + gridCoreUtils.normalizeLookupDataSource({ dataSource }); + + expect(dataSource).not.toHaveBeenCalled(); + }); + }); +}); + +describe('getWrappedLookupDataSource', () => { + describe('when there is no data source adapter', () => { + it('should return an empty array', () => { + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn(), + null, + null, + ); + + expect(lookupDataSource).toEqual([]); + }); + }); + + describe('when the column calculates its own cell value', () => { + it('should return the lookup data source as it is', async () => { + const { dataSourceAdapter, customLoad } = createDataSourceAdapter([{ key: 1 }]); + const column = createLookupColumn({ calculateCellValue: () => 1 }); + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + column, + dataSourceAdapter, + null, + ) as NormalizedDataSourceOptions; + + expect(lookupDataSource).not.toHaveProperty('byKey'); + await expect(toPromise(lookupDataSource.store.load())).resolves.toEqual(LOOKUP_ITEMS); + expect(customLoad).not.toHaveBeenCalled(); + }); + }); + + describe('when the lookup items are loaded', () => { + it('should load only the items whose keys are used in the grid', async () => { + const { dataSourceAdapter, customLoad } = createDataSourceAdapter([{ key: 1 }, { key: 3 }]); + const filter: DataFilter = ['categoryId', '>', 0]; + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn(), + dataSourceAdapter, + filter, + ) as WrappedLookupDataSource; + + await expect(toPromise(lookupDataSource.load({}))).resolves.toEqual([ + { id: 1, text: 'One' }, + { id: 3, text: 'Three' }, + ]); + expect(customLoad).toHaveBeenCalledWith({ + filter, + group: [{ selector: 'categoryId', isExpanded: false }], + take: undefined, + skip: undefined, + }); + }); + + it('should group by the display field too when it is set', async () => { + const { dataSourceAdapter, customLoad } = createDataSourceAdapter([{ key: 1 }]); + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn({ displayField: 'categoryName' }), + dataSourceAdapter, + null, + ) as WrappedLookupDataSource; + await toPromise(lookupDataSource.load({})); + + expect(customLoad).toHaveBeenCalledWith(expect.objectContaining({ + group: [ + { selector: 'categoryId', isExpanded: true }, + { selector: 'categoryName', isExpanded: false }, + ], + })); + }); + + it('should resolve an empty array when no keys are used', async () => { + const { dataSourceAdapter } = createDataSourceAdapter([]); + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn(), + dataSourceAdapter, + null, + ) as WrappedLookupDataSource; + + await expect(toPromise(lookupDataSource.load({}))).resolves.toEqual([]); + }); + }); + + describe('when the items are loaded again without group paging', () => { + it('should take the keys from the cache and slice them', async () => { + const { dataSourceAdapter, customLoad } = createDataSourceAdapter([{ key: 1 }, { key: 3 }]); + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn(), + dataSourceAdapter, + null, + ) as WrappedLookupDataSource; + await toPromise(lookupDataSource.load({})); + const items = await toPromise(lookupDataSource.load({ skip: 1, take: 1 })); + + expect(items).toEqual([{ id: 3, text: 'Three' }]); + expect(customLoad).toHaveBeenCalledTimes(1); + }); + }); + + describe('when the items are loaded again with group paging', () => { + it('should pass the page to the loader and use the cache for the same page only', async () => { + const { dataSourceAdapter, customLoad } = createDataSourceAdapter( + [{ key: 1 }, { key: 3 }], + true, + ); + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn(), + dataSourceAdapter, + null, + ) as WrappedLookupDataSource; + await toPromise(lookupDataSource.load({ skip: 0, take: 2 })); + await toPromise(lookupDataSource.load({ skip: 0, take: 2 })); + + expect(customLoad).toHaveBeenCalledTimes(1); + expect(customLoad).toHaveBeenCalledWith(expect.objectContaining({ skip: 0, take: 2 })); + + await toPromise(lookupDataSource.load({ skip: 2, take: 2 })); + + expect(customLoad).toHaveBeenCalledTimes(2); + expect(customLoad).toHaveBeenLastCalledWith(expect.objectContaining({ skip: 2, take: 2 })); + }); + }); + + describe('when an item is requested by key', () => { + it('should resolve the item when its key is used in the grid', async () => { + const { dataSourceAdapter } = createDataSourceAdapter([{ key: 1 }, { key: 3 }]); + + const lookupDataSource = gridCoreUtils.getWrappedLookupDataSource( + createLookupColumn(), + dataSourceAdapter, + null, + ) as WrappedLookupDataSource; + + await expect(lookupDataSource.byKey(3)).resolves.toEqual({ id: 3, text: 'Three' }); + await expect(lookupDataSource.byKey(2)).resolves.toBeUndefined(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 634dfd626ebc..6aa7eaace495 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -225,6 +225,20 @@ const addPointIfNeed = ( } }; +const markBoundaryPoints = ( + point: ColumnPointProps, + prevPoint: ColumnPointProps, + rtlEnabled: boolean, +): void => { + if (rtlEnabled) { + point.isRightBoundary = true; + prevPoint.isLeftBoundary = true; + } else { + point.isLeftBoundary = true; + prevPoint.isRightBoundary = true; + } +}; + const getColumnWidths = (columns: Column[]): number[] => columns .map((column) => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -250,6 +264,18 @@ function normalizeGroupingLoadOptions( }); } +const getLookupDataSourceOptions = (lookup: NonNullable): unknown => { + if (lookup.items) { + return lookup.items; + } + + const { dataSource } = lookup; + + return isFunction(dataSource) && !variableWrapper.isWrapped(dataSource) + ? dataSource({}) + : dataSource; +}; + export default { renderNoDataText($container?: dxElementWrapper): void { const $element = $container || this.element(); @@ -496,17 +522,14 @@ export default { ): ColumnPoint[] { const result: ColumnPoint[] = []; const cellsLength: number = items.length; - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the loop - let $item: dxElementWrapper | undefined; let offset: { left: number; top: number } = { left: 0, top: 0 }; let itemRect: { width: number; height: number } = { width: 0, height: 0 }; let columnIndex = startColumnIndex; - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the loop - let rtlEnabled: boolean | undefined; + let rtlEnabled = false; for (let i = 0; i <= cellsLength; i += 1) { if (i < cellsLength) { - $item = items.eq(i); + const $item = items.eq(i); // @ts-expect-error offset() of a wrapper with an element is always defined offset = $item.offset(); itemRect = getBoundingRect($item.get(0)); @@ -519,7 +542,7 @@ export default { const pointProps: ColumnPointProps = { index: columnIndex, columnIndex, - item: $item?.get(0), + item: items[Math.min(i, cellsLength - 1)], x: !isVertical && rtlEnabled !== (i === cellsLength) ? offsetRight : offset.left, y: isVertical && i === cellsLength ? offsetBottom : offset.top, }; @@ -543,15 +566,7 @@ export default { x: prevItemOffsetX, }; - // eslint-disable-next-line max-depth -- inside a loop; flattening needs continue - if (rtlEnabled) { - pointProps.isRightBoundary = true; - prevPointProps.isLeftBoundary = true; - } else { - pointProps.isLeftBoundary = true; - prevPointProps.isRightBoundary = true; - } - + markBoundaryPoints(pointProps, prevPointProps, rtlEnabled); addPointIfNeed(result, prevPointProps, pointCreated); } } @@ -717,20 +732,7 @@ export default { }, normalizeLookupDataSource(lookup: NonNullable): NormalizedDataSourceOptions { - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in the if-else - let lookupDataSourceOptions: typeof lookup.items | typeof lookup.dataSource; - if (lookup.items) { - lookupDataSourceOptions = lookup.items; - } else { - lookupDataSourceOptions = lookup.dataSource; - if ( - isFunction(lookupDataSourceOptions) - && !variableWrapper.isWrapped(lookupDataSourceOptions) - ) { - lookupDataSourceOptions = lookupDataSourceOptions({}); - } - } - return normalizeDataSourceOptions(lookupDataSourceOptions); + return normalizeDataSourceOptions(getLookupDataSourceOptions(lookup)); }, getWrappedLookupDataSource( @@ -754,13 +756,7 @@ export default { const hasGroupPaging = dataSourceAdapter.remoteOperations().groupPaging; const hasLookupOptimization = column.displayField && isString(column.displayField); - - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in a closure - let cachedUniqueRelevantItems: RawItemData[] | undefined; - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in a closure - let previousTake: number | undefined; - // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in a closure - let previousSkip: number | undefined; + const cache: { items?: RawItemData[]; skip?: number; take?: number } = {}; const sliceItems = (items: RawItemData[], loadOptions: StoreLoadOptions): RawItemData[] => { const start = loadOptions.skip ?? 0; @@ -779,20 +775,20 @@ export default { const d = new Deferred(); const isSamePage = !hasGroupPaging - || (loadOptions.skip === previousSkip && loadOptions.take === previousTake); + || (loadOptions.skip === cache.skip && loadOptions.take === cache.take); - if (cachedUniqueRelevantItems && isSamePage) { - d.resolve(sliceItems(cachedUniqueRelevantItems, loadOptions)); + if (cache.items && isSamePage) { + d.resolve(sliceItems(cache.items, loadOptions)); } else { - previousSkip = loadOptions.skip; - previousTake = loadOptions.take; + cache.skip = loadOptions.skip; + cache.take = loadOptions.take; dataSourceAdapter.customLoader.load({ filter, group, take: hasGroupPaging ? loadOptions.take : undefined, skip: hasGroupPaging ? loadOptions.skip : undefined, }).done(({ data }) => { - cachedUniqueRelevantItems = data; + cache.items = data; d.resolve(hasGroupPaging ? data : sliceItems(data, loadOptions)); }).fail(d.fail); } From 5fd5deb60297e3e69cc279a0f5c809ba1a45a53a Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 8 Oct 2026 16:33:39 +0400 Subject: [PATCH 04/11] Grids: Refine interval and key types in grid_core m_utils --- .../js/__internal/grids/grid_core/m_utils.ts | 27 ++++++++++--------- .../js/__internal/grids/grid_core/types.ts | 2 ++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 6aa7eaace495..d819fd36853e 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -34,6 +34,7 @@ import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_control import type { ColumnPoint, ColumnPointProps, + EmptyDateValue, ExpandCellTemplate, ExpandCellTemplateOptions, FormatOptions, @@ -42,6 +43,7 @@ import type { LoadPanelPosition, LookupDataSource, OptionsReader, + RowKey, SelectionRange, SummaryTextItem, TextSelectionElement, @@ -70,25 +72,25 @@ const LEGACY_SCROLLING_MODE = 'scrolling.legacyMode'; const SCROLLING_MODE_OPTION = 'scrolling.mode'; const ROW_RENDERING_MODE_OPTION = 'scrolling.rowRenderingMode'; const DATE_INTERVAL_SELECTORS = { - year(value: Date): number { + year(value: Date | EmptyDateValue): number | EmptyDateValue { return value && value.getFullYear(); }, - month(value: Date): number { + month(value: Date | EmptyDateValue): number | EmptyDateValue { return value && (value.getMonth() + 1); }, - day(value: Date): number { + day(value: Date | EmptyDateValue): number | EmptyDateValue { return value && value.getDate(); }, - quarter(value: Date): number { + quarter(value: Date | EmptyDateValue): number | EmptyDateValue { return value && (Math.floor(value.getMonth() / 3) + 1); }, - hour(value: Date): number { + hour(value: Date | EmptyDateValue): number | EmptyDateValue { return value && value.getHours(); }, - minute(value: Date): number { + minute(value: Date | EmptyDateValue): number | EmptyDateValue { return value && value.getMinutes(); }, - second(value: Date): number { + second(value: Date | EmptyDateValue): number | EmptyDateValue { return value && value.getSeconds(); }, }; @@ -103,16 +105,15 @@ const getIntervalSelector = function getIntervalSelector( this: Column & Required>, interval: HeaderFilterGroupInterval | number, data: unknown, -): number | null | undefined { +): number | EmptyDateValue | null | undefined { const value = this.calculateCellValue(data); if (!isDefined(value)) { return null; - } if (isDateType(this.dataType)) { - const nameIntervalSelector = interval as HeaderFilterGroupInterval; - return DATE_INTERVAL_SELECTORS[nameIntervalSelector](value); + } if (isDateType(this.dataType) && isString(interval)) { + return DATE_INTERVAL_SELECTORS[interval](value); } if (this.dataType === 'number') { - const groupInterval = interval as number; + const groupInterval = Number(interval); return Math.floor(Number(value) / groupInterval) * groupInterval; } @@ -345,7 +346,7 @@ export default { return { of: $element }; }, - getIndexByKey(key: unknown, items: unknown, keyName?: string | string[] | null): number { + getIndexByKey(key: RowKey, items: unknown, keyName?: string | string[] | null): number { let index = -1; if (key !== undefined && Array.isArray(items)) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/types.ts b/packages/devextreme/js/__internal/grids/grid_core/types.ts index 61c78e6a3293..f753cfc9853a 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/types.ts @@ -70,6 +70,8 @@ export interface ColumnPointProps extends ColumnPoint { isRightBoundary?: boolean; } +export type EmptyDateValue = '' | false; + export type HeaderFilterGroupSelector = (data: unknown) => unknown; export interface HeaderFilterGroupDescriptor { From d416a267846c3905888495ad51934907df9c5870 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 9 Oct 2026 15:26:22 +0400 Subject: [PATCH 05/11] Grids: Refine items, data and lookup key types in grid_core m_utils --- .../grids/grid_core/__tests__/utils.test.ts | 2 +- .../js/__internal/grids/grid_core/m_utils.ts | 20 +++++++++++++------ .../js/__internal/grids/grid_core/types.ts | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts index 0ff8e21e163c..8f6357acf07f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts @@ -8,7 +8,7 @@ import { import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { Deferred } from '@js/core/utils/deferred'; -import { variableWrapper } from '@ts/core/utils/m_variable_wrapper'; +import { variableWrapper } from '@ts/core/utils/variable_wrapper'; import type { NormalizedDataSourceOptions } from '@ts/data/data_source/types'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index d819fd36853e..bc3aa90f1964 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -110,9 +110,13 @@ const getIntervalSelector = function getIntervalSelector( if (!isDefined(value)) { return null; - } if (isDateType(this.dataType) && isString(interval)) { + } + + if (isDateType(this.dataType) && isString(interval)) { return DATE_INTERVAL_SELECTORS[interval](value); - } if (this.dataType === 'number') { + } + + if (this.dataType === 'number') { const groupInterval = Number(interval); return Math.floor(Number(value) / groupInterval) * groupInterval; } @@ -346,13 +350,17 @@ export default { return { of: $element }; }, - getIndexByKey(key: RowKey, items: unknown, keyName?: string | string[] | null): number { + getIndexByKey( + key: RowKey, + items: unknown[] | null | undefined, + keyName?: string | string[] | null, + ): number { let index = -1; if (key !== undefined && Array.isArray(items)) { const keyField = arguments.length <= 2 ? 'key' : keyName; for (let i = 0; i < items.length; i += 1) { - const item = isDefined(keyField) ? items[i][String(keyField)] : items[i]; + const item = isDefined(keyField) ? (items[i] as RawItemData)[String(keyField)] : items[i]; if (equalByValue(key, item)) { index = i; @@ -393,7 +401,7 @@ export default { }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- callers use it as their own type - getDisplayValue(column: Column, value: unknown, data: unknown, rowType?: string): any { + getDisplayValue(column: Column, value: unknown, data: RawItemData | null, rowType?: string): any { if (column.displayValueMap?.[String(value)] !== undefined) { return column.displayValueMap[String(value)]; } @@ -833,7 +841,7 @@ export default { return d; }, key: lookup.valueExpr, - byKey(key: unknown): Promise { + byKey(key: RowKey): Promise { const d = Deferred(); this.load({ filter: [lookup.valueExpr, '=', key], diff --git a/packages/devextreme/js/__internal/grids/grid_core/types.ts b/packages/devextreme/js/__internal/grids/grid_core/types.ts index f753cfc9853a..6c7df32ce0df 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/types.ts @@ -111,7 +111,7 @@ export type WrappedLookupDataSource = NormalizedDataSourceOptions & { __dataGridSourceFilter: unknown; load: (loadOptions: StoreLoadOptions) => DeferredObj; key: string | undefined; - byKey: (key: unknown) => Promise; + byKey: (key: RowKey) => Promise; }; export type LookupDataSource = never[] | NormalizedDataSourceOptions | WrappedLookupDataSource; From 285f753c95ff64115a0a5aec6307ba7cde8dcaf3 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Fri, 9 Oct 2026 17:13:42 +0400 Subject: [PATCH 06/11] Grids: Move header filter group parameters to the header_filter module --- .../header_filter/__tests__/utils.test.ts | 20 ++-- .../header_filter/m_header_filter.ts | 10 +- .../grids/grid_core/header_filter/types.ts | 14 +++ .../grids/grid_core/header_filter/utils.ts | 95 +++++++++++++++++++ .../js/__internal/grids/grid_core/m_utils.ts | 94 ------------------ .../js/__internal/grids/grid_core/types.ts | 15 --- .../header_filter/legacy_header_filter.ts | 3 +- .../headerFilter.tests.js | 5 +- 8 files changed, 130 insertions(+), 126 deletions(-) create mode 100644 packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts 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 93470bc8c7b5..519eab026c7c 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,10 +5,10 @@ import { jest, } from '@jest/globals'; import filterUtils from '@js/ui/shared/filtering'; -import type { HeaderFilterGroupSelector } from '@ts/grids/grid_core/types'; -import gridCoreUtils from '../../m_utils'; import { getFormatOptions } from '../m_header_filter'; +import type { HeaderFilterGroupSelector } from '../types'; +import { getHeaderFilterGroupParameters } from '../utils'; describe('getGroupInterval', () => { describe('headerFilter.groupInterval normalization', () => { @@ -72,7 +72,7 @@ describe('getGroupInterval', () => { }); }); -type HeaderFilterColumn = Parameters[0]; +type HeaderFilterColumn = Parameters[0]; const asHeaderFilterColumn = (column: object): HeaderFilterColumn => column as HeaderFilterColumn; @@ -82,7 +82,7 @@ describe('getHeaderFilterGroupParameters', () => { dataField: 'Price', dataType: 'number', headerFilter: { groupInterval: [100, 10] }, }); - expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ + expect(getHeaderFilterGroupParameters(column, true)).toEqual([ { selector: 'Price', groupInterval: 100, isExpanded: true }, { selector: 'Price', groupInterval: 10, isExpanded: false }, ]); @@ -95,7 +95,7 @@ describe('getHeaderFilterGroupParameters', () => { headerFilter: { groupInterval: [100000, 10000, 1000] }, }); - expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ + expect(getHeaderFilterGroupParameters(column, true)).toEqual([ { selector: 'Population', groupInterval: 100000, isExpanded: true }, { selector: 'Population', groupInterval: 10000, isExpanded: true }, { selector: 'Population', groupInterval: 1000, isExpanded: false }, @@ -110,7 +110,7 @@ describe('getHeaderFilterGroupParameters', () => { calculateCellValue: (data: { Price: number }) => data.Price, }); - const params = gridCoreUtils.getHeaderFilterGroupParameters( + const params = getHeaderFilterGroupParameters( column, ) as HeaderFilterGroupSelector[]; @@ -124,7 +124,7 @@ describe('getHeaderFilterGroupParameters', () => { it('should group remotely by the data field', () => { const column = asHeaderFilterColumn({ dataField: 'Name', dataType: 'string' }); - expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ + expect(getHeaderFilterGroupParameters(column, true)).toEqual([ { selector: 'Name', isExpanded: false }, ]); }); @@ -132,7 +132,7 @@ describe('getHeaderFilterGroupParameters', () => { it('should group remotely by the column name when there is no data field', () => { const column = asHeaderFilterColumn({ name: 'nameColumn', dataType: 'string' }); - expect(gridCoreUtils.getHeaderFilterGroupParameters(column, true)).toEqual([ + expect(getHeaderFilterGroupParameters(column, true)).toEqual([ { selector: 'nameColumn', isExpanded: false }, ]); }); @@ -144,7 +144,7 @@ describe('getHeaderFilterGroupParameters', () => { calculateCellValue: (data: { Name?: unknown }) => data.Name, }); - const selector = gridCoreUtils.getHeaderFilterGroupParameters( + const selector = getHeaderFilterGroupParameters( column, ) as HeaderFilterGroupSelector; @@ -163,7 +163,7 @@ describe('getHeaderFilterGroupParameters', () => { sortingMethod, }); - const [descriptor] = gridCoreUtils.getHeaderFilterGroupParameters(column) as { + const [descriptor] = getHeaderFilterGroupParameters(column) as { selector: HeaderFilterGroupSelector; compare: (value1: unknown, value2: unknown) => number; }[]; 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 5618e3192fca..ea1992f1f0c9 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 @@ -30,7 +30,11 @@ import { HeaderFilterView, updateHeaderFilterItemSelectionState, } from './m_header_filter_core'; -import { allowHeaderFiltering, createHeaderFilterExpressions } from './utils'; +import { + allowHeaderFiltering, + createHeaderFilterExpressions, + getHeaderFilterGroupParameters, +} from './utils'; const DATE_INTERVAL_FORMATS = { year(value) { @@ -83,7 +87,7 @@ function ungroupUTCDates(items, dateParts?, dates?) { export function convertDataFromUTCToLocal(data, column) { const dates = ungroupUTCDates(data); const query = dataQuery(dates); - const group = gridCoreUtils.getHeaderFilterGroupParameters({ + const group = getHeaderFilterGroupParameters({ ...column, calculateCellValue: (date) => date, }); @@ -251,7 +255,7 @@ export class HeaderFilterController extends Modules.ViewController { public getDataSource(column) { const dataSourceAdapter = this.dataSourceController.getAdapter(); const remoteGrouping = this.dataSourceController.remoteOperations().grouping; - const group = gridCoreUtils.getHeaderFilterGroupParameters(column, remoteGrouping); + const group = getHeaderFilterGroupParameters(column, remoteGrouping); const headerFilterDataSource = column.headerFilter?.dataSource; const headerFilterOptions = this.option('headerFilter'); let isLookup = false; 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 new file mode 100644 index 000000000000..757d7edb541d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/types.ts @@ -0,0 +1,14 @@ +export type EmptyDateValue = '' | false; + +export type HeaderFilterGroupSelector = (data: unknown) => unknown; + +export interface HeaderFilterGroupDescriptor { + selector: string | HeaderFilterGroupSelector | undefined; + groupInterval?: string | number; + isExpanded?: boolean; + compare?: (value1: unknown, value2: unknown) => number; +} + +export type HeaderFilterGroupItem = HeaderFilterGroupDescriptor | HeaderFilterGroupSelector; + +export type HeaderFilterGroup = HeaderFilterGroupSelector | HeaderFilterGroupItem[]; 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 a0ee2a525761..c18dab9ebf5a 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 @@ -1,8 +1,14 @@ +import type { HeaderFilterGroupInterval } from '@js/common/grids'; +import { each } from '@js/core/utils/iterator'; +import { isDefined, isString } from '@js/core/utils/type'; +import sharedFiltering from '@js/ui/shared/filtering'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; 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/m_utils'; +import type { EmptyDateValue, HeaderFilterGroup, HeaderFilterGroupItem } from './types'; + export const allowHeaderFiltering = ( column: Column, ): boolean | undefined => column.allowHeaderFiltering ?? column.allowFiltering; @@ -62,3 +68,92 @@ export const createHeaderFilterExpressions = ( && hasHeaderFilterValues(column) && !isColumnExcluded(column, excludedColumn)) .map(createHeaderFilterExpression); + +const DATE_INTERVAL_SELECTORS = { + year(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && value.getFullYear(); + }, + month(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && (value.getMonth() + 1); + }, + day(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && value.getDate(); + }, + quarter(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && (Math.floor(value.getMonth() / 3) + 1); + }, + hour(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && value.getHours(); + }, + minute(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && value.getMinutes(); + }, + second(value: Date | EmptyDateValue): number | EmptyDateValue { + return value && value.getSeconds(); + }, +}; + +const getIntervalSelector = function getIntervalSelector( + this: Column & Required>, + interval: HeaderFilterGroupInterval | number, + data: unknown, +): number | EmptyDateValue | null | undefined { + const value = this.calculateCellValue(data); + + if (!isDefined(value)) { + return null; + } + + if (gridCoreUtils.isDateType(this.dataType) && isString(interval)) { + return DATE_INTERVAL_SELECTORS[interval](value); + } + + if (this.dataType === 'number') { + const groupInterval = Number(interval); + return Math.floor(Number(value) / groupInterval) * groupInterval; + } + + return undefined; +}; + +export function getHeaderFilterGroupParameters( + column: Column & Required>, + remoteGrouping?: boolean, +): HeaderFilterGroup { + const dataField = column.dataField || column.name; + const groupInterval = sharedFiltering.getGroupInterval(column); + + if (groupInterval) { + const result: HeaderFilterGroupItem[] = []; + + each(groupInterval, (index, interval) => { + result.push(remoteGrouping ? { + selector: dataField, + groupInterval: interval, + isExpanded: index < groupInterval.length - 1, + // @ts-ignore + } : getIntervalSelector.bind(column, interval)); + }); + + return result; + } + + if (remoteGrouping) { + return [{ selector: dataField, isExpanded: false }]; + } + + const selector = (data: unknown): unknown => { + let value = column.calculateCellValue(data); + + if (value === undefined || value === '') { + value = null; + } + return value; + }; + + if (column.sortingMethod) { + return [{ selector, compare: column.sortingMethod.bind(column) }]; + } + + return selector; +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index bc3aa90f1964..bc2c8647e182 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -6,7 +6,6 @@ import type { GroupDescriptor } from '@js/common/data'; import DataSource from '@js/common/data/data_source'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import { normalizeSortingInfo as normalizeSortingInfoUtility } from '@js/common/data/utils'; -import type { HeaderFilterGroupInterval } from '@js/common/grids'; import { data as elementData } from '@js/core/element_data'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; @@ -15,7 +14,6 @@ import { equalByValue } from '@js/core/utils/common'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; import { getBoundingRect } from '@js/core/utils/position'; import { getHeight, getInnerWidth, getOuterWidth } from '@js/core/utils/size'; import { format } from '@js/core/utils/string'; @@ -25,7 +23,6 @@ import { getWindow } from '@js/core/utils/window'; import formatHelper from '@js/format_helper'; import type { Format } from '@js/localization'; import LoadPanel from '@js/ui/load_panel'; -import sharedFiltering from '@js/ui/shared/filtering'; import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; import type { NormalizedDataSourceOptions, StoreLoadOptions } from '@ts/data/data_source/types'; @@ -34,12 +31,9 @@ import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_control import type { ColumnPoint, ColumnPointProps, - EmptyDateValue, ExpandCellTemplate, ExpandCellTemplateOptions, FormatOptions, - HeaderFilterGroup, - HeaderFilterGroupItem, LoadPanelPosition, LookupDataSource, OptionsReader, @@ -71,29 +65,6 @@ const SCROLLING_MODE_VIRTUAL = 'virtual'; const LEGACY_SCROLLING_MODE = 'scrolling.legacyMode'; const SCROLLING_MODE_OPTION = 'scrolling.mode'; const ROW_RENDERING_MODE_OPTION = 'scrolling.rowRenderingMode'; -const DATE_INTERVAL_SELECTORS = { - year(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && value.getFullYear(); - }, - month(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && (value.getMonth() + 1); - }, - day(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && value.getDate(); - }, - quarter(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && (Math.floor(value.getMonth() / 3) + 1); - }, - hour(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && value.getHours(); - }, - minute(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && value.getMinutes(); - }, - second(value: Date | EmptyDateValue): number | EmptyDateValue { - return value && value.getSeconds(); - }, -}; const DEFAULT_COLUMN_WIDTH = 50; @@ -101,29 +72,6 @@ export function isDateType(dataType: string | undefined): boolean { return dataType === 'date' || dataType === 'datetime'; } -const getIntervalSelector = function getIntervalSelector( - this: Column & Required>, - interval: HeaderFilterGroupInterval | number, - data: unknown, -): number | EmptyDateValue | null | undefined { - const value = this.calculateCellValue(data); - - if (!isDefined(value)) { - return null; - } - - if (isDateType(this.dataType) && isString(interval)) { - return DATE_INTERVAL_SELECTORS[interval](value); - } - - if (this.dataType === 'number') { - const groupInterval = Number(interval); - return Math.floor(Number(value) / groupInterval) * groupInterval; - } - - return undefined; -}; - const getGlobalFormat = (dataType: string): Format | undefined => { const globalFormat = getGlobalFormatByDataType(dataType); @@ -451,48 +399,6 @@ export default { } }, - getHeaderFilterGroupParameters( - column: Column & Required>, - remoteGrouping?: boolean, - ): HeaderFilterGroup { - const dataField = column.dataField || column.name; - const groupInterval = sharedFiltering.getGroupInterval(column); - - if (groupInterval) { - const result: HeaderFilterGroupItem[] = []; - - each(groupInterval, (index, interval) => { - result.push(remoteGrouping ? { - selector: dataField, - groupInterval: interval, - isExpanded: index < groupInterval.length - 1, - // @ts-ignore - } : getIntervalSelector.bind(column, interval)); - }); - - return result; - } - - if (remoteGrouping) { - return [{ selector: dataField, isExpanded: false }]; - } - - const selector = (data: unknown): unknown => { - let value = column.calculateCellValue(data); - - if (value === undefined || value === '') { - value = null; - } - return value; - }; - - if (column.sortingMethod) { - return [{ selector, compare: column.sortingMethod.bind(column) }]; - } - - return selector; - }, - equalSortParameters( sortParameters1: unknown, sortParameters2: unknown, diff --git a/packages/devextreme/js/__internal/grids/grid_core/types.ts b/packages/devextreme/js/__internal/grids/grid_core/types.ts index 6c7df32ce0df..fb1e6d745333 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/types.ts @@ -70,21 +70,6 @@ export interface ColumnPointProps extends ColumnPoint { isRightBoundary?: boolean; } -export type EmptyDateValue = '' | false; - -export type HeaderFilterGroupSelector = (data: unknown) => unknown; - -export interface HeaderFilterGroupDescriptor { - selector: string | HeaderFilterGroupSelector | undefined; - groupInterval?: string | number; - isExpanded?: boolean; - compare?: (value1: unknown, value2: unknown) => number; -} - -export type HeaderFilterGroupItem = HeaderFilterGroupDescriptor | HeaderFilterGroupSelector; - -export type HeaderFilterGroup = HeaderFilterGroupSelector | HeaderFilterGroupItem[]; - export interface ExpandCellTemplateOptions { value?: unknown; data?: { isContinuation?: boolean }; 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 1072316e25ba..669a83d63fa8 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 @@ -23,6 +23,7 @@ import { isUTCFormat, } from '@ts/grids/grid_core/header_filter/m_header_filter'; import { updateHeaderFilterItemSelectionState } from '@ts/grids/grid_core/header_filter/m_header_filter_core'; +import { getHeaderFilterGroupParameters } from '@ts/grids/grid_core/header_filter/utils'; import gridCoreUtils from '@ts/grids/grid_core/m_utils'; import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; @@ -153,7 +154,7 @@ export const getDataSourceOptions = ( const { grouping: localGrouping } = storeLoadAdapter.getLocalLoadOperations(); const remoteGrouping = !localGrouping; - const group = gridCoreUtils.getHeaderFilterGroupParameters(column, remoteGrouping); + const group = getHeaderFilterGroupParameters(column, remoteGrouping); const headerFilterDataSource = column.headerFilter?.dataSource; const options: any = {}; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js index 96cce6af831c..3bbd3a6edd9e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/headerFilter.tests.js @@ -1,7 +1,6 @@ import 'fluent_blue_light.css!'; import 'ui/data_grid'; -import gridCoreUtils from '__internal/grids/grid_core/m_utils'; import $ from 'jquery'; import ArrayStore from 'common/data/array_store'; @@ -9,7 +8,7 @@ import ODataStore from 'common/data/odata/store'; import devices from '__internal/core/devices'; import { DataSource } from 'common/data/data_source/data_source'; import { isFunction } from 'core/utils/type'; -import { invertFilterExpression } from '__internal/grids/grid_core/header_filter/utils'; +import { getHeaderFilterGroupParameters, invertFilterExpression } from '__internal/grids/grid_core/header_filter/utils'; import dragEvents from 'common/core/events/drag'; import { setupDataGridModules, MockDataController, MockColumnsController } from '../../helpers/dataGridMocks.js'; import viewPortUtils from 'core/utils/view_port'; @@ -4272,7 +4271,7 @@ QUnit.module('Header Filter with real columnsController', { const dataSourceOptions = this.headerFilterController.getDataSource(column); // act - const group = gridCoreUtils.getHeaderFilterGroupParameters(column, remoteOperations); + const group = getHeaderFilterGroupParameters(column, remoteOperations); dataSourceOptions.load({ group From b36da4860cb277aa74045abeec5513548f250b64 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Mon, 12 Oct 2026 17:17:00 +0400 Subject: [PATCH 07/11] Grids: Fix grid_core m_utils types after rebase on main --- .../grids/grid_core/__tests__/utils.test.ts | 2 +- .../grids/grid_core/columns_controller/types.ts | 5 +++++ .../js/__internal/grids/grid_core/m_utils.ts | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts b/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts index 8f6357acf07f..83ad32871d01 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/__tests__/utils.test.ts @@ -11,7 +11,7 @@ import { Deferred } from '@js/core/utils/deferred'; import { variableWrapper } from '@ts/core/utils/variable_wrapper'; import type { NormalizedDataSourceOptions } from '@ts/data/data_source/types'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; -import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/m_data_source_adapter'; +import type DataSourceAdapter from '@ts/grids/grid_core/data_source_adapter/data_source_adapter'; import type { DataFilter } from '@ts/grids/grid_core/filter/types'; import type { WrappedLookupDataSource } from '@ts/grids/grid_core/types'; diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts index 851c82ff8a93..ee6e7badddb0 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts @@ -162,6 +162,11 @@ export interface ColumnsDataSourceParameters { export type GroupColumn = Column & { groupIndex: number }; +export type DisplayValueColumn = Pick< + Column, + 'displayValueMap' | 'calculateDisplayValue' | 'type' | 'calculateGroupValue' +> & { lookup?: Pick, 'calculateCellValue'> }; + export interface ColumnsChanges { changeTypes: { sorting?: boolean; diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index bc2c8647e182..292354927865 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -27,7 +27,11 @@ import { getGlobalFormatByDataType } from '@ts/core/global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; import type { NormalizedDataSourceOptions, StoreLoadOptions } from '@ts/data/data_source/types'; import type { SortingInfo } from '@ts/data/utils'; -import type { Column, ColumnsChanges } from '@ts/grids/grid_core/columns_controller/types'; +import type { + Column, + ColumnsChanges, + DisplayValueColumn, +} from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnPoint, ColumnPointProps, @@ -52,6 +56,7 @@ import type { DataFilter } from './filter/types'; import { combineFilters } from './filter/utils'; import type { ModuleItem } from './modules/modules'; import { isEqualSelectors, isSelectorEqualWithCallback } from './utils/index'; +import type { ColumnWidth } from './views/types'; const BASE_LOAD_PANEL_Z_INDEX = 1000; @@ -348,8 +353,13 @@ export default { }; }, + getDisplayValue( + column: DisplayValueColumn, + value: unknown, + data: RawItemData | null, + rowType?: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- callers use it as their own type - getDisplayValue(column: Column, value: unknown, data: RawItemData | null, rowType?: string): any { + ): any { if (column.displayValueMap?.[String(value)] !== undefined) { return column.displayValueMap[String(value)]; } @@ -585,7 +595,7 @@ export default { getWidgetInstance, - getLastResizableColumnIndex(columns: Column[], resultWidths?: (number | string)[]): number { + getLastResizableColumnIndex(columns: Column[], resultWidths?: ColumnWidth[]): number { const hasResizableColumns = columns.some( (column) => column && !column.command && !column.fixed && column.allowResizing !== false, ); From 9bf81db605f5e3cecd950c0c9342d292ba845ea1 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Tue, 13 Oct 2026 15:24:00 +0400 Subject: [PATCH 08/11] Fix comment --- .../grid_core/columns_controller/types.ts | 12 +++++++-- .../js/__internal/grids/grid_core/m_utils.ts | 27 ++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts index ee6e7badddb0..d4e5ff5a726b 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts @@ -4,6 +4,8 @@ import type { dxElementWrapper } from '@js/core/renderer'; import type { Callback } from '@js/core/utils/callbacks'; import type { DeferredObj } from '@js/core/utils/deferred'; import type { Properties as DataGridProperties } from '@js/ui/data_grid'; +import type { StoreLoadOptions } from '@ts/data/data_source/types'; +import type { SortingInfo } from '@ts/data/utils'; import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types'; import type { DataFilter } from '../filter/types'; @@ -154,9 +156,15 @@ export interface ColumnDataSourceParameter { isExpanded?: boolean; } +export type SortParameters = StoreLoadOptions['sort'] + | StoreLoadOptions['group'] + | ColumnDataSourceParameter[] + | SortingInfo[] + | null; + export interface ColumnsDataSourceParameters { - sorting: unknown; - grouping: unknown; + sorting: SortParameters; + grouping: SortParameters; filtering?: DataFilter; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 292354927865..a60ee5bcb1a8 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -31,6 +31,7 @@ import type { Column, ColumnsChanges, DisplayValueColumn, + SortParameters, } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnPoint, @@ -98,17 +99,25 @@ const setEmptyText = ($container: dxElementWrapper): void => { $container.get(0).textContent = '\u00A0'; }; -const normalizeSortingInfo = (sort: unknown): SortingInfo[] => { +const normalizeSortingInfo = (sort: SortParameters): SortingInfo[] => { const sortItems = sort || []; const result = normalizeSortingInfoUtility(sortItems); - if (Array.isArray(sortItems)) { - for (let i = 0; i < sortItems.length; i += 1) { - if (sortItems?.[i]?.isExpanded !== undefined) { - result[i].isExpanded = sortItems[i].isExpanded; + if (!Array.isArray(sortItems)) { + return result; + } + + for (let i = 0; i < sortItems.length; i += 1) { + const item = sortItems[i]; + + if (item && typeof item === 'object') { + const { isExpanded, groupInterval }: Partial = item; + + if (isExpanded !== undefined) { + result[i].isExpanded = isExpanded; } - if (sortItems?.[i]?.groupInterval !== undefined) { - result[i].groupInterval = sortItems[i].groupInterval; + if (groupInterval !== undefined) { + result[i].groupInterval = groupInterval; } } } @@ -410,8 +419,8 @@ export default { }, equalSortParameters( - sortParameters1: unknown, - sortParameters2: unknown, + sortParameters1: SortParameters, + sortParameters2: SortParameters, ignoreIsExpanded?: boolean, ): boolean { const sortInfo1 = normalizeSortingInfo(sortParameters1); From 857e36c7426cede759b6110dcb3be2cf3d6d9624 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Wed, 14 Oct 2026 12:14:21 +0400 Subject: [PATCH 09/11] Grids: Fix grid_core m_utils types after the m_rows_view typing --- packages/devextreme/js/__internal/grids/grid_core/m_utils.ts | 2 +- .../js/__internal/grids/grid_core/views/m_rows_view.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index a60ee5bcb1a8..d40ea665b071 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -365,7 +365,7 @@ export default { getDisplayValue( column: DisplayValueColumn, value: unknown, - data: RawItemData | null, + data: RawItemData | null | undefined, rowType?: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- callers use it as their own type ): any { diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts index cda107c3254c..c9d599640310 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_rows_view.ts @@ -41,7 +41,7 @@ import type { FocusController } from '@ts/grids/grid_core/focus/m_focus'; import type { KeyboardNavigationController, } from '@ts/grids/grid_core/keyboard_navigation/m_keyboard_navigation'; -import type { OptionChanged } from '@ts/grids/grid_core/types'; +import type { OptionChanged, SummaryTextItem } from '@ts/grids/grid_core/types'; import type { ValidatingController } from '@ts/grids/grid_core/validating/m_validating'; import type { ResizingController } from '@ts/grids/grid_core/views/grid_view'; import Scrollable from '@ts/ui/scroll_view/scrollable'; @@ -239,7 +239,7 @@ export class RowsView extends ColumnsView { const container = $container.get(0); if (options.summaryItems?.length) { - text += ` ${gridCoreUtils.getGroupRowSummaryText(options.summaryItems, summaryTexts)}`; + text += ` ${gridCoreUtils.getGroupRowSummaryText(options.summaryItems as SummaryTextItem[], summaryTexts)}`; } if (data) { From 4dce9022c60b1d4fa40ffa0eaf16ce834bf5071e Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 15 Oct 2026 13:00:01 +0400 Subject: [PATCH 10/11] Grids: Fix grid_core m_utils types after the m_rows_view refactoring --- packages/devextreme/js/__internal/grids/grid_core/m_utils.ts | 4 +++- packages/devextreme/js/__internal/grids/grid_core/types.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index d40ea665b071..576a08eba6a3 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -32,6 +32,7 @@ import type { ColumnsChanges, DisplayValueColumn, SortParameters, + TemplateModel, } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnPoint, @@ -514,7 +515,8 @@ export default { getExpandCellTemplate(): ExpandCellTemplate { return { allowRenderToDetachedContainer: true, - render(container: dxElementWrapper, options: ExpandCellTemplateOptions): void { + render(container: dxElementWrapper, model: TemplateModel): void { + const options = model as ExpandCellTemplateOptions; const $container = $(container); if ( diff --git a/packages/devextreme/js/__internal/grids/grid_core/types.ts b/packages/devextreme/js/__internal/grids/grid_core/types.ts index fb1e6d745333..82a436999052 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/types.ts @@ -17,6 +17,7 @@ import type Widget from '@js/ui/widget/ui.widget'; import type { IntegrationOptions } from '@ts/core/template_manager'; import type { ActionConfig } from '@ts/core/widget/component'; import type { NormalizedDataSourceOptions, StoreLoadOptions } from '@ts/data/data_source/types'; +import type { TemplateModel } from '@ts/grids/grid_core/columns_controller/types'; import type { FilterValue } from '@ts/grids/grid_core/filter/types'; import type { ModuleItem } from '@ts/grids/grid_core/modules/modules'; @@ -79,7 +80,7 @@ export interface ExpandCellTemplateOptions { export interface ExpandCellTemplate { allowRenderToDetachedContainer: boolean; - render: (container: dxElementWrapper, options: ExpandCellTemplateOptions) => void; + render: (container: dxElementWrapper, model: TemplateModel) => void; } export type TextSelectionElement = Element & { From e808e3277e03d38307aac8b6d5411d8370603fc9 Mon Sep 17 00:00:00 2001 From: Alyar <> Date: Thu, 15 Oct 2026 15:08:00 +0400 Subject: [PATCH 11/11] Fix review comments --- .../grid_core/columns_controller/types.ts | 8 +++++-- .../grids/grid_core/header_filter/utils.ts | 10 ++++---- .../js/__internal/grids/grid_core/m_utils.ts | 23 ++++++++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts index d4e5ff5a726b..c85d1ef39abb 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/types.ts @@ -157,14 +157,18 @@ export interface ColumnDataSourceParameter { } export type SortParameters = StoreLoadOptions['sort'] - | StoreLoadOptions['group'] + | ColumnDataSourceParameter[] + | SortingInfo[] + | null; + +export type GroupParameters = StoreLoadOptions['group'] | ColumnDataSourceParameter[] | SortingInfo[] | null; export interface ColumnsDataSourceParameters { sorting: SortParameters; - grouping: SortParameters; + grouping: GroupParameters; filtering?: DataFilter; } 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 c18dab9ebf5a..e92eb9e51bc7 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 @@ -131,7 +131,7 @@ export function getHeaderFilterGroupParameters( selector: dataField, groupInterval: interval, isExpanded: index < groupInterval.length - 1, - // @ts-ignore + // @ts-expect-error getGroupInterval() types interval names and numbers as string[] } : getIntervalSelector.bind(column, interval)); }); @@ -143,12 +143,12 @@ export function getHeaderFilterGroupParameters( } const selector = (data: unknown): unknown => { - let value = column.calculateCellValue(data); + const cellValue = column.calculateCellValue(data); - if (value === undefined || value === '') { - value = null; + if (cellValue === undefined || cellValue === '') { + return null; } - return value; + return cellValue; }; if (column.sortingMethod) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index 576a08eba6a3..592ff35d263d 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -31,6 +31,7 @@ import type { Column, ColumnsChanges, DisplayValueColumn, + GroupParameters, SortParameters, TemplateModel, } from '@ts/grids/grid_core/columns_controller/types'; @@ -100,7 +101,7 @@ const setEmptyText = ($container: dxElementWrapper): void => { $container.get(0).textContent = '\u00A0'; }; -const normalizeSortingInfo = (sort: SortParameters): SortingInfo[] => { +const normalizeSortingInfo = (sort: SortParameters | GroupParameters): SortingInfo[] => { const sortItems = sort || []; const result = normalizeSortingInfoUtility(sortItems); @@ -420,8 +421,8 @@ export default { }, equalSortParameters( - sortParameters1: SortParameters, - sortParameters2: SortParameters, + sortParameters1: SortParameters | GroupParameters, + sortParameters2: SortParameters | GroupParameters, ignoreIsExpanded?: boolean, ): boolean { const sortInfo1 = normalizeSortingInfo(sortParameters1); @@ -432,12 +433,18 @@ export default { return false; } for (let i = 0; i < sortInfo1.length; i += 1) { + const sortItem1 = sortInfo1[i]; + const sortItem2 = sortInfo2[i]; + const isSameSelector = isEqualSelectors(sortItem1.selector, sortItem2.selector); + const isSameSortOrder = sortItem1.desc === sortItem2.desc; + const isSameGroupInterval = sortItem1.groupInterval === sortItem2.groupInterval; + const isSameExpandState = Boolean(sortItem1.isExpanded) === Boolean(sortItem2.isExpanded); + if ( - !isEqualSelectors(sortInfo1[i].selector, sortInfo2[i].selector) - || sortInfo1[i].desc !== sortInfo2[i].desc - || sortInfo1[i].groupInterval !== sortInfo2[i].groupInterval - || (!ignoreIsExpanded - && Boolean(sortInfo1[i].isExpanded) !== Boolean(sortInfo2[i].isExpanded)) + !isSameSelector + || !isSameSortOrder + || !isSameGroupInterval + || (!ignoreIsExpanded && !isSameExpandState) ) { return false; }