diff --git a/.changeset/fix-grouped-column-aggregation.md b/.changeset/fix-grouped-column-aggregation.md new file mode 100644 index 0000000000..c63f910eb3 --- /dev/null +++ b/.changeset/fix-grouped-column-aggregation.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': patch +--- + +fix(grouping): aggregate grouped columns at shallower group levels instead of showing the first row's value diff --git a/packages/table-core/src/utils/getGroupedRowModel.ts b/packages/table-core/src/utils/getGroupedRowModel.ts index 33267bcaa1..cf2a577871 100644 --- a/packages/table-core/src/utils/getGroupedRowModel.ts +++ b/packages/table-core/src/utils/getGroupedRowModel.ts @@ -92,8 +92,12 @@ export function getGroupedRowModel(): ( subRows, leafRows, getValue: (columnId: string) => { - // Don't aggregate columns that are in the grouping - if (existingGrouping.includes(columnId)) { + // Don't aggregate columns that group this row at the current + // level or an ancestor level - their value is constant across + // the group. Columns grouped at a deeper level still need to + // be aggregated here. + const groupingIndex = existingGrouping.indexOf(columnId) + if (groupingIndex > -1 && groupingIndex <= depth) { if (row._valuesCache.hasOwnProperty(columnId)) { return row._valuesCache[columnId] } diff --git a/packages/table-core/tests/getGroupedRowModel.test.ts b/packages/table-core/tests/getGroupedRowModel.test.ts index 16e3632588..66445febf8 100644 --- a/packages/table-core/tests/getGroupedRowModel.test.ts +++ b/packages/table-core/tests/getGroupedRowModel.test.ts @@ -5,6 +5,17 @@ import { createTable } from '../src/core/table' import { getGroupedRowModel } from '../src/utils/getGroupedRowModel' import { makeData, Person } from './makeTestData' +function createPerson(firstName: string, age: number): Person { + return { + firstName, + lastName: 'Doe', + age, + visits: 0, + progress: 0, + status: 'single', + } +} + type personKeys = keyof Person type PersonColumn = ColumnDef @@ -49,4 +60,43 @@ describe('#getGroupedRowModel', () => { ).toEqual(50000) expect(end.valueOf() - start.valueOf()).toBeLessThan(5000) }) + + it('aggregates a secondary grouped column at parent group levels', () => { + const data: Person[] = [ + // first Engineering row is intentionally not the min, so the bug + // (returning the first row's raw value) differs from the aggregate + createPerson('Engineering', 30), + createPerson('Engineering', 24), + createPerson('Sales', 40), + ] + + const columnHelper = createColumnHelper() + const columns = [ + columnHelper.accessor('firstName', { id: 'firstName' }), + columnHelper.accessor('age', { id: 'age', aggregationFn: 'min' }), + ] + + const table = createTable({ + onStateChange() {}, + renderFallbackValue: '', + data, + state: { grouping: ['firstName', 'age'] }, + columns, + getCoreRowModel: getCoreRowModel(), + getGroupedRowModel: getGroupedRowModel(), + }) + + const rowsById = table.getGroupedRowModel().rowsById + const engineering = rowsById['firstName:Engineering'] + + // `age` is grouped at a deeper level, so the Engineering group row should + // still expose the aggregated (min) age across its leaf rows. + expect(engineering?.getValue('age')).toBe(24) + expect(rowsById['firstName:Sales']?.getValue('age')).toBe(40) + + // at the `age` group level the column is the grouping key, so it keeps the + // grouping value rather than aggregating + expect(rowsById['firstName:Engineering>age:24']?.getValue('age')).toBe(24) + expect(rowsById['firstName:Engineering>age:30']?.getValue('age')).toBe(30) + }) })