@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,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.2.20",
4
+ "version": "0.2.21",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -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 is read from context.
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={columns?.length ?? 1}
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={columns?.length ?? 1}
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={columns?.length ?? 1}
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 key={rowIndex}>
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
  }
@@ -22,6 +22,7 @@ export type {
22
22
  UseCollectionReturn,
23
23
  UseDataTableOptions,
24
24
  UseDataTableReturn,
25
+ RowAction,
25
26
  RowOperations,
26
27
  FieldName,
27
28
  TableFieldName,
@@ -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
  // =============================================================================