datool 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,18 @@
1
1
  /* eslint-disable react-hooks/incompatible-library, react-refresh/only-export-components */
2
2
  import {
3
+ getExpandedRowModel,
4
+ getGroupedRowModel,
3
5
  functionalUpdate,
4
6
  getCoreRowModel,
5
7
  getFilteredRowModel,
6
8
  getSortedRowModel,
7
9
  useReactTable,
10
+ type AggregationFnOption,
8
11
  type ColumnDef,
9
12
  type ColumnFiltersState,
13
+ type ExpandedState,
10
14
  type FilterFn,
15
+ type GroupingState,
11
16
  type OnChangeFn,
12
17
  type Row,
13
18
  type RowData,
@@ -19,6 +24,8 @@ import {
19
24
  import { useVirtualizer } from "@tanstack/react-virtual"
20
25
  import {
21
26
  Check,
27
+ ChevronDown,
28
+ ChevronRight,
22
29
  CircleAlert,
23
30
  EyeOff,
24
31
  LayoutGrid,
@@ -46,7 +53,10 @@ import {
46
53
  inferDataTableColumnKind,
47
54
  type DataTableColumnKind,
48
55
  } from "./data-table-col-icon"
49
- import type { DatoolEnumColorMap } from "../../shared/types"
56
+ import type {
57
+ DatoolDateFormat,
58
+ DatoolEnumColorMap,
59
+ } from "../../shared/types"
50
60
  import { Button } from "@/components/ui/button"
51
61
  import {
52
62
  ContextMenu,
@@ -142,10 +152,13 @@ export type DataTableRowAction<TData extends DataTableRow> = {
142
152
  export type DataTableColumnConfig<TData extends DataTableRow> = {
143
153
  accessorFn?: (row: TData) => unknown
144
154
  accessorKey?: Extract<keyof TData, string>
155
+ aggregatedCell?: ColumnDef<TData>["aggregatedCell"]
156
+ aggregationFn?: AggregationFnOption<TData>
145
157
  align?: DataTableAlign
146
158
  cell?: (args: { row: TData; value: unknown }) => React.ReactNode
147
159
  cellClassName?: string
148
160
  enableFiltering?: boolean
161
+ enableGrouping?: boolean
149
162
  enableHiding?: boolean
150
163
  enableResizing?: boolean
151
164
  enableSorting?: boolean
@@ -161,6 +174,7 @@ export type DataTableColumnConfig<TData extends DataTableRow> = {
161
174
  minWidth?: number
162
175
  truncate?: boolean
163
176
  width?: number
177
+ getGroupingValue?: (row: TData) => unknown
164
178
  }
165
179
 
166
180
  export type DataTableProps<TData extends DataTableRow> = {
@@ -170,10 +184,12 @@ export type DataTableProps<TData extends DataTableRow> = {
170
184
  columnVisibility?: VisibilityState
171
185
  columns?: DataTableColumnConfig<TData>[]
172
186
  data: TData[]
187
+ dateFormat?: DatoolDateFormat
173
188
  edgeHorizontalPadding?: React.CSSProperties["paddingLeft"]
174
189
  enableRowSelection?: boolean
175
190
  filterPlaceholder?: string
176
191
  globalFilter?: string
192
+ grouping?: GroupingState
177
193
  getRowId?: (row: TData, index: number) => string
178
194
  height?: React.CSSProperties["height"]
179
195
  highlightQuery?: string
@@ -181,6 +197,7 @@ export type DataTableProps<TData extends DataTableRow> = {
181
197
  onColumnFiltersChange?: (value: ColumnFiltersState) => void
182
198
  onColumnVisibilityChange?: (value: VisibilityState) => void
183
199
  onGlobalFilterChange?: (value: string) => void
200
+ onGroupingChange?: (value: GroupingState) => void
184
201
  resolveColumnHighlightTerms?: (columnId: string, query: string) => string[]
185
202
  rowActions?: DataTableRowAction<TData>[]
186
203
  rowClassName?: (row: TData) => string | undefined
@@ -227,6 +244,7 @@ type PersistedTableState = {
227
244
  highlightedColumns?: Record<string, boolean>
228
245
  columnVisibility?: VisibilityState
229
246
  globalFilter?: string
247
+ grouping?: GroupingState
230
248
  sorting?: SortingState
231
249
  }
232
250
 
@@ -402,6 +420,7 @@ function getPersistedUrlParam(id: string) {
402
420
  function isPersistedStateEmpty(state: PersistedTableState) {
403
421
  return (
404
422
  (state.sorting?.length ?? 0) === 0 &&
423
+ (state.grouping?.length ?? 0) === 0 &&
405
424
  (state.columnFilters?.length ?? 0) === 0 &&
406
425
  Object.keys(state.highlightedColumns ?? {}).length === 0 &&
407
426
  Object.keys(state.columnVisibility ?? {}).length === 0 &&
@@ -496,6 +515,111 @@ function inferWidth(kind: DataTableColumnKind) {
496
515
  }
497
516
  }
498
517
 
518
+ type DateRangeAggregate = {
519
+ durationMs: number
520
+ endMs: number
521
+ startMs: number
522
+ }
523
+
524
+ function toTimestamp(value: unknown) {
525
+ if (value instanceof Date) {
526
+ return Number.isNaN(value.getTime()) ? null : value.getTime()
527
+ }
528
+
529
+ if (typeof value === "number") {
530
+ return Number.isFinite(value) ? value : null
531
+ }
532
+
533
+ if (typeof value === "string") {
534
+ const timestamp = Date.parse(value)
535
+
536
+ return Number.isNaN(timestamp) ? null : timestamp
537
+ }
538
+
539
+ return null
540
+ }
541
+
542
+ function buildDateRangeAggregate<TData extends DataTableRow>(
543
+ columnId: string,
544
+ leafRows: Row<TData>[]
545
+ ) {
546
+ let startMs = Number.POSITIVE_INFINITY
547
+ let endMs = Number.NEGATIVE_INFINITY
548
+
549
+ for (const row of leafRows) {
550
+ const timestamp = toTimestamp(row.getValue(columnId))
551
+
552
+ if (timestamp === null) {
553
+ continue
554
+ }
555
+
556
+ startMs = Math.min(startMs, timestamp)
557
+ endMs = Math.max(endMs, timestamp)
558
+ }
559
+
560
+ if (
561
+ startMs === Number.POSITIVE_INFINITY ||
562
+ endMs === Number.NEGATIVE_INFINITY
563
+ ) {
564
+ return null
565
+ }
566
+
567
+ return {
568
+ durationMs: Math.max(0, endMs - startMs),
569
+ endMs,
570
+ startMs,
571
+ } satisfies DateRangeAggregate
572
+ }
573
+
574
+ function isDateRangeAggregate(value: unknown): value is DateRangeAggregate {
575
+ return (
576
+ value !== null &&
577
+ typeof value === "object" &&
578
+ "durationMs" in value &&
579
+ "endMs" in value &&
580
+ "startMs" in value
581
+ )
582
+ }
583
+
584
+ function formatSummaryNumber(value: number) {
585
+ return new Intl.NumberFormat(undefined, {
586
+ maximumFractionDigits: Number.isInteger(value) ? 0 : 2,
587
+ }).format(value)
588
+ }
589
+
590
+ function formatDuration(durationMs: number) {
591
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000))
592
+ const days = Math.floor(totalSeconds / 86_400)
593
+ const hours = Math.floor((totalSeconds % 86_400) / 3_600)
594
+ const minutes = Math.floor((totalSeconds % 3_600) / 60)
595
+ const seconds = totalSeconds % 60
596
+ const parts = [
597
+ days > 0 ? `${days}d` : null,
598
+ hours > 0 ? `${hours}h` : null,
599
+ minutes > 0 ? `${minutes}m` : null,
600
+ seconds > 0 || totalSeconds === 0 ? `${seconds}s` : null,
601
+ ].filter(Boolean)
602
+
603
+ return parts.join(" ")
604
+ }
605
+
606
+ function resolveGroupedPadding(
607
+ padding: React.CSSProperties["paddingLeft"],
608
+ depth: number
609
+ ) {
610
+ const indent = depth * 16
611
+
612
+ if (typeof padding === "number") {
613
+ return padding + indent
614
+ }
615
+
616
+ if (typeof padding === "string") {
617
+ return `calc(${padding} + ${indent}px)`
618
+ }
619
+
620
+ return indent
621
+ }
622
+
499
623
  function resolveColumnId<TData extends DataTableRow>(
500
624
  column: DataTableColumnConfig<TData>,
501
625
  index: number
@@ -512,6 +636,7 @@ function resolveColumnId<TData extends DataTableRow>(
512
636
  function buildColumns<TData extends DataTableRow>(
513
637
  data: TData[],
514
638
  columns?: DataTableColumnConfig<TData>[],
639
+ dateFormat?: DatoolDateFormat,
515
640
  showRowSelectionColumn?: boolean,
516
641
  showRowActionButtonsColumn?: boolean,
517
642
  rowActionsColumnSize?: number
@@ -553,18 +678,29 @@ function buildColumns<TData extends DataTableRow>(
553
678
  return {
554
679
  accessorFn: column.accessorFn,
555
680
  accessorKey: column.accessorKey,
681
+ aggregatedCell: column.aggregatedCell,
682
+ aggregationFn:
683
+ column.aggregationFn ??
684
+ (kind === "date"
685
+ ? buildDateRangeAggregate
686
+ : kind === "number"
687
+ ? "sum"
688
+ : undefined),
556
689
  cell: ({ getValue, row }) =>
557
690
  column.cell
558
691
  ? column.cell({ row: row.original, value: getValue() })
559
692
  : fallbackCellValue(getValue(), kind, {
693
+ dateFormat,
560
694
  enumColors: kind === "enum" ? column.enumColors : undefined,
561
695
  enumOptions: kind === "enum" ? column.enumOptions : undefined,
562
696
  }),
563
697
  enableGlobalFilter: column.enableFiltering ?? true,
698
+ enableGrouping: column.enableGrouping ?? true,
564
699
  filterFn: column.filterFn,
565
700
  enableHiding: column.enableHiding ?? true,
566
701
  enableResizing: column.enableResizing ?? true,
567
702
  enableSorting: column.enableSorting ?? true,
703
+ getGroupingValue: column.getGroupingValue,
568
704
  header: column.header ?? formatHeaderLabel(id),
569
705
  id,
570
706
  maxSize: column.maxWidth ?? 420,
@@ -583,6 +719,7 @@ function buildColumns<TData extends DataTableRow>(
583
719
  ...builtColumns,
584
720
  {
585
721
  cell: () => null,
722
+ enableGrouping: false,
586
723
  enableGlobalFilter: false,
587
724
  enableHiding: false,
588
725
  enableResizing: false,
@@ -604,13 +741,16 @@ function buildColumns<TData extends DataTableRow>(
604
741
  {
605
742
  cell: ({ row }) => (
606
743
  <div className="flex items-center justify-center">
607
- <DataTableCheckbox
608
- ariaLabel={`Select row ${row.index + 1}`}
609
- checked={row.getIsSelected()}
610
- onCheckedChange={(checked) => row.toggleSelected(checked)}
611
- />
744
+ {row.getCanSelect() ? (
745
+ <DataTableCheckbox
746
+ ariaLabel={`Select row ${row.index + 1}`}
747
+ checked={row.getIsSelected()}
748
+ onCheckedChange={(checked) => row.toggleSelected(checked)}
749
+ />
750
+ ) : null}
612
751
  </div>
613
752
  ),
753
+ enableGrouping: false,
614
754
  enableGlobalFilter: false,
615
755
  enableHiding: false,
616
756
  enableResizing: false,
@@ -649,6 +789,7 @@ function buildColumns<TData extends DataTableRow>(
649
789
  ...withSelectionColumn,
650
790
  {
651
791
  cell: () => null,
792
+ enableGrouping: false,
652
793
  enableGlobalFilter: false,
653
794
  enableHiding: false,
654
795
  enableResizing: false,
@@ -704,6 +845,31 @@ function shouldIgnoreRowSelectionTarget(target: EventTarget | null) {
704
845
  )
705
846
  }
706
847
 
848
+ function shouldIgnoreSelectionShortcutTarget(target: EventTarget | null) {
849
+ return (
850
+ target instanceof Element &&
851
+ Boolean(
852
+ target.closest(
853
+ [
854
+ "a",
855
+ "button",
856
+ "input",
857
+ "select",
858
+ "textarea",
859
+ "[contenteditable=true]",
860
+ '[role="button"]',
861
+ '[role="combobox"]',
862
+ '[role="dialog"]',
863
+ '[role="menu"]',
864
+ '[role="menuitem"]',
865
+ '[role="searchbox"]',
866
+ '[role="textbox"]',
867
+ ].join(",")
868
+ )
869
+ )
870
+ )
871
+ }
872
+
707
873
  function resolveRowActionRows<TData extends DataTableRow>(
708
874
  action: DataTableRowAction<TData>,
709
875
  row: Row<TData>,
@@ -1126,10 +1292,12 @@ function DataTableView<TData extends DataTableRow>({
1126
1292
  columnVisibility: controlledColumnVisibility,
1127
1293
  columns,
1128
1294
  data,
1295
+ dateFormat,
1129
1296
  edgeHorizontalPadding = "16px",
1130
1297
  enableRowSelection = false,
1131
1298
  filterPlaceholder = "Search across visible columns",
1132
1299
  globalFilter,
1300
+ grouping: controlledGrouping,
1133
1301
  getRowId,
1134
1302
  height = 620,
1135
1303
  highlightQuery = "",
@@ -1137,6 +1305,7 @@ function DataTableView<TData extends DataTableRow>({
1137
1305
  onColumnFiltersChange,
1138
1306
  onColumnVisibilityChange,
1139
1307
  onGlobalFilterChange,
1308
+ onGroupingChange,
1140
1309
  resolveColumnHighlightTerms,
1141
1310
  rowActions,
1142
1311
  rowClassName,
@@ -1163,6 +1332,12 @@ function DataTableView<TData extends DataTableRow>({
1163
1332
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
1164
1333
  () => persistedState.columnFilters ?? []
1165
1334
  )
1335
+ const [grouping, setGrouping] = React.useState<GroupingState>(
1336
+ () => persistedState.grouping ?? []
1337
+ )
1338
+ const [expanded, setExpanded] = React.useState<ExpandedState>(() =>
1339
+ (controlledGrouping ?? persistedState.grouping ?? []).length > 0 ? true : {}
1340
+ )
1166
1341
  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
1167
1342
  const [rowActionStatuses, setRowActionStatuses] = React.useState<
1168
1343
  Record<string, RowActionStatus>
@@ -1185,9 +1360,15 @@ function DataTableView<TData extends DataTableRow>({
1185
1360
  const isColumnFiltersControlled = controlledColumnFilters !== undefined
1186
1361
  const isColumnVisibilityControlled = controlledColumnVisibility !== undefined
1187
1362
  const isGlobalFilterControlled = globalFilter !== undefined
1363
+ const isGroupingControlled = controlledGrouping !== undefined
1188
1364
  const resolvedColumnFilters = controlledColumnFilters ?? columnFilters
1189
1365
  const resolvedColumnVisibility =
1190
1366
  controlledColumnVisibility ?? columnVisibility
1367
+ const resolvedGrouping = controlledGrouping ?? grouping
1368
+ const groupingKey = React.useMemo(
1369
+ () => resolvedGrouping.join("\u001f"),
1370
+ [resolvedGrouping]
1371
+ )
1191
1372
  const hasSelectionActions = rowActions
1192
1373
  ? hasSelectionScopedAction(rowActions)
1193
1374
  : false
@@ -1234,6 +1415,7 @@ function DataTableView<TData extends DataTableRow>({
1234
1415
  buildColumns(
1235
1416
  data,
1236
1417
  columnsWithEnumOptions,
1418
+ dateFormat,
1237
1419
  showRowSelectionColumn,
1238
1420
  showRowActionButtonsColumn,
1239
1421
  rowActionsColumnSize
@@ -1241,6 +1423,7 @@ function DataTableView<TData extends DataTableRow>({
1241
1423
  [
1242
1424
  columnsWithEnumOptions,
1243
1425
  data,
1426
+ dateFormat,
1244
1427
  rowActionsColumnSize,
1245
1428
  showRowActionButtonsColumn,
1246
1429
  showRowSelectionColumn,
@@ -1314,28 +1497,50 @@ function DataTableView<TData extends DataTableRow>({
1314
1497
  resolvedColumnVisibility,
1315
1498
  ]
1316
1499
  )
1500
+ const handleGroupingChange = React.useCallback<OnChangeFn<GroupingState>>(
1501
+ (updater) => {
1502
+ const nextValue = functionalUpdate(updater, resolvedGrouping)
1503
+
1504
+ if (!isGroupingControlled) {
1505
+ setGrouping(nextValue)
1506
+ }
1507
+
1508
+ onGroupingChange?.(nextValue)
1509
+ },
1510
+ [isGroupingControlled, onGroupingChange, resolvedGrouping]
1511
+ )
1317
1512
 
1318
1513
  const table = useReactTable({
1514
+ autoResetExpanded: false,
1319
1515
  columnResizeMode: "onChange",
1320
1516
  columns: tableColumns,
1321
1517
  data,
1322
1518
  enableColumnResizing: true,
1323
- enableRowSelection: canSelectRows,
1519
+ enableGrouping: true,
1520
+ enableRowSelection: canSelectRows ? (row) => !row.getIsGrouped() : false,
1521
+ enableSubRowSelection: false,
1324
1522
  getCoreRowModel: getCoreRowModel(),
1523
+ getExpandedRowModel: getExpandedRowModel(),
1325
1524
  getFilteredRowModel: getFilteredRowModel(),
1525
+ getGroupedRowModel: getGroupedRowModel(),
1326
1526
  getRowId,
1327
1527
  getSortedRowModel: getSortedRowModel(),
1528
+ groupedColumnMode: false,
1328
1529
  globalFilterFn: globalFilterFn as FilterFn<TData>,
1329
1530
  onColumnFiltersChange: handleColumnFiltersChange,
1330
1531
  onColumnSizingChange: handleColumnSizingChange,
1331
1532
  onColumnVisibilityChange: handleColumnVisibilityChange,
1533
+ onExpandedChange: setExpanded,
1534
+ onGroupingChange: handleGroupingChange,
1332
1535
  onRowSelectionChange: setRowSelection,
1333
1536
  onSortingChange: setSorting,
1334
1537
  state: {
1335
1538
  columnFilters: resolvedColumnFilters,
1336
1539
  columnSizing,
1337
1540
  columnVisibility: resolvedColumnVisibility,
1541
+ expanded,
1338
1542
  globalFilter: deferredSearch.trim(),
1543
+ grouping: resolvedGrouping,
1339
1544
  rowSelection,
1340
1545
  sorting,
1341
1546
  },
@@ -1346,6 +1551,10 @@ function DataTableView<TData extends DataTableRow>({
1346
1551
  const hasAutoScrolledOnMountRef = React.useRef(false)
1347
1552
  const previousDataLengthRef = React.useRef(0)
1348
1553
  const rows = table.getRowModel().rows
1554
+ const selectableRows = React.useMemo(
1555
+ () => rows.filter((row) => row.getCanSelect()),
1556
+ [rows]
1557
+ )
1349
1558
  const selectedTableRows = table.getSelectedRowModel().rows
1350
1559
  const resolvedHeight = typeof height === "number" ? height : 0
1351
1560
  const initialOffsetRef = React.useRef(
@@ -1387,6 +1596,15 @@ function DataTableView<TData extends DataTableRow>({
1387
1596
  }
1388
1597
  setHighlightedColumns(nextState.highlightedColumns ?? {})
1389
1598
  setColumnFilters(nextState.columnFilters ?? [])
1599
+ if (!isGroupingControlled) {
1600
+ setGrouping(nextState.grouping ?? [])
1601
+ }
1602
+ setExpanded(
1603
+ (isGroupingControlled ? controlledGrouping : nextState.grouping ?? [])
1604
+ .length > 0
1605
+ ? true
1606
+ : {}
1607
+ )
1390
1608
  setSearchDraft(nextState.globalFilter ?? "")
1391
1609
  setRowSelection({})
1392
1610
  dragSelectionRef.current = null
@@ -1398,7 +1616,14 @@ function DataTableView<TData extends DataTableRow>({
1398
1616
  }
1399
1617
  rowActionStatusTimersRef.current = {}
1400
1618
  setRowActionStatuses({})
1401
- }, [id, isColumnFiltersControlled, isColumnVisibilityControlled, statePersistence])
1619
+ }, [
1620
+ controlledGrouping,
1621
+ id,
1622
+ isColumnFiltersControlled,
1623
+ isColumnVisibilityControlled,
1624
+ isGroupingControlled,
1625
+ statePersistence,
1626
+ ])
1402
1627
 
1403
1628
  React.useEffect(() => {
1404
1629
  if (!isColumnFiltersControlled) {
@@ -1416,6 +1641,14 @@ function DataTableView<TData extends DataTableRow>({
1416
1641
  setColumnVisibility(controlledColumnVisibility)
1417
1642
  }, [controlledColumnVisibility, isColumnVisibilityControlled])
1418
1643
 
1644
+ React.useEffect(() => {
1645
+ if (!isGroupingControlled) {
1646
+ return
1647
+ }
1648
+
1649
+ setGrouping(controlledGrouping)
1650
+ }, [controlledGrouping, isGroupingControlled])
1651
+
1419
1652
  React.useEffect(() => {
1420
1653
  if (!isGlobalFilterControlled) {
1421
1654
  return
@@ -1439,6 +1672,15 @@ function DataTableView<TData extends DataTableRow>({
1439
1672
  }
1440
1673
  setHighlightedColumns(nextState.highlightedColumns ?? {})
1441
1674
  setColumnFilters(nextState.columnFilters ?? [])
1675
+ if (!isGroupingControlled) {
1676
+ setGrouping(nextState.grouping ?? [])
1677
+ }
1678
+ setExpanded(
1679
+ (isGroupingControlled ? controlledGrouping : nextState.grouping ?? [])
1680
+ .length > 0
1681
+ ? true
1682
+ : {}
1683
+ )
1442
1684
  setSearchDraft(nextState.globalFilter ?? "")
1443
1685
  setRowSelection({})
1444
1686
  dragSelectionRef.current = null
@@ -1450,7 +1692,13 @@ function DataTableView<TData extends DataTableRow>({
1450
1692
  window.addEventListener("popstate", syncFromUrl)
1451
1693
 
1452
1694
  return () => window.removeEventListener("popstate", syncFromUrl)
1453
- }, [id, isColumnVisibilityControlled, statePersistence])
1695
+ }, [
1696
+ controlledGrouping,
1697
+ id,
1698
+ isColumnVisibilityControlled,
1699
+ isGroupingControlled,
1700
+ statePersistence,
1701
+ ])
1454
1702
 
1455
1703
  React.useEffect(() => {
1456
1704
  if (isGlobalFilterControlled || isColumnFiltersControlled) {
@@ -1463,6 +1711,7 @@ function DataTableView<TData extends DataTableRow>({
1463
1711
  highlightedColumns,
1464
1712
  columnVisibility: resolvedColumnVisibility,
1465
1713
  globalFilter: deferredSearch.trim(),
1714
+ grouping: resolvedGrouping,
1466
1715
  sorting,
1467
1716
  })
1468
1717
  }, [
@@ -1473,11 +1722,17 @@ function DataTableView<TData extends DataTableRow>({
1473
1722
  id,
1474
1723
  isColumnFiltersControlled,
1475
1724
  isGlobalFilterControlled,
1725
+ isGroupingControlled,
1476
1726
  resolvedColumnVisibility,
1727
+ resolvedGrouping,
1477
1728
  sorting,
1478
1729
  statePersistence,
1479
1730
  ])
1480
1731
 
1732
+ React.useEffect(() => {
1733
+ setExpanded(resolvedGrouping.length > 0 ? true : {})
1734
+ }, [groupingKey, resolvedGrouping.length])
1735
+
1481
1736
  React.useEffect(() => {
1482
1737
  const container = containerRef.current
1483
1738
 
@@ -1579,6 +1834,13 @@ function DataTableView<TData extends DataTableRow>({
1579
1834
 
1580
1835
  onGlobalFilterChange?.(value)
1581
1836
  }
1837
+ const clearRowSelection = React.useCallback(() => {
1838
+ setRowSelection({})
1839
+ dragSelectionRef.current = null
1840
+ dragPointerRef.current = null
1841
+ setIsDragSelecting(false)
1842
+ selectionAnchorIdRef.current = null
1843
+ }, [])
1582
1844
 
1583
1845
  const isColumnHighlightEnabled = React.useCallback(
1584
1846
  (columnId: string, meta: DataTableColumnMeta) =>
@@ -1591,10 +1853,10 @@ function DataTableView<TData extends DataTableRow>({
1591
1853
  rowId: string,
1592
1854
  baseSelection: RowSelectionState = {}
1593
1855
  ) => {
1594
- const anchorIndex = rows.findIndex(
1856
+ const anchorIndex = selectableRows.findIndex(
1595
1857
  (candidateRow) => candidateRow.id === anchorId
1596
1858
  )
1597
- const rowIndex = rows.findIndex(
1859
+ const rowIndex = selectableRows.findIndex(
1598
1860
  (candidateRow) => candidateRow.id === rowId
1599
1861
  )
1600
1862
 
@@ -1610,13 +1872,13 @@ function DataTableView<TData extends DataTableRow>({
1610
1872
  setRowSelection({
1611
1873
  ...baseSelection,
1612
1874
  ...Object.fromEntries(
1613
- rows
1875
+ selectableRows
1614
1876
  .slice(start, end + 1)
1615
1877
  .map((candidateRow) => [candidateRow.id, true])
1616
1878
  ),
1617
1879
  })
1618
1880
  },
1619
- [rows]
1881
+ [selectableRows]
1620
1882
  )
1621
1883
  const selectSingleRow = React.useCallback((rowId: string) => {
1622
1884
  selectionAnchorIdRef.current = rowId
@@ -1663,6 +1925,7 @@ function DataTableView<TData extends DataTableRow>({
1663
1925
  if (
1664
1926
  event.button !== 0 ||
1665
1927
  !canSelectRows ||
1928
+ !row.getCanSelect() ||
1666
1929
  shouldIgnoreRowSelectionTarget(event.target)
1667
1930
  ) {
1668
1931
  return
@@ -1724,6 +1987,7 @@ function DataTableView<TData extends DataTableRow>({
1724
1987
  (event: React.MouseEvent<HTMLTableRowElement>, row: Row<TData>) => {
1725
1988
  if (
1726
1989
  !canSelectRows ||
1990
+ !row.getCanSelect() ||
1727
1991
  event.buttons !== 1 ||
1728
1992
  shouldIgnoreRowSelectionTarget(event.target) ||
1729
1993
  !dragSelectionRef.current
@@ -1739,7 +2003,7 @@ function DataTableView<TData extends DataTableRow>({
1739
2003
  )
1740
2004
  const handleRowContextMenu = React.useCallback(
1741
2005
  (row: Row<TData>) => {
1742
- if (!canSelectRows) {
2006
+ if (!canSelectRows || !row.getCanSelect()) {
1743
2007
  return
1744
2008
  }
1745
2009
 
@@ -1819,6 +2083,27 @@ function DataTableView<TData extends DataTableRow>({
1819
2083
  window.cancelAnimationFrame(frameId)
1820
2084
  }
1821
2085
  }, [isDragSelecting, updateDragSelectionFromPointer])
2086
+ React.useEffect(() => {
2087
+ if (!canSelectRows) {
2088
+ return
2089
+ }
2090
+
2091
+ const handleWindowKeyDown = (event: KeyboardEvent) => {
2092
+ if (
2093
+ event.key !== "Escape" ||
2094
+ Object.keys(rowSelection).length === 0 ||
2095
+ shouldIgnoreSelectionShortcutTarget(event.target)
2096
+ ) {
2097
+ return
2098
+ }
2099
+
2100
+ clearRowSelection()
2101
+ }
2102
+
2103
+ window.addEventListener("keydown", handleWindowKeyDown)
2104
+
2105
+ return () => window.removeEventListener("keydown", handleWindowKeyDown)
2106
+ }, [canSelectRows, clearRowSelection, rowSelection])
1822
2107
 
1823
2108
  return (
1824
2109
  <section
@@ -1896,66 +2181,160 @@ function DataTableView<TData extends DataTableRow>({
1896
2181
  ) : (
1897
2182
  virtualRows.map((virtualRow) => {
1898
2183
  const row = rows[virtualRow.index]
1899
- const visibleRowActions = resolveVisibleRowActions(
1900
- rowActions ?? [],
1901
- row,
1902
- selectedTableRows
1903
- )
2184
+ const isGroupRow = row.getIsGrouped()
2185
+ const visibleRowActions = isGroupRow
2186
+ ? []
2187
+ : resolveVisibleRowActions(
2188
+ rowActions ?? [],
2189
+ row,
2190
+ selectedTableRows
2191
+ )
1904
2192
  const isSelected = row.getIsSelected()
2193
+ const groupVisibleCells = row.getVisibleCells()
2194
+ const groupingColumn = row.groupingColumnId
2195
+ ? table.getColumn(row.groupingColumnId)
2196
+ : undefined
2197
+ const groupingMeta = groupingColumn?.columnDef.meta as
2198
+ | DataTableColumnMeta
2199
+ | undefined
2200
+ const groupingValue = groupingColumn
2201
+ ? fallbackCellValue(row.groupingValue, groupingMeta?.kind, {
2202
+ dateFormat,
2203
+ enumColors: groupingMeta?.enumColors,
2204
+ enumOptions: groupingMeta?.enumOptions,
2205
+ })
2206
+ : null
2207
+ const hasVisibleGroupedCell = groupVisibleCells.some((cell) =>
2208
+ cell.getIsGrouped()
2209
+ )
2210
+ const primaryGroupCellId = groupVisibleCells.find(
2211
+ (cell) =>
2212
+ cell.column.id !== "__select" && cell.column.id !== "__actions"
2213
+ )?.id
1905
2214
  const rowContent = (
1906
2215
  <tr
1907
2216
  aria-selected={isSelected}
1908
2217
  className={cn(
1909
- "absolute left-0 flex w-full bg-card transition-colors",
1910
- canSelectRows && "cursor-pointer",
1911
- rowClassName?.(row.original),
1912
- isSelected &&
2218
+ "absolute left-0 flex w-full transition-colors",
2219
+ isGroupRow
2220
+ ? "bg-transparent"
2221
+ : "bg-card",
2222
+ canSelectRows &&
2223
+ row.getCanSelect() &&
2224
+ "cursor-pointer",
2225
+ !isGroupRow && rowClassName?.(row.original),
2226
+ !isGroupRow &&
2227
+ isSelected &&
1913
2228
  "bg-primary/10 before:absolute before:-top-px before:left-0 before:h-px before:w-full before:bg-primary before:content-[''] after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-primary after:content-['']"
1914
2229
  )}
1915
2230
  data-index={virtualRow.index}
1916
2231
  data-row-id={row.id}
1917
- data-state={isSelected ? "selected" : undefined}
2232
+ data-state={!isGroupRow && isSelected ? "selected" : undefined}
1918
2233
  key={row.id}
1919
- onContextMenu={() => handleRowContextMenu(row)}
1920
- onMouseDown={(event) => handleRowMouseDown(event, row)}
1921
- onMouseEnter={(event) => handleRowMouseEnter(event, row)}
2234
+ onContextMenu={
2235
+ isGroupRow ? undefined : () => handleRowContextMenu(row)
2236
+ }
2237
+ onMouseDown={
2238
+ isGroupRow
2239
+ ? undefined
2240
+ : (event) => handleRowMouseDown(event, row)
2241
+ }
2242
+ onMouseEnter={
2243
+ isGroupRow
2244
+ ? undefined
2245
+ : (event) => handleRowMouseEnter(event, row)
2246
+ }
1922
2247
  ref={(node) => {
1923
2248
  if (node) {
1924
2249
  rowVirtualizer.measureElement(node)
1925
2250
  }
1926
2251
  }}
1927
2252
  style={{
1928
- ...rowStyle?.(row.original),
1929
- minHeight: rowHeight,
2253
+ ...(!isGroupRow ? rowStyle?.(row.original) : undefined),
2254
+ minHeight: isGroupRow ? Math.max(rowHeight, 44) : rowHeight,
1930
2255
  transform: `translateY(${virtualRow.start}px)`,
1931
2256
  width: table.getTotalSize(),
1932
2257
  }}
1933
2258
  >
1934
- {row.getVisibleCells().map((cell, index, visibleCells) => {
1935
- const meta = (cell.column.columnDef.meta ??
1936
- {}) as DataTableColumnMeta
1937
- const isActionsCell = cell.column.id === "__actions"
1938
- const highlightTerms =
1939
- meta.kind === "text" &&
1940
- resolveColumnHighlightTerms &&
1941
- isColumnHighlightEnabled(cell.column.id, meta)
1942
- ? resolveColumnHighlightTerms(
1943
- cell.column.id,
1944
- highlightQuery
2259
+ {isGroupRow ? (
2260
+ groupVisibleCells.map((cell, index, visibleCells) => {
2261
+ const meta = (cell.column.columnDef.meta ??
2262
+ {}) as DataTableColumnMeta
2263
+ const isActionsCell = cell.column.id === "__actions"
2264
+ const isSelectionCell = meta.kind === "selection"
2265
+ const shouldRenderGroupLabel =
2266
+ cell.getIsGrouped() ||
2267
+ (!hasVisibleGroupedCell && cell.id === primaryGroupCellId)
2268
+ const value = cell.getValue()
2269
+ let content: React.ReactNode = null
2270
+
2271
+ if (!isActionsCell && !isSelectionCell) {
2272
+ if (shouldRenderGroupLabel) {
2273
+ content = (
2274
+ <div className="flex min-w-0 items-center gap-0.5">
2275
+ {row.getCanExpand() ? (
2276
+ <button
2277
+ aria-label={
2278
+ row.getIsExpanded()
2279
+ ? "Collapse group"
2280
+ : "Expand group"
2281
+ }
2282
+ className="-ml-4"
2283
+ onClick={() => row.toggleExpanded()}
2284
+ type="button"
2285
+ >
2286
+ {row.getIsExpanded() ? (
2287
+ <ChevronDown className="size-3.5" />
2288
+ ) : (
2289
+ <ChevronRight className="size-3.5" />
2290
+ )}
2291
+ </button>
2292
+ ) : null}
2293
+ <div className="min-w-0 truncate font-medium text-foreground">
2294
+ {groupingColumn ? groupingValue : "Group"}
2295
+ </div>
2296
+ </div>
1945
2297
  )
1946
- : []
2298
+ } else if (meta.kind === "date" && isDateRangeAggregate(value)) {
2299
+ content = (
2300
+ <div className="min-w-0 truncate">
2301
+ <span className="font-medium text-foreground">
2302
+ {formatDuration(value.durationMs)}
2303
+ </span>
2304
+ <span className="ml-2 text-[11px] text-muted-foreground">
2305
+ span
2306
+ </span>
2307
+ </div>
2308
+ )
2309
+ } else if (meta.kind === "number" && typeof value === "number") {
2310
+ content = (
2311
+ <div className="min-w-0 truncate font-medium text-foreground">
2312
+ {formatSummaryNumber(value)}
2313
+ </div>
2314
+ )
2315
+ }
2316
+ }
1947
2317
 
1948
- if (isActionsCell) {
1949
2318
  return (
1950
2319
  <td
1951
2320
  className={cn(
1952
- "flex shrink-0 border-b border-border px-2 py-1.5 align-middle text-sm text-foreground justify-end text-right",
1953
- meta.cellClassName
2321
+ "flex shrink-0 items-center border-y border-border/70 px-2 py-2 align-middle text-xs text-muted-foreground",
2322
+ meta.align === "center" && "justify-center text-center",
2323
+ meta.align === "right" && "justify-end text-right",
2324
+ meta.sticky === "left" &&
2325
+ "sticky left-0 z-10 border-r border-r-border"
1954
2326
  )}
1955
2327
  key={cell.id}
1956
2328
  style={{
2329
+ background:
2330
+ "var(--color-table-gap, color-mix(in oklab, var(--color-muted) 84%, transparent))",
1957
2331
  paddingLeft:
1958
- index === 0 ? edgeHorizontalPadding : undefined,
2332
+ index === 0
2333
+ ? resolveGroupedPadding(
2334
+ edgeHorizontalPadding,
2335
+ row.depth
2336
+ )
2337
+ : undefined,
1959
2338
  paddingRight:
1960
2339
  index === visibleCells.length - 1
1961
2340
  ? edgeHorizontalPadding
@@ -1963,33 +2342,72 @@ function DataTableView<TData extends DataTableRow>({
1963
2342
  width: cell.column.getSize(),
1964
2343
  }}
1965
2344
  >
1966
- <div className="w-full min-w-0">
1967
- <RowActionButtonGroup
1968
- resolvedActions={visibleRowActions}
1969
- setStatus={setRowActionStatus}
1970
- statuses={rowActionStatuses}
1971
- />
1972
- </div>
2345
+ <div className="w-full min-w-0">{content}</div>
1973
2346
  </td>
1974
2347
  )
1975
- }
2348
+ })
2349
+ ) : (
2350
+ row.getVisibleCells().map((cell, index, visibleCells) => {
2351
+ const meta = (cell.column.columnDef.meta ??
2352
+ {}) as DataTableColumnMeta
2353
+ const isActionsCell = cell.column.id === "__actions"
2354
+ const highlightTerms =
2355
+ meta.kind === "text" &&
2356
+ resolveColumnHighlightTerms &&
2357
+ isColumnHighlightEnabled(cell.column.id, meta)
2358
+ ? resolveColumnHighlightTerms(
2359
+ cell.column.id,
2360
+ highlightQuery
2361
+ )
2362
+ : []
2363
+
2364
+ if (isActionsCell) {
2365
+ return (
2366
+ <td
2367
+ className={cn(
2368
+ "flex shrink-0 border-b border-border px-2 py-1.5 align-middle text-sm text-foreground justify-end text-right",
2369
+ meta.cellClassName
2370
+ )}
2371
+ key={cell.id}
2372
+ style={{
2373
+ paddingLeft:
2374
+ index === 0 ? edgeHorizontalPadding : undefined,
2375
+ paddingRight:
2376
+ index === visibleCells.length - 1
2377
+ ? edgeHorizontalPadding
2378
+ : undefined,
2379
+ width: cell.column.getSize(),
2380
+ }}
2381
+ >
2382
+ <div className="w-full min-w-0">
2383
+ <RowActionButtonGroup
2384
+ resolvedActions={visibleRowActions}
2385
+ setStatus={setRowActionStatus}
2386
+ statuses={rowActionStatuses}
2387
+ />
2388
+ </div>
2389
+ </td>
2390
+ )
2391
+ }
1976
2392
 
1977
- return (
1978
- <DataTableBodyCell
1979
- cell={cell}
1980
- highlightTerms={highlightTerms}
1981
- key={cell.id}
1982
- paddingLeft={
1983
- index === 0 ? edgeHorizontalPadding : undefined
1984
- }
1985
- paddingRight={
1986
- index === visibleCells.length - 1
1987
- ? edgeHorizontalPadding
1988
- : undefined
1989
- }
1990
- />
1991
- )
1992
- })}
2393
+ return (
2394
+ <DataTableBodyCell
2395
+ cell={cell}
2396
+ dateFormat={dateFormat}
2397
+ highlightTerms={highlightTerms}
2398
+ key={cell.id}
2399
+ paddingLeft={
2400
+ index === 0 ? edgeHorizontalPadding : undefined
2401
+ }
2402
+ paddingRight={
2403
+ index === visibleCells.length - 1
2404
+ ? edgeHorizontalPadding
2405
+ : undefined
2406
+ }
2407
+ />
2408
+ )
2409
+ })
2410
+ )}
1993
2411
  </tr>
1994
2412
  )
1995
2413