@postxl/generators 1.0.13 → 1.0.14
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/frontend-core/template/src/components/ui/data-grid/cell-variants/gantt-cell.tsx +5 -3
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-context-menu.tsx +12 -3
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.stories.tsx +780 -0
- package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-data-grid.tsx +5 -1
- package/package.json +1 -1
package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/gantt-cell.tsx
CHANGED
|
@@ -39,9 +39,11 @@ export function GanttCell<TData>({
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const timelineDurationMs = timelineEndMs - timelineStartMs
|
|
42
|
-
const
|
|
42
|
+
const hasValidDates = initialValue && initialValue.start instanceof Date && initialValue.end instanceof Date
|
|
43
|
+
|
|
44
|
+
const msUntilStart = hasValidDates ? initialValue.start.getTime() - timelineStartMs : 0
|
|
43
45
|
// end - start time considering timeline bounds
|
|
44
|
-
const barWidthMs =
|
|
46
|
+
const barWidthMs = hasValidDates
|
|
45
47
|
? Math.min(initialValue.end.getTime(), timelineEndMs) - Math.max(initialValue.start.getTime(), timelineStartMs)
|
|
46
48
|
: 0
|
|
47
49
|
|
|
@@ -58,7 +60,7 @@ export function GanttCell<TData>({
|
|
|
58
60
|
className="px-1"
|
|
59
61
|
>
|
|
60
62
|
<div className="size-full flex overflow-hidden">
|
|
61
|
-
{
|
|
63
|
+
{hasValidDates && (
|
|
62
64
|
<>
|
|
63
65
|
<div
|
|
64
66
|
className="shrink-0"
|
|
@@ -172,24 +172,33 @@ function ContextMenuImpl<TData>({
|
|
|
172
172
|
)
|
|
173
173
|
}, [table, selectionState])
|
|
174
174
|
|
|
175
|
-
// Determine whether the selected cells are all editable. If any selected cell belongs to a non-editable column (meta.
|
|
175
|
+
// Determine whether the selected cells are all editable. If any selected cell belongs to a non-editable column (meta.editable === false), disable the Clear action.
|
|
176
176
|
const canClear = React.useMemo(() => {
|
|
177
177
|
if (!selectionState?.selectedCells || selectionState.selectedCells.size === 0) {
|
|
178
178
|
return false
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
const visibleCols = table.getVisibleLeafColumns()
|
|
182
|
+
const rows = table.getRowModel().rows
|
|
182
183
|
|
|
183
184
|
for (const cellKey of selectionState.selectedCells) {
|
|
184
|
-
const { columnId } = parseCellKey(cellKey)
|
|
185
|
+
const { rowIndex, columnId } = parseCellKey(cellKey)
|
|
185
186
|
if (!columnId) {
|
|
186
187
|
continue
|
|
187
188
|
}
|
|
188
189
|
const col = visibleCols.find((c) => c.id === columnId)
|
|
189
|
-
const editable =
|
|
190
|
+
const editable = col?.columnDef?.meta?.editable
|
|
191
|
+
|
|
190
192
|
if (editable === false) {
|
|
191
193
|
return false
|
|
192
194
|
}
|
|
195
|
+
|
|
196
|
+
if (typeof editable === 'function') {
|
|
197
|
+
const row = rows[rowIndex]
|
|
198
|
+
if (row && !editable(row.original)) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
return true
|
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { DataGrid } from './data-grid'
|
|
4
|
+
import { useDataGrid } from './hooks/use-data-grid'
|
|
5
|
+
import { Checkbox } from '../checkbox/checkbox'
|
|
6
|
+
import { Button } from '../button/button'
|
|
7
|
+
import { MinusIcon, MoonIcon, PlusIcon, SunIcon, TrashIcon } from '@radix-ui/react-icons'
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
9
|
+
import { ColumnDef, getExpandedRowModel } from '@tanstack/react-table'
|
|
10
|
+
import { RowHeightValue } from './data-grid-types'
|
|
11
|
+
import { ISODateRange, isoToLocalDate } from './cell-variants/utils/gantt-timerange-picker'
|
|
12
|
+
import { DataGridViewMenu } from './data-grid-view-menu'
|
|
13
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select/select'
|
|
14
|
+
|
|
15
|
+
const meta = {
|
|
16
|
+
title: 'DataGrid',
|
|
17
|
+
component: DataGrid,
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
parameters: {
|
|
20
|
+
layout: 'centered',
|
|
21
|
+
},
|
|
22
|
+
argTypes: {},
|
|
23
|
+
} satisfies Meta<typeof DataGrid>
|
|
24
|
+
export default meta
|
|
25
|
+
|
|
26
|
+
type Story = StoryObj<typeof meta>
|
|
27
|
+
|
|
28
|
+
type ExampleData = {
|
|
29
|
+
shortText: string
|
|
30
|
+
longText: string | null
|
|
31
|
+
valueLabelSelect: string | null
|
|
32
|
+
boolean: boolean
|
|
33
|
+
enumSelect: string
|
|
34
|
+
multiSelect: string[] | null
|
|
35
|
+
number: number | null
|
|
36
|
+
startDate: Date | null
|
|
37
|
+
endDate: Date | null
|
|
38
|
+
accessorFn?: 'day' | 'night'
|
|
39
|
+
customNode?: React.ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getCustomReactNodeCell = (time: 'day' | 'night') => {
|
|
43
|
+
return (
|
|
44
|
+
<div className="size-full flex items-center justify-center">
|
|
45
|
+
{time === 'day' ? <SunIcon className="text-yellow-500" /> : <MoonIcon className="text-blue-900" />}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const initialData: ExampleData[] = [
|
|
51
|
+
{
|
|
52
|
+
shortText: 'Reduce energy consumption',
|
|
53
|
+
longText: 'Implement energy-efficient practices to lower consumption by 15%.',
|
|
54
|
+
valueLabelSelect: 'ja982ika782',
|
|
55
|
+
boolean: true,
|
|
56
|
+
enumSelect: 'Red',
|
|
57
|
+
multiSelect: ['optionA', 'optionC'],
|
|
58
|
+
number: 1500,
|
|
59
|
+
startDate: new Date('2024-12-31'),
|
|
60
|
+
endDate: new Date('2025-12-31'),
|
|
61
|
+
accessorFn: 'day',
|
|
62
|
+
customNode: <div className="size-full flex items-center justify-center text-blue-500 font-bold">Direct Node 1</div>,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
shortText: 'Waste reduction program',
|
|
66
|
+
longText: 'Launch a waste reduction initiative to decrease waste by 20%.',
|
|
67
|
+
valueLabelSelect: 'jd81j980905',
|
|
68
|
+
boolean: false,
|
|
69
|
+
enumSelect: 'Green',
|
|
70
|
+
multiSelect: null,
|
|
71
|
+
number: 800,
|
|
72
|
+
startDate: null,
|
|
73
|
+
endDate: null,
|
|
74
|
+
accessorFn: 'night',
|
|
75
|
+
customNode: (
|
|
76
|
+
<div className="size-full flex items-center justify-center">
|
|
77
|
+
<Button size="xs2" className="h-5">
|
|
78
|
+
Action
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
shortText: 'Water conservation efforts',
|
|
85
|
+
longText: null,
|
|
86
|
+
valueLabelSelect: null,
|
|
87
|
+
boolean: false,
|
|
88
|
+
enumSelect: 'Amber',
|
|
89
|
+
multiSelect: ['optionB'],
|
|
90
|
+
number: 600,
|
|
91
|
+
startDate: new Date('2025-01-15'),
|
|
92
|
+
endDate: new Date('2025-10-20'),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
shortText: 'Sustainable sourcing',
|
|
96
|
+
longText: 'Source 25% of materials from sustainable suppliers.',
|
|
97
|
+
valueLabelSelect: null,
|
|
98
|
+
boolean: true,
|
|
99
|
+
enumSelect: 'Red',
|
|
100
|
+
multiSelect: ['optionA', 'optionD'],
|
|
101
|
+
number: 2000,
|
|
102
|
+
startDate: new Date('2024-10-20'),
|
|
103
|
+
endDate: null,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
shortText: 'Employee training on sustainability',
|
|
107
|
+
longText: 'Conduct training sessions to educate employees on sustainable practices.',
|
|
108
|
+
valueLabelSelect: 'jd0920982ljkna8',
|
|
109
|
+
boolean: true,
|
|
110
|
+
enumSelect: 'Green',
|
|
111
|
+
multiSelect: ['optionA', 'optionC', 'optionD'],
|
|
112
|
+
number: 300,
|
|
113
|
+
startDate: new Date('2024-09-15'),
|
|
114
|
+
endDate: new Date('2024-12-15'),
|
|
115
|
+
accessorFn: 'day',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
shortText: 'Carbon footprint assessment',
|
|
119
|
+
longText: 'Evaluate and report the company’s carbon footprint.',
|
|
120
|
+
valueLabelSelect: 'jad982ja9802ijlljk',
|
|
121
|
+
boolean: false,
|
|
122
|
+
enumSelect: 'NA',
|
|
123
|
+
multiSelect: null,
|
|
124
|
+
number: null,
|
|
125
|
+
startDate: new Date('2024-08-31'),
|
|
126
|
+
endDate: new Date('2024-11-30'),
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
const parseDate = (value: string | Date | null): Date | null => {
|
|
131
|
+
if (!value) return null
|
|
132
|
+
if (value instanceof Date) return value
|
|
133
|
+
if (typeof value === 'string') {
|
|
134
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
|
135
|
+
if (match) {
|
|
136
|
+
return new Date(Number.parseInt(match[1], 10), Number.parseInt(match[2], 10) - 1, Number.parseInt(match[3], 10))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const parsed = new Date(value)
|
|
140
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const DataGridDemo = () => {
|
|
144
|
+
const [data, setData] = useState<ExampleData[]>(initialData)
|
|
145
|
+
const [selectedGanttRange, setSelectedGanttRange] = useState<ISODateRange | undefined>(undefined)
|
|
146
|
+
|
|
147
|
+
const columns = useMemo<ColumnDef<ExampleData>[]>(
|
|
148
|
+
() => [
|
|
149
|
+
{
|
|
150
|
+
id: 'select',
|
|
151
|
+
header: ({ table }) => (
|
|
152
|
+
<div className="size-full p-0 m-0 flex items-center gap-2 ">
|
|
153
|
+
<Checkbox
|
|
154
|
+
variant={table.getIsSomePageRowsSelected() ? 'default' : 'simple'}
|
|
155
|
+
iconStyle={table.getIsSomePageRowsSelected() ? 'default' : 'simple'}
|
|
156
|
+
checkIcon="check"
|
|
157
|
+
checked={table.getIsAllPageRowsSelected() || table.getIsSomePageRowsSelected()}
|
|
158
|
+
onChange={(e) => table.toggleAllPageRowsSelected(!!e.target.checked)}
|
|
159
|
+
/>
|
|
160
|
+
<Button
|
|
161
|
+
variant="ghost"
|
|
162
|
+
className="size-5 hover:[&_svg]:text-background hover:bg-destructive/90"
|
|
163
|
+
onClick={() => {
|
|
164
|
+
const toRemove = Array.from(table.getSelectedRowModel().rows)
|
|
165
|
+
if (toRemove.length === 0) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
const idsToRemove = new Set(toRemove.map((row) => row.index))
|
|
169
|
+
setData((prev) => prev.filter((_, index) => !idsToRemove.has(index)))
|
|
170
|
+
table.toggleAllRowsSelected(false)
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<TrashIcon />
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
),
|
|
177
|
+
cell: ({ row }) => (
|
|
178
|
+
<Checkbox
|
|
179
|
+
variant="simple"
|
|
180
|
+
iconStyle="simple"
|
|
181
|
+
checkIcon="check"
|
|
182
|
+
checked={row.getIsSelected()}
|
|
183
|
+
onChange={(e) => row.toggleSelected(!!e.target.checked)}
|
|
184
|
+
/>
|
|
185
|
+
),
|
|
186
|
+
size: 70,
|
|
187
|
+
enableSorting: false,
|
|
188
|
+
enableHiding: false,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'shortText',
|
|
192
|
+
accessorKey: 'shortText',
|
|
193
|
+
header: 'Short Text',
|
|
194
|
+
meta: { cell: { variant: 'short-text' } },
|
|
195
|
+
size: 150,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'longText',
|
|
199
|
+
accessorKey: 'longText',
|
|
200
|
+
header: 'Long Text',
|
|
201
|
+
meta: { cell: { variant: 'long-text' } },
|
|
202
|
+
size: 150,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'boolean',
|
|
206
|
+
accessorKey: 'boolean',
|
|
207
|
+
header: 'Boolean',
|
|
208
|
+
meta: { cell: { variant: 'checkbox' } },
|
|
209
|
+
size: 150,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'enumSelect',
|
|
213
|
+
accessorKey: 'enumSelect',
|
|
214
|
+
header: 'Enum Select',
|
|
215
|
+
meta: {
|
|
216
|
+
cell: {
|
|
217
|
+
variant: 'select',
|
|
218
|
+
options: [
|
|
219
|
+
{ label: 'Red', value: 'Red' },
|
|
220
|
+
{ label: 'Amber', value: 'Amber' },
|
|
221
|
+
{ label: 'Green', value: 'Green' },
|
|
222
|
+
{ label: 'NA', value: 'NA' },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
size: 150,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'valueLabelSelect',
|
|
230
|
+
accessorKey: 'valueLabelSelect',
|
|
231
|
+
header: 'Value-label Select',
|
|
232
|
+
meta: {
|
|
233
|
+
cell: {
|
|
234
|
+
variant: 'select',
|
|
235
|
+
options: [
|
|
236
|
+
{ label: 'Max Mustermann', value: 'ja982ika782' },
|
|
237
|
+
{ label: 'Erika Musterfrau', value: 'jd81j980905' },
|
|
238
|
+
{ label: 'John Doe', value: 'jia82813jh4g' },
|
|
239
|
+
{ label: 'Jane Doe', value: '28jnfg8a8x9' },
|
|
240
|
+
{ label: 'Anna Schmidt', value: 'jd0920982ljkna8' },
|
|
241
|
+
{ label: 'Paul Müller', value: 'jad982ja9802ijlljk' },
|
|
242
|
+
],
|
|
243
|
+
hasSearch: true,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
size: 170,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'multiSelect',
|
|
250
|
+
accessorKey: 'multiSelect',
|
|
251
|
+
header: 'Multi Select',
|
|
252
|
+
meta: {
|
|
253
|
+
cell: {
|
|
254
|
+
variant: 'multi-select',
|
|
255
|
+
options: [
|
|
256
|
+
{ label: 'Option A', value: 'optionA' },
|
|
257
|
+
{ label: 'Option B', value: 'optionB' },
|
|
258
|
+
{ label: 'Option C', value: 'optionC' },
|
|
259
|
+
{ label: 'Option D', value: 'optionD' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
size: 210,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'number',
|
|
267
|
+
accessorKey: 'number',
|
|
268
|
+
header: 'Number',
|
|
269
|
+
meta: { cell: { variant: 'number' } },
|
|
270
|
+
size: 150,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'startDate',
|
|
274
|
+
accessorKey: 'startDate',
|
|
275
|
+
header: 'Start Date',
|
|
276
|
+
meta: { cell: { variant: 'date' } },
|
|
277
|
+
size: 150,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'endDate',
|
|
281
|
+
accessorKey: 'endDate',
|
|
282
|
+
header: 'End Date',
|
|
283
|
+
meta: { cell: { variant: 'date' } },
|
|
284
|
+
size: 150,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'timeline',
|
|
288
|
+
header: 'Timeline',
|
|
289
|
+
accessorFn: (row) => {
|
|
290
|
+
const start = parseDate(row.startDate as unknown as string | Date | null)
|
|
291
|
+
const end = parseDate(row.endDate as unknown as string | Date | null)
|
|
292
|
+
|
|
293
|
+
if (!start || !end) {
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
start,
|
|
298
|
+
end,
|
|
299
|
+
barClassName: row.boolean ? 'bg-primary' : 'bg-primary/50',
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
meta: {
|
|
303
|
+
cell: {
|
|
304
|
+
variant: 'gantt',
|
|
305
|
+
timelineStart: new Date(new Date().getFullYear() - 1, 6, 0),
|
|
306
|
+
timelineEnd: new Date(new Date().getFullYear() + 1, 3, 1),
|
|
307
|
+
dateRangeFrom: selectedGanttRange?.from ? isoToLocalDate(selectedGanttRange.from) : undefined,
|
|
308
|
+
dateRangeTo: selectedGanttRange?.to ? isoToLocalDate(selectedGanttRange.to) : undefined,
|
|
309
|
+
onRangeChange: setSelectedGanttRange,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
maxSize: 1000,
|
|
313
|
+
size: 500,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'withAccessorFn',
|
|
317
|
+
accessorFn: (row) => (row.accessorFn ? getCustomReactNodeCell(row.accessorFn) : null),
|
|
318
|
+
header: 'With accessorFn',
|
|
319
|
+
meta: { cell: { variant: 'react-node' } },
|
|
320
|
+
size: 150,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'customNode',
|
|
324
|
+
accessorKey: 'customNode',
|
|
325
|
+
header: 'Custom React Node',
|
|
326
|
+
meta: { cell: { variant: 'react-node' } },
|
|
327
|
+
size: 150,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
[selectedGanttRange],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const onRowAdd = useCallback(() => {
|
|
334
|
+
setData((prev) => [...prev, { shortText: 'New Item', boolean: false, enumSelect: 'NA' } as ExampleData])
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
rowIndex: data.length,
|
|
338
|
+
columnId: 'shortText',
|
|
339
|
+
}
|
|
340
|
+
}, [data.length])
|
|
341
|
+
|
|
342
|
+
const { table, ...dataGridProps } = useDataGrid({
|
|
343
|
+
columns,
|
|
344
|
+
data,
|
|
345
|
+
onDataChange: setData,
|
|
346
|
+
onRowAdd,
|
|
347
|
+
initialState: {
|
|
348
|
+
columnPinning: {
|
|
349
|
+
left: ['select'],
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
enableSearch: true,
|
|
353
|
+
})
|
|
354
|
+
return (
|
|
355
|
+
<div className="w-220 flex flex-col gap-2">
|
|
356
|
+
<DataGridViewMenu table={table} />
|
|
357
|
+
<DataGrid {...dataGridProps} table={table} />
|
|
358
|
+
</div>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const Default: Story = {
|
|
363
|
+
args: {} as any,
|
|
364
|
+
render: () => <DataGridDemo />,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
type PropertyDemoData = {
|
|
368
|
+
normal: string
|
|
369
|
+
editable: boolean
|
|
370
|
+
withAlignment: string
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const initialPropertyDemoData: PropertyDemoData[] = [
|
|
374
|
+
{
|
|
375
|
+
normal: 'Text',
|
|
376
|
+
editable: false,
|
|
377
|
+
withAlignment: 'Aligned Text',
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
normal: 'Text',
|
|
381
|
+
editable: true,
|
|
382
|
+
withAlignment: 'Aligned Text',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
normal: 'Text',
|
|
386
|
+
editable: false,
|
|
387
|
+
withAlignment: 'Aligned Text',
|
|
388
|
+
},
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
const DataGridPropertyDemo = () => {
|
|
392
|
+
const [data, setData] = useState<PropertyDemoData[]>(initialPropertyDemoData)
|
|
393
|
+
|
|
394
|
+
const [alignment, setAlignment] = useState<'left' | 'center' | 'right' | undefined>(undefined)
|
|
395
|
+
const [rowHeight, setRowHeight] = useState<RowHeightValue>('short')
|
|
396
|
+
const [enableColumnSelection, setEnableColumnSelection] = useState(true)
|
|
397
|
+
|
|
398
|
+
const columns = useMemo<ColumnDef<PropertyDemoData>[]>(
|
|
399
|
+
() => [
|
|
400
|
+
{
|
|
401
|
+
id: 'normal',
|
|
402
|
+
accessorKey: 'normal',
|
|
403
|
+
header: 'Normal',
|
|
404
|
+
meta: { cell: { variant: 'short-text' } },
|
|
405
|
+
size: 150,
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: 'editable',
|
|
409
|
+
accessorKey: 'editable',
|
|
410
|
+
header: 'Editable',
|
|
411
|
+
meta: {
|
|
412
|
+
cell: { variant: 'checkbox' },
|
|
413
|
+
},
|
|
414
|
+
size: 110,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: 'withEditable',
|
|
418
|
+
accessorFn: (row) => (row.editable ? 'Cell is editable' : 'Cell is not editable'),
|
|
419
|
+
header: 'With Editable',
|
|
420
|
+
meta: { cell: { variant: 'short-text' }, editable: (row: PropertyDemoData) => row.editable },
|
|
421
|
+
size: 150,
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'withAlignment',
|
|
425
|
+
accessorKey: 'withAlignment',
|
|
426
|
+
header: 'With Alignment',
|
|
427
|
+
meta: { cell: { variant: 'short-text' }, align: alignment },
|
|
428
|
+
size: 150,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: 'withClassName',
|
|
432
|
+
accessorFn: (row) => (row.editable ? 'Row has editable set' : 'Row has not editable set'),
|
|
433
|
+
header: 'With Class name',
|
|
434
|
+
meta: {
|
|
435
|
+
cell: { variant: 'short-text' },
|
|
436
|
+
className: (row: PropertyDemoData) => (row.editable ? undefined : 'opacity-50'),
|
|
437
|
+
},
|
|
438
|
+
size: 170,
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'notSortable',
|
|
442
|
+
accessorKey: 'normal',
|
|
443
|
+
header: 'Not sortable',
|
|
444
|
+
meta: { cell: { variant: 'short-text' } },
|
|
445
|
+
enableSorting: false,
|
|
446
|
+
size: 150,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'notPinnable',
|
|
450
|
+
accessorKey: 'normal',
|
|
451
|
+
header: 'Not pinnable',
|
|
452
|
+
meta: { cell: { variant: 'short-text' } },
|
|
453
|
+
enablePinning: false,
|
|
454
|
+
size: 150,
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: 'notHideable',
|
|
458
|
+
accessorKey: 'normal',
|
|
459
|
+
header: 'Not hideable',
|
|
460
|
+
meta: { cell: { variant: 'short-text' } },
|
|
461
|
+
enableHiding: false,
|
|
462
|
+
size: 150,
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: 'notResizable',
|
|
466
|
+
accessorKey: 'normal',
|
|
467
|
+
header: 'Not resizable',
|
|
468
|
+
meta: { cell: { variant: 'short-text' } },
|
|
469
|
+
enableResizing: false,
|
|
470
|
+
size: 150,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
id: 'controlledResizeable',
|
|
474
|
+
accessorKey: 'normal',
|
|
475
|
+
header: 'Controlled resizable (200-300 px)',
|
|
476
|
+
meta: { cell: { variant: 'short-text' } },
|
|
477
|
+
size: 250,
|
|
478
|
+
minSize: 200,
|
|
479
|
+
maxSize: 300,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: 'customHeader',
|
|
483
|
+
header: () => <span className="text-primary font-bold">Custom Header & Cells</span>,
|
|
484
|
+
cell: ({ row }) => (
|
|
485
|
+
<a href={`https://www.example.com/${row.index + 1}`} className="text-primary font-bold hover:underline">
|
|
486
|
+
Custom Cell {row.index + 1}
|
|
487
|
+
</a>
|
|
488
|
+
),
|
|
489
|
+
size: 180,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
[alignment],
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
const { table, ...dataGridProps } = useDataGrid({
|
|
496
|
+
columns,
|
|
497
|
+
data,
|
|
498
|
+
onDataChange: setData,
|
|
499
|
+
enableSearch: true,
|
|
500
|
+
rowHeight,
|
|
501
|
+
enableColumnSelection,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<div className="w-220 flex flex-col gap-2">
|
|
506
|
+
<div className="flex gap-2 items-center flex-wrap">
|
|
507
|
+
<Select value={alignment} onValueChange={(value) => setAlignment(value as 'left' | 'center' | 'right')}>
|
|
508
|
+
<SelectTrigger size="sm" className="w-[150px]">
|
|
509
|
+
Select alignment
|
|
510
|
+
</SelectTrigger>
|
|
511
|
+
<SelectContent>
|
|
512
|
+
<SelectItem value="left">Left</SelectItem>
|
|
513
|
+
<SelectItem value="center">Center</SelectItem>
|
|
514
|
+
<SelectItem value="right">Right</SelectItem>
|
|
515
|
+
</SelectContent>
|
|
516
|
+
</Select>
|
|
517
|
+
|
|
518
|
+
<Select value={rowHeight} onValueChange={(value) => setRowHeight(value as RowHeightValue)}>
|
|
519
|
+
<SelectTrigger size="sm" className="w-[150px]">
|
|
520
|
+
Row height
|
|
521
|
+
</SelectTrigger>
|
|
522
|
+
<SelectContent>
|
|
523
|
+
<SelectItem value="short">Short</SelectItem>
|
|
524
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
525
|
+
<SelectItem value="tall">Tall</SelectItem>
|
|
526
|
+
<SelectItem value="extra-tall">Extra Tall</SelectItem>
|
|
527
|
+
</SelectContent>
|
|
528
|
+
</Select>
|
|
529
|
+
|
|
530
|
+
<div className="flex items-center gap-2 border rounded-md px-3 py-1 h-8 text-sm bg-background">
|
|
531
|
+
<Checkbox
|
|
532
|
+
id="col-select"
|
|
533
|
+
checked={enableColumnSelection}
|
|
534
|
+
onChange={(e) => setEnableColumnSelection(e.target.checked)}
|
|
535
|
+
/>
|
|
536
|
+
<label htmlFor="col-select">Enable Column Selection</label>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<DataGridViewMenu table={table} />
|
|
540
|
+
</div>
|
|
541
|
+
<DataGrid {...dataGridProps} table={table} />
|
|
542
|
+
</div>
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export const PropertyDemo: Story = {
|
|
547
|
+
args: {} as any,
|
|
548
|
+
render: () => <DataGridPropertyDemo />,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
type ExpandableData = {
|
|
552
|
+
id: string
|
|
553
|
+
name: string
|
|
554
|
+
foundedYear?: number
|
|
555
|
+
type: 'company' | 'department' | 'team' | 'employee'
|
|
556
|
+
role?: string
|
|
557
|
+
salary?: number
|
|
558
|
+
subRows?: ExpandableData[]
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const expandableData: ExpandableData[] = [
|
|
562
|
+
{
|
|
563
|
+
id: 'company-1',
|
|
564
|
+
name: 'Tech Corp',
|
|
565
|
+
foundedYear: 1998,
|
|
566
|
+
type: 'company',
|
|
567
|
+
subRows: [
|
|
568
|
+
{
|
|
569
|
+
id: 'dept-1',
|
|
570
|
+
name: 'Engineering',
|
|
571
|
+
foundedYear: 1999,
|
|
572
|
+
type: 'department',
|
|
573
|
+
subRows: [
|
|
574
|
+
{
|
|
575
|
+
id: 'team-1',
|
|
576
|
+
name: 'Frontend Team',
|
|
577
|
+
foundedYear: 2010,
|
|
578
|
+
type: 'team',
|
|
579
|
+
subRows: [
|
|
580
|
+
{
|
|
581
|
+
id: 'emp-1',
|
|
582
|
+
name: 'Alice Johnson',
|
|
583
|
+
role: 'Senior Frontend Engineer',
|
|
584
|
+
type: 'employee',
|
|
585
|
+
salary: 90000,
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: 'emp-2',
|
|
589
|
+
name: 'Bob Smith',
|
|
590
|
+
role: 'Frontend Engineer',
|
|
591
|
+
type: 'employee',
|
|
592
|
+
salary: 75000,
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
id: 'team-2',
|
|
598
|
+
name: 'Backend Team',
|
|
599
|
+
foundedYear: 2008,
|
|
600
|
+
type: 'team',
|
|
601
|
+
subRows: [
|
|
602
|
+
{
|
|
603
|
+
id: 'emp-3',
|
|
604
|
+
name: 'Charlie Brown',
|
|
605
|
+
role: 'Backend Engineer',
|
|
606
|
+
type: 'employee',
|
|
607
|
+
salary: 80000,
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
id: 'dept-2',
|
|
615
|
+
name: 'Design',
|
|
616
|
+
foundedYear: 2005,
|
|
617
|
+
type: 'department',
|
|
618
|
+
subRows: [
|
|
619
|
+
{
|
|
620
|
+
id: 'emp-4',
|
|
621
|
+
name: 'Diana Miller',
|
|
622
|
+
role: 'Product Designer',
|
|
623
|
+
type: 'employee',
|
|
624
|
+
salary: 70000,
|
|
625
|
+
},
|
|
626
|
+
],
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
},
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
function aggregateSalaries(data: ExpandableData[]): ExpandableData[] {
|
|
633
|
+
return data.map((node) => {
|
|
634
|
+
if (!node.subRows || node.subRows.length === 0) {
|
|
635
|
+
return {
|
|
636
|
+
...node,
|
|
637
|
+
salary: node.salary ?? 0,
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const aggregatedSubRows = aggregateSalaries(node.subRows)
|
|
641
|
+
const totalSalary = aggregatedSubRows.reduce((sum, child) => sum + (child.salary ?? 0), 0)
|
|
642
|
+
return {
|
|
643
|
+
...node,
|
|
644
|
+
subRows: aggregatedSubRows,
|
|
645
|
+
salary: totalSalary,
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const aggregatedData = aggregateSalaries(expandableData)
|
|
651
|
+
|
|
652
|
+
const DataGridExpandableDemo = () => {
|
|
653
|
+
const [data] = useState(aggregatedData)
|
|
654
|
+
|
|
655
|
+
const columns = useMemo<ColumnDef<ExpandableData>[]>(
|
|
656
|
+
() => [
|
|
657
|
+
{
|
|
658
|
+
id: 'name',
|
|
659
|
+
accessorKey: 'name',
|
|
660
|
+
header: 'Name',
|
|
661
|
+
cell: ({ row, getValue }) => {
|
|
662
|
+
const indent = row.depth * 16
|
|
663
|
+
const canExpand = row.getCanExpand()
|
|
664
|
+
const isExpanded = row.getIsExpanded()
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
<div className="size-full flex items-center px-2">
|
|
668
|
+
<div style={{ width: indent, minWidth: indent }} />
|
|
669
|
+
{canExpand ? (
|
|
670
|
+
<>
|
|
671
|
+
<Button
|
|
672
|
+
variant="ghost"
|
|
673
|
+
size="iconSm"
|
|
674
|
+
className="size-5.5 mr-1"
|
|
675
|
+
onClick={row.getToggleExpandedHandler()}
|
|
676
|
+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
677
|
+
>
|
|
678
|
+
{isExpanded ? <MinusIcon /> : <PlusIcon />}
|
|
679
|
+
</Button>
|
|
680
|
+
<span className="truncate font-semibold">{getValue<string>()}</span>
|
|
681
|
+
</>
|
|
682
|
+
) : (
|
|
683
|
+
<span className="truncate">{getValue<string>()}</span>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
)
|
|
687
|
+
},
|
|
688
|
+
meta: { cell: { variant: 'short-text' } },
|
|
689
|
+
size: 200,
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
id: 'type',
|
|
693
|
+
accessorKey: 'type',
|
|
694
|
+
header: 'Type',
|
|
695
|
+
meta: {
|
|
696
|
+
cell: {
|
|
697
|
+
variant: 'select',
|
|
698
|
+
options: [
|
|
699
|
+
{ label: 'Company', value: 'company' },
|
|
700
|
+
{ label: 'Department', value: 'department' },
|
|
701
|
+
{ label: 'Team', value: 'team' },
|
|
702
|
+
{ label: 'Employee', value: 'employee' },
|
|
703
|
+
],
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
size: 150,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: 'foundedYear',
|
|
710
|
+
accessorKey: 'foundedYear',
|
|
711
|
+
header: 'Founded Year',
|
|
712
|
+
meta: { cell: { variant: 'number' } },
|
|
713
|
+
size: 150,
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: 'salary',
|
|
717
|
+
accessorKey: 'salary',
|
|
718
|
+
header: 'Salary',
|
|
719
|
+
meta: {
|
|
720
|
+
cell: {
|
|
721
|
+
variant: 'number',
|
|
722
|
+
suffix: ' €',
|
|
723
|
+
fallbackValue: 'N/A',
|
|
724
|
+
},
|
|
725
|
+
className(row) {
|
|
726
|
+
return row?.type === 'employee' ? undefined : 'font-semibold'
|
|
727
|
+
},
|
|
728
|
+
editable(row) {
|
|
729
|
+
return row?.type === 'employee'
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
size: 150,
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
id: 'role',
|
|
736
|
+
accessorKey: 'role',
|
|
737
|
+
header: 'Role',
|
|
738
|
+
meta: { cell: { variant: 'short-text' } },
|
|
739
|
+
size: 150,
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
[],
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
const defaultExpanded = useMemo(() => {
|
|
746
|
+
const expanded: Record<string, boolean> = {}
|
|
747
|
+
for (const p of data) {
|
|
748
|
+
if (p.subRows && p.subRows.length > 0) {
|
|
749
|
+
expanded[p.id] = true
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return expanded
|
|
753
|
+
}, [data])
|
|
754
|
+
|
|
755
|
+
const { table, ...dataGridProps } = useDataGrid({
|
|
756
|
+
columns,
|
|
757
|
+
data,
|
|
758
|
+
getRowId: (row) => row.id,
|
|
759
|
+
getSubRows: (row) => row.subRows,
|
|
760
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
761
|
+
initialState: { expanded: defaultExpanded, columnPinning: { left: ['name'] } },
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
useEffect(() => {
|
|
765
|
+
if (table && defaultExpanded) {
|
|
766
|
+
table.setExpanded(defaultExpanded)
|
|
767
|
+
}
|
|
768
|
+
}, [table, defaultExpanded])
|
|
769
|
+
|
|
770
|
+
return (
|
|
771
|
+
<div className="w-220 flex flex-col gap-2">
|
|
772
|
+
<DataGrid {...dataGridProps} table={table} />
|
|
773
|
+
</div>
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export const ExpandableRows: Story = {
|
|
778
|
+
args: {} as any,
|
|
779
|
+
render: () => <DataGridExpandableDemo />,
|
|
780
|
+
}
|
|
@@ -206,6 +206,10 @@ function useDataGrid<TData>({
|
|
|
206
206
|
}
|
|
207
207
|
}, [listenersRef, stateRef])
|
|
208
208
|
|
|
209
|
+
React.useEffect(() => {
|
|
210
|
+
store.setState('rowHeight', rowHeightProp)
|
|
211
|
+
}, [rowHeightProp, store])
|
|
212
|
+
|
|
209
213
|
const focusedCell = useStore(store, (state) => state.focusedCell)
|
|
210
214
|
const editingCell = useStore(store, (state) => state.editingCell)
|
|
211
215
|
const selectionState = useStore(store, (state) => state.selectionState)
|
|
@@ -1181,7 +1185,7 @@ function useDataGrid<TData>({
|
|
|
1181
1185
|
}
|
|
1182
1186
|
const { columnId } = parseCellKey(cellKey)
|
|
1183
1187
|
const col = visibleCols.find((c) => c.id === columnId)
|
|
1184
|
-
const editable =
|
|
1188
|
+
const editable = col?.columnDef?.meta?.editable
|
|
1185
1189
|
if (editable === false) {
|
|
1186
1190
|
canClear = false
|
|
1187
1191
|
}
|
package/package.json
CHANGED