Skip to content

Commit c350ada

Browse files
joonas-asanteri0200
authored andcommitted
[OodiTable] Implement aggregation/summary rows
1 parent 59b79d2 commit c350ada

File tree

9 files changed

+281
-26
lines changed

9 files changed

+281
-26
lines changed

services/frontend/src/components/OodiTable/OodiTable.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import TableContainer from '@mui/material/TableContainer'
55
import TableHead from '@mui/material/TableHead'
66

77
import { type Table as TableType } from '@tanstack/react-table'
8+
import { OodiTableAggregationRow } from './components/AggregationRow'
89
import { OodiTableDataRow } from './components/Cell'
910
import { OodiTableHeaderGroup } from './components/Header'
1011
import { OodiTablePagination } from './components/Pagination'
@@ -49,6 +50,7 @@ export const OodiTableContainer = <OTData,>({ table }: { table: TableType<OTData
4950
},
5051
}}
5152
>
53+
{table.getAggregationRowModel().rows.map(OodiTableAggregationRow)}
5254
{table.getRowModel().rows.map(OodiTableDataRow)}
5355
</TableBody>
5456
</Table>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
2+
import TableRow from '@mui/material/TableRow'
3+
import Typography from '@mui/material/Typography'
4+
import { flexRender } from '@tanstack/react-table'
5+
6+
import type { FC } from 'react'
7+
8+
import { AggregationRow } from '../features/aggregationRows'
9+
import { getCommonPinningStyles } from '../styles'
10+
11+
const OodiTableAggregationCell: FC<TableCellProps> = ({ children, ...props }) => {
12+
return (
13+
<TableCell
14+
{...props}
15+
sx={{
16+
borderWidth: '0 1px 1px 0',
17+
borderStyle: 'solid',
18+
borderColor: 'grey.300',
19+
padding: '0 1em',
20+
height: '3em',
21+
...props?.sx,
22+
}}
23+
>
24+
<Typography
25+
sx={{
26+
height: '100%',
27+
width: '100%',
28+
display: 'flex',
29+
justifyContent: 'center',
30+
alignItems: 'center',
31+
whiteSpace: 'nowrap',
32+
}}
33+
>
34+
{children}
35+
</Typography>
36+
</TableCell>
37+
)
38+
}
39+
40+
export const OodiTableAggregationRow = <OTData,>(row: AggregationRow<OTData>) => (
41+
<TableRow key={row.id}>
42+
{row.getVisibleCells().map(cell => {
43+
const { maxSize } = cell.column.columnDef
44+
return (
45+
<OodiTableAggregationCell
46+
key={cell.id}
47+
sx={{
48+
...getCommonPinningStyles(cell.column),
49+
maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em',
50+
}}
51+
>
52+
{flexRender(cell.column.columnDef.aggregatedCell, cell.getContext())}
53+
</OodiTableAggregationCell>
54+
)
55+
})}
56+
</TableRow>
57+
)

services/frontend/src/components/OodiTable/components/Header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { flexRender } from '@tanstack/react-table'
66
import type { HeaderGroup } from '@tanstack/react-table'
77
import type { FC, ReactNode } from 'react'
88

9-
import { getCommonPinningStyles, getVerticalStyles } from '../styles'
9+
import { getCommonPinningStyles, verticalStyles } from '../styles'
1010
import { OodiTableSortIcons } from './SortIcons'
1111

