@izumisy-tailor/tailor-data-viewer 0.2.21 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.2.21",
4
+ "version": "0.2.22",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,5 +1,11 @@
1
1
  import { createContext, useContext } from "react";
2
- import type { Column, PageInfo, RowAction, RowOperations, SortState } from "../types";
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`.
@@ -218,30 +218,21 @@ function DataTableBody({
218
218
  <Table.Body className={className}>
219
219
  {loading && (!rows || rows.length === 0) && (
220
220
  <Table.Row>
221
- <Table.Cell
222
- colSpan={totalColSpan}
223
- className="h-24 text-center"
224
- >
221
+ <Table.Cell colSpan={totalColSpan} className="h-24 text-center">
225
222
  <span className="text-muted-foreground">Loading...</span>
226
223
  </Table.Cell>
227
224
  </Table.Row>
228
225
  )}
229
226
  {error && (
230
227
  <Table.Row>
231
- <Table.Cell
232
- colSpan={totalColSpan}
233
- className="h-24 text-center"
234
- >
228
+ <Table.Cell colSpan={totalColSpan} className="h-24 text-center">
235
229
  <span className="text-destructive">Error: {error.message}</span>
236
230
  </Table.Cell>
237
231
  </Table.Row>
238
232
  )}
239
233
  {!loading && !error && (!rows || rows.length === 0) && (
240
234
  <Table.Row>
241
- <Table.Cell
242
- colSpan={totalColSpan}
243
- className="h-24 text-center"
244
- >
235
+ <Table.Cell colSpan={totalColSpan} className="h-24 text-center">
245
236
  <span className="text-muted-foreground">No data</span>
246
237
  </Table.Cell>
247
238
  </Table.Row>
@@ -374,15 +365,16 @@ function RowActionsMenu<TRow extends Record<string, unknown>>({
374
365
  }, [open]);
375
366
 
376
367
  // Close on Escape
377
- const handleKeyDown = useCallback(
378
- (e: React.KeyboardEvent) => {
379
- if (e.key === "Escape") setOpen(false);
380
- },
381
- [],
382
- );
368
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
369
+ if (e.key === "Escape") setOpen(false);
370
+ }, []);
383
371
 
384
372
  return (
385
- <div className="relative inline-block" ref={menuRef} onKeyDown={handleKeyDown}>
373
+ <div
374
+ className="relative inline-block"
375
+ ref={menuRef}
376
+ onKeyDown={handleKeyDown}
377
+ >
386
378
  <button
387
379
  type="button"
388
380
  className="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm hover:bg-accent"
@@ -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 `display` helpers with TRow bound once.
90
+ * Create `field`, `display`, and `inferColumns` helpers with TRow bound once.
91
91
  *
92
- * This avoids repeating the row type parameter on every `field()` / `display()` call.
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
- * const columns = [
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
 
@@ -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* (not property syntax) intentionally
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(): TQueryArgs;
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[]): void;
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(): void;
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(): void;
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): void;
471
+ nextPage: (endCursor: string) => void;
470
472
  /** Navigate to previous page using startCursor from pageInfo */
471
- prevPage(startCursor: string): void;
473
+ prevPage: (startCursor: string) => void;
472
474
  /** Reset to first page */
473
- resetPage(): void;
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): void;
481
+ setPageInfo: (pageInfo: PageInfo) => void;
480
482
  }
481
483
 
482
484
  // =============================================================================