From 127362f0ac21d02edca03863ecb69aa722d35637 Mon Sep 17 00:00:00 2001 From: Rob Juffermans Date: Sun, 28 Dec 2025 13:55:40 +0100 Subject: [PATCH 1/3] feat(chart): add default row/column expansion depth to Pivot Table Add controls to configure how deeply rows and columns are expanded by default when a Pivot Table chart loads. This allows charts to start with deeper hierarchy levels collapsed for better overview. - Add defaultRowExpansionDepth and defaultColExpansionDepth controls - Controls only appear when subtotals are enabled and depth > 1 - Add computeCollapsedMap helper function for initial state seeding - Apply initial collapse in componentDidMount (not render) to avoid React anti-pattern - Add unit tests for computeCollapsedMap helper --- .../src/PivotTableChart.tsx | 4 + .../src/plugin/controlPanel.tsx | 58 +++++++++++++ .../src/plugin/transformProps.ts | 4 + .../src/react-pivottable/TableRenderers.tsx | 83 +++++++++++++++++++ .../plugin-chart-pivot-table/src/types.ts | 2 + .../test/plugin/transformProps.test.ts | 29 +++++++ .../computeCollapsedMap.test.ts | 81 ++++++++++++++++++ 7 files changed, 261 insertions(+) create mode 100644 superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx index 56ed2cb5c907..99d20985ea9d 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx @@ -257,6 +257,8 @@ export default function PivotTableChart(props: PivotTableProps) { onContextMenu, timeGrainSqla, allowRenderHtml, + defaultRowExpansionDepth = 0, + defaultColExpansionDepth = 0, } = props; const theme = useTheme(); @@ -711,6 +713,8 @@ export default function PivotTableChart(props: PivotTableProps) { namesMapping={verboseMap} onContextMenu={handleContextMenu} allowRenderHtml={allowRenderHtml} + defaultRowExpansionDepth={defaultRowExpansionDepth} + defaultColExpansionDepth={defaultColExpansionDepth} /> diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx index 4e916857b274..8578046d014a 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx @@ -225,6 +225,35 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'defaultRowExpansionDepth', + config: { + type: 'SelectControl', + label: t('Default row expansion depth'), + default: 0, + renderTrigger: true, + description: t( + 'Number of row group levels to show expanded initially. ' + + 'Set to 0 to fully expand all rows.', + ), + mapStateToProps: explore => { + const rowCount = ensureIsArray( + explore?.controls?.groupbyRows?.value, + ).length; + // Generate choices: 0 = fully expanded, 1..rowCount-1 = collapse deeper levels + const choices: [number, string][] = [[0, t('Fully expanded')]]; + for (let i = 1; i < rowCount; i += 1) { + choices.push([i, String(i)]); + } + return { choices }; + }, + visibility: ({ controls }) => + !!controls?.rowSubTotals?.value && + ensureIsArray(controls?.groupbyRows?.value).length > 1, + }, + }, + ], [ { name: 'colTotals', @@ -249,6 +278,35 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'defaultColExpansionDepth', + config: { + type: 'SelectControl', + label: t('Default column expansion depth'), + default: 0, + renderTrigger: true, + description: t( + 'Number of column group levels to show expanded initially. ' + + 'Set to 0 to fully expand all columns.', + ), + mapStateToProps: explore => { + const colCount = ensureIsArray( + explore?.controls?.groupbyColumns?.value, + ).length; + // Generate choices: 0 = fully expanded, 1..colCount-1 = collapse deeper levels + const choices: [number, string][] = [[0, t('Fully expanded')]]; + for (let i = 1; i < colCount; i += 1) { + choices.push([i, String(i)]); + } + return { choices }; + }, + visibility: ({ controls }) => + !!controls?.colSubTotals?.value && + ensureIsArray(controls?.groupbyColumns?.value).length > 1, + }, + }, + ], [ { name: 'transposePivot', diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts index 1bba188ca05b..7f1772450b5c 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts @@ -117,6 +117,8 @@ export default function transformProps(chartProps: ChartProps) { timeGrainSqla, currencyFormat, allowRenderHtml, + defaultRowExpansionDepth = 0, + defaultColExpansionDepth = 0, } = formData; const { selectedFilters } = filterState; const granularity = extractTimegrain(rawFormData); @@ -195,5 +197,7 @@ export default function transformProps(chartProps: ChartProps) { onContextMenu, timeGrainSqla, allowRenderHtml, + defaultRowExpansionDepth, + defaultColExpansionDepth, }; } diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx index d3e587638276..3e23b214cc47 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx @@ -105,6 +105,8 @@ interface TableRendererProps { filters?: Record, ) => void; allowRenderHtml?: boolean; + defaultRowExpansionDepth?: number; + defaultColExpansionDepth?: number; [key: string]: unknown; } @@ -134,6 +136,31 @@ interface PivotSettings { colAttrSpans?: number[][]; } +/** + * Computes the initial collapsed-state map for a set of keys and a depth. + * Keys whose length equals `depth` are marked collapsed, hiding their children. + * + * @param keys - Array of key arrays (e.g. rowKeys or colKeys) + * @param depth - The (1-based) depth at which to collapse. Keys at this depth + * are collapsed; a falsy or non-positive depth collapses nothing. + * @returns A map of flatKey => true for every key that should be collapsed. + */ +export function computeCollapsedMap( + keys: string[][], + depth?: number, +): Record { + if (!depth || depth <= 0) { + return {}; + } + const collapsed: Record = {}; + keys + .filter(k => k.length === depth) + .forEach(k => { + collapsed[flatKey(k)] = true; + }); + return collapsed; +} + const parseLabel = (value: unknown): string | number => { if (typeof value === 'string') { if (value === 'metric') return t('metric'); @@ -338,6 +365,8 @@ export function TableRenderer(props: TableRendererProps) { namesMapping: namesMappingProp, onContextMenu, allowRenderHtml, + defaultRowExpansionDepth = 0, + defaultColExpansionDepth = 0, } = props; const [collapsedRows, setCollapsedRows] = useState>( @@ -743,6 +772,60 @@ export function TableRenderer(props: TableRendererProps) { [getBasePivotSettings], ); + // Seed the initial collapsed state once, on mount, from the configured + // default expansion depths. Keeping this in an effect (rather than the render + // body or a state initializer) avoids the setState-during-render anti-pattern + // while still hiding deeper hierarchy levels on first load. A depth of 0 (or + // an invalid value) means "fully expanded" and collapses nothing. + const didApplyInitialCollapse = useRef(false); + useEffect(() => { + if (didApplyInitialCollapse.current) { + return; + } + didApplyInitialCollapse.current = true; + + const rowDepth = Number(defaultRowExpansionDepth); + const colDepth = Number(defaultColExpansionDepth); + const hasValidRowDepth = Number.isInteger(rowDepth) && rowDepth > 0; + const hasValidColDepth = Number.isInteger(colDepth) && colDepth > 0; + if (!hasValidRowDepth && !hasValidColDepth) { + return; + } + + const { + rowKeys, + colKeys, + rowAttrs, + colAttrs, + rowSubtotalDisplay, + colSubtotalDisplay, + } = basePivotSettings; + + // Only collapse when subtotals are enabled and the requested depth sits + // above the deepest level (otherwise there is nothing to hide). + const initialCollapsedRows = + hasValidRowDepth && + rowSubtotalDisplay.enabled && + rowAttrs.length > 1 && + rowDepth < rowAttrs.length + ? computeCollapsedMap(rowKeys, rowDepth) + : {}; + const initialCollapsedCols = + hasValidColDepth && + colSubtotalDisplay.enabled && + colAttrs.length > 1 && + colDepth < colAttrs.length + ? computeCollapsedMap(colKeys, colDepth) + : {}; + + if (Object.keys(initialCollapsedRows).length > 0) { + setCollapsedRows(initialCollapsedRows); + } + if (Object.keys(initialCollapsedCols).length > 0) { + setCollapsedCols(initialCollapsedCols); + } + }, [basePivotSettings, defaultRowExpansionDepth, defaultColExpansionDepth]); + // Reset sort state and cache when structural props change. Scoping this to // an effect (instead of running inside the memo) prevents the cache from // being wiped on unrelated state updates like collapse/expand. diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts index d154031397b7..3d0680740e10 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts @@ -90,6 +90,8 @@ interface PivotTableCustomizeProps { time_grain_sqla?: TimeGranularity; granularity_sqla?: string; allowRenderHtml?: boolean; + defaultRowExpansionDepth?: number; + defaultColExpansionDepth?: number; } export type PivotTableQueryFormData = QueryFormData & diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts index a900e43fb0ea..391b5dd4b0f2 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts @@ -93,6 +93,8 @@ test('should transform chart props for viz', () => { columnFormats: {}, currencyFormats: {}, currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, + defaultRowExpansionDepth: 0, + defaultColExpansionDepth: 0, }); }); @@ -283,6 +285,33 @@ test('should preserve static currency format when not using AUTO mode', () => { }); }); +test('should pass default row/column expansion depth through to the renderer', () => { + const expansionChartProps = new ChartProps({ + formData: { + ...formData, + defaultRowExpansionDepth: 2, + defaultColExpansionDepth: 1, + }, + width: 800, + height: 600, + queriesData: [ + { + data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }], + colnames: ['name', 'sum__num', '__timestamp'], + coltypes: [1, 0, 2], + }, + ], + hooks: { setDataMask }, + filterState: { selectedFilters: {} }, + datasource: { verboseMap: {}, columnFormats: {} }, + theme: supersetTheme, + }); + + const result = transformProps(expansionChartProps); + expect(result.defaultRowExpansionDepth).toBe(2); + expect(result.defaultColExpansionDepth).toBe(1); +}); + test('should map conditional formatting rules to metricColorFormatters with correct colors', () => { const formattingFormData = { ...formData, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts new file mode 100644 index 000000000000..163d4a0ee544 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { computeCollapsedMap } from '../../src/react-pivottable/TableRenderers'; +import { flatKey } from '../../src/react-pivottable/utilities'; + +test('computeCollapsedMap returns empty object when depth is 0', () => { + const keys = [['A'], ['A', 'B'], ['A', 'B', 'C']]; + expect(computeCollapsedMap(keys, 0)).toEqual({}); +}); + +test('computeCollapsedMap returns empty object when depth is negative', () => { + const keys = [['A'], ['A', 'B'], ['A', 'B', 'C']]; + expect(computeCollapsedMap(keys, -1)).toEqual({}); +}); + +test('computeCollapsedMap returns empty object when depth is undefined', () => { + const keys = [['A'], ['A', 'B'], ['A', 'B', 'C']]; + // @ts-expect-error testing undefined input + expect(computeCollapsedMap(keys, undefined)).toEqual({}); +}); + +test('computeCollapsedMap collapses keys at depth 1', () => { + const keys = [ + ['A'], + ['A', 'X'], + ['A', 'Y'], + ['B'], + ['B', 'Z'], + ]; + const result = computeCollapsedMap(keys, 1); + expect(result).toEqual({ + [flatKey(['A'])]: true, + [flatKey(['B'])]: true, + }); +}); + +test('computeCollapsedMap collapses keys at depth 2', () => { + const keys = [ + ['A'], + ['A', 'X'], + ['A', 'X', '1'], + ['A', 'X', '2'], + ['A', 'Y'], + ['B'], + ['B', 'Z'], + ]; + const result = computeCollapsedMap(keys, 2); + expect(result).toEqual({ + [flatKey(['A', 'X'])]: true, + [flatKey(['A', 'Y'])]: true, + [flatKey(['B', 'Z'])]: true, + }); +}); + +test('computeCollapsedMap returns empty object when no keys match the depth', () => { + const keys = [['A'], ['B'], ['C']]; + // Depth 2, but all keys are only length 1 + expect(computeCollapsedMap(keys, 2)).toEqual({}); +}); + +test('computeCollapsedMap handles empty keys array', () => { + expect(computeCollapsedMap([], 1)).toEqual({}); +}); + From e3934d7e85513ed579bb4532a0b0d5ace7ade332 Mon Sep 17 00:00:00 2001 From: Rob Juffermans Date: Tue, 30 Dec 2025 13:11:59 +0100 Subject: [PATCH 2/3] fix(pivot-table): clarify expansion depth semantics (0=fully expanded) - Update control labels to use 'Expand N level(s)' format - Improve description to explicitly state 0=fully expanded, 1=top-level only - Add semantic documentation in code comments - Add regression test to lock depth=0 behavior --- .../src/plugin/controlPanel.tsx | 18 ++++--- .../src/react-pivottable/TableRenderers.tsx | 52 +++++++++++++++---- .../computeCollapsedMap.test.ts | 28 +++++++++- 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx index 8578046d014a..fbf98712bc35 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/controlPanel.tsx @@ -234,17 +234,18 @@ const config: ControlPanelConfig = { default: 0, renderTrigger: true, description: t( - 'Number of row group levels to show expanded initially. ' + - 'Set to 0 to fully expand all rows.', + 'How many row group levels to show expanded on initial load. ' + + 'Select "Fully expanded" (0) to show all levels. ' + + 'Select "1" to show only top-level rows (children collapsed).', ), mapStateToProps: explore => { const rowCount = ensureIsArray( explore?.controls?.groupbyRows?.value, ).length; - // Generate choices: 0 = fully expanded, 1..rowCount-1 = collapse deeper levels + // 0 = fully expanded (no collapse), 1..N = collapse deeper levels const choices: [number, string][] = [[0, t('Fully expanded')]]; for (let i = 1; i < rowCount; i += 1) { - choices.push([i, String(i)]); + choices.push([i, t('Expand %s level(s)', i)]); } return { choices }; }, @@ -287,17 +288,18 @@ const config: ControlPanelConfig = { default: 0, renderTrigger: true, description: t( - 'Number of column group levels to show expanded initially. ' + - 'Set to 0 to fully expand all columns.', + 'How many column group levels to show expanded on initial load. ' + + 'Select "Fully expanded" (0) to show all levels. ' + + 'Select "1" to show only top-level columns (children collapsed).', ), mapStateToProps: explore => { const colCount = ensureIsArray( explore?.controls?.groupbyColumns?.value, ).length; - // Generate choices: 0 = fully expanded, 1..colCount-1 = collapse deeper levels + // 0 = fully expanded (no collapse), 1..N = collapse deeper levels const choices: [number, string][] = [[0, t('Fully expanded')]]; for (let i = 1; i < colCount; i += 1) { - choices.push([i, String(i)]); + choices.push([i, t('Expand %s level(s)', i)]); } return { choices }; }, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx index 3e23b214cc47..f2604af5c8e5 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx @@ -138,26 +138,56 @@ interface PivotSettings { /** * Computes the initial collapsed-state map for a set of keys and a depth. - * Keys whose length equals `depth` are marked collapsed, hiding their children. + * + * Semantics: + * - depth = 0 (or invalid): disabled / fully expanded (returns an empty map) + * - depth = 1: collapse at level 1 (show only top-level rows/columns) + * - depth = N: collapse at level N (show N levels expanded) + * + * Every intermediate level from `depth` up to `maxDepth - 1` is collapsed, so + * expanding a group reveals only the next level while deeper levels stay + * collapsed until clicked. The leaf level (`maxDepth`) is never collapsed. * * @param keys - Array of key arrays (e.g. rowKeys or colKeys) - * @param depth - The (1-based) depth at which to collapse. Keys at this depth - * are collapsed; a falsy or non-positive depth collapses nothing. + * @param depth - The (1-based) depth at which to collapse. Must be a positive + * integer (>= 1) to apply any collapse. + * @param maxDepth - Optional total number of grouping levels (e.g. + * rows.length or cols.length). Inferred from the longest key when omitted. * @returns A map of flatKey => true for every key that should be collapsed. */ export function computeCollapsedMap( keys: string[][], depth?: number, + maxDepth?: number, ): Record { - if (!depth || depth <= 0) { + // depth must be a positive integer (>= 1); 0/invalid means fully expanded. + if (!Number.isInteger(depth) || (depth as number) <= 0) { + return {}; + } + const collapseDepth = depth as number; + + const effectiveMaxDepth = Number.isInteger(maxDepth) + ? (maxDepth as number) + : Math.max(0, ...keys.map(k => (Array.isArray(k) ? k.length : 0))); + if (effectiveMaxDepth <= collapseDepth) { return {}; } + const collapsed: Record = {}; - keys - .filter(k => k.length === depth) - .forEach(k => { - collapsed[flatKey(k)] = true; - }); + const seen = new Set(); + keys.forEach(k => { + if (!Array.isArray(k) || k.length < collapseDepth) { + return; + } + const limit = Math.min(k.length, effectiveMaxDepth - 1); + for (let i = collapseDepth; i <= limit; i += 1) { + const keyStr = flatKey(k.slice(0, i)); + if (!seen.has(keyStr)) { + seen.add(keyStr); + collapsed[keyStr] = true; + } + } + }); return collapsed; } @@ -808,14 +838,14 @@ export function TableRenderer(props: TableRendererProps) { rowSubtotalDisplay.enabled && rowAttrs.length > 1 && rowDepth < rowAttrs.length - ? computeCollapsedMap(rowKeys, rowDepth) + ? computeCollapsedMap(rowKeys, rowDepth, rowAttrs.length) : {}; const initialCollapsedCols = hasValidColDepth && colSubtotalDisplay.enabled && colAttrs.length > 1 && colDepth < colAttrs.length - ? computeCollapsedMap(colKeys, colDepth) + ? computeCollapsedMap(colKeys, colDepth, colAttrs.length) : {}; if (Object.keys(initialCollapsedRows).length > 0) { diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts index 163d4a0ee544..39836ebe55c1 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts @@ -20,8 +20,13 @@ import { computeCollapsedMap } from '../../src/react-pivottable/TableRenderers'; import { flatKey } from '../../src/react-pivottable/utilities'; -test('computeCollapsedMap returns empty object when depth is 0', () => { +/** + * Semantic tests: depth = 0 means "fully expanded" (no collapse). + * This is the intentional behavior - users select 0 to show all levels. + */ +test('computeCollapsedMap returns empty object when depth is 0 (fully expanded semantic)', () => { const keys = [['A'], ['A', 'B'], ['A', 'B', 'C']]; + // depth = 0 means "fully expanded" - no keys should be collapsed expect(computeCollapsedMap(keys, 0)).toEqual({}); }); @@ -79,3 +84,24 @@ test('computeCollapsedMap handles empty keys array', () => { expect(computeCollapsedMap([], 1)).toEqual({}); }); +/** + * Semantic documentation: depth = 1 means "show top-level only" (children collapsed). + * This test locks down the intended behavior for the UI control. + */ +test('computeCollapsedMap with depth=1 collapses top-level keys (show top-level only semantic)', () => { + const keys = [ + ['Region A'], + ['Region A', 'City 1'], + ['Region A', 'City 2'], + ['Region B'], + ['Region B', 'City 3'], + ]; + const result = computeCollapsedMap(keys, 1); + // depth = 1 means: collapse at level 1, so top-level rows are collapsed + // (their children are hidden, showing only the subtotal rows) + expect(result).toEqual({ + [flatKey(['Region A'])]: true, + [flatKey(['Region B'])]: true, + }); +}); + From 587a978a903a337faf3528bf6f4d0238a7d901c9 Mon Sep 17 00:00:00 2001 From: Rob Juffermans Date: Tue, 30 Dec 2025 15:33:08 +0100 Subject: [PATCH 3/3] fix(pivot-table): make default depth expansion stepwise --- .../computeCollapsedMap.test.ts | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts index 39836ebe55c1..6b1a3fe23d18 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts +++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/computeCollapsedMap.test.ts @@ -37,18 +37,11 @@ test('computeCollapsedMap returns empty object when depth is negative', () => { test('computeCollapsedMap returns empty object when depth is undefined', () => { const keys = [['A'], ['A', 'B'], ['A', 'B', 'C']]; - // @ts-expect-error testing undefined input expect(computeCollapsedMap(keys, undefined)).toEqual({}); }); test('computeCollapsedMap collapses keys at depth 1', () => { - const keys = [ - ['A'], - ['A', 'X'], - ['A', 'Y'], - ['B'], - ['B', 'Z'], - ]; + const keys = [['A'], ['A', 'X'], ['A', 'Y'], ['B'], ['B', 'Z']]; const result = computeCollapsedMap(keys, 1); expect(result).toEqual({ [flatKey(['A'])]: true, @@ -85,23 +78,23 @@ test('computeCollapsedMap handles empty keys array', () => { }); /** - * Semantic documentation: depth = 1 means "show top-level only" (children collapsed). - * This test locks down the intended behavior for the UI control. + * Stepwise expansion behavior: + * When total depth is > 2, depth = 1 should pre-collapse deeper levels so expanding a top-level + * group reveals only the next level (which remains collapsed until clicked). */ -test('computeCollapsedMap with depth=1 collapses top-level keys (show top-level only semantic)', () => { +test('computeCollapsedMap with depth=1 and maxDepth=3 collapses level 1 and 2 keys (stepwise)', () => { const keys = [ - ['Region A'], - ['Region A', 'City 1'], - ['Region A', 'City 2'], - ['Region B'], - ['Region B', 'City 3'], + ['Region A', 'City 1', 'Store 1'], + ['Region A', 'City 1', 'Store 2'], + ['Region A', 'City 2', 'Store 3'], + ['Region B', 'City 3', 'Store 4'], ]; - const result = computeCollapsedMap(keys, 1); - // depth = 1 means: collapse at level 1, so top-level rows are collapsed - // (their children are hidden, showing only the subtotal rows) + const result = computeCollapsedMap(keys, 1, 3); expect(result).toEqual({ [flatKey(['Region A'])]: true, + [flatKey(['Region A', 'City 1'])]: true, + [flatKey(['Region A', 'City 2'])]: true, [flatKey(['Region B'])]: true, + [flatKey(['Region B', 'City 3'])]: true, }); }); -