1212
const OodiTableHeader: FC<TableCellProps & { children?: ReactNode }> = ({ children, ...props }) => {
@@ -44,7 +44,7 @@ export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>,
4444

4545
const sx = {
4646
cursor: header.column.getCanSort() ? 'pointer' : 'inherit',
47-
...(isVertical && getVerticalStyles()),
47+
...(isVertical && verticalStyles),
4848
...getCommonPinningStyles(header.column),
4949
}
5050

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Cell, CellContext, Column, Row, Table, TableFeature } from '@tanstack/react-table'
2+
3+
/**
4+
* This doesn't implement all the features of Row, so be aware of that
5+
*/
6+
export interface AggregationRow<TData> extends Row<TData> {
7+
getIsAggregationRow: () => true
8+
}
9+
10+
type AggregationRowValueFn<TData, TValue = unknown> = (ctx: {
11+
table: Table<TData>
12+
rowId: string
13+
column: Column<TData, TValue>
14+
}) => TValue
15+
16+
interface AggregationRowCellDef<TData, TValue = unknown> {
17+
id: string
18+
value: TValue | AggregationRowValueFn<TData, TValue>
19+
}
20+
21+
export type AggregationRowsInput<TData, TValue = unknown> =
22+
| AggregationRowCellDef<TData, TValue>[]
23+
| ((ctx: { table: Table<TData>; column: Column<TData> }) => AggregationRowCellDef<TData, TValue>[])
24+
25+
export interface AggregationRowModel<TData> {
26+
rows: AggregationRow<TData>[]
27+
flatRows: AggregationRow<TData>[]
28+
rowsById: Record<string, AggregationRow<TData>>
29+
}
30+
31+
const resolveAggregationRowsArray = <TData, TValue>(
32+
table: Table<TData>,
33+
column: Column<TData, TValue>
34+
): AggregationRowCellDef<TData, TValue>[] => {
35+
const input = column.columnDef.aggregationRows
36+
if (!input) return []
37+
return typeof input === 'function' ? (input({ table, column }) ?? []) : input
38+
}
39+
40+
const collectAggregationRowIds = <TData>(table: Table<TData>): string[] => {
41+
const seen: string[] = []
42+
const set = new Set<string>()
43+
44+
table.getAllLeafColumns().forEach(col => {
45+
const defs = resolveAggregationRowsArray(table, col)
46+
if (!defs) return
47+
for (const def of defs) {
48+
if (!set.has(def.id)) {
49+
set.add(def.id)
50+
seen.push(def.id)
51+
}
52+
}
53+
})
54+
return seen
55+
}
56+
57+
const resolveAggregationCellValue = <TData, TValue>(
58+
table: Table<TData>,
59+
column: Column<TData, unknown>,
60+
aggRowId: string
61+
): TValue | undefined => {
62+
const defs = resolveAggregationRowsArray(table, column)
63+
const def = defs.find(def => def.id === aggRowId)
64+
if (!def) return undefined
65+
const { value } = def
66+
if (typeof value === 'function') {
67+
return (value as AggregationRowValueFn<TData>)({
68+
table,
69+
rowId: aggRowId,
70+
column,
71+
}) as TValue
72+
}
73+
return value as TValue
74+
}
75+
76+
const buildAggregationRowModel = <TData>(table: Table<TData>): AggregationRowModel<TData> => {
77+
const aggRowIds = collectAggregationRowIds(table)
78+
79+
if (!aggRowIds.length) {
80+
return {
81+
rows: [],
82+
flatRows: [],
83+
rowsById: {},
84+
} as unknown as AggregationRowModel<TData>
85+
}
86+
87+
const visibleLeafColumns =
88+
table.getVisibleLeafColumns?.() ?? table.getAllLeafColumns().filter((col: any) => col.getIsVisible?.() ?? true)
89+
90+
const rowsById: Record<string, AggregationRow<TData>> = {}
91+
const rows: AggregationRow<TData>[] = []
92+
const flatRows: AggregationRow<TData>[] = []
93+
94+
aggRowIds.forEach((aggRowId, index) => {
95+
const row: AggregationRow<TData> = {
96+
id: aggRowId,
97+
index,
98+
depth: 0,
99+
original: undefined as any,
100+
parentId: undefined,
101+
_valuesCache: {},
102+
_uniqueValuesCache: {},
103+
_groupingValuesCache: {},
104+
getValue: <TValue>(columnId: string): TValue => {
105+
const column = table.getColumn(columnId)
106+
return column
107+
? (resolveAggregationCellValue<TData, TValue>(table, column, aggRowId) as TValue)
108+
: (undefined as TValue)
109+
},
110+
111+
getVisibleCells: () => {
112+
if (!(row as any)._cells) {
113+
;(row as any)._cells = visibleLeafColumns.map(column => {
114+
const cell: Cell<TData, unknown> = {
115+
id: `${column.id}_${row.id}`,
116+
row,
117+
column,
118+
getValue: () => row.getValue(column.id),
119+
getContext: () =>
120+
({
121+
table,
122+
column,
123+
row,
124+
cell,
125+
getValue: cell.getValue,
126+
renderValue: cell.getValue(),
127+
}) as CellContext<TData, unknown>,
128+
renderValue: () => cell.getValue(),
129+
} as Cell<TData, unknown>
130+
return cell
131+
})
132+
}
133+
return (row as any)._cells as Cell<TData, unknown>[]
134+
},
135+
136+
/** Unimplemented and redundant, use getVisibleCells instead */
137+
getAllCells: () => undefined,
138+
getLeafRows: () => [row],
139+
getParentRow: () => undefined,
140+
subRows: [],
141+
getIsAggregationRow: () => true,
142+
} as unknown as AggregationRow<TData>
143+
144+
rows.push(row)
145+
flatRows.push(row)
146+
rowsById[row.id] = row
147+
})
148+
149+
return { rows, flatRows, rowsById }
150+
}
151+
152+
export const AggregationRowFeature: TableFeature<any> = {
153+
createTable: <TData>(table: Table<TData>) => {
154+
table.getAllAggregationRowIds = () => collectAggregationRowIds(table)
155+
156+
table.hasAggregationRows = () => collectAggregationRowIds(table).length > 0
157+
158+
table.getAggregationRowModel = () => buildAggregationRowModel(table)
159+
},
160+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* eslint-disable import-x/no-unused-modules */
2+
export {}
3+
4+
import '@tanstack/react-table'
5+
6+
import { AggregationRowModel, AggregationRowsInput } from './aggregationRows'
7+
import { VerticalHeaders } from './verticalHeader'
8+
9+
declare module '@tanstack/react-table' {
10+
interface TableState extends VerticalHeaders {}
11+
12+
interface ColumnDefBase<TData, TValue = unknown> {
13+
/**
14+
* Accepts two kinds of inputs:
15+
*
16+
* 1. An array directly `{ id: String, value: T }[]`
17+
* 2. A function `({ table, column }) => {id: String, value:T}[]`
18+
*
19+
* `id` is used for grouping
20+
*
21+
* Use same `id` on multiple rows to supply data to the same aggregation row,
22+
* or different `id` to create multiple rows.
23+
*
24+
* Order matters and is based on the first occurence of id.
25+
*/
26+
aggregationRows?: AggregationRowsInput<TData, TValue>
27+
}
28+
29+
interface Table<TData> {
30+
getAggregationRowModel: () => AggregationRowModel<TData>
31+
getAllAggregationRowIds: () => string[]
32+
hasAggregationRows: () => boolean
33+
}
34+
35+
interface Row {
36+
getIsAggregationRow?: () => boolean
37+
}
38+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { VerticalHeaderFeature } from './verticalHeader'
2+
export { AggregationRowFeature } from './aggregationRows'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TableFeature } from '@tanstack/react-table'
2+
3+
export interface VerticalHeaders {
4+
useVerticalHeaders: string[]
5+
}
6+
7+
export const VerticalHeaderFeature: TableFeature<unknown> = {
8+
getInitialState: (state): VerticalHeaders => {
9+
return {
10+
useVerticalHeaders: [],
11+
...state,
12+
}
13+
},
14+
}

services/frontend/src/components/OodiTable/index.tsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,10 @@
1-
// 03-07-2025 summer hackathon never forget
2-
3-
import type { ColumnDef, TableFeature, TableOptions } from '@tanstack/react-table'
1+
import type { ColumnDef, TableOptions } from '@tanstack/react-table'
42
import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel } from '@tanstack/react-table'
53
import { useState } from 'react'
64

