@izumisy-tailor/tailor-data-viewer 0.2.20 → 0.2.22
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/package.json +1 -1
- package/src/component/data-table/data-table-context.tsx +13 -1
- package/src/component/data-table/data-table.tsx +132 -17
- package/src/component/data-table/use-data-table.ts +6 -0
- package/src/component/field-helpers.test.ts +37 -0
- package/src/component/field-helpers.ts +24 -5
- package/src/component/index.ts +1 -0
- package/src/component/types.ts +56 -9
package/package.json
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Column,
|
|
4
|
+
PageInfo,
|
|
5
|
+
RowAction,
|
|
6
|
+
RowOperations,
|
|
7
|
+
SortState,
|
|
8
|
+
} from "../types";
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* Context value provided by `DataTable.Provider`.
|
|
@@ -31,6 +37,12 @@ export interface DataTableContextValue<TRow extends Record<string, unknown>> {
|
|
|
31
37
|
|
|
32
38
|
// Page info from GraphQL response (populated by DataTable.Provider)
|
|
33
39
|
pageInfo: PageInfo;
|
|
40
|
+
|
|
41
|
+
// Row interaction (populated by DataTable.Root)
|
|
42
|
+
/** Handler called when a row is clicked */
|
|
43
|
+
onClickRow?: (row: TRow) => void;
|
|
44
|
+
/** Row action definitions for the actions column */
|
|
45
|
+
rowActions?: RowAction<TRow>[];
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
// Using `any` for the context default since generic contexts need a base type.
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createElement,
|
|
3
3
|
useContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
4
8
|
type ComponentProps,
|
|
5
9
|
type ReactNode,
|
|
6
10
|
} from "react";
|
|
7
11
|
import { cn } from "../lib/utils";
|
|
8
12
|
import { CollectionProvider } from "../collection/collection-provider";
|
|
9
13
|
import { Table } from "../table";
|
|
10
|
-
import type { Column, UseDataTableReturn } from "../types";
|
|
14
|
+
import type { Column, RowAction, UseDataTableReturn } from "../types";
|
|
11
15
|
import {
|
|
12
16
|
DataTableContext,
|
|
13
17
|
type DataTableContextValue,
|
|
@@ -21,7 +25,16 @@ import {
|
|
|
21
25
|
* Root container for the DataTable compound component.
|
|
22
26
|
*
|
|
23
27
|
* Must be used within `DataTable.Provider`.
|
|
24
|
-
* Renders a `<Table.Root>` wrapper; all data
|
|
28
|
+
* Renders a `<Table.Root>` wrapper; all data (including `onClickRow` and
|
|
29
|
+
* `rowActions`) is read from context via `useDataTable()` options.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <DataTable.Root>
|
|
34
|
+
* <DataTable.Headers />
|
|
35
|
+
* <DataTable.Body />
|
|
36
|
+
* </DataTable.Root>
|
|
37
|
+
* ```
|
|
25
38
|
*/
|
|
26
39
|
function DataTableRoot({
|
|
27
40
|
children,
|
|
@@ -84,6 +97,8 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
84
97
|
showAllColumns: value.showAllColumns,
|
|
85
98
|
hideAllColumns: value.hideAllColumns,
|
|
86
99
|
pageInfo: value.pageInfo,
|
|
100
|
+
onClickRow: value.onClickRow,
|
|
101
|
+
rowActions: value.rowActions,
|
|
87
102
|
};
|
|
88
103
|
|
|
89
104
|
const collectionValue = value.collection ?? null;
|
|
@@ -115,7 +130,7 @@ function DataTableHeaders({ className }: { className?: string }) {
|
|
|
115
130
|
"<DataTable.Headers> must be used within <DataTable.Provider>",
|
|
116
131
|
);
|
|
117
132
|
}
|
|
118
|
-
const { columns, sortStates, onSort } = ctx;
|
|
133
|
+
const { columns, sortStates, onSort, rowActions } = ctx;
|
|
119
134
|
|
|
120
135
|
return (
|
|
121
136
|
<Table.Headers className={className}>
|
|
@@ -154,6 +169,11 @@ function DataTableHeaders({ className }: { className?: string }) {
|
|
|
154
169
|
</Table.HeaderCell>
|
|
155
170
|
);
|
|
156
171
|
})}
|
|
172
|
+
{rowActions && rowActions.length > 0 && (
|
|
173
|
+
<Table.HeaderCell style={{ width: 50 }}>
|
|
174
|
+
<span className="sr-only">操作</span>
|
|
175
|
+
</Table.HeaderCell>
|
|
176
|
+
)}
|
|
157
177
|
</Table.HeaderRow>
|
|
158
178
|
</Table.Headers>
|
|
159
179
|
);
|
|
@@ -184,7 +204,9 @@ function DataTableBody({
|
|
|
184
204
|
"<DataTable.Body> must be used within <DataTable.Provider>",
|
|
185
205
|
);
|
|
186
206
|
}
|
|
187
|
-
const { columns, rows, loading, error } = ctx;
|
|
207
|
+
const { columns, rows, loading, error, onClickRow, rowActions } = ctx;
|
|
208
|
+
const hasRowActions = rowActions && rowActions.length > 0;
|
|
209
|
+
const totalColSpan = (columns?.length ?? 1) + (hasRowActions ? 1 : 0);
|
|
188
210
|
|
|
189
211
|
// If children are provided, render them directly (custom rendering)
|
|
190
212
|
if (children) {
|
|
@@ -196,36 +218,31 @@ function DataTableBody({
|
|
|
196
218
|
<Table.Body className={className}>
|
|
197
219
|
{loading && (!rows || rows.length === 0) && (
|
|
198
220
|
<Table.Row>
|
|
199
|
-
<Table.Cell
|
|
200
|
-
colSpan={columns?.length ?? 1}
|
|
201
|
-
className="h-24 text-center"
|
|
202
|
-
>
|
|
221
|
+
<Table.Cell colSpan={totalColSpan} className="h-24 text-center">
|
|
203
222
|
<span className="text-muted-foreground">Loading...</span>
|
|
204
223
|
</Table.Cell>
|
|
205
224
|
</Table.Row>
|
|
206
225
|
)}
|
|
207
226
|
{error && (
|
|
208
227
|
<Table.Row>
|
|
209
|
-
<Table.Cell
|
|
210
|
-
colSpan={columns?.length ?? 1}
|
|
211
|
-
className="h-24 text-center"
|
|
212
|
-
>
|
|
228
|
+
<Table.Cell colSpan={totalColSpan} className="h-24 text-center">
|
|
213
229
|
<span className="text-destructive">Error: {error.message}</span>
|
|
214
230
|
</Table.Cell>
|
|
215
231
|
</Table.Row>
|
|
216
232
|
)}
|
|
217
233
|
{!loading && !error && (!rows || rows.length === 0) && (
|
|
218
234
|
<Table.Row>
|
|
219
|
-
<Table.Cell
|
|
220
|
-
colSpan={columns?.length ?? 1}
|
|
221
|
-
className="h-24 text-center"
|
|
222
|
-
>
|
|
235
|
+
<Table.Cell colSpan={totalColSpan} className="h-24 text-center">
|
|
223
236
|
<span className="text-muted-foreground">No data</span>
|
|
224
237
|
</Table.Cell>
|
|
225
238
|
</Table.Row>
|
|
226
239
|
)}
|
|
227
240
|
{rows?.map((row, rowIndex) => (
|
|
228
|
-
<Table.Row
|
|
241
|
+
<Table.Row
|
|
242
|
+
key={rowIndex}
|
|
243
|
+
className={cn(onClickRow && "cursor-pointer")}
|
|
244
|
+
onClick={onClickRow ? () => onClickRow(row) : undefined}
|
|
245
|
+
>
|
|
229
246
|
{columns?.map((col) => {
|
|
230
247
|
const key = col.kind === "field" ? col.dataKey : col.id;
|
|
231
248
|
return (
|
|
@@ -237,6 +254,14 @@ function DataTableBody({
|
|
|
237
254
|
</Table.Cell>
|
|
238
255
|
);
|
|
239
256
|
})}
|
|
257
|
+
{hasRowActions && (
|
|
258
|
+
<Table.Cell
|
|
259
|
+
style={{ width: 50 }}
|
|
260
|
+
onClick={(e) => e.stopPropagation()}
|
|
261
|
+
>
|
|
262
|
+
<RowActionsMenu actions={rowActions} row={row} />
|
|
263
|
+
</Table.Cell>
|
|
264
|
+
)}
|
|
240
265
|
</Table.Row>
|
|
241
266
|
))}
|
|
242
267
|
</Table.Body>
|
|
@@ -308,6 +333,96 @@ function formatValue(value: unknown): ReactNode {
|
|
|
308
333
|
return String(value);
|
|
309
334
|
}
|
|
310
335
|
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// RowActionsMenu
|
|
338
|
+
// =============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Dropdown menu for row-level actions.
|
|
342
|
+
*
|
|
343
|
+
* Uses a simple toggle-based dropdown without external dependencies.
|
|
344
|
+
*/
|
|
345
|
+
function RowActionsMenu<TRow extends Record<string, unknown>>({
|
|
346
|
+
actions,
|
|
347
|
+
row,
|
|
348
|
+
}: {
|
|
349
|
+
actions: RowAction<TRow>[];
|
|
350
|
+
row: TRow;
|
|
351
|
+
}) {
|
|
352
|
+
const [open, setOpen] = useState(false);
|
|
353
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
354
|
+
|
|
355
|
+
// Close on outside click
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (!open) return;
|
|
358
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
359
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
360
|
+
setOpen(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
364
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
365
|
+
}, [open]);
|
|
366
|
+
|
|
367
|
+
// Close on Escape
|
|
368
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
369
|
+
if (e.key === "Escape") setOpen(false);
|
|
370
|
+
}, []);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<div
|
|
374
|
+
className="relative inline-block"
|
|
375
|
+
ref={menuRef}
|
|
376
|
+
onKeyDown={handleKeyDown}
|
|
377
|
+
>
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm hover:bg-accent"
|
|
381
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
382
|
+
aria-label="Row actions"
|
|
383
|
+
aria-haspopup="true"
|
|
384
|
+
aria-expanded={open}
|
|
385
|
+
>
|
|
386
|
+
<span aria-hidden>⋯</span>
|
|
387
|
+
</button>
|
|
388
|
+
{open && (
|
|
389
|
+
<div
|
|
390
|
+
className="absolute right-0 z-50 mt-1 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
391
|
+
role="menu"
|
|
392
|
+
>
|
|
393
|
+
{actions.map((action) => {
|
|
394
|
+
const disabled = action.isDisabled?.(row) ?? false;
|
|
395
|
+
return (
|
|
396
|
+
<button
|
|
397
|
+
key={action.id}
|
|
398
|
+
type="button"
|
|
399
|
+
role="menuitem"
|
|
400
|
+
disabled={disabled}
|
|
401
|
+
className={cn(
|
|
402
|
+
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
|
|
403
|
+
disabled
|
|
404
|
+
? "cursor-not-allowed opacity-50"
|
|
405
|
+
: "cursor-pointer hover:bg-accent",
|
|
406
|
+
action.variant === "destructive" && "text-destructive",
|
|
407
|
+
)}
|
|
408
|
+
onClick={() => {
|
|
409
|
+
if (!disabled) {
|
|
410
|
+
action.onClick(row);
|
|
411
|
+
setOpen(false);
|
|
412
|
+
}
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
{action.icon}
|
|
416
|
+
{action.label}
|
|
417
|
+
</button>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
311
426
|
// =============================================================================
|
|
312
427
|
// DataTable namespace
|
|
313
428
|
// =============================================================================
|
|
@@ -40,6 +40,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
40
40
|
loading = false,
|
|
41
41
|
error = null,
|
|
42
42
|
collection,
|
|
43
|
+
onClickRow,
|
|
44
|
+
rowActions,
|
|
43
45
|
} = options;
|
|
44
46
|
|
|
45
47
|
// ---------------------------------------------------------------------------
|
|
@@ -241,5 +243,9 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
241
243
|
|
|
242
244
|
// Collection (passthrough for DataTable.Provider)
|
|
243
245
|
collection,
|
|
246
|
+
|
|
247
|
+
// Row interaction (passthrough for DataTable.Provider)
|
|
248
|
+
onClickRow,
|
|
249
|
+
rowActions,
|
|
244
250
|
};
|
|
245
251
|
}
|
|
@@ -73,6 +73,43 @@ describe("createColumnHelper()", () => {
|
|
|
73
73
|
expect(col.width).toBe(100);
|
|
74
74
|
expect(typeof col.render).toBe("function");
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it("inferColumns returns column/columns helpers with TRow bound", () => {
|
|
78
|
+
type TaskRow = { id: string; title: string; status: string };
|
|
79
|
+
const metadata = {
|
|
80
|
+
name: "task",
|
|
81
|
+
pluralForm: "tasks",
|
|
82
|
+
readAllowedRoles: [],
|
|
83
|
+
fields: [
|
|
84
|
+
{ name: "id", type: "uuid", required: true },
|
|
85
|
+
{ name: "title", type: "string", required: true },
|
|
86
|
+
{
|
|
87
|
+
name: "status",
|
|
88
|
+
type: "enum",
|
|
89
|
+
required: true,
|
|
90
|
+
enumValues: ["todo", "done"],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
const { inferColumns, display } = createColumnHelper<TaskRow>();
|
|
96
|
+
const { column, columns } = inferColumns(metadata);
|
|
97
|
+
|
|
98
|
+
// column() works with auto-detection
|
|
99
|
+
const titleCol = column("title");
|
|
100
|
+
expect(titleCol.dataKey).toBe("title");
|
|
101
|
+
expect(titleCol.sort).toEqual({ type: "string" });
|
|
102
|
+
|
|
103
|
+
// columns() works
|
|
104
|
+
const cols = columns(["title", "status"]);
|
|
105
|
+
expect(cols).toHaveLength(2);
|
|
106
|
+
|
|
107
|
+
// display still works alongside inferColumns
|
|
108
|
+
const actionsCol = display("actions", {
|
|
109
|
+
render: (row) => `${row.title}`,
|
|
110
|
+
});
|
|
111
|
+
expect(actionsCol.kind).toBe("display");
|
|
112
|
+
});
|
|
76
113
|
});
|
|
77
114
|
|
|
78
115
|
describe("fieldTypeToSortConfig", () => {
|
|
@@ -87,21 +87,27 @@ export function display<TRow extends Record<string, unknown>>(
|
|
|
87
87
|
// =============================================================================
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Create `field` and `
|
|
90
|
+
* Create `field`, `display`, and `inferColumns` helpers with TRow bound once.
|
|
91
91
|
*
|
|
92
|
-
* This avoids repeating the row type parameter on every
|
|
92
|
+
* This avoids repeating the row type parameter on every helper call.
|
|
93
|
+
* Use `inferColumns(tableMetadata)` to create metadata-inferred columns
|
|
94
|
+
* without specifying TRow again.
|
|
93
95
|
*
|
|
94
96
|
* @example
|
|
95
97
|
* ```tsx
|
|
96
98
|
* type Order = { id: string; title: string; amount: number };
|
|
97
99
|
*
|
|
98
|
-
* const { field, display } = createColumnHelper<Order>();
|
|
100
|
+
* const { field, display, inferColumns } = createColumnHelper<Order>();
|
|
99
101
|
*
|
|
100
|
-
*
|
|
102
|
+
* // Manual columns
|
|
103
|
+
* const manualColumns = [
|
|
101
104
|
* field("title", { label: "Title", sort: { type: "string" } }),
|
|
102
|
-
* field("amount", { label: "Amount", sort: { type: "number" } }),
|
|
103
105
|
* display("actions", { render: (row) => <ActionMenu row={row} /> }),
|
|
104
106
|
* ];
|
|
107
|
+
*
|
|
108
|
+
* // Metadata-inferred columns (no need to pass TRow again)
|
|
109
|
+
* const { column } = inferColumns(tableMetadata.order);
|
|
110
|
+
* const inferredColumns = [column("title"), column("amount")];
|
|
105
111
|
* ```
|
|
106
112
|
*/
|
|
107
113
|
export function createColumnHelper<TRow extends Record<string, unknown>>(): {
|
|
@@ -113,10 +119,23 @@ export function createColumnHelper<TRow extends Record<string, unknown>>(): {
|
|
|
113
119
|
id: string,
|
|
114
120
|
options: DisplayColumnOptions<TRow>,
|
|
115
121
|
) => DisplayColumn<TRow>;
|
|
122
|
+
inferColumns: <const TTable extends TableMetadata>(
|
|
123
|
+
tableMetadata: TTable,
|
|
124
|
+
) => {
|
|
125
|
+
column: (
|
|
126
|
+
dataKey: TableFieldName<TTable>,
|
|
127
|
+
options?: MetadataFieldOptions<TRow>,
|
|
128
|
+
) => FieldColumn<TRow>;
|
|
129
|
+
columns: (
|
|
130
|
+
dataKeys: TableFieldName<TTable>[],
|
|
131
|
+
options?: MetadataFieldsOptions,
|
|
132
|
+
) => FieldColumn<TRow>[];
|
|
133
|
+
};
|
|
116
134
|
} {
|
|
117
135
|
return {
|
|
118
136
|
field: (dataKey, options) => field<TRow>(dataKey, options),
|
|
119
137
|
display: (id, options) => display<TRow>(id, options),
|
|
138
|
+
inferColumns: (tableMetadata) => inferColumnHelper<TRow>(tableMetadata),
|
|
120
139
|
};
|
|
121
140
|
}
|
|
122
141
|
|
package/src/component/index.ts
CHANGED
package/src/component/types.ts
CHANGED
|
@@ -405,9 +405,11 @@ export interface UseCollectionOptions<
|
|
|
405
405
|
* Methods that accept a field name are typed with `TFieldName` so that
|
|
406
406
|
* auto-completion works when a concrete union is supplied.
|
|
407
407
|
*
|
|
408
|
-
* **Note:** Methods use *method syntax*
|
|
408
|
+
* **Note:** Methods that accept `TFieldName` use *method syntax* intentionally
|
|
409
409
|
* so that `UseCollectionReturn<"a" | "b">` remains assignable to
|
|
410
410
|
* `UseCollectionReturn<string>` (bivariant method check).
|
|
411
|
+
* Methods that don't depend on `TFieldName` use property syntax so they
|
|
412
|
+
* can be safely destructured without triggering `unbound-method` lint rules.
|
|
411
413
|
*
|
|
412
414
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
413
415
|
* @typeParam TQueryArgs - Type returned by `toQueryArgs()`. Contains both
|
|
@@ -428,7 +430,7 @@ export interface UseCollectionReturn<
|
|
|
428
430
|
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
429
431
|
* ```
|
|
430
432
|
*/
|
|
431
|
-
toQueryArgs()
|
|
433
|
+
toQueryArgs: () => TQueryArgs;
|
|
432
434
|
|
|
433
435
|
// Filter operations
|
|
434
436
|
/** Current active filters */
|
|
@@ -440,11 +442,11 @@ export interface UseCollectionReturn<
|
|
|
440
442
|
value: unknown,
|
|
441
443
|
): void;
|
|
442
444
|
/** Replace all filters at once */
|
|
443
|
-
setFilters(filters: Filter[])
|
|
445
|
+
setFilters: (filters: Filter[]) => void;
|
|
444
446
|
/** Remove filter for a specific field */
|
|
445
447
|
removeFilter(field: TFieldName): void;
|
|
446
448
|
/** Clear all filters */
|
|
447
|
-
clearFilters()
|
|
449
|
+
clearFilters: () => void;
|
|
448
450
|
|
|
449
451
|
// Sort operations
|
|
450
452
|
/** Current sort states (supports multi-sort) */
|
|
@@ -456,7 +458,7 @@ export interface UseCollectionReturn<
|
|
|
456
458
|
append?: boolean,
|
|
457
459
|
): void;
|
|
458
460
|
/** Clear all sort states */
|
|
459
|
-
clearSort()
|
|
461
|
+
clearSort: () => void;
|
|
460
462
|
|
|
461
463
|
// Pagination operations
|
|
462
464
|
/** Number of items per page */
|
|
@@ -466,17 +468,17 @@ export interface UseCollectionReturn<
|
|
|
466
468
|
/** Current pagination direction */
|
|
467
469
|
paginationDirection: "forward" | "backward";
|
|
468
470
|
/** Navigate to next page using endCursor from pageInfo */
|
|
469
|
-
nextPage(endCursor: string)
|
|
471
|
+
nextPage: (endCursor: string) => void;
|
|
470
472
|
/** Navigate to previous page using startCursor from pageInfo */
|
|
471
|
-
prevPage(startCursor: string)
|
|
473
|
+
prevPage: (startCursor: string) => void;
|
|
472
474
|
/** Reset to first page */
|
|
473
|
-
resetPage()
|
|
475
|
+
resetPage: () => void;
|
|
474
476
|
/** Whether there is a previous page (from GraphQL pageInfo) */
|
|
475
477
|
hasPrevPage: boolean;
|
|
476
478
|
/** Whether there is a next page (from GraphQL pageInfo) */
|
|
477
479
|
hasNextPage: boolean;
|
|
478
480
|
/** Set pageInfo from graphql result to track hasPrevPage/hasNextPage */
|
|
479
|
-
setPageInfo(pageInfo: PageInfo)
|
|
481
|
+
setPageInfo: (pageInfo: PageInfo) => void;
|
|
480
482
|
}
|
|
481
483
|
|
|
482
484
|
// =============================================================================
|
|
@@ -497,6 +499,10 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
|
|
|
497
499
|
error?: Error | null;
|
|
498
500
|
/** Collection state for sort/pagination integration */
|
|
499
501
|
collection?: UseCollectionReturn<string, unknown>;
|
|
502
|
+
/** Handler called when a row is clicked */
|
|
503
|
+
onClickRow?: (row: TRow) => void;
|
|
504
|
+
/** Row action definitions for the actions column */
|
|
505
|
+
rowActions?: RowAction<TRow>[];
|
|
500
506
|
}
|
|
501
507
|
|
|
502
508
|
/**
|
|
@@ -511,6 +517,41 @@ export interface RowOperations<TRow extends Record<string, unknown>> {
|
|
|
511
517
|
insertRow: (row: TRow) => { rollback: () => void };
|
|
512
518
|
}
|
|
513
519
|
|
|
520
|
+
// =============================================================================
|
|
521
|
+
// Row Actions
|
|
522
|
+
// =============================================================================
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* A single row action definition for the actions column.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```tsx
|
|
529
|
+
* const actions: RowAction<Order>[] = [
|
|
530
|
+
* {
|
|
531
|
+
* id: "delete",
|
|
532
|
+
* label: "削除",
|
|
533
|
+
* icon: <Trash2 className="h-4 w-4" />,
|
|
534
|
+
* variant: "destructive",
|
|
535
|
+
* onClick: (row) => handleDelete(row.id),
|
|
536
|
+
* },
|
|
537
|
+
* ];
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
export interface RowAction<TRow extends Record<string, unknown>> {
|
|
541
|
+
/** Unique action identifier */
|
|
542
|
+
id: string;
|
|
543
|
+
/** Action label text */
|
|
544
|
+
label: string;
|
|
545
|
+
/** Optional icon element */
|
|
546
|
+
icon?: ReactNode;
|
|
547
|
+
/** Visual variant */
|
|
548
|
+
variant?: "default" | "destructive";
|
|
549
|
+
/** Whether the action is disabled for a given row */
|
|
550
|
+
isDisabled?: (row: TRow) => boolean;
|
|
551
|
+
/** Click handler receiving the row data */
|
|
552
|
+
onClick: (row: TRow) => void;
|
|
553
|
+
}
|
|
554
|
+
|
|
514
555
|
/**
|
|
515
556
|
* Return type of `useDataTable` hook.
|
|
516
557
|
*/
|
|
@@ -564,6 +605,12 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
564
605
|
// Collection (passthrough for DataTable.Provider)
|
|
565
606
|
/** Collection state passed through from options */
|
|
566
607
|
collection: UseCollectionReturn<string, unknown> | undefined;
|
|
608
|
+
|
|
609
|
+
// Row interaction (passthrough for DataTable.Provider)
|
|
610
|
+
/** Handler called when a row is clicked */
|
|
611
|
+
onClickRow?: (row: TRow) => void;
|
|
612
|
+
/** Row action definitions for the actions column */
|
|
613
|
+
rowActions?: RowAction<TRow>[];
|
|
567
614
|
}
|
|
568
615
|
|
|
569
616
|
// =============================================================================
|