@fastnd/components 1.0.32 → 1.0.34
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/dist/examples/dashboard/DashboardPage/DashboardPage.tsx +1 -1
- package/dist/examples/dashboard/ProjectList/ProjectList.tsx +92 -52
- package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +4 -4
- package/dist/examples/dashboard/StatusFilterLegend/StatusFilterLegend.tsx +1 -1
- package/dist/examples/dashboard/StatusOverview/StatusOverview.tsx +1 -1
- package/dist/examples/dashboard/constants.ts +20 -6
- package/dist/examples/dashboard/types.ts +2 -0
- package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +46 -16
- package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +1 -1
- package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +30 -22
- package/dist/examples/data-visualization/DataListView/DataListView.tsx +101 -48
- package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +70 -17
- package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +12 -1
- package/dist/examples/data-visualization/constants.ts +8 -7
- package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +6 -1
- package/dist/examples/data-visualization/types.ts +19 -0
- package/dist/fastnd-components.js +4 -4
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ export interface DataListViewProps {
|
|
|
23
23
|
onToggleExpansion: (rowId: string, field: string) => void
|
|
24
24
|
favorites: Set<string>
|
|
25
25
|
onToggleFavorite: (id: string) => void
|
|
26
|
+
showHeader?: boolean
|
|
26
27
|
className?: string
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -122,6 +123,52 @@ function ExpansionCell({ ec, value }: ExpansionCellProps) {
|
|
|
122
123
|
return <span>{text}</span>
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
interface ListHeaderProps {
|
|
127
|
+
columns: Record<string, ColumnDef>
|
|
128
|
+
layout: ListLayout
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ListHeader({ columns, layout }: ListHeaderProps) {
|
|
132
|
+
const titleCol = columns[layout.titleField]
|
|
133
|
+
const expandFields = layout.expandFields ?? (layout.expandField ? [layout.expandField] : [])
|
|
134
|
+
const secondaryLabel =
|
|
135
|
+
titleCol?.type === 'double-text' && titleCol.secondary
|
|
136
|
+
? columns[titleCol.secondary]?.label
|
|
137
|
+
: undefined
|
|
138
|
+
const titleLabel = secondaryLabel
|
|
139
|
+
? `${titleCol!.label} / ${secondaryLabel}`
|
|
140
|
+
: (titleCol?.label ?? '')
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-border bg-secondary">
|
|
144
|
+
{/* Placeholder matching FavoriteButton size */}
|
|
145
|
+
<div className="size-[28px] flex-none" aria-hidden="true" />
|
|
146
|
+
<div className="flex-1 min-w-0 text-xs font-medium text-foreground uppercase tracking-[0.03em]">
|
|
147
|
+
{titleLabel}
|
|
148
|
+
</div>
|
|
149
|
+
{layout.badgeFields.map((field) => (
|
|
150
|
+
<span
|
|
151
|
+
key={field}
|
|
152
|
+
style={{ minWidth: columns[field]?.badgeColumnWidth ?? 96 }}
|
|
153
|
+
className="shrink-0 text-xs font-medium text-foreground uppercase tracking-[0.03em] text-right"
|
|
154
|
+
>
|
|
155
|
+
{columns[field]?.label ?? ''}
|
|
156
|
+
</span>
|
|
157
|
+
))}
|
|
158
|
+
{expandFields.map((field) => (
|
|
159
|
+
<span key={field} className="shrink-0 text-xs font-medium text-foreground uppercase tracking-[0.03em]">
|
|
160
|
+
{columns[field]?.expandLabel ?? columns[field]?.label ?? ''}
|
|
161
|
+
</span>
|
|
162
|
+
))}
|
|
163
|
+
{Object.values(columns).some((c) => c.type === 'actions') && (
|
|
164
|
+
<span className="shrink-0 text-xs font-medium text-foreground uppercase tracking-[0.03em]">
|
|
165
|
+
{Object.values(columns).find((c) => c.type === 'actions')?.label ?? 'Aktionen'}
|
|
166
|
+
</span>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
125
172
|
function DataListView({
|
|
126
173
|
data,
|
|
127
174
|
columns,
|
|
@@ -130,82 +177,81 @@ function DataListView({
|
|
|
130
177
|
onToggleExpansion,
|
|
131
178
|
favorites,
|
|
132
179
|
onToggleFavorite,
|
|
180
|
+
showHeader = false,
|
|
133
181
|
className,
|
|
134
182
|
}: DataListViewProps) {
|
|
135
183
|
const expandFields = layout.expandFields ?? (layout.expandField ? [layout.expandField] : [])
|
|
136
184
|
const titleCol = columns[layout.titleField]
|
|
137
185
|
const isDoubleText = titleCol?.type === 'double-text'
|
|
186
|
+
const actionsColKey = Object.keys(columns).find((k) => columns[k]?.type === 'actions')
|
|
138
187
|
|
|
139
188
|
return (
|
|
140
189
|
<div
|
|
141
190
|
data-slot="data-list-view"
|
|
142
|
-
className={cn(
|
|
143
|
-
'border border-border rounded-lg overflow-hidden divide-y divide-border',
|
|
144
|
-
className,
|
|
145
|
-
)}
|
|
191
|
+
className={cn('divide-y divide-border', className)}
|
|
146
192
|
>
|
|
193
|
+
{showHeader && <ListHeader columns={columns} layout={layout} />}
|
|
147
194
|
{data.map((row) => {
|
|
148
195
|
const rowId = String(row.id ?? '')
|
|
149
196
|
const titleValue = row[layout.titleField]
|
|
150
197
|
const subtitleValue = isDoubleText && titleCol.secondary ? row[titleCol.secondary] : undefined
|
|
198
|
+
const hasBadges = layout.badgeFields.length > 0
|
|
199
|
+
const hasExpand = expandFields.length > 0
|
|
151
200
|
|
|
152
201
|
return (
|
|
153
|
-
<div
|
|
154
|
-
|
|
155
|
-
className="
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
onPressedChange={() => onToggleFavorite(rowId)}
|
|
163
|
-
/>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{/* Col 2: Title block */}
|
|
167
|
-
<div className="min-w-0">
|
|
168
|
-
<div className="font-semibold text-sm truncate max-w-[280px] sm:max-w-none">
|
|
169
|
-
{titleValue != null ? String(titleValue) : ''}
|
|
202
|
+
<div key={rowId}>
|
|
203
|
+
{/* Row: Fav | Title + dot-meta | Fixed-width badge columns | Expand */}
|
|
204
|
+
<div className="flex items-start gap-3 px-4 py-3 hover:bg-accent transition-colors">
|
|
205
|
+
<div className="flex-none mt-0.5">
|
|
206
|
+
<FavoriteButton
|
|
207
|
+
pressed={favorites.has(rowId)}
|
|
208
|
+
itemName={titleValue != null ? String(titleValue) : ''}
|
|
209
|
+
onPressedChange={() => onToggleFavorite(rowId)}
|
|
210
|
+
/>
|
|
170
211
|
</div>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
212
|
+
|
|
213
|
+
{/* Title block — grows, no fixed width */}
|
|
214
|
+
<div className="flex-1 min-w-0">
|
|
215
|
+
<div className="font-semibold text-sm truncate">
|
|
216
|
+
{titleValue != null ? String(titleValue) : ''}
|
|
217
|
+
</div>
|
|
218
|
+
{/* Meta line: subtitle + metaFields, dot-separated, each segment truncated */}
|
|
219
|
+
{(subtitleValue != null || layout.metaFields.some(f => row[f] != null && row[f] !== '')) && (
|
|
220
|
+
<p className="text-xs text-muted-foreground mt-0.5 flex min-w-0">
|
|
221
|
+
{[
|
|
222
|
+
...(subtitleValue != null ? [String(subtitleValue)] : []),
|
|
223
|
+
...layout.metaFields
|
|
224
|
+
.map(f => row[f])
|
|
225
|
+
.filter(v => v != null && v !== '')
|
|
226
|
+
.map(String),
|
|
227
|
+
].map((item, i) => (
|
|
228
|
+
<React.Fragment key={i}>
|
|
229
|
+
{i > 0 && <span className="shrink-0 px-1">·</span>}
|
|
230
|
+
<span className="truncate max-w-[160px]">{item}</span>
|
|
231
|
+
</React.Fragment>
|
|
232
|
+
))}
|
|
233
|
+
</p>
|
|
176
234
|
)}
|
|
177
|
-
{layout.metaFields.map((field) => {
|
|
178
|
-
const v = row[field]
|
|
179
|
-
if (v == null || v === '') return null
|
|
180
|
-
return (
|
|
181
|
-
<span
|
|
182
|
-
key={field}
|
|
183
|
-
className="shrink-0 bg-secondary rounded-md px-2 py-0.5 text-xs max-w-[180px] truncate"
|
|
184
|
-
>
|
|
185
|
-
{String(v)}
|
|
186
|
-
</span>
|
|
187
|
-
)
|
|
188
|
-
})}
|
|
189
235
|
</div>
|
|
190
|
-
</div>
|
|
191
236
|
|
|
192
|
-
|
|
193
|
-
<div className="flex items-center gap-2 pt-0.5 max-sm:col-start-2 max-sm:col-end-3 max-sm:row-start-2">
|
|
237
|
+
{/* Badge columns — fixed min-width per column for vertical alignment */}
|
|
194
238
|
{layout.badgeFields.map((field) => {
|
|
195
239
|
const col = columns[field]
|
|
196
240
|
if (!col) return null
|
|
197
241
|
return (
|
|
198
|
-
<
|
|
242
|
+
<div
|
|
243
|
+
key={field}
|
|
244
|
+
style={{ minWidth: col.badgeColumnWidth ?? 96 }}
|
|
245
|
+
className="flex justify-end items-start"
|
|
246
|
+
>
|
|
199
247
|
{renderCell(field, col, row, {
|
|
200
248
|
mode: col.type === 'inventory' ? 'inventory-label' : 'default',
|
|
201
249
|
})}
|
|
202
|
-
</
|
|
250
|
+
</div>
|
|
203
251
|
)
|
|
204
252
|
})}
|
|
205
|
-
</div>
|
|
206
253
|
|
|
207
|
-
|
|
208
|
-
<div className="flex items-center gap-2 pt-0.5 max-sm:col-start-2 max-sm:col-end-3 max-sm:row-start-3">
|
|
254
|
+
{/* Expand buttons — trailing edge */}
|
|
209
255
|
{expandFields.map((field) => {
|
|
210
256
|
const col = columns[field]
|
|
211
257
|
if (!col) return null
|
|
@@ -220,16 +266,23 @@ function DataListView({
|
|
|
220
266
|
</React.Fragment>
|
|
221
267
|
)
|
|
222
268
|
})}
|
|
269
|
+
|
|
270
|
+
{/* Actions */}
|
|
271
|
+
{actionsColKey && renderCell(actionsColKey, columns[actionsColKey], row, {
|
|
272
|
+
onAddToProject: () => {},
|
|
273
|
+
onAddToCollection: () => {},
|
|
274
|
+
onDelete: () => {},
|
|
275
|
+
})}
|
|
223
276
|
</div>
|
|
224
277
|
|
|
225
|
-
{/* Expansion
|
|
278
|
+
{/* Expansion panels — full width, below the row */}
|
|
226
279
|
{expandFields.map((field) => {
|
|
227
280
|
const expandKey = `${rowId}::${field}`
|
|
228
281
|
if (!expandedRows.has(expandKey)) return null
|
|
229
282
|
return (
|
|
230
283
|
<div
|
|
231
284
|
key={field}
|
|
232
|
-
className="
|
|
285
|
+
className="px-4 pb-3 pt-3 border-t border-border"
|
|
233
286
|
data-slot="expansion-content"
|
|
234
287
|
>
|
|
235
288
|
<ExpansionTable row={row} colKey={field} columns={columns} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback } from 'react'
|
|
2
|
-
import { ChevronUp, ChevronDown, ArrowLeftRight, Sparkles, Plus } from 'lucide-react'
|
|
1
|
+
import React, { useState, useRef, useCallback, useLayoutEffect } from 'react'
|
|
2
|
+
import { ChevronUp, ChevronDown, ArrowLeftRight, Sparkles, Plus, Star } from 'lucide-react'
|
|
3
3
|
import {
|
|
4
4
|
Table,
|
|
5
5
|
TableBody,
|
|
@@ -24,6 +24,7 @@ import type { ColumnDef, ExpandColumnDef, SortState } from '../types'
|
|
|
24
24
|
const HEADER_ICONS: Record<string, React.ElementType> = {
|
|
25
25
|
arrowLeftRight: ArrowLeftRight,
|
|
26
26
|
sparkles: Sparkles,
|
|
27
|
+
star: Star,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Default minimum widths per column type (px)
|
|
@@ -35,6 +36,7 @@ const DEFAULT_MIN_WIDTHS: Partial<Record<string, number>> = {
|
|
|
35
36
|
currency: 100,
|
|
36
37
|
'double-text': 200,
|
|
37
38
|
link: 140,
|
|
39
|
+
actions: 112,
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
// Column types that should wrap text (line-clamp handles truncation)
|
|
@@ -62,7 +64,7 @@ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: E
|
|
|
62
64
|
if (mainColKey === expandColKey) {
|
|
63
65
|
return (
|
|
64
66
|
<Button
|
|
65
|
-
variant="
|
|
67
|
+
variant="outline"
|
|
66
68
|
size="icon"
|
|
67
69
|
className="size-7"
|
|
68
70
|
aria-label="Hinzufügen"
|
|
@@ -83,7 +85,7 @@ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: E
|
|
|
83
85
|
const sec = item[ec.secondaryKey]
|
|
84
86
|
return (
|
|
85
87
|
<>
|
|
86
|
-
<span className={cn('font-
|
|
88
|
+
<span className={cn('font-medium text-[13px] line-clamp-2', ec.bold && 'font-bold')}>
|
|
87
89
|
{val != null ? String(val) : ''}
|
|
88
90
|
</span>
|
|
89
91
|
<span className="text-muted-foreground text-xs line-clamp-1">
|
|
@@ -110,17 +112,13 @@ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: E
|
|
|
110
112
|
if (mainCol.type === 'status-badge' && mainCol.statusMap && val != null) {
|
|
111
113
|
const status = mainCol.statusMap[String(val)] ?? 'active'
|
|
112
114
|
const STATUS_COLORS: Record<string, string> = {
|
|
113
|
-
active:
|
|
114
|
-
nrnd:
|
|
115
|
-
eol:
|
|
116
|
-
production: '
|
|
115
|
+
active: 'border-[var(--lifecycle-active)]',
|
|
116
|
+
nrnd: 'border-[var(--lifecycle-nrnd)]',
|
|
117
|
+
eol: 'border-[var(--lifecycle-eol)]',
|
|
118
|
+
production: 'border-[var(--lifecycle-production)]',
|
|
117
119
|
}
|
|
118
120
|
return (
|
|
119
|
-
<Badge className={
|
|
120
|
-
<span
|
|
121
|
-
className={cn('size-1.5 rounded-full', `bg-[var(--lifecycle-${status})]`)}
|
|
122
|
-
aria-hidden="true"
|
|
123
|
-
/>
|
|
121
|
+
<Badge variant="outline" className={STATUS_COLORS[status]}>
|
|
124
122
|
{String(val)}
|
|
125
123
|
</Badge>
|
|
126
124
|
)
|
|
@@ -154,9 +152,10 @@ interface ExpansionSectionProps {
|
|
|
154
152
|
colKey: string
|
|
155
153
|
visibleColumns: string[]
|
|
156
154
|
columns: Record<string, ColumnDef>
|
|
155
|
+
columnWidths: Record<string, number>
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
function ExpansionSection({ row, colKey, visibleColumns, columns }: ExpansionSectionProps) {
|
|
158
|
+
function ExpansionSection({ row, colKey, visibleColumns, columns, columnWidths }: ExpansionSectionProps) {
|
|
160
159
|
const col = columns[colKey]
|
|
161
160
|
const items = row[colKey] as Record<string, unknown>[] | undefined
|
|
162
161
|
if (!items?.length || !col.expandColumns) return null
|
|
@@ -200,6 +199,7 @@ function ExpansionSection({ row, colKey, visibleColumns, columns }: ExpansionSec
|
|
|
200
199
|
mainCol.hideTablet && 'hidden lg:table-cell',
|
|
201
200
|
mainCol.hideMobile && 'hidden sm:table-cell',
|
|
202
201
|
)}
|
|
202
|
+
style={columnWidths[mainKey] != null ? { width: columnWidths[mainKey] } : undefined}
|
|
203
203
|
>
|
|
204
204
|
<ExpansionCell
|
|
205
205
|
mainColKey={mainKey}
|
|
@@ -227,6 +227,7 @@ export interface DataTableViewProps {
|
|
|
227
227
|
onToggleExpansion: (rowId: string, field: string) => void
|
|
228
228
|
favorites: Set<string>
|
|
229
229
|
onToggleFavorite: (id: string) => void
|
|
230
|
+
footer?: React.ReactNode
|
|
230
231
|
className?: string
|
|
231
232
|
}
|
|
232
233
|
|
|
@@ -240,11 +241,18 @@ export function DataTableView({
|
|
|
240
241
|
onToggleExpansion,
|
|
241
242
|
favorites,
|
|
242
243
|
onToggleFavorite,
|
|
244
|
+
footer,
|
|
243
245
|
className,
|
|
244
246
|
}: DataTableViewProps) {
|
|
245
247
|
// Column widths: keyed by column key, value in px
|
|
246
248
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
|
|
247
249
|
|
|
250
|
+
// Ref to the header <tr> for snapshotting rendered column widths
|
|
251
|
+
const headerRowRef = useRef<HTMLTableRowElement>(null)
|
|
252
|
+
|
|
253
|
+
// Tracks the previous visibleColumns reference to detect changes
|
|
254
|
+
const prevVisibleRef = useRef<string[]>(visibleColumns)
|
|
255
|
+
|
|
248
256
|
// Resize tracking ref (not state — no re-render during drag)
|
|
249
257
|
const resizeRef = useRef<{
|
|
250
258
|
colKey: string
|
|
@@ -252,6 +260,31 @@ export function DataTableView({
|
|
|
252
260
|
startWidth: number
|
|
253
261
|
} | null>(null)
|
|
254
262
|
|
|
263
|
+
// Measure-then-lock: snapshot <th> widths while table is still table-layout:auto,
|
|
264
|
+
// then switch to table-fixed via colgroup so expansion rows can never widen columns.
|
|
265
|
+
useLayoutEffect(() => {
|
|
266
|
+
const prev = prevVisibleRef.current
|
|
267
|
+
const changed =
|
|
268
|
+
prev.length !== visibleColumns.length || prev.some((k, i) => k !== visibleColumns[i])
|
|
269
|
+
if (changed) {
|
|
270
|
+
// Column set changed — clear all widths so the table re-snapshots with natural auto sizing
|
|
271
|
+
prevVisibleRef.current = visibleColumns
|
|
272
|
+
setColumnWidths({})
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if (!headerRowRef.current) return
|
|
276
|
+
const ths = Array.from(headerRowRef.current.querySelectorAll('th')) as HTMLTableCellElement[]
|
|
277
|
+
setColumnWidths((prev) => {
|
|
278
|
+
const next = { ...prev }
|
|
279
|
+
visibleColumns.forEach((colKey, i) => {
|
|
280
|
+
if (next[colKey] == null && ths[i]) {
|
|
281
|
+
next[colKey] = ths[i].getBoundingClientRect().width
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
return next
|
|
285
|
+
})
|
|
286
|
+
}, [visibleColumns])
|
|
287
|
+
|
|
255
288
|
const handleResizeMouseDown = useCallback(
|
|
256
289
|
(e: React.MouseEvent, colKey: string) => {
|
|
257
290
|
e.preventDefault()
|
|
@@ -285,16 +318,30 @@ export function DataTableView({
|
|
|
285
318
|
// Collect expand column keys for the current domain
|
|
286
319
|
const expandColKeys = visibleColumns.filter((k) => columns[k]?.type === 'expand')
|
|
287
320
|
|
|
321
|
+
// Switch to table-fixed only after all columns have been measured — prevents content in
|
|
322
|
+
// expansion rows from ever forcing a column to widen.
|
|
323
|
+
const isFixed = visibleColumns.every((k) => columnWidths[k] != null)
|
|
324
|
+
|
|
288
325
|
return (
|
|
326
|
+
<div className={cn('mx-4 mb-4 border border-border rounded-md overflow-hidden', className)}>
|
|
289
327
|
<div
|
|
290
|
-
className=
|
|
328
|
+
className="overflow-x-auto"
|
|
291
329
|
role="region"
|
|
292
330
|
aria-label="Datentabelle"
|
|
293
331
|
tabIndex={0}
|
|
294
332
|
>
|
|
295
|
-
<Table>
|
|
333
|
+
<Table className={isFixed ? 'table-fixed' : undefined}>
|
|
334
|
+
{/* colgroup enforces widths on ALL rows (including expansion rows) */}
|
|
335
|
+
<colgroup>
|
|
336
|
+
{visibleColumns.map((colKey) => (
|
|
337
|
+
<col
|
|
338
|
+
key={colKey}
|
|
339
|
+
style={columnWidths[colKey] != null ? { width: columnWidths[colKey] } : undefined}
|
|
340
|
+
/>
|
|
341
|
+
))}
|
|
342
|
+
</colgroup>
|
|
296
343
|
<TableHeader>
|
|
297
|
-
<TableRow className="bg-secondary hover:bg-secondary">
|
|
344
|
+
<TableRow ref={headerRowRef} className="bg-secondary hover:bg-secondary">
|
|
298
345
|
{visibleColumns.map((colKey) => {
|
|
299
346
|
const col = columns[colKey]
|
|
300
347
|
if (!col) return null
|
|
@@ -423,6 +470,9 @@ export function DataTableView({
|
|
|
423
470
|
isFavorite,
|
|
424
471
|
onToggleExpand: () => onToggleExpansion(rowId, colKey),
|
|
425
472
|
onToggleFavorite: () => onToggleFavorite(rowId),
|
|
473
|
+
onAddToProject: () => {},
|
|
474
|
+
onAddToCollection: () => {},
|
|
475
|
+
onDelete: () => {},
|
|
426
476
|
})}
|
|
427
477
|
</TableCell>
|
|
428
478
|
)
|
|
@@ -437,6 +487,7 @@ export function DataTableView({
|
|
|
437
487
|
colKey={expColKey}
|
|
438
488
|
visibleColumns={visibleColumns}
|
|
439
489
|
columns={columns}
|
|
490
|
+
columnWidths={columnWidths}
|
|
440
491
|
/>
|
|
441
492
|
))}
|
|
442
493
|
</React.Fragment>
|
|
@@ -445,5 +496,7 @@ export function DataTableView({
|
|
|
445
496
|
</TableBody>
|
|
446
497
|
</Table>
|
|
447
498
|
</div>
|
|
499
|
+
{footer && <div className="border-t border-border">{footer}</div>}
|
|
500
|
+
</div>
|
|
448
501
|
)
|
|
449
502
|
}
|
|
@@ -97,6 +97,17 @@ export function DataVisualizationPage({ className }: { className?: string }) {
|
|
|
97
97
|
onToggleExpansion={state.toggleExpansion}
|
|
98
98
|
favorites={state.favorites}
|
|
99
99
|
onToggleFavorite={state.toggleFavorite}
|
|
100
|
+
footer={
|
|
101
|
+
<DataExplorerPagination
|
|
102
|
+
currentPage={state.currentPage}
|
|
103
|
+
totalPages={state.totalPages}
|
|
104
|
+
pageSize={state.pageSize}
|
|
105
|
+
totalFiltered={state.totalFiltered}
|
|
106
|
+
resultLabel={state.domainConfig.resultLabel}
|
|
107
|
+
onPageChange={state.setPage}
|
|
108
|
+
onPageSizeChange={state.setPageSize}
|
|
109
|
+
/>
|
|
110
|
+
}
|
|
100
111
|
/>
|
|
101
112
|
)}
|
|
102
113
|
{state.viewMode === 'list' && (
|
|
@@ -124,7 +135,7 @@ export function DataVisualizationPage({ className }: { className?: string }) {
|
|
|
124
135
|
</>
|
|
125
136
|
)}
|
|
126
137
|
|
|
127
|
-
{state.totalFiltered > 0 && (
|
|
138
|
+
{state.totalFiltered > 0 && state.viewMode !== 'table' && (
|
|
128
139
|
<DataExplorerPagination
|
|
129
140
|
currentPage={state.currentPage}
|
|
130
141
|
totalPages={state.totalPages}
|
|
@@ -3,8 +3,8 @@ import type { ColumnDef, DomainConfig, DomainKey, DomainLayout } from './types'
|
|
|
3
3
|
// ===== PRODUCT DOMAIN =====
|
|
4
4
|
|
|
5
5
|
const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
|
|
6
|
-
product_category: { label: '
|
|
7
|
-
product_group: { label: '
|
|
6
|
+
product_category: { label: 'Kategorie', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
|
|
7
|
+
product_group: { label: 'Gruppe', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
|
|
8
8
|
part_number: { label: 'Produkt', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'product_family_name', searchable: true },
|
|
9
9
|
manufacturer_name: { label: 'Hersteller', type: 'link', sortable: true, filterable: true, primaryFilter: false, visible: true, searchable: true },
|
|
10
10
|
description: { label: 'Beschreibung', type: 'text', sortable: false, filterable: false, visible: true, searchable: true, rowLines: 2 },
|
|
@@ -32,7 +32,7 @@ const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
|
|
|
32
32
|
expandColumns: [
|
|
33
33
|
{ key: 'alt_product_category', label: 'Kategorie', mapTo: 'product_category' },
|
|
34
34
|
{ key: 'alt_family_name', label: 'Familie', mapTo: 'product_group' },
|
|
35
|
-
{ key: 'alt_part_number', label: 'Teilenummer',
|
|
35
|
+
{ key: 'alt_part_number', label: 'Teilenummer', mapTo: 'part_number', secondaryKey: 'alt_product_family_name' },
|
|
36
36
|
{ key: 'alt_manufacturer', label: 'Hersteller', mapTo: 'manufacturer_name' },
|
|
37
37
|
{ key: 'alt_description', label: 'Beschreibung', mapTo: 'description' },
|
|
38
38
|
{ key: 'alt_lifecycle', label: 'Status', mapTo: 'lifecycle' },
|
|
@@ -54,7 +54,7 @@ const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
|
|
|
54
54
|
expandColumns: [
|
|
55
55
|
{ key: 'cross_sell_product_category', label: 'Kategorie', mapTo: 'product_category' },
|
|
56
56
|
{ key: 'cross_sell_family_name', label: 'Familie', mapTo: 'product_group' },
|
|
57
|
-
{ key: 'cross_sell_part_number', label: 'Teilenummer',
|
|
57
|
+
{ key: 'cross_sell_part_number', label: 'Teilenummer', mapTo: 'part_number', secondaryKey: 'cross_sell_product_family_name' },
|
|
58
58
|
{ key: 'cross_sell_manufacturer', label: 'Hersteller', mapTo: 'manufacturer_name' },
|
|
59
59
|
{ key: 'cross_sell_description', label: 'Beschreibung', mapTo: 'description' },
|
|
60
60
|
{ key: 'recommendation_source', label: 'Quelle', muted: true, mapTo: 'lifecycle' },
|
|
@@ -83,6 +83,7 @@ const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
|
|
|
83
83
|
return v < 1000
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
|
+
actions: { label: 'Aktionen', type: 'actions', sortable: false, filterable: false, visible: true, configurable: false },
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
const PRODUCT_LAYOUT: DomainLayout = {
|
|
@@ -283,7 +284,7 @@ export const MOCK_PRODUCTS: Record<string, unknown>[] = [
|
|
|
283
284
|
// ===== PROJECT DOMAIN =====
|
|
284
285
|
|
|
285
286
|
const PROJECT_COLUMNS: Record<string, ColumnDef> = {
|
|
286
|
-
favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true },
|
|
287
|
+
favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true, headerIcon: 'star', headerTooltip: 'Favorit' },
|
|
287
288
|
name: { label: 'Projektname', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'customer_name', searchable: true },
|
|
288
289
|
customer_name: { label: 'Kunde', type: 'link', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
|
|
289
290
|
status: {
|
|
@@ -354,7 +355,7 @@ export const MOCK_PROJECTS: Record<string, unknown>[] = [
|
|
|
354
355
|
// ===== CUSTOMER DOMAIN =====
|
|
355
356
|
|
|
356
357
|
const CUSTOMER_COLUMNS: Record<string, ColumnDef> = {
|
|
357
|
-
favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true },
|
|
358
|
+
favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true, headerIcon: 'star', headerTooltip: 'Favorit' },
|
|
358
359
|
name: { label: 'Kunde', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'main_customer', searchable: true },
|
|
359
360
|
category: { label: 'Kategorie', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
|
|
360
361
|
region: { label: 'Region', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, hideTablet: true },
|
|
@@ -418,7 +419,7 @@ export const MOCK_CUSTOMERS: Record<string, unknown>[] = [
|
|
|
418
419
|
// ===== APPLICATION DOMAIN =====
|
|
419
420
|
|
|
420
421
|
const APPLICATION_COLUMNS: Record<string, ColumnDef> = {
|
|
421
|
-
favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true },
|
|
422
|
+
favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true, headerIcon: 'star', headerTooltip: 'Favorit' },
|
|
422
423
|
name: { label: 'Applikation', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'description', searchable: true },
|
|
423
424
|
url: { label: 'Referenz', type: 'link', sortable: false, filterable: false, visible: true, hideMobile: true },
|
|
424
425
|
trends: { label: 'Trend', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, hideTablet: true, searchable: true },
|
|
@@ -63,7 +63,7 @@ export function useDataExplorerState(
|
|
|
63
63
|
initialDomain: DomainKey = 'products',
|
|
64
64
|
): DataExplorerState {
|
|
65
65
|
const [activeDomain, setActiveDomainRaw] = useState<DomainKey>(initialDomain)
|
|
66
|
-
const [viewMode,
|
|
66
|
+
const [viewMode, setViewModeRaw] = useState<ViewMode>('table')
|
|
67
67
|
const [sort, setSort] = useState<SortState>({ column: null, direction: 'asc' })
|
|
68
68
|
const [filters, setFilters] = useState<FilterState>({})
|
|
69
69
|
const [searchTerm, setSearchTermRaw] = useState('')
|
|
@@ -79,6 +79,11 @@ export function useDataExplorerState(
|
|
|
79
79
|
[activeDomain],
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
+
const setViewMode = useCallback((mode: ViewMode) => {
|
|
83
|
+
setViewModeRaw(mode)
|
|
84
|
+
setSort({ column: null, direction: 'asc' })
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
82
87
|
const setActiveDomain = useCallback((domain: DomainKey) => {
|
|
83
88
|
setActiveDomainRaw(domain)
|
|
84
89
|
setSort({ column: null, direction: 'asc' })
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
1
3
|
export type DomainKey = 'products' | 'projects' | 'customers' | 'applications'
|
|
2
4
|
|
|
3
5
|
export type CellType =
|
|
@@ -10,6 +12,7 @@ export type CellType =
|
|
|
10
12
|
| 'favorite'
|
|
11
13
|
| 'expand'
|
|
12
14
|
| 'score-bar'
|
|
15
|
+
| 'actions'
|
|
13
16
|
|
|
14
17
|
export type ViewMode = 'table' | 'list' | 'card'
|
|
15
18
|
|
|
@@ -32,6 +35,17 @@ export interface ExpandColumnDef {
|
|
|
32
35
|
type?: CellType
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
export interface RenderCellOptions {
|
|
39
|
+
mode?: 'default' | 'compact' | 'inventory-label'
|
|
40
|
+
isExpanded?: boolean
|
|
41
|
+
isFavorite?: boolean
|
|
42
|
+
onToggleExpand?: () => void
|
|
43
|
+
onToggleFavorite?: () => void
|
|
44
|
+
onAddToProject?: () => void
|
|
45
|
+
onAddToCollection?: () => void
|
|
46
|
+
onDelete?: () => void
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
export interface ColumnDef {
|
|
36
50
|
label: string
|
|
37
51
|
type: CellType
|
|
@@ -39,6 +53,8 @@ export interface ColumnDef {
|
|
|
39
53
|
filterable: boolean
|
|
40
54
|
primaryFilter?: boolean
|
|
41
55
|
visible: boolean
|
|
56
|
+
/** When false, column is excluded from the column-config popover (always visible, not reorderable) */
|
|
57
|
+
configurable?: boolean
|
|
42
58
|
searchable?: boolean
|
|
43
59
|
secondary?: string
|
|
44
60
|
currencyField?: string
|
|
@@ -57,6 +73,9 @@ export interface ColumnDef {
|
|
|
57
73
|
hideTablet?: boolean
|
|
58
74
|
hideMobile?: boolean
|
|
59
75
|
rowLines?: number
|
|
76
|
+
/** Min-width in px for badge column alignment in DataListView. Default: 96 */
|
|
77
|
+
badgeColumnWidth?: number
|
|
78
|
+
render?: (val: unknown, row: Record<string, unknown>, opts: RenderCellOptions) => React.ReactNode
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
export interface ListLayout {
|