5+
import { AggregationRowFeature, VerticalHeaderFeature } from './features'
76
import { OodiTableContainer } from './OodiTable'
87

9-
interface VerticalHeaders {
10-
useVerticalHeaders: string[]
11-
}
12-
13-
const VerticalHeaderFeature: TableFeature<unknown> = {
14-
getInitialState: (state): VerticalHeaders => {
15-
return {
16-
useVerticalHeaders: [],
17-
...state,
18-
}
19-
},
20-
}
21-
22-
declare module '@tanstack/react-table' {
23-
interface TableState extends VerticalHeaders {}
24-
}
25-
268
/**
279
* In loving memory of SortableTable
2810
*
@@ -54,7 +36,7 @@ export const OodiTable = <TData,>({
5436
}
5537

5638
const table = useReactTable<TData>({
57-
_features: [VerticalHeaderFeature],
39+
_features: [VerticalHeaderFeature, AggregationRowFeature],
5840
data,
5941
columns,
6042
getCoreRowModel: getCoreRowModel(),

services/frontend/src/components/OodiTable/styles.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ export const getCommonPinningStyles = <TData>(column: Column<TData>): SxProps<Th
2020
: {}
2121
}
2222

23-
export const getVerticalStyles = (): SxProps<Theme> => ({
23+
export const verticalStyles: SxProps<Theme> = {
2424
overflow: 'visible',
2525
borderWidth: '1px 1px 1px 0',
2626
borderStyle: 'solid',
2727
borderColor: 'grey.300',
28-
padding: '0.1em',
29-
})
28+
padding: '0.5em',
29+
}

0 commit comments

Comments
 (0)