@izumisy-tailor/tailor-data-viewer 0.2.20 → 0.2.21
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import type { Column, PageInfo, RowOperations, SortState } from "../types";
|
|
2
|
+
import type { Column, PageInfo, RowAction, RowOperations, SortState } from "../types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Context value provided by `DataTable.Provider`.
|
|
@@ -31,6 +31,12 @@ export interface DataTableContextValue<TRow extends Record<string, unknown>> {
|
|
|
31
31
|
|
|
32
32
|
// Page info from GraphQL response (populated by DataTable.Provider)
|
|
33
33
|
pageInfo: PageInfo;
|
|
34
|
+
|
|
35
|
+
// Row interaction (populated by DataTable.Root)
|
|
36
|
+
/** Handler called when a row is clicked */
|
|
37
|
+
onClickRow?: (row: TRow) => void;
|
|
38
|
+
/** Row action definitions for the actions column */
|
|
39
|
+
rowActions?: RowAction<TRow>[];
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
// 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) {
|
|
@@ -197,7 +219,7 @@ function DataTableBody({
|
|
|
197
219
|
{loading && (!rows || rows.length === 0) && (
|
|
198
220
|
<Table.Row>
|
|
199
221
|
<Table.Cell
|
|
200
|
-
colSpan={
|
|
222
|
+
colSpan={totalColSpan}
|
|
201
223
|
className="h-24 text-center"
|
|
202
224
|
>
|
|
203
225
|
<span className="text-muted-foreground">Loading...</span>
|
|
@@ -207,7 +229,7 @@ function DataTableBody({
|
|
|
207
229
|
{error && (
|
|
208
230
|
<Table.Row>
|
|
209
231
|
<Table.Cell
|
|
210
|
-
colSpan={
|
|
232
|
+
colSpan={totalColSpan}
|
|
211
233
|
className="h-24 text-center"
|
|
212
234
|
>
|
|
213
235
|
<span className="text-destructive">Error: {error.message}</span>
|
|
@@ -217,7 +239,7 @@ function DataTableBody({
|
|
|
217
239
|
{!loading && !error && (!rows || rows.length === 0) && (
|
|
218
240
|
<Table.Row>
|
|
219
241
|
<Table.Cell
|
|
220
|
-
colSpan={
|
|
242
|
+
colSpan={totalColSpan}
|
|
221
243
|
className="h-24 text-center"
|
|
222
244
|
>
|
|
223
245
|
<span className="text-muted-foreground">No data</span>
|
|
@@ -225,7 +247,11 @@ function DataTableBody({
|
|
|
225
247
|
</Table.Row>
|
|
226
248
|
)}
|
|
227
249
|
{rows?.map((row, rowIndex) => (
|
|
228
|
-
<Table.Row
|
|
250
|
+
<Table.Row
|
|
251
|
+
key={rowIndex}
|
|
252
|
+
className={cn(onClickRow && "cursor-pointer")}
|
|
253
|
+
onClick={onClickRow ? () => onClickRow(row) : undefined}
|
|
254
|
+
>
|
|
229
255
|
{columns?.map((col) => {
|
|
230
256
|
const key = col.kind === "field" ? col.dataKey : col.id;
|
|
231
257
|
return (
|
|
@@ -237,6 +263,14 @@ function DataTableBody({
|
|
|
237
263
|
</Table.Cell>
|
|
238
264
|
);
|
|
239
265
|
})}
|
|
266
|
+
{hasRowActions && (
|
|
267
|
+
<Table.Cell
|
|
268
|
+
style={{ width: 50 }}
|
|
269
|
+
onClick={(e) => e.stopPropagation()}
|
|
270
|
+
>
|
|
271
|
+
<RowActionsMenu actions={rowActions} row={row} />
|
|
272
|
+
</Table.Cell>
|
|
273
|
+
)}
|
|
240
274
|
</Table.Row>
|
|
241
275
|
))}
|
|
242
276
|
</Table.Body>
|
|
@@ -308,6 +342,95 @@ function formatValue(value: unknown): ReactNode {
|
|
|
308
342
|
return String(value);
|
|
309
343
|
}
|
|
310
344
|
|
|
345
|
+
// =============================================================================
|
|
346
|
+
// RowActionsMenu
|
|
347
|
+
// =============================================================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Dropdown menu for row-level actions.
|
|
351
|
+
*
|
|
352
|
+
* Uses a simple toggle-based dropdown without external dependencies.
|
|
353
|
+
*/
|
|
354
|
+
function RowActionsMenu<TRow extends Record<string, unknown>>({
|
|
355
|
+
actions,
|
|
356
|
+
row,
|
|
357
|
+
}: {
|
|
358
|
+
actions: RowAction<TRow>[];
|
|
359
|
+
row: TRow;
|
|
360
|
+
}) {
|
|
361
|
+
const [open, setOpen] = useState(false);
|
|
362
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
363
|
+
|
|
364
|
+
// Close on outside click
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (!open) return;
|
|
367
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
368
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
369
|
+
setOpen(false);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
373
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
374
|
+
}, [open]);
|
|
375
|
+
|
|
376
|
+
// Close on Escape
|
|
377
|
+
const handleKeyDown = useCallback(
|
|
378
|
+
(e: React.KeyboardEvent) => {
|
|
379
|
+
if (e.key === "Escape") setOpen(false);
|
|
380
|
+
},
|
|
381
|
+
[],
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div className="relative inline-block" ref={menuRef} onKeyDown={handleKeyDown}>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm hover:bg-accent"
|
|
389
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
390
|
+
aria-label="Row actions"
|
|
391
|
+
aria-haspopup="true"
|
|
392
|
+
aria-expanded={open}
|
|
393
|
+
>
|
|
394
|
+
<span aria-hidden>⋯</span>
|
|
395
|
+
</button>
|
|
396
|
+
{open && (
|
|
397
|
+
<div
|
|
398
|
+
className="absolute right-0 z-50 mt-1 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
399
|
+
role="menu"
|
|
400
|
+
>
|
|
401
|
+
{actions.map((action) => {
|
|
402
|
+
const disabled = action.isDisabled?.(row) ?? false;
|
|
403
|
+
return (
|
|
404
|
+
<button
|
|
405
|
+
key={action.id}
|
|
406
|
+
type="button"
|
|
407
|
+
role="menuitem"
|
|
408
|
+
disabled={disabled}
|
|
409
|
+
className={cn(
|
|
410
|
+
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
|
|
411
|
+
disabled
|
|
412
|
+
? "cursor-not-allowed opacity-50"
|
|
413
|
+
: "cursor-pointer hover:bg-accent",
|
|
414
|
+
action.variant === "destructive" && "text-destructive",
|
|
415
|
+
)}
|
|
416
|
+
onClick={() => {
|
|
417
|
+
if (!disabled) {
|
|
418
|
+
action.onClick(row);
|
|
419
|
+
setOpen(false);
|
|
420
|
+
}
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
{action.icon}
|
|
424
|
+
{action.label}
|
|
425
|
+
</button>
|
|
426
|
+
);
|
|
427
|
+
})}
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
311
434
|
// =============================================================================
|
|
312
435
|
// DataTable namespace
|
|
313
436
|
// =============================================================================
|
|
@@ -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
|
}
|
package/src/component/index.ts
CHANGED
package/src/component/types.ts
CHANGED
|
@@ -497,6 +497,10 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
|
|
|
497
497
|
error?: Error | null;
|
|
498
498
|
/** Collection state for sort/pagination integration */
|
|
499
499
|
collection?: UseCollectionReturn<string, unknown>;
|
|
500
|
+
/** Handler called when a row is clicked */
|
|
501
|
+
onClickRow?: (row: TRow) => void;
|
|
502
|
+
/** Row action definitions for the actions column */
|
|
503
|
+
rowActions?: RowAction<TRow>[];
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
/**
|
|
@@ -511,6 +515,41 @@ export interface RowOperations<TRow extends Record<string, unknown>> {
|
|
|
511
515
|
insertRow: (row: TRow) => { rollback: () => void };
|
|
512
516
|
}
|
|
513
517
|
|
|
518
|
+
// =============================================================================
|
|
519
|
+
// Row Actions
|
|
520
|
+
// =============================================================================
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* A single row action definition for the actions column.
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```tsx
|
|
527
|
+
* const actions: RowAction<Order>[] = [
|
|
528
|
+
* {
|
|
529
|
+
* id: "delete",
|
|
530
|
+
* label: "削除",
|
|
531
|
+
* icon: <Trash2 className="h-4 w-4" />,
|
|
532
|
+
* variant: "destructive",
|
|
533
|
+
* onClick: (row) => handleDelete(row.id),
|
|
534
|
+
* },
|
|
535
|
+
* ];
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
export interface RowAction<TRow extends Record<string, unknown>> {
|
|
539
|
+
/** Unique action identifier */
|
|
540
|
+
id: string;
|
|
541
|
+
/** Action label text */
|
|
542
|
+
label: string;
|
|
543
|
+
/** Optional icon element */
|
|
544
|
+
icon?: ReactNode;
|
|
545
|
+
/** Visual variant */
|
|
546
|
+
variant?: "default" | "destructive";
|
|
547
|
+
/** Whether the action is disabled for a given row */
|
|
548
|
+
isDisabled?: (row: TRow) => boolean;
|
|
549
|
+
/** Click handler receiving the row data */
|
|
550
|
+
onClick: (row: TRow) => void;
|
|
551
|
+
}
|
|
552
|
+
|
|
514
553
|
/**
|
|
515
554
|
* Return type of `useDataTable` hook.
|
|
516
555
|
*/
|
|
@@ -564,6 +603,12 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
564
603
|
// Collection (passthrough for DataTable.Provider)
|
|
565
604
|
/** Collection state passed through from options */
|
|
566
605
|
collection: UseCollectionReturn<string, unknown> | undefined;
|
|
606
|
+
|
|
607
|
+
// Row interaction (passthrough for DataTable.Provider)
|
|
608
|
+
/** Handler called when a row is clicked */
|
|
609
|
+
onClickRow?: (row: TRow) => void;
|
|
610
|
+
/** Row action definitions for the actions column */
|
|
611
|
+
rowActions?: RowAction<TRow>[];
|
|
567
612
|
}
|
|
568
613
|
|
|
569
614
|
// =============================================================================
|