datool 0.0.3 → 0.0.5
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.
- package/README.md +39 -2
- package/client-dist/assets/index-DUkIilaZ.css +1 -0
- package/client-dist/assets/index-OdNyDkx7.js +164 -0
- package/client-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/client/App.tsx +147 -6
- package/src/client/components/data-table-cell.tsx +25 -2
- package/src/client/components/data-table-header-col.tsx +3 -0
- package/src/client/components/data-table.tsx +43 -3
- package/src/client/components/enum-badge.tsx +133 -0
- package/src/client/components/viewer-settings.tsx +67 -0
- package/src/client/index.css +3 -0
- package/src/client/lib/data-table-search.ts +0 -1
- package/src/client/lib/datool-url-state.ts +29 -2
- package/src/client/lib/filterable-table.ts +3 -1
- package/src/shared/types.ts +28 -0
- package/client-dist/assets/index-BeRNeRUq.css +0 -1
- package/client-dist/assets/index-uoZ4c_I8.js +0 -164
package/client-dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>datool</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-OdNyDkx7.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DUkIilaZ.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/client/App.tsx
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "@/lib/data-table-search"
|
|
42
42
|
import {
|
|
43
43
|
readDatoolColumnVisibility,
|
|
44
|
+
readDatoolGrouping,
|
|
44
45
|
readDatoolSearch,
|
|
45
46
|
readSelectedStreamId,
|
|
46
47
|
writeDatoolUrlState,
|
|
@@ -59,10 +60,89 @@ type ViewerExportColumn = {
|
|
|
59
60
|
label: string
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
const GROUPED_ROW_GAP = 32
|
|
64
|
+
|
|
62
65
|
function toActionRows(rows: ViewerRow[]): Record<string, unknown>[] {
|
|
63
66
|
return rows.map(({ __datoolRowId: _datoolRowId, ...row }) => row)
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
function stringifyGroupingValue(value: unknown) {
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
return "undefined:"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (value === null) {
|
|
75
|
+
return "null:"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value instanceof Date) {
|
|
79
|
+
return `date:${value.toISOString()}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof value === "object") {
|
|
83
|
+
try {
|
|
84
|
+
return `object:${JSON.stringify(value)}`
|
|
85
|
+
} catch {
|
|
86
|
+
return `object:${String(value)}`
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${typeof value}:${String(value)}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function groupViewerRows(
|
|
94
|
+
rows: ViewerRow[],
|
|
95
|
+
columns: ViewerExportColumn[]
|
|
96
|
+
): {
|
|
97
|
+
groupStartRowIds: Set<string>
|
|
98
|
+
rows: ViewerRow[]
|
|
99
|
+
} {
|
|
100
|
+
if (columns.length === 0 || rows.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
groupStartRowIds: new Set<string>(),
|
|
103
|
+
rows,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const groupOrder: string[] = []
|
|
108
|
+
const rowsByGroup = new Map<string, ViewerRow[]>()
|
|
109
|
+
|
|
110
|
+
for (const row of rows) {
|
|
111
|
+
const groupKey = columns
|
|
112
|
+
.map((column) =>
|
|
113
|
+
stringifyGroupingValue(getValueAtPath(row, column.accessorKey))
|
|
114
|
+
)
|
|
115
|
+
.join("\u001f")
|
|
116
|
+
const existingRows = rowsByGroup.get(groupKey)
|
|
117
|
+
|
|
118
|
+
if (existingRows) {
|
|
119
|
+
existingRows.push(row)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
groupOrder.push(groupKey)
|
|
124
|
+
rowsByGroup.set(groupKey, [row])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const groupedRows: ViewerRow[] = []
|
|
128
|
+
const groupStartRowIds = new Set<string>()
|
|
129
|
+
|
|
130
|
+
groupOrder.forEach((groupKey, index) => {
|
|
131
|
+
const groupRows = rowsByGroup.get(groupKey) ?? []
|
|
132
|
+
|
|
133
|
+
if (index > 0 && groupRows[0]) {
|
|
134
|
+
groupStartRowIds.add(groupRows[0].__datoolRowId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
groupedRows.push(...groupRows)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
groupStartRowIds,
|
|
142
|
+
rows: groupedRows,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
66
146
|
function applyActionRowChanges(
|
|
67
147
|
currentRows: ViewerRow[],
|
|
68
148
|
targetRowIds: string[],
|
|
@@ -225,6 +305,7 @@ function buildTableColumns(
|
|
|
225
305
|
? undefined
|
|
226
306
|
: (column.accessorKey as Extract<keyof ViewerRow, string>),
|
|
227
307
|
align: column.align,
|
|
308
|
+
enumColors: column.enumColors,
|
|
228
309
|
header: column.header,
|
|
229
310
|
id: resolveDatoolColumnId(column, index),
|
|
230
311
|
kind: column.kind,
|
|
@@ -346,7 +427,10 @@ function DatoolTable({
|
|
|
346
427
|
isLoadingConfig,
|
|
347
428
|
rows,
|
|
348
429
|
settingsColumns,
|
|
430
|
+
groupedColumnIds,
|
|
431
|
+
groupedRowStartIds,
|
|
349
432
|
selectedStreamId,
|
|
433
|
+
setGroupedColumnIds,
|
|
350
434
|
setColumnVisibility,
|
|
351
435
|
searchInputRef,
|
|
352
436
|
handleExport,
|
|
@@ -368,7 +452,10 @@ function DatoolTable({
|
|
|
368
452
|
label: string
|
|
369
453
|
visible: boolean
|
|
370
454
|
}>
|
|
455
|
+
groupedColumnIds: string[]
|
|
456
|
+
groupedRowStartIds: Set<string>
|
|
371
457
|
selectedStreamId: string | null
|
|
458
|
+
setGroupedColumnIds: React.Dispatch<React.SetStateAction<string[]>>
|
|
372
459
|
setColumnVisibility: React.Dispatch<React.SetStateAction<VisibilityState>>
|
|
373
460
|
searchInputRef: React.RefObject<DataTableSearchInputHandle | null>
|
|
374
461
|
handleExport: (format: "csv" | "md") => void
|
|
@@ -377,6 +464,19 @@ function DatoolTable({
|
|
|
377
464
|
setShouldConnect: React.Dispatch<React.SetStateAction<boolean>>
|
|
378
465
|
}) {
|
|
379
466
|
const { search, setSearch } = useDataTableContext<ViewerRow>()
|
|
467
|
+
const resolveRowStyle = React.useCallback(
|
|
468
|
+
(row: ViewerRow) => {
|
|
469
|
+
if (!groupedRowStartIds.has(row.__datoolRowId)) {
|
|
470
|
+
return undefined
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
borderTop: `${GROUPED_ROW_GAP}px solid var(--color-table-gap)`,
|
|
475
|
+
boxShadow: `inset 0 1px 0 0 var(--color-border)`,
|
|
476
|
+
} satisfies React.CSSProperties
|
|
477
|
+
},
|
|
478
|
+
[groupedRowStartIds]
|
|
479
|
+
)
|
|
380
480
|
const rowActions = React.useMemo<DataTableRowAction<ViewerRow>[]>(
|
|
381
481
|
() => {
|
|
382
482
|
const configActions =
|
|
@@ -525,9 +625,24 @@ function DatoolTable({
|
|
|
525
625
|
/>
|
|
526
626
|
<ViewerSettings
|
|
527
627
|
columns={settingsColumns}
|
|
628
|
+
groupedColumnIds={groupedColumnIds}
|
|
528
629
|
isDisabled={isLoadingConfig || !activeStream}
|
|
529
630
|
onExportCsv={() => handleExport("csv")}
|
|
530
631
|
onExportMarkdown={() => handleExport("md")}
|
|
632
|
+
onClearGrouping={() => setGroupedColumnIds([])}
|
|
633
|
+
onToggleGrouping={(columnId, grouped) =>
|
|
634
|
+
setGroupedColumnIds((current) => {
|
|
635
|
+
if (grouped) {
|
|
636
|
+
return current.includes(columnId)
|
|
637
|
+
? current
|
|
638
|
+
: [...current, columnId]
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return current.filter(
|
|
642
|
+
(currentColumnId) => currentColumnId !== columnId
|
|
643
|
+
)
|
|
644
|
+
})
|
|
645
|
+
}
|
|
531
646
|
onToggleColumn={(columnId, visible) =>
|
|
532
647
|
setColumnVisibility((current) => ({
|
|
533
648
|
...current,
|
|
@@ -544,7 +659,7 @@ function DatoolTable({
|
|
|
544
659
|
|
|
545
660
|
<div className="min-h-0 flex-1">
|
|
546
661
|
{activeStream ? (
|
|
547
|
-
<DataTable rowActions={rowActions} />
|
|
662
|
+
<DataTable rowActions={rowActions} rowStyle={resolveRowStyle} />
|
|
548
663
|
) : (
|
|
549
664
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
|
550
665
|
No stream selected.
|
|
@@ -581,6 +696,7 @@ export default function App() {
|
|
|
581
696
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
|
|
582
697
|
{}
|
|
583
698
|
)
|
|
699
|
+
const [groupedColumnIds, setGroupedColumnIds] = React.useState<string[]>([])
|
|
584
700
|
const eventSourceRef = React.useRef<EventSource | null>(null)
|
|
585
701
|
const hasInitializedStreamRef = React.useRef(false)
|
|
586
702
|
const [hydratedTableId, setHydratedTableId] = React.useState<string | null>(
|
|
@@ -676,6 +792,23 @@ export default function App() {
|
|
|
676
792
|
exportColumns.filter((column) => columnVisibility[column.id] !== false),
|
|
677
793
|
[columnVisibility, exportColumns]
|
|
678
794
|
)
|
|
795
|
+
const exportColumnsById = React.useMemo(
|
|
796
|
+
() => new Map(exportColumns.map((column) => [column.id, column])),
|
|
797
|
+
[exportColumns]
|
|
798
|
+
)
|
|
799
|
+
const groupedExportColumns = React.useMemo(
|
|
800
|
+
() =>
|
|
801
|
+
groupedColumnIds.flatMap((columnId) => {
|
|
802
|
+
const column = exportColumnsById.get(columnId)
|
|
803
|
+
|
|
804
|
+
return column ? [column] : []
|
|
805
|
+
}),
|
|
806
|
+
[exportColumnsById, groupedColumnIds]
|
|
807
|
+
)
|
|
808
|
+
const groupedRowsState = React.useMemo(
|
|
809
|
+
() => groupViewerRows(rows, groupedExportColumns),
|
|
810
|
+
[groupedExportColumns, rows]
|
|
811
|
+
)
|
|
679
812
|
const isConnecting = Boolean(selectedStreamId) && shouldConnect && !isConnected
|
|
680
813
|
const columnIds = React.useMemo(
|
|
681
814
|
() => exportColumns.map((column) => column.id),
|
|
@@ -693,12 +826,14 @@ export default function App() {
|
|
|
693
826
|
|
|
694
827
|
React.useEffect(() => {
|
|
695
828
|
if (!activeStream) {
|
|
829
|
+
setGroupedColumnIds([])
|
|
696
830
|
setHydratedTableId(null)
|
|
697
831
|
return
|
|
698
832
|
}
|
|
699
833
|
|
|
700
834
|
setSearch(readDatoolSearch(tableId))
|
|
701
835
|
setColumnVisibility(readDatoolColumnVisibility(tableId, columnIds))
|
|
836
|
+
setGroupedColumnIds(readDatoolGrouping(tableId, columnIds))
|
|
702
837
|
setHydratedTableId(tableId)
|
|
703
838
|
}, [activeStream, columnIds, tableId])
|
|
704
839
|
|
|
@@ -711,6 +846,7 @@ export default function App() {
|
|
|
711
846
|
writeDatoolUrlState({
|
|
712
847
|
columnIds,
|
|
713
848
|
columnVisibility,
|
|
849
|
+
groupBy: groupedColumnIds,
|
|
714
850
|
search,
|
|
715
851
|
selectedStreamId,
|
|
716
852
|
tableId,
|
|
@@ -722,6 +858,7 @@ export default function App() {
|
|
|
722
858
|
activeStream,
|
|
723
859
|
columnIds,
|
|
724
860
|
columnVisibility,
|
|
861
|
+
groupedColumnIds,
|
|
725
862
|
hydratedTableId,
|
|
726
863
|
search,
|
|
727
864
|
selectedStreamId,
|
|
@@ -739,6 +876,7 @@ export default function App() {
|
|
|
739
876
|
setColumnVisibility(
|
|
740
877
|
readDatoolColumnVisibility(nextTableId, columnIds)
|
|
741
878
|
)
|
|
879
|
+
setGroupedColumnIds(readDatoolGrouping(nextTableId, columnIds))
|
|
742
880
|
setHydratedTableId(nextTableId)
|
|
743
881
|
}
|
|
744
882
|
|
|
@@ -852,8 +990,8 @@ export default function App() {
|
|
|
852
990
|
const fileBaseName = `${sanitizeFilePart(activeStream.label)}-${timeStamp}`
|
|
853
991
|
const content =
|
|
854
992
|
format === "csv"
|
|
855
|
-
? buildCsvContent(rows, visibleExportColumns)
|
|
856
|
-
: buildMarkdownContent(rows, visibleExportColumns)
|
|
993
|
+
? buildCsvContent(groupedRowsState.rows, visibleExportColumns)
|
|
994
|
+
: buildMarkdownContent(groupedRowsState.rows, visibleExportColumns)
|
|
857
995
|
|
|
858
996
|
downloadTextFile(
|
|
859
997
|
content,
|
|
@@ -861,15 +999,15 @@ export default function App() {
|
|
|
861
999
|
format === "csv" ? "text/csv" : "text/markdown"
|
|
862
1000
|
)
|
|
863
1001
|
},
|
|
864
|
-
[activeStream, rows, visibleExportColumns]
|
|
1002
|
+
[activeStream, groupedRowsState.rows, visibleExportColumns]
|
|
865
1003
|
)
|
|
866
1004
|
|
|
867
1005
|
return (
|
|
868
1006
|
<DataTableProvider
|
|
869
|
-
autoScrollToBottom
|
|
1007
|
+
autoScrollToBottom={groupedColumnIds.length === 0}
|
|
870
1008
|
columnVisibility={columnVisibility}
|
|
871
1009
|
columns={columns}
|
|
872
|
-
data={rows}
|
|
1010
|
+
data={groupedRowsState.rows}
|
|
873
1011
|
getRowId={(row) => row.__datoolRowId}
|
|
874
1012
|
height="100%"
|
|
875
1013
|
id={tableId}
|
|
@@ -888,8 +1026,11 @@ export default function App() {
|
|
|
888
1026
|
isConnected={isConnected}
|
|
889
1027
|
isConnecting={isConnecting}
|
|
890
1028
|
isLoadingConfig={isLoadingConfig}
|
|
1029
|
+
groupedColumnIds={groupedColumnIds}
|
|
1030
|
+
groupedRowStartIds={groupedRowsState.groupStartRowIds}
|
|
891
1031
|
rows={rows}
|
|
892
1032
|
settingsColumns={settingsColumns}
|
|
1033
|
+
setGroupedColumnIds={setGroupedColumnIds}
|
|
893
1034
|
setColumnVisibility={setColumnVisibility}
|
|
894
1035
|
searchInputRef={searchInputRef}
|
|
895
1036
|
selectedStreamId={selectedStreamId}
|
|
@@ -5,7 +5,9 @@ import * as React from "react"
|
|
|
5
5
|
|
|
6
6
|
import type { DataTableColumnKind } from "./data-table-col-icon"
|
|
7
7
|
import type { DataTableColumnMeta } from "./data-table-header-col"
|
|
8
|
+
import { EnumBadge } from "./enum-badge"
|
|
8
9
|
import { cn } from "@/lib/utils"
|
|
10
|
+
import type { DatoolEnumColorMap } from "../../shared/types"
|
|
9
11
|
|
|
10
12
|
function getAlignmentClassName(align: DataTableColumnMeta["align"] = "left") {
|
|
11
13
|
switch (align) {
|
|
@@ -36,11 +38,28 @@ function formatNumber(value: number) {
|
|
|
36
38
|
}).format(value)
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
function fallbackCellValue(
|
|
41
|
+
function fallbackCellValue(
|
|
42
|
+
value: unknown,
|
|
43
|
+
kind?: DataTableColumnKind,
|
|
44
|
+
options?: {
|
|
45
|
+
enumColors?: DatoolEnumColorMap
|
|
46
|
+
enumOptions?: string[]
|
|
47
|
+
}
|
|
48
|
+
) {
|
|
40
49
|
if (value === null || value === undefined || value === "") {
|
|
41
50
|
return <span className="text-muted-foreground">-</span>
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
if (kind === "enum") {
|
|
54
|
+
return (
|
|
55
|
+
<EnumBadge
|
|
56
|
+
colors={options?.enumColors}
|
|
57
|
+
options={options?.enumOptions}
|
|
58
|
+
value={String(value)}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
if (kind === "boolean" || typeof value === "boolean") {
|
|
45
64
|
return (
|
|
46
65
|
<span
|
|
@@ -200,7 +219,11 @@ export function DataTableBodyCell<TData>({
|
|
|
200
219
|
highlightTerms.length > 0
|
|
201
220
|
const content = shouldHighlight
|
|
202
221
|
? renderHighlightedText(rawValue, highlightTerms, rendered)
|
|
203
|
-
: (rendered ??
|
|
222
|
+
: (rendered ??
|
|
223
|
+
fallbackCellValue(rawValue, meta.kind, {
|
|
224
|
+
enumColors: meta.enumColors,
|
|
225
|
+
enumOptions: meta.enumOptions,
|
|
226
|
+
}))
|
|
204
227
|
|
|
205
228
|
return (
|
|
206
229
|
<td
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
DataTableColIcon,
|
|
8
8
|
type DataTableColumnKind,
|
|
9
9
|
} from "./data-table-col-icon"
|
|
10
|
+
import type { DatoolEnumColorMap } from "../../shared/types"
|
|
10
11
|
import { cn } from "@/lib/utils"
|
|
11
12
|
|
|
12
13
|
export type DataTableAlign = "left" | "center" | "right"
|
|
@@ -14,6 +15,8 @@ export type DataTableAlign = "left" | "center" | "right"
|
|
|
14
15
|
export type DataTableColumnMeta = {
|
|
15
16
|
align?: DataTableAlign
|
|
16
17
|
cellClassName?: string
|
|
18
|
+
enumColors?: DatoolEnumColorMap
|
|
19
|
+
enumOptions?: string[]
|
|
17
20
|
headerClassName?: string
|
|
18
21
|
highlightMatches?: boolean
|
|
19
22
|
kind?: DataTableColumnKind
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
inferDataTableColumnKind,
|
|
47
47
|
type DataTableColumnKind,
|
|
48
48
|
} from "./data-table-col-icon"
|
|
49
|
+
import type { DatoolEnumColorMap } from "../../shared/types"
|
|
49
50
|
import { Button } from "@/components/ui/button"
|
|
50
51
|
import {
|
|
51
52
|
ContextMenu,
|
|
@@ -148,6 +149,8 @@ export type DataTableColumnConfig<TData extends DataTableRow> = {
|
|
|
148
149
|
enableHiding?: boolean
|
|
149
150
|
enableResizing?: boolean
|
|
150
151
|
enableSorting?: boolean
|
|
152
|
+
enumColors?: DatoolEnumColorMap
|
|
153
|
+
enumOptions?: string[]
|
|
151
154
|
filterFn?: FilterFn<TData>
|
|
152
155
|
header?: string
|
|
153
156
|
headerClassName?: string
|
|
@@ -181,6 +184,7 @@ export type DataTableProps<TData extends DataTableRow> = {
|
|
|
181
184
|
resolveColumnHighlightTerms?: (columnId: string, query: string) => string[]
|
|
182
185
|
rowActions?: DataTableRowAction<TData>[]
|
|
183
186
|
rowClassName?: (row: TData) => string | undefined
|
|
187
|
+
rowStyle?: (row: TData) => React.CSSProperties | undefined
|
|
184
188
|
rowHeight?: number
|
|
185
189
|
statePersistence?: "localStorage" | "none" | "url"
|
|
186
190
|
}
|
|
@@ -537,6 +541,8 @@ function buildColumns<TData extends DataTableRow>(
|
|
|
537
541
|
const meta: DataTableColumnMeta = {
|
|
538
542
|
align: column.align ?? inferAlignment(kind),
|
|
539
543
|
cellClassName: column.cellClassName,
|
|
544
|
+
enumColors: kind === "enum" ? column.enumColors : undefined,
|
|
545
|
+
enumOptions: kind === "enum" ? column.enumOptions : undefined,
|
|
540
546
|
headerClassName: column.headerClassName,
|
|
541
547
|
highlightMatches:
|
|
542
548
|
column.highlightMatches ?? (kind === "text" ? true : false),
|
|
@@ -550,7 +556,10 @@ function buildColumns<TData extends DataTableRow>(
|
|
|
550
556
|
cell: ({ getValue, row }) =>
|
|
551
557
|
column.cell
|
|
552
558
|
? column.cell({ row: row.original, value: getValue() })
|
|
553
|
-
: fallbackCellValue(getValue(), kind
|
|
559
|
+
: fallbackCellValue(getValue(), kind, {
|
|
560
|
+
enumColors: kind === "enum" ? column.enumColors : undefined,
|
|
561
|
+
enumOptions: kind === "enum" ? column.enumOptions : undefined,
|
|
562
|
+
}),
|
|
554
563
|
enableGlobalFilter: column.enableFiltering ?? true,
|
|
555
564
|
filterFn: column.filterFn,
|
|
556
565
|
enableHiding: column.enableHiding ?? true,
|
|
@@ -1131,9 +1140,11 @@ function DataTableView<TData extends DataTableRow>({
|
|
|
1131
1140
|
resolveColumnHighlightTerms,
|
|
1132
1141
|
rowActions,
|
|
1133
1142
|
rowClassName,
|
|
1143
|
+
rowStyle,
|
|
1134
1144
|
rowHeight = 48,
|
|
1135
1145
|
statePersistence = "localStorage",
|
|
1136
1146
|
}: DataTableProps<TData>) {
|
|
1147
|
+
const context = useOptionalDataTableContext<TData>()
|
|
1137
1148
|
const persistedState = React.useMemo(
|
|
1138
1149
|
() => readPersistedState(id, statePersistence),
|
|
1139
1150
|
[id, statePersistence]
|
|
@@ -1190,17 +1201,45 @@ function DataTableView<TData extends DataTableRow>({
|
|
|
1190
1201
|
() => Math.max(160, Math.min(320, rowActionButtonCount * 96)),
|
|
1191
1202
|
[rowActionButtonCount]
|
|
1192
1203
|
)
|
|
1204
|
+
const columnsWithEnumOptions = React.useMemo(() => {
|
|
1205
|
+
if (!columns || columns.length === 0) {
|
|
1206
|
+
return columns
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const fieldOptionsById = new Map(
|
|
1210
|
+
(context?.searchFields ?? [])
|
|
1211
|
+
.filter((field) => field.kind === "enum")
|
|
1212
|
+
.map((field) => [field.id, field.options ?? []])
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
return columns.map((column, index) => {
|
|
1216
|
+
if (column.kind !== "enum" || column.enumOptions?.length) {
|
|
1217
|
+
return column
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const enumOptions = fieldOptionsById.get(resolveColumnId(column, index))
|
|
1221
|
+
|
|
1222
|
+
if (!enumOptions || enumOptions.length === 0) {
|
|
1223
|
+
return column
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return {
|
|
1227
|
+
...column,
|
|
1228
|
+
enumOptions,
|
|
1229
|
+
} satisfies DataTableColumnConfig<TData>
|
|
1230
|
+
})
|
|
1231
|
+
}, [columns, context?.searchFields])
|
|
1193
1232
|
const tableColumns = React.useMemo(
|
|
1194
1233
|
() =>
|
|
1195
1234
|
buildColumns(
|
|
1196
1235
|
data,
|
|
1197
|
-
|
|
1236
|
+
columnsWithEnumOptions,
|
|
1198
1237
|
showRowSelectionColumn,
|
|
1199
1238
|
showRowActionButtonsColumn,
|
|
1200
1239
|
rowActionsColumnSize
|
|
1201
1240
|
),
|
|
1202
1241
|
[
|
|
1203
|
-
|
|
1242
|
+
columnsWithEnumOptions,
|
|
1204
1243
|
data,
|
|
1205
1244
|
rowActionsColumnSize,
|
|
1206
1245
|
showRowActionButtonsColumn,
|
|
@@ -1886,6 +1925,7 @@ function DataTableView<TData extends DataTableRow>({
|
|
|
1886
1925
|
}
|
|
1887
1926
|
}}
|
|
1888
1927
|
style={{
|
|
1928
|
+
...rowStyle?.(row.original),
|
|
1889
1929
|
minHeight: rowHeight,
|
|
1890
1930
|
transform: `translateY(${virtualRow.start}px)`,
|
|
1891
1931
|
width: table.getTotalSize(),
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DATOOL_ENUM_BADGE_COLORS,
|
|
5
|
+
type DatoolEnumBadgeColor,
|
|
6
|
+
type DatoolEnumColorMap,
|
|
7
|
+
} from "../../shared/types"
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
|
|
10
|
+
const ENUM_BADGE_STYLES: Record<DatoolEnumBadgeColor, string> = {
|
|
11
|
+
amber:
|
|
12
|
+
"bg-amber-50/90 text-amber-700 dark:bg-amber-400/10 dark:text-amber-200",
|
|
13
|
+
blue:
|
|
14
|
+
"bg-blue-50/90 text-blue-700 dark:bg-blue-400/10 dark:text-blue-200",
|
|
15
|
+
coral:
|
|
16
|
+
"bg-rose-50/90 text-rose-700 dark:bg-rose-400/10 dark:text-rose-200",
|
|
17
|
+
cyan:
|
|
18
|
+
"bg-cyan-50/90 text-cyan-700 dark:bg-cyan-400/10 dark:text-cyan-200",
|
|
19
|
+
emerald:
|
|
20
|
+
"bg-emerald-50/90 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-200",
|
|
21
|
+
fuchsia:
|
|
22
|
+
"bg-fuchsia-50/90 text-fuchsia-700 dark:bg-fuchsia-400/10 dark:text-fuchsia-200",
|
|
23
|
+
green:
|
|
24
|
+
"bg-green-50/90 text-green-700 dark:bg-green-400/10 dark:text-green-200",
|
|
25
|
+
indigo:
|
|
26
|
+
"bg-indigo-50/90 text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-200",
|
|
27
|
+
lime:
|
|
28
|
+
"bg-lime-50/90 text-lime-700 dark:bg-lime-400/10 dark:text-lime-200",
|
|
29
|
+
orange:
|
|
30
|
+
"bg-orange-50/90 text-orange-700 dark:bg-orange-400/10 dark:text-orange-200",
|
|
31
|
+
pink:
|
|
32
|
+
"bg-pink-50/90 text-pink-700 dark:bg-pink-400/10 dark:text-pink-200",
|
|
33
|
+
purple:
|
|
34
|
+
"bg-purple-50/90 text-purple-700 dark:bg-purple-400/10 dark:text-purple-200",
|
|
35
|
+
red: "bg-red-50/90 text-red-700 dark:bg-red-400/10 dark:text-red-200",
|
|
36
|
+
rose:
|
|
37
|
+
"bg-rose-50/90 text-rose-700 dark:bg-rose-400/10 dark:text-rose-200",
|
|
38
|
+
sky: "bg-sky-50/90 text-sky-700 dark:bg-sky-400/10 dark:text-sky-200",
|
|
39
|
+
stone:
|
|
40
|
+
"bg-stone-50/95 text-stone-700 dark:bg-white/5 dark:text-stone-100",
|
|
41
|
+
teal:
|
|
42
|
+
"bg-teal-50/90 text-teal-700 dark:bg-teal-400/10 dark:text-teal-200",
|
|
43
|
+
violet:
|
|
44
|
+
"bg-violet-50/90 text-violet-700 dark:bg-violet-400/10 dark:text-violet-200",
|
|
45
|
+
yellow:
|
|
46
|
+
"bg-yellow-50/90 text-yellow-700 dark:bg-yellow-400/10 dark:text-yellow-200",
|
|
47
|
+
zinc: "bg-zinc-50/95 text-zinc-700 dark:bg-white/5 dark:text-zinc-100",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeEnumValue(value: string) {
|
|
51
|
+
return value.trim().toUpperCase()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getFallbackStyleIndex(value: string) {
|
|
55
|
+
let hash = 0
|
|
56
|
+
|
|
57
|
+
for (const character of value) {
|
|
58
|
+
hash = (hash * 31 + character.charCodeAt(0)) >>> 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return hash % DATOOL_ENUM_BADGE_COLORS.length
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveConfiguredColor(
|
|
65
|
+
value: string,
|
|
66
|
+
colors?: DatoolEnumColorMap
|
|
67
|
+
): DatoolEnumBadgeColor | null {
|
|
68
|
+
if (!colors) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const normalizedValue = normalizeEnumValue(value)
|
|
73
|
+
|
|
74
|
+
for (const [candidateValue, color] of Object.entries(colors)) {
|
|
75
|
+
if (color && normalizeEnumValue(candidateValue) === normalizedValue) {
|
|
76
|
+
return color
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveEnumBadgeStyle(
|
|
84
|
+
value: string,
|
|
85
|
+
options?: string[],
|
|
86
|
+
colors?: DatoolEnumColorMap
|
|
87
|
+
) {
|
|
88
|
+
const normalizedValue = normalizeEnumValue(value)
|
|
89
|
+
const configuredColor = resolveConfiguredColor(value, colors)
|
|
90
|
+
|
|
91
|
+
if (configuredColor) {
|
|
92
|
+
return ENUM_BADGE_STYLES[configuredColor]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const optionIndex =
|
|
96
|
+
options?.findIndex((option) => normalizeEnumValue(option) === normalizedValue) ??
|
|
97
|
+
-1
|
|
98
|
+
|
|
99
|
+
if (optionIndex >= 0) {
|
|
100
|
+
return ENUM_BADGE_STYLES[
|
|
101
|
+
DATOOL_ENUM_BADGE_COLORS[optionIndex % DATOOL_ENUM_BADGE_COLORS.length]
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return ENUM_BADGE_STYLES[
|
|
106
|
+
DATOOL_ENUM_BADGE_COLORS[getFallbackStyleIndex(normalizedValue)]
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function EnumBadge({
|
|
111
|
+
className,
|
|
112
|
+
colors,
|
|
113
|
+
options,
|
|
114
|
+
value,
|
|
115
|
+
}: {
|
|
116
|
+
className?: string
|
|
117
|
+
colors?: DatoolEnumColorMap
|
|
118
|
+
options?: string[]
|
|
119
|
+
value: string
|
|
120
|
+
}) {
|
|
121
|
+
return (
|
|
122
|
+
<span
|
|
123
|
+
className={cn(
|
|
124
|
+
"inline-flex max-w-full items-center rounded-md px-2 py-1 text-sm leading-none font-medium whitespace-nowrap shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:shadow-none",
|
|
125
|
+
resolveEnumBadgeStyle(value, options, colors),
|
|
126
|
+
className
|
|
127
|
+
)}
|
|
128
|
+
title={value}
|
|
129
|
+
>
|
|
130
|
+
<span className="truncate">{value}</span>
|
|
131
|
+
</span>
|
|
132
|
+
)
|
|
133
|
+
}
|