Skip to content

Commit fac5aac

Browse files
DataGrid: extract grouping dataController extender (#34973)
1 parent 7e49496 commit fac5aac

14 files changed

Lines changed: 420 additions & 296 deletions

File tree

packages/devextreme/js/__internal/grids/data_grid/grouping/__tests__/utils.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import {
22
describe, expect, it,
33
} from '@jest/globals';
44
import type { ProcessedItem } from '@ts/grids/grid_core/data_controller/types';
5+
import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types';
56

6-
import { isSameContinuationState, isSameExpandedState } from '../utils';
7+
import {
8+
isGroupNode, isGroupRow, isSameContinuationState, isSameExpandedState,
9+
} from '../utils';
710

811
const groupRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => ({
912
rowType: 'group',
@@ -13,6 +16,62 @@ const groupRow = (partial: Partial<ProcessedItem> = {}): ProcessedItem => ({
1316
...partial,
1417
});
1518

19+
describe('isGroupNode', () => {
20+
it('should return true for a group node with children', () => {
21+
expect(isGroupNode({ key: 'Alex', items: [{ id: 1 }] })).toBe(true);
22+
});
23+
24+
it('should return true for a collapsed group node, whose items are null', () => {
25+
expect(isGroupNode({ key: 'Alex', items: null, count: 3 })).toBe(true);
26+
});
27+
28+
it('should return true when the items key is present but undefined', () => {
29+
expect(isGroupNode({ key: 'Alex', items: undefined })).toBe(true);
30+
});
31+
32+
it('should return false for a data row, which has no items key', () => {
33+
expect(isGroupNode({ id: 1, name: 'Alex' })).toBe(false);
34+
});
35+
});
36+
37+
describe('isGroupRow', () => {
38+
it('should return true for a group row', () => {
39+
expect(isGroupRow({ rowType: 'group', groupIndex: 1 })).toBe(true);
40+
});
41+
42+
it('should return true for a group footer row added by the summary module', () => {
43+
expect(isGroupRow({ rowType: 'groupFooter', groupIndex: 1 })).toBe(true);
44+
});
45+
46+
it('should return true when groupIndex is zero', () => {
47+
expect(isGroupRow({ rowType: 'group', groupIndex: 0 })).toBe(true);
48+
});
49+
50+
it('should return false when groupIndex is missing', () => {
51+
expect(isGroupRow({ rowType: 'group' })).toBe(false);
52+
});
53+
54+
it('should return false when groupIndex is null', () => {
55+
expect(isGroupRow({ rowType: 'group', groupIndex: null })).toBe(false);
56+
});
57+
58+
it('should return false when rowType is not a string', () => {
59+
expect(isGroupRow({ rowType: 1, groupIndex: 0 })).toBe(false);
60+
});
61+
62+
it('should return false for a data row', () => {
63+
expect(isGroupRow({ rowType: 'data', groupIndex: 0 })).toBe(false);
64+
});
65+
66+
it('should return false when rowType only contains group', () => {
67+
expect(isGroupRow({ rowType: 'detailGroup', groupIndex: 0 })).toBe(false);
68+
});
69+
70+
it('should return false for a primitive item', () => {
71+
expect(isGroupRow('Alex' as unknown as RawItemData)).toBe(false);
72+
});
73+
});
74+
1675
describe('isSameExpandedState', () => {
1776
it('should return true when both rows are expanded', () => {
1877
expect(isSameExpandedState(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import gridCore from '@ts/grids/data_grid/m_core';
2+
import type { ColumnsController } from '@ts/grids/grid_core/columns_controller/m_columns_controller';
3+
import type { ModuleType } from '@ts/grids/grid_core/m_types';
4+
5+
export const groupingColumnsControllerExtender = (
6+
Base: ModuleType<ColumnsController>,
7+
): ModuleType<ColumnsController> => class GroupingColumnsExtender extends Base {
8+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
9+
public _getExpandColumnOptions() {
10+
const options = super._getExpandColumnOptions();
11+
12+
// @ts-expect-error
13+
options.cellTemplate = gridCore.getExpandCellTemplate();
14+
15+
return options;
16+
}
17+
};
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import type { DeferredObj } from '@js/core/utils/deferred';
2+
import { Deferred, when } from '@js/core/utils/deferred';
3+
import type { Properties } from '@js/ui/data_grid';
4+
import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller';
5+
import type { ItemProcessingOptions, ProcessedItem } from '@ts/grids/grid_core/data_controller/types';
6+
import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types';
7+
import type {
8+
ModuleType,
9+
OptionChanged,
10+
OptionChangedFor,
11+
RowKey,
12+
} from '@ts/grids/grid_core/m_types';
13+
14+
import type {
15+
ChangeRowExpandArgs, GroupItem, ProcessGroupItemsOptions,
16+
} from '../types';
17+
import {
18+
isGroupNode, isGroupRow, isSameContinuationState, isSameExpandedState,
19+
} from '../utils';
20+
21+
export const groupingDataControllerExtender = (
22+
Base: ModuleType<DataController>,
23+
): ModuleType<DataController> => class GroupingDataControllerExtender extends Base {
24+
public init(): void {
25+
super.init();
26+
27+
this.createAction('onRowExpanding');
28+
this.createAction('onRowExpanded');
29+
this.createAction('onRowCollapsing');
30+
this.createAction('onRowCollapsed');
31+
}
32+
33+
protected _beforeProcessItems(items: RawItemData[]): (RawItemData | GroupItem)[] {
34+
const baseItems = super._beforeProcessItems(items);
35+
36+
const groupColumns = this._columnsController.getGroupColumns();
37+
38+
if (!baseItems.length || !groupColumns.length) {
39+
return baseItems;
40+
}
41+
42+
return this.processGroupItems(baseItems, groupColumns.length);
43+
}
44+
45+
protected _processItem(
46+
dataItem: RawItemData | GroupItem,
47+
options: ItemProcessingOptions,
48+
): ProcessedItem {
49+
if (isGroupRow(dataItem)) {
50+
const processedGroupItem = this.processGroupItem(dataItem, options);
51+
options.dataIndex = 0;
52+
return processedGroupItem;
53+
}
54+
55+
return super._processItem(dataItem, options);
56+
}
57+
58+
protected processGroupItem(
59+
groupItem: GroupItem,
60+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
61+
options: ItemProcessingOptions,
62+
): ProcessedItem {
63+
return groupItem;
64+
}
65+
66+
private getDefaultProcessGroupItemsOptions(): ProcessGroupItemsOptions {
67+
const scrollingMode = this.option('scrolling.mode');
68+
69+
return {
70+
collectContinuationItems: scrollingMode !== 'virtual' && scrollingMode !== 'infinite',
71+
resultItems: [],
72+
path: [],
73+
values: [],
74+
};
75+
}
76+
77+
protected processGroupItems(
78+
items: RawItemData[] | null | undefined,
79+
groupsCount: number,
80+
parentOptions?: ProcessGroupItemsOptions,
81+
): (RawItemData | GroupItem)[] {
82+
const groupedColumns = this._columnsController.getGroupColumns();
83+
const column = groupedColumns[groupedColumns.length - groupsCount];
84+
85+
const options = parentOptions ?? this.getDefaultProcessGroupItemsOptions();
86+
const { resultItems } = options;
87+
88+
if (options.data) {
89+
if (options.collectContinuationItems || !options.data.isContinuation) {
90+
resultItems.push({
91+
rowType: 'group',
92+
data: options.data,
93+
groupIndex: options.path.length - 1,
94+
isExpanded: !!options.data.items,
95+
key: options.path.slice(),
96+
values: options.values.slice(),
97+
});
98+
}
99+
}
100+
101+
if (!items) {
102+
return resultItems;
103+
}
104+
105+
if (groupsCount === 0) {
106+
resultItems.push(...items);
107+
108+
return resultItems;
109+
}
110+
111+
for (const item of items) {
112+
if (item && isGroupNode(item)) {
113+
options.data = item;
114+
options.path.push(item.key);
115+
options.values.push(
116+
column?.deserializeValue && !column.calculateDisplayValue
117+
? column.deserializeValue(item.key)
118+
: item.key,
119+
);
120+
121+
this.processGroupItems(item.items, groupsCount - 1, options);
122+
123+
options.data = undefined;
124+
options.path.pop();
125+
options.values.pop();
126+
} else {
127+
resultItems.push(item);
128+
}
129+
}
130+
131+
return resultItems;
132+
}
133+
134+
protected isSameRowState(item1: ProcessedItem, item2: ProcessedItem): boolean {
135+
if (item1.rowType === 'group'
136+
&& (!isSameExpandedState(item1, item2) || !isSameContinuationState(item1, item2))) {
137+
return false;
138+
}
139+
140+
return super.isSameRowState(item1, item2);
141+
}
142+
143+
public publicMethods(): string[] {
144+
const groupingPublicMethods = ['collapseAll', 'expandAll', 'isRowExpanded', 'expandRow', 'collapseRow'];
145+
146+
return [...super.publicMethods(), ...groupingPublicMethods];
147+
}
148+
149+
private collapseAll(groupIndex: number): void {
150+
const dataSource = this._dataSource;
151+
if (dataSource?.collapseAll(groupIndex)) {
152+
dataSource.pageIndex(0);
153+
dataSource.reload();
154+
}
155+
}
156+
157+
private expandAll(groupIndex: number): void {
158+
const dataSource = this._dataSource;
159+
if (dataSource?.expandAll(groupIndex)) {
160+
dataSource.pageIndex(0);
161+
dataSource.reload();
162+
}
163+
}
164+
165+
private changeRowExpand(key: RowKey): DeferredObj<unknown> {
166+
const expanded = this.isRowExpanded(key);
167+
const args: ChangeRowExpandArgs = {
168+
key,
169+
expanded,
170+
};
171+
172+
this.executeAction(expanded ? 'onRowCollapsing' : 'onRowExpanding', args);
173+
174+
if (!args.cancel) {
175+
return when(this.changeRowExpandCore(key)).done(() => {
176+
args.expanded = !expanded;
177+
this.executeAction(expanded ? 'onRowCollapsed' : 'onRowExpanded', args);
178+
});
179+
}
180+
181+
return Deferred().resolve();
182+
}
183+
184+
protected changeRowExpandCore(key: RowKey): DeferredObj<unknown> {
185+
const dataSource = this._dataSource;
186+
187+
const d = Deferred();
188+
if (!dataSource) {
189+
d.resolve();
190+
} else {
191+
when(dataSource.changeRowExpand(key)).done(() => {
192+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
193+
this.load().done(d.resolve).fail(d.reject);
194+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
195+
}).fail(d.reject);
196+
}
197+
198+
return d;
199+
}
200+
201+
private isRowExpanded(key: RowKey): boolean {
202+
return !!this._dataSource?.isRowExpanded(key);
203+
}
204+
205+
private expandRow(key: RowKey): DeferredObj<unknown> {
206+
if (!this.isRowExpanded(key)) {
207+
return this.changeRowExpand(key);
208+
}
209+
210+
return Deferred().resolve();
211+
}
212+
213+
private collapseRow(key: RowKey): DeferredObj<unknown> {
214+
if (this.isRowExpanded(key)) {
215+
return this.changeRowExpand(key);
216+
}
217+
218+
return Deferred().resolve();
219+
}
220+
221+
public optionChanged(e: OptionChanged | OptionChangedFor<Pick<Properties, 'grouping'>>): void {
222+
if (e.name === 'grouping') {
223+
e.handled = true;
224+
this.reset();
225+
return;
226+
}
227+
228+
super.optionChanged(e);
229+
}
230+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { RawItemData } from '@ts/grids/grid_core/data_source_adapter/types';
2+
import type { EditingController } from '@ts/grids/grid_core/editing/m_editing';
3+
import type { ModuleType } from '@ts/grids/grid_core/m_types';
4+
5+
import type { GroupItem } from '../types';
6+
import { isGroupRow } from '../utils';
7+
8+
export const groupingEditingControllerExtender = (
9+
Base: ModuleType<EditingController>,
10+
): ModuleType<EditingController> => class GroupingEditingExtender extends Base {
11+
protected _isProcessedItem(item: RawItemData | GroupItem): boolean {
12+
return isGroupRow(item);
13+
}
14+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import messageLocalization from '@js/common/core/localization/message';
2+
import type { Properties } from '@js/ui/data_grid';
3+
import gridCore from '@ts/grids/data_grid/m_core';
4+
5+
import { groupingColumnsControllerExtender } from './extenders/grouping_columns_controller';
6+
import { groupingDataControllerExtender } from './extenders/grouping_data_controller';
7+
import { groupingEditingControllerExtender } from './extenders/grouping_editing_controller';
8+
import {
9+
columnHeadersViewExtender,
10+
GroupingHeaderPanelExtender,
11+
GroupingRowsViewExtender,
12+
} from './m_grouping';
13+
14+
gridCore.registerModule('grouping', {
15+
defaultOptions(): Pick<Properties, 'grouping' | 'groupPanel'> {
16+
return {
17+
grouping: {
18+
autoExpandAll: true,
19+
allowCollapsing: true,
20+
contextMenuEnabled: true,
21+
expandMode: 'buttonClick',
22+
texts: {
23+
groupContinuesMessage: messageLocalization.format('dxDataGrid-groupContinuesMessage'),
24+
groupContinuedMessage: messageLocalization.format('dxDataGrid-groupContinuedMessage'),
25+
groupByThisColumn: messageLocalization.format('dxDataGrid-groupHeaderText'),
26+
ungroup: messageLocalization.format('dxDataGrid-ungroupHeaderText'),
27+
ungroupAll: messageLocalization.format('dxDataGrid-ungroupAllText'),
28+
},
29+
},
30+
groupPanel: {
31+
visible: false,
32+
emptyPanelText: messageLocalization.format('dxDataGrid-groupPanelEmptyText'),
33+
allowColumnDragging: true,
34+
},
35+
};
36+
},
37+
extenders: {
38+
controllers: {
39+
data: groupingDataControllerExtender,
40+
columns: groupingColumnsControllerExtender,
41+
editing: groupingEditingControllerExtender,
42+
},
43+
views: {
44+
headerPanel: GroupingHeaderPanelExtender,
45+
rowsView: GroupingRowsViewExtender,
46+
columnHeadersView: columnHeadersViewExtender,
47+
},
48+
},
49+
});

0 commit comments

Comments
 (0)