mirror of
https://github.com/apache/superset.git
synced 2026-04-17 23:25:05 +00:00
feat: add interactive column sorting to pivot table (#36050)
This commit is contained in:
1
superset-frontend/package-lock.json
generated
1
superset-frontend/package-lock.json
generated
@@ -66465,6 +66465,7 @@
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@apache-superset/core": "*",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"lodash": "^4.17.11",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@apache-superset/core": "*",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
|
||||
@@ -21,6 +21,9 @@ import { Component } from 'react';
|
||||
import { safeHtmlSpan } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FaSort } from '@react-icons/all-files/fa/FaSort';
|
||||
import { FaSortDown as FaSortDesc } from '@react-icons/all-files/fa/FaSortDown';
|
||||
import { FaSortUp as FaSortAsc } from '@react-icons/all-files/fa/FaSortUp';
|
||||
import { PivotData, flatKey } from './utilities';
|
||||
import { Styles } from './Styles';
|
||||
|
||||
@@ -72,6 +75,88 @@ function displayHeaderCell(
|
||||
);
|
||||
}
|
||||
|
||||
function sortHierarchicalObject(obj, objSort, rowPartialOnTop) {
|
||||
// Performs a recursive sort of nested object structures. Sorts objects based on
|
||||
// their currentVal property. The function preserves the hierarchical structure
|
||||
// while sorting each level according to the specified criteria.
|
||||
const sortedKeys = Object.keys(obj).sort((a, b) => {
|
||||
const valA = obj[a].currentVal || 0;
|
||||
const valB = obj[b].currentVal || 0;
|
||||
if (rowPartialOnTop) {
|
||||
if (obj[a].currentVal !== undefined && obj[b].currentVal === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (obj[b].currentVal !== undefined && obj[a].currentVal === undefined) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return objSort === 'asc' ? valA - valB : valB - valA;
|
||||
});
|
||||
|
||||
const result = new Map();
|
||||
sortedKeys.forEach(key => {
|
||||
const value = obj[key];
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
result.set(key, sortHierarchicalObject(value, objSort, rowPartialOnTop));
|
||||
} else {
|
||||
result.set(key, value);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function convertToArray(
|
||||
obj,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
parentKeys = [],
|
||||
result = [],
|
||||
flag = false,
|
||||
) {
|
||||
// Recursively flattens a hierarchical Map structure into an array of key paths.
|
||||
// Handles different rendering scenarios based on row grouping configurations and
|
||||
// depth limitations. The function supports complex hierarchy flattening with
|
||||
let updatedFlag = flag;
|
||||
|
||||
const keys = Array.from(obj.keys());
|
||||
const getValue = key => obj.get(key);
|
||||
|
||||
keys.forEach(key => {
|
||||
if (key === 'currentVal') {
|
||||
return;
|
||||
}
|
||||
const value = getValue(key);
|
||||
if (rowEnabled && rowPartialOnTop && parentKeys.length < maxRowIndex - 1) {
|
||||
result.push(parentKeys.length > 0 ? [...parentKeys, key] : [key]);
|
||||
updatedFlag = true;
|
||||
}
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
convertToArray(
|
||||
value,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
[...parentKeys, key],
|
||||
result,
|
||||
);
|
||||
}
|
||||
if (
|
||||
parentKeys.length >= maxRowIndex - 1 ||
|
||||
(rowEnabled && !rowPartialOnTop)
|
||||
) {
|
||||
if (!updatedFlag) {
|
||||
result.push(parentKeys.length > 0 ? [...parentKeys, key] : [key]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (parentKeys.length === 0 && maxRowIndex === 1) {
|
||||
result.push([key]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export class TableRenderer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -79,8 +164,8 @@ export class TableRenderer extends Component {
|
||||
// We need state to record which entries are collapsed and which aren't.
|
||||
// This is an object with flat-keys indicating if the corresponding rows
|
||||
// should be collapsed.
|
||||
this.state = { collapsedRows: {}, collapsedCols: {} };
|
||||
|
||||
this.state = { collapsedRows: {}, collapsedCols: {}, sortingOrder: [] };
|
||||
this.sortCache = new Map();
|
||||
this.clickHeaderHandler = this.clickHeaderHandler.bind(this);
|
||||
this.clickHandler = this.clickHandler.bind(this);
|
||||
}
|
||||
@@ -349,6 +434,108 @@ export class TableRenderer extends Component {
|
||||
return spans;
|
||||
}
|
||||
|
||||
getAggregatedData(pivotData, visibleColName, rowPartialOnTop) {
|
||||
// Transforms flat row keys into a hierarchical group structure where each level
|
||||
// represents a grouping dimension. For each row key path, it calculates the
|
||||
// aggregated value for the specified column and builds a nested object that
|
||||
// preserves the hierarchy while storing aggregation values at each level.
|
||||
const groups = {};
|
||||
const rows = pivotData.rowKeys;
|
||||
rows.forEach(rowKey => {
|
||||
const aggValue =
|
||||
pivotData.getAggregator(rowKey, visibleColName).value() ?? 0;
|
||||
|
||||
if (rowPartialOnTop) {
|
||||
const parent = rowKey
|
||||
.slice(0, -1)
|
||||
.reduce((acc, key) => (acc[key] ??= {}), groups);
|
||||
parent[rowKey.at(-1)] = { currentVal: aggValue };
|
||||
} else {
|
||||
rowKey.reduce((acc, key) => {
|
||||
acc[key] = acc[key] || { currentVal: 0 };
|
||||
acc[key].currentVal = aggValue;
|
||||
return acc[key];
|
||||
}, groups);
|
||||
}
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
sortAndCacheData(
|
||||
groups,
|
||||
sortOrder,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
) {
|
||||
// Processes hierarchical data by first sorting it according to the specified order
|
||||
// and then converting the sorted structure into a flat array format. This function
|
||||
// serves as an intermediate step between hierarchical data representation and
|
||||
// flat array representation needed for rendering.
|
||||
const sortedGroups = sortHierarchicalObject(
|
||||
groups,
|
||||
sortOrder,
|
||||
rowPartialOnTop,
|
||||
);
|
||||
return convertToArray(
|
||||
sortedGroups,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
);
|
||||
}
|
||||
|
||||
sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex) {
|
||||
// Handles column sorting with direction toggling (asc/desc) and implements
|
||||
// caching mechanism to avoid redundant sorting operations. When sorting the same
|
||||
// column multiple times, it cycles through sorting directions. Uses composite
|
||||
// cache keys based on sorting parameters for optimal performance.
|
||||
this.setState(state => {
|
||||
const { sortingOrder, activeSortColumn } = state;
|
||||
|
||||
const newSortingOrder = [];
|
||||
let newDirection = 'asc';
|
||||
|
||||
if (activeSortColumn === columnIndex) {
|
||||
newDirection = sortingOrder[columnIndex] === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
const { rowEnabled, rowPartialOnTop } = pivotData.subtotals;
|
||||
newSortingOrder[columnIndex] = newDirection;
|
||||
|
||||
const cacheKey = `${columnIndex}-${visibleColKeys.length}-${rowEnabled}-${rowPartialOnTop}-${newDirection}`;
|
||||
let newRowKeys;
|
||||
if (this.sortCache.has(cacheKey)) {
|
||||
const cachedRowKeys = this.sortCache.get(cacheKey);
|
||||
newRowKeys = cachedRowKeys;
|
||||
} else {
|
||||
const groups = this.getAggregatedData(
|
||||
pivotData,
|
||||
visibleColKeys[columnIndex],
|
||||
rowPartialOnTop,
|
||||
);
|
||||
const sortedRowKeys = this.sortAndCacheData(
|
||||
groups,
|
||||
newDirection,
|
||||
rowEnabled,
|
||||
rowPartialOnTop,
|
||||
maxRowIndex,
|
||||
);
|
||||
this.sortCache.set(cacheKey, sortedRowKeys);
|
||||
newRowKeys = sortedRowKeys;
|
||||
}
|
||||
this.cachedBasePivotSettings = {
|
||||
...this.cachedBasePivotSettings,
|
||||
rowKeys: newRowKeys,
|
||||
};
|
||||
|
||||
return {
|
||||
sortingOrder: newSortingOrder,
|
||||
activeSortColumn: columnIndex,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
renderColHeaderRow(attrName, attrIdx, pivotSettings) {
|
||||
// Render a single row in the column header at the top of the pivot table.
|
||||
|
||||
@@ -434,11 +621,35 @@ export class TableRenderer extends Component {
|
||||
) {
|
||||
colLabelClass += ' active';
|
||||
}
|
||||
const { maxRowVisible: maxRowIndex, maxColVisible } = pivotSettings;
|
||||
const visibleSortIcon = maxColVisible - 1 === attrIdx;
|
||||
const columnName = colKey[maxColVisible - 1];
|
||||
|
||||
const rowSpan = 1 + (attrIdx === colAttrs.length - 1 ? rowIncrSpan : 0);
|
||||
const flatColKey = flatKey(colKey.slice(0, attrIdx + 1));
|
||||
const onArrowClick = needToggle ? this.toggleColKey(flatColKey) : null;
|
||||
const getSortIcon = key => {
|
||||
const { activeSortColumn, sortingOrder } = this.state;
|
||||
|
||||
if (activeSortColumn !== key) {
|
||||
return (
|
||||
<FaSort
|
||||
onClick={() =>
|
||||
this.sortData(key, visibleColKeys, pivotData, maxRowIndex)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SortIcon = sortingOrder[key] === 'asc' ? FaSortAsc : FaSortDesc;
|
||||
return (
|
||||
<SortIcon
|
||||
onClick={() =>
|
||||
this.sortData(key, visibleColKeys, pivotData, maxRowIndex)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const headerCellFormattedValue =
|
||||
dateFormatters &&
|
||||
dateFormatters[attrName] &&
|
||||
@@ -471,6 +682,22 @@ export class TableRenderer extends Component {
|
||||
namesMapping,
|
||||
allowRenderHtml,
|
||||
)}
|
||||
<span
|
||||
role="columnheader"
|
||||
tabIndex={0}
|
||||
// Prevents event bubbling to avoid conflict with column header click handlers
|
||||
// Ensures sort operation executes without triggering cross-filtration
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
aria-label={
|
||||
this.state.activeSortColumn === i
|
||||
? `Sorted by ${columnName} ${this.state.sortingOrder[i] === 'asc' ? 'ascending' : 'descending'}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{visibleSortIcon && getSortIcon(i)}
|
||||
</span>
|
||||
</th>,
|
||||
);
|
||||
} else if (attrIdx === colKey.length) {
|
||||
@@ -879,8 +1106,15 @@ export class TableRenderer extends Component {
|
||||
return document.contains(document.querySelector('.dashboard--editing'));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.sortCache.clear();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.cachedProps !== this.props) {
|
||||
this.sortCache.clear();
|
||||
this.state.sortingOrder = [];
|
||||
this.state.activeSortColumn = null;
|
||||
this.cachedProps = this.props;
|
||||
this.cachedBasePivotSettings = this.getBasePivotSettings();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { TableRenderer } from '../../src/react-pivottable/TableRenderers';
|
||||
|
||||
let tableRenderer: TableRenderer;
|
||||
let mockGetAggregatedData: jest.Mock;
|
||||
let mockSortAndCacheData: jest.Mock;
|
||||
|
||||
const columnIndex = 0;
|
||||
const visibleColKeys = [['col1'], ['col2']];
|
||||
const pivotData = {
|
||||
subtotals: {
|
||||
rowEnabled: true,
|
||||
rowPartialOnTop: false,
|
||||
},
|
||||
} as any;
|
||||
const maxRowIndex = 2;
|
||||
|
||||
const mockProps = {
|
||||
rows: ['row1'],
|
||||
cols: ['col1'],
|
||||
data: [],
|
||||
aggregatorName: 'Sum',
|
||||
vals: ['value'],
|
||||
valueFilter: {},
|
||||
sorters: {},
|
||||
rowOrder: 'key_a_to_z',
|
||||
colOrder: 'key_a_to_z',
|
||||
tableOptions: {},
|
||||
namesMapping: {},
|
||||
allowRenderHtml: false,
|
||||
onContextMenu: jest.fn(),
|
||||
aggregatorsFactory: jest.fn(),
|
||||
defaultFormatter: jest.fn(),
|
||||
customFormatters: {},
|
||||
rowEnabled: true,
|
||||
rowPartialOnTop: false,
|
||||
colEnabled: false,
|
||||
colPartialOnTop: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
|
||||
mockGetAggregatedData = jest.fn();
|
||||
mockSortAndCacheData = jest.fn();
|
||||
|
||||
tableRenderer.getAggregatedData = mockGetAggregatedData;
|
||||
tableRenderer.sortAndCacheData = mockSortAndCacheData;
|
||||
|
||||
tableRenderer.cachedBasePivotSettings = {
|
||||
pivotData: {
|
||||
subtotals: {
|
||||
rowEnabled: true,
|
||||
rowPartialOnTop: false,
|
||||
colEnabled: false,
|
||||
colPartialOnTop: false,
|
||||
},
|
||||
},
|
||||
rowKeys: [['A'], ['B'], ['C']],
|
||||
} as any;
|
||||
|
||||
tableRenderer.state = {
|
||||
sortingOrder: [],
|
||||
activeSortColumn: null,
|
||||
collapsedRows: {},
|
||||
collapsedCols: {},
|
||||
} as any;
|
||||
});
|
||||
|
||||
const mockGroups = {
|
||||
B: {
|
||||
currentVal: 20,
|
||||
B1: { currentVal: 15 },
|
||||
B2: { currentVal: 5 },
|
||||
},
|
||||
A: {
|
||||
currentVal: 10,
|
||||
A1: { currentVal: 8 },
|
||||
A2: { currentVal: 2 },
|
||||
},
|
||||
C: {
|
||||
currentVal: 30,
|
||||
C1: { currentVal: 25 },
|
||||
C2: { currentVal: 5 },
|
||||
},
|
||||
};
|
||||
|
||||
const createMockPivotData = (rowData: Record<string, number>) => {
|
||||
return {
|
||||
rowKeys: Object.keys(rowData).map(key => key.split('.')),
|
||||
getAggregator: (rowKey: string[], colName: string) => ({
|
||||
value: () => rowData[rowKey.join('.')],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
test('should set initial ascending sort when no active sort column', () => {
|
||||
mockGetAggregatedData.mockReturnValue({
|
||||
A: { currentVal: 30 },
|
||||
B: { currentVal: 10 },
|
||||
C: { currentVal: 20 },
|
||||
});
|
||||
|
||||
const setStateMock = jest.fn();
|
||||
tableRenderer.setState = setStateMock;
|
||||
|
||||
tableRenderer.sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex);
|
||||
|
||||
expect(setStateMock).toHaveBeenCalled();
|
||||
|
||||
const [stateUpdater] = setStateMock.mock.calls[0];
|
||||
|
||||
expect(typeof stateUpdater).toBe('function');
|
||||
|
||||
const previousState = {
|
||||
sortingOrder: [],
|
||||
activeSortColumn: 0,
|
||||
};
|
||||
|
||||
const newState = stateUpdater(previousState);
|
||||
|
||||
expect(newState.sortingOrder[columnIndex]).toBe('asc');
|
||||
expect(newState.activeSortColumn).toBe(columnIndex);
|
||||
|
||||
expect(mockGetAggregatedData).toHaveBeenCalledWith(
|
||||
pivotData,
|
||||
visibleColKeys[columnIndex],
|
||||
false,
|
||||
);
|
||||
|
||||
expect(mockSortAndCacheData).toHaveBeenCalledWith(
|
||||
{ A: { currentVal: 30 }, B: { currentVal: 10 }, C: { currentVal: 20 } },
|
||||
'asc',
|
||||
true,
|
||||
false,
|
||||
maxRowIndex,
|
||||
);
|
||||
});
|
||||
|
||||
test('should toggle from asc to desc when clicking same column', () => {
|
||||
mockGetAggregatedData.mockReturnValue({
|
||||
A: { currentVal: 30 },
|
||||
B: { currentVal: 10 },
|
||||
C: { currentVal: 20 },
|
||||
});
|
||||
const setStateMock = jest.fn(stateUpdater => {
|
||||
if (typeof stateUpdater === 'function') {
|
||||
const newState = stateUpdater({
|
||||
sortingOrder: ['asc' as never],
|
||||
activeSortColumn: 0,
|
||||
});
|
||||
|
||||
tableRenderer.state = {
|
||||
...tableRenderer.state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
});
|
||||
tableRenderer.setState = setStateMock;
|
||||
|
||||
tableRenderer.sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex);
|
||||
|
||||
expect(mockSortAndCacheData).toHaveBeenCalledWith(
|
||||
{ A: { currentVal: 30 }, B: { currentVal: 10 }, C: { currentVal: 20 } },
|
||||
'desc',
|
||||
true,
|
||||
false,
|
||||
maxRowIndex,
|
||||
);
|
||||
});
|
||||
|
||||
test('should check second call in sequence', () => {
|
||||
mockGetAggregatedData.mockReturnValue({
|
||||
A: { currentVal: 30 },
|
||||
B: { currentVal: 10 },
|
||||
C: { currentVal: 20 },
|
||||
});
|
||||
|
||||
mockSortAndCacheData.mockClear();
|
||||
|
||||
const setStateMock = jest.fn(stateUpdater => {
|
||||
if (typeof stateUpdater === 'function') {
|
||||
const newState = stateUpdater(tableRenderer.state);
|
||||
tableRenderer.state = {
|
||||
...tableRenderer.state,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
});
|
||||
tableRenderer.setState = setStateMock;
|
||||
|
||||
tableRenderer.state = {
|
||||
sortingOrder: [],
|
||||
activeSortColumn: 0,
|
||||
collapsedRows: {},
|
||||
collapsedCols: {},
|
||||
} as any;
|
||||
tableRenderer.sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex);
|
||||
|
||||
tableRenderer.state = {
|
||||
sortingOrder: ['asc' as never],
|
||||
activeSortColumn: 0 as any,
|
||||
collapsedRows: {},
|
||||
collapsedCols: {},
|
||||
} as any;
|
||||
tableRenderer.sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex);
|
||||
|
||||
expect(mockSortAndCacheData).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockSortAndCacheData.mock.calls[0]).toEqual([
|
||||
{ A: { currentVal: 30 }, B: { currentVal: 10 }, C: { currentVal: 20 } },
|
||||
'asc',
|
||||
true,
|
||||
false,
|
||||
maxRowIndex,
|
||||
]);
|
||||
|
||||
expect(mockSortAndCacheData.mock.calls[1]).toEqual([
|
||||
{ A: { currentVal: 30 }, B: { currentVal: 10 }, C: { currentVal: 20 } },
|
||||
'desc',
|
||||
true,
|
||||
false,
|
||||
maxRowIndex,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should sort hierarchical data in descending order', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const groups = {
|
||||
A: {
|
||||
currentVal: 30,
|
||||
A1: { currentVal: 13 },
|
||||
A2: { currentVal: 17 },
|
||||
},
|
||||
B: {
|
||||
currentVal: 10,
|
||||
B1: { currentVal: 7 },
|
||||
B2: { currentVal: 3 },
|
||||
},
|
||||
|
||||
C: {
|
||||
currentVal: 18,
|
||||
C1: { currentVal: 7 },
|
||||
C2: { currentVal: 11 },
|
||||
},
|
||||
};
|
||||
|
||||
const result = tableRenderer.sortAndCacheData(groups, 'desc', true, false, 2);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
|
||||
expect(result).toEqual([
|
||||
['A', 'A2'],
|
||||
['A', 'A1'],
|
||||
['A'],
|
||||
['C', 'C2'],
|
||||
['C', 'C1'],
|
||||
['C'],
|
||||
['B', 'B1'],
|
||||
['B', 'B2'],
|
||||
['B'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should sort hierarchical data in ascending order', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const groups = {
|
||||
A: {
|
||||
currentVal: 30,
|
||||
A1: { currentVal: 13 },
|
||||
A2: { currentVal: 17 },
|
||||
},
|
||||
B: {
|
||||
currentVal: 10,
|
||||
B1: { currentVal: 7 },
|
||||
B2: { currentVal: 3 },
|
||||
},
|
||||
|
||||
C: {
|
||||
currentVal: 18,
|
||||
C1: { currentVal: 7 },
|
||||
C2: { currentVal: 11 },
|
||||
},
|
||||
};
|
||||
|
||||
const result = tableRenderer.sortAndCacheData(groups, 'asc', true, false, 2);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
|
||||
expect(result).toEqual([
|
||||
['B', 'B2'],
|
||||
['B', 'B1'],
|
||||
['B'],
|
||||
['C', 'C1'],
|
||||
['C', 'C2'],
|
||||
['C'],
|
||||
['A', 'A1'],
|
||||
['A', 'A2'],
|
||||
['A'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should calculate groups from pivot data', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const mockAggregator = (value: number) => ({
|
||||
value: () => value,
|
||||
format: jest.fn(),
|
||||
isSubtotal: false,
|
||||
});
|
||||
|
||||
const mockPivotData = {
|
||||
rowKeys: [['A'], ['B'], ['C']],
|
||||
getAggregator: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockAggregator(30))
|
||||
.mockReturnValueOnce(mockAggregator(10))
|
||||
.mockReturnValueOnce(mockAggregator(20)),
|
||||
};
|
||||
|
||||
const result = tableRenderer.getAggregatedData(
|
||||
mockPivotData as any,
|
||||
['col1'],
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
A: { currentVal: 30 },
|
||||
B: { currentVal: 10 },
|
||||
C: { currentVal: 20 },
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort groups and convert to array in ascending order', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const result = tableRenderer.sortAndCacheData(
|
||||
mockGroups,
|
||||
'asc',
|
||||
true,
|
||||
false,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
['A', 'A2'],
|
||||
['A', 'A1'],
|
||||
['A'],
|
||||
['B', 'B2'],
|
||||
['B', 'B1'],
|
||||
['B'],
|
||||
['C', 'C2'],
|
||||
['C', 'C1'],
|
||||
['C'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should sort groups and convert to array in descending order', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const result = tableRenderer.sortAndCacheData(
|
||||
mockGroups,
|
||||
'desc',
|
||||
true,
|
||||
false,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
['C', 'C1'],
|
||||
['C', 'C2'],
|
||||
['C'],
|
||||
['B', 'B1'],
|
||||
['B', 'B2'],
|
||||
['B'],
|
||||
['A', 'A1'],
|
||||
['A', 'A2'],
|
||||
['A'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle rowPartialOnTop = true configuration', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const result = tableRenderer.sortAndCacheData(
|
||||
mockGroups,
|
||||
'asc',
|
||||
true,
|
||||
true,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
['A'],
|
||||
['A', 'A2'],
|
||||
['A', 'A1'],
|
||||
['B'],
|
||||
['B', 'B2'],
|
||||
['B', 'B1'],
|
||||
['C'],
|
||||
['C', 'C2'],
|
||||
['C', 'C1'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle rowEnabled = false and rowPartialOnTop = false, sorting asc', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
|
||||
const result = tableRenderer.sortAndCacheData(
|
||||
mockGroups,
|
||||
'asc',
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
['A', 'A2'],
|
||||
['A', 'A1'],
|
||||
['B', 'B2'],
|
||||
['B', 'B1'],
|
||||
['C', 'C2'],
|
||||
['C', 'C1'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle rowEnabled = false and rowPartialOnTop = false , sorting desc', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
|
||||
const result = tableRenderer.sortAndCacheData(
|
||||
mockGroups,
|
||||
'desc',
|
||||
false,
|
||||
false,
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
['C', 'C1'],
|
||||
['C', 'C2'],
|
||||
['B', 'B1'],
|
||||
['B', 'B2'],
|
||||
['A', 'A1'],
|
||||
['A', 'A2'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('create hierarchical structure with subtotal at bottom', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const rowData = {
|
||||
'A.A1': 10,
|
||||
'A.A2': 20,
|
||||
A: 30,
|
||||
'B.B1': 30,
|
||||
'B.B2': 40,
|
||||
B: 70,
|
||||
'C.C1': 50,
|
||||
'C.C2': 60,
|
||||
C: 110,
|
||||
};
|
||||
|
||||
const pivotData = createMockPivotData(rowData);
|
||||
const result = tableRenderer.getAggregatedData(pivotData, 'Col1', false);
|
||||
|
||||
expect(result).toEqual({
|
||||
A: {
|
||||
A1: { currentVal: 10 },
|
||||
A2: { currentVal: 20 },
|
||||
currentVal: 30,
|
||||
},
|
||||
B: {
|
||||
B1: { currentVal: 30 },
|
||||
B2: { currentVal: 40 },
|
||||
currentVal: 70,
|
||||
},
|
||||
C: {
|
||||
C1: { currentVal: 50 },
|
||||
C2: { currentVal: 60 },
|
||||
currentVal: 110,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('create hierarchical structure with subtotal at top', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const rowData = {
|
||||
A: 30,
|
||||
'A.A1': 10,
|
||||
'A.A2': 20,
|
||||
B: 70,
|
||||
'B.B1': 30,
|
||||
'B.B2': 40,
|
||||
C: 110,
|
||||
'C.C1': 50,
|
||||
'C.C2': 60,
|
||||
};
|
||||
|
||||
const pivotData = createMockPivotData(rowData);
|
||||
const result = tableRenderer.getAggregatedData(pivotData, 'Col1', true);
|
||||
|
||||
expect(result).toEqual({
|
||||
A: {
|
||||
A1: { currentVal: 10 },
|
||||
A2: { currentVal: 20 },
|
||||
currentVal: 30,
|
||||
},
|
||||
B: {
|
||||
B1: { currentVal: 30 },
|
||||
B2: { currentVal: 40 },
|
||||
currentVal: 70,
|
||||
},
|
||||
C: {
|
||||
C1: { currentVal: 50 },
|
||||
C2: { currentVal: 60 },
|
||||
currentVal: 110,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('values from the 3rd level of the hierarchy with a subtotal at the bottom', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const rowData = {
|
||||
'A.A1.A11': 10,
|
||||
'A.A1.A12': 20,
|
||||
'A.A1': 30,
|
||||
'A.A2': 30,
|
||||
'A.A3': 50,
|
||||
A: 110,
|
||||
};
|
||||
|
||||
const pivotData = createMockPivotData(rowData);
|
||||
const result = tableRenderer.getAggregatedData(pivotData, 'Col1', false);
|
||||
|
||||
expect(result).toEqual({
|
||||
A: {
|
||||
A1: {
|
||||
A11: { currentVal: 10 },
|
||||
A12: { currentVal: 20 },
|
||||
currentVal: 30,
|
||||
},
|
||||
A2: { currentVal: 30 },
|
||||
A3: { currentVal: 50 },
|
||||
currentVal: 110,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('values from the 3rd level of the hierarchy with a subtotal at the top', () => {
|
||||
tableRenderer = new TableRenderer(mockProps);
|
||||
const rowData = {
|
||||
A: 110,
|
||||
'A.A1': 30,
|
||||
'A.A1.A11': 10,
|
||||
'A.A1.A12': 20,
|
||||
'A.A2': 30,
|
||||
'A.A3': 50,
|
||||
};
|
||||
|
||||
const pivotData = createMockPivotData(rowData);
|
||||
const result = tableRenderer.getAggregatedData(pivotData, 'Col1', true);
|
||||
|
||||
expect(result).toEqual({
|
||||
A: {
|
||||
A1: {
|
||||
A11: { currentVal: 10 },
|
||||
A12: { currentVal: 20 },
|
||||
currentVal: 30,
|
||||
},
|
||||
A2: { currentVal: 30 },
|
||||
A3: { currentVal: 50 },
|
||||
currentVal: 110,
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user