@izumisy-tailor/tailor-data-viewer 0.2.22 → 0.2.23

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/README.md CHANGED
@@ -11,7 +11,7 @@ A low-level React component library for building data table interfaces with Tail
11
11
  - **`DataTable.*` Compound Components**: Data-bound table with sort indicators, cell renderers, and `useDataTable` integration
12
12
  - **`useDataTable` Hook**: Integrates data, column visibility, row operations (optimistic updates), and props generators
13
13
  - **Column Definition Helpers**: `field()` for data columns with sort/filter, `display()` for render-only columns
14
- - **Metadata-based Inference**: `inferColumnHelper()` auto-derives sort/filter config from generated table metadata
14
+ - **Metadata-based Inference**: `createColumnHelper().inferColumns()` auto-derives sort/filter config from generated table metadata
15
15
  - **Utility Components**: `ColumnSelector`, `CsvButton`, `SearchFilterForm`, `Pagination` — all props-based, spreadable from hooks
16
16
  - **Multi-sort Support**: Multiple simultaneous sort fields
17
17
  - **Optimistic Updates**: `updateRow`, `deleteRow`, `insertRow` with rollback
@@ -49,27 +49,27 @@ npm install react react-dom
49
49
  import {
50
50
  useCollection,
51
51
  useDataTable,
52
- Collection,
53
52
  DataTable,
54
53
  Pagination,
55
- field,
56
- display,
54
+ createColumnHelper,
57
55
  } from "@izumisy-tailor/tailor-data-viewer/component";
58
56
  import "@izumisy-tailor/tailor-data-viewer/styles/theme.css";
59
57
 
60
58
  // 1. Define columns
59
+ const { field, display } = createColumnHelper<Order>();
60
+
61
61
  const columns = [
62
- field<Order>("name", {
62
+ field("name", {
63
63
  label: "Name",
64
64
  sort: { type: "string" },
65
65
  filter: { type: "string" },
66
66
  }),
67
- field<Order>("amount", {
67
+ field("amount", {
68
68
  label: "Amount",
69
69
  sort: { type: "number" },
70
70
  filter: { type: "number" },
71
71
  }),
72
- field<Order>("status", {
72
+ field("status", {
73
73
  label: "Status",
74
74
  filter: {
75
75
  type: "enum",
@@ -79,7 +79,7 @@ const columns = [
79
79
  ],
80
80
  },
81
81
  }),
82
- display<Order>("actions", {
82
+ display("actions", {
83
83
  width: 50,
84
84
  render: (row) => <button onClick={() => handleEdit(row)}>Edit</button>,
85
85
  }),
@@ -98,13 +98,13 @@ function OrdersPage() {
98
98
  });
99
99
 
100
100
  return (
101
- <Collection.Provider value={collection}>
102
- <DataTable.Root {...table.rootProps}>
101
+ <DataTable.Provider value={table}>
102
+ <DataTable.Root>
103
103
  <DataTable.Headers />
104
104
  <DataTable.Body />
105
105
  </DataTable.Root>
106
- <Pagination {...table} />
107
- </Collection.Provider>
106
+ <Pagination />
107
+ </DataTable.Provider>
108
108
  );
109
109
  }
110
110
  ```
@@ -128,8 +128,8 @@ const collection = useCollection({
128
128
  const [result] = useQuery({ ...collection.toQueryArgs() });
129
129
 
130
130
  // Filter operations
131
- collection.addFilter("status", "ACTIVE", "eq");
132
- collection.setFilters([{ field: "status", fieldType: "enum", operator: "eq", value: "ACTIVE" }]);
131
+ collection.addFilter("status", "eq", "ACTIVE");
132
+ collection.setFilters([{ field: "status", operator: "eq", value: "ACTIVE" }]);
133
133
  collection.removeFilter("status");
134
134
  collection.clearFilters();
135
135
 
@@ -140,26 +140,40 @@ collection.clearSort();
140
140
 
141
141
  // Pagination
142
142
  collection.nextPage(endCursor);
143
- collection.prevPage();
143
+ collection.prevPage(startCursor);
144
144
  collection.resetPage();
145
+
146
+ // Page info tracking
147
+ collection.setPageInfo(pageInfo);
148
+ collection.hasPrevPage; // boolean
149
+ collection.hasNextPage; // boolean
145
150
  ```
146
151
 
147
- ### `Collection.Provider` / `useCollectionContext()`
152
+ ### `DataTable.Provider` / `useDataTableContext()` / `useCollectionContext()`
148
153
 
149
- Shares `useCollection` return value via Context. Child components access it with `useCollectionContext()`.
154
+ `DataTable.Provider` wraps the table UI and provides both data table and collection context. All utility components (`Pagination`, `ColumnSelector`, `CsvButton`, `SearchFilterForm`) read from this context — no prop spreading needed.
155
+
156
+ When `collection` is passed to `useDataTable`, `DataTable.Provider` automatically wraps a `Collection.Provider` so child components can use `useCollectionContext()`.
150
157
 
151
158
  ```tsx
152
- <Collection.Provider value={collection}>
159
+ <DataTable.Provider value={table}>
153
160
  <StatusFilter /> {/* useCollectionContext() inside */}
154
- <DataTable.Root {...table.rootProps}>
161
+ <DataTable.Root>
155
162
  <DataTable.Headers />
156
163
  <DataTable.Body />
157
164
  </DataTable.Root>
158
- <Pagination {...table} />
159
- </Collection.Provider>
165
+ <Pagination />
166
+ </DataTable.Provider>
160
167
  ```
161
168
 
162
- Provider is optional for simple cases, pass params directly via props.
169
+ For cases where you need `Collection.Provider` without `DataTable.Provider` (e.g., non-table UIs), you can use it standalone:
170
+
171
+ ```tsx
172
+ <Collection.Provider value={collection}>
173
+ <StatusFilter />
174
+ <CustomKanbanBoard />
175
+ </Collection.Provider>
176
+ ```
163
177
 
164
178
  ### Column Definition Helpers
165
179
 
@@ -189,7 +203,7 @@ display("actions", {
189
203
 
190
204
  ### Table Metadata Generator
191
205
 
192
- This library includes a metadata generator for [Tailor Platform SDK](https://www.npmjs.com/package/@tailor-platform/sdk) that produces type-safe table metadata with `as const` assertions. The generated metadata is used by `inferColumnHelper()` for automatic sort/filter configuration.
206
+ This library includes a metadata generator for [Tailor Platform SDK](https://www.npmjs.com/package/@tailor-platform/sdk) that produces type-safe table metadata with `as const` assertions. The generated metadata is used by `createColumnHelper().inferColumns()` for automatic sort/filter configuration.
193
207
 
194
208
  1. Configure the generator in your `tailor.config.ts`:
195
209
 
@@ -215,15 +229,16 @@ export default defineConfig({
215
229
  tailor-sdk generate
216
230
  ```
217
231
 
218
- ### `inferColumnHelper(tableMetadata)`
232
+ ### `createColumnHelper().inferColumns(tableMetadata)`
219
233
 
220
- `field()` requires manually specifying `sort`/`filter` type configs and enum `options` for every column. `inferColumnHelper()` eliminates this boilerplate by automatically deriving these from the generated table metadata. Based on each field's type (string, number, date, enum, etc.), the appropriate `SortConfig` / `FilterConfig` is set automatically, and enum fields get their options populated from the schema.
234
+ `field()` requires manually specifying `sort`/`filter` type configs and enum `options` for every column. `inferColumns()` eliminates this boilerplate by automatically deriving these from the generated table metadata. Based on each field's type (string, number, date, enum, etc.), the appropriate `SortConfig` / `FilterConfig` is set automatically, and enum fields get their options populated from the schema.
221
235
 
222
236
  ```tsx
223
- import { inferColumnHelper, display } from "@izumisy-tailor/tailor-data-viewer/component";
237
+ import { createColumnHelper } from "@izumisy-tailor/tailor-data-viewer/component";
224
238
  import { tableMetadata } from "./generated/data-viewer-metadata.generated";
225
239
 
226
- const { column, columns } = inferColumnHelper(tableMetadata.task);
240
+ const { inferColumns, display } = createColumnHelper<Task>();
241
+ const { column, columns } = inferColumns(tableMetadata.task);
227
242
 
228
243
  const taskColumns = [
229
244
  column("title"), // sort/filter auto-configured from metadata
@@ -237,7 +252,7 @@ const taskColumns = [
237
252
  ];
238
253
  ```
239
254
 
240
- `inferColumnHelper()` can be freely mixed with manual `field()` / `display()` definitions. Use `field()` for fields not in the metadata or those requiring custom configuration, and `inferColumnHelper()` for everything else.
255
+ `inferColumns()` can be freely mixed with manual `field()` / `display()` definitions. Use `field()` for fields not in the metadata or those requiring custom configuration, and `inferColumns()` for everything else.
241
256
 
242
257
  ### `useDataTable(options)`
243
258
 
@@ -250,13 +265,19 @@ const table = useDataTable<Order>({
250
265
  loading: result.fetching,
251
266
  error: result.error,
252
267
  collection,
268
+ onClickRow: (row) => navigate(`/orders/${row.id}`),
269
+ rowActions: [
270
+ { id: "delete", label: "Delete", variant: "destructive", onClick: (row) => handleDelete(row.id) },
271
+ ],
253
272
  });
254
273
 
255
- // Spread props to components
256
- <DataTable.Root {...table.rootProps}>...</DataTable.Root>
257
- <ColumnSelector {...table} />
258
- <CsvButton {...table} />
259
- <Pagination {...table} />
274
+ // Wrap with DataTable.Provider (utility components read from context)
275
+ <DataTable.Provider value={table}>
276
+ <DataTable.Root>...</DataTable.Root>
277
+ <ColumnSelector />
278
+ <CsvButton filename="orders" />
279
+ <Pagination />
280
+ </DataTable.Provider>
260
281
 
261
282
  // Column visibility
262
283
  table.toggleColumn("amount");
@@ -296,52 +317,63 @@ Low-level table primitives without data binding. Use for fully custom layouts or
296
317
  Pair with `useDataTable` for automatic header sorting, cell rendering, and row operations.
297
318
 
298
319
  ```tsx
299
- // Basic usage (spread rootProps)
300
- <DataTable.Root {...table.rootProps}>
301
- <DataTable.Headers />
302
- <DataTable.Body />
303
- </DataTable.Root>
320
+ // Basic usage (wrap with DataTable.Provider)
321
+ <DataTable.Provider value={table}>
322
+ <DataTable.Root>
323
+ <DataTable.Headers />
324
+ <DataTable.Body />
325
+ </DataTable.Root>
326
+ </DataTable.Provider>
304
327
 
305
328
  // Custom row rendering
306
- <DataTable.Root {...table.rootProps}>
307
- <DataTable.Headers />
308
- <DataTable.Body>
309
- {table.rows.map((row, rowIndex) => (
310
- <DataTable.Row
311
- key={row.id}
312
- {...table.getRowProps(row)}
313
- onClick={() => navigate(`/orders/${row.id}`)}
314
- >
315
- {table.visibleColumns.map((col) => (
316
- <DataTable.Cell key={col.id} {...table.getCellProps(row, col, rowIndex)} />
317
- ))}
318
- </DataTable.Row>
319
- ))}
320
- </DataTable.Body>
321
- </DataTable.Root>
329
+ <DataTable.Provider value={table}>
330
+ <DataTable.Root>
331
+ <DataTable.Headers />
332
+ <DataTable.Body>
333
+ {table.rows.map((row) => (
334
+ <DataTable.Row
335
+ key={row.id}
336
+ onClick={() => navigate(`/orders/${row.id}`)}
337
+ >
338
+ {table.visibleColumns.map((col) => (
339
+ <DataTable.Cell key={col.kind === "field" ? col.dataKey : col.id} />
340
+ ))}
341
+ </DataTable.Row>
342
+ ))}
343
+ </DataTable.Body>
344
+ </DataTable.Root>
345
+ </DataTable.Provider>
322
346
  ```
323
347
 
324
348
  ### Utility Components
325
349
 
326
- All utility components are props-based and designed to be used with spread from `useDataTable` / `useCollection`.
350
+ All utility components read from `DataTable.Provider` context.
351
+
352
+ | Component | Props | Description |
353
+ |-----------|-------|-------------|
354
+ | `Pagination` | (none) | Previous/Next page controls |
355
+ | `ColumnSelector` | (none) | Column visibility toggle UI |
356
+ | `CsvButton` | `filename?` | Export visible data as CSV |
357
+ | `SearchFilterForm` | `labels?`, `trigger?` | Multi-field filter form with operator selection |
327
358
 
328
- | Component | Spread from | Description |
329
- |-----------|------------|-------------|
330
- | `ColumnSelector` | `{...table}` | Column visibility toggle UI |
331
- | `CsvButton` | `{...table}` | Export visible data as CSV |
332
- | `SearchFilterForm` | `{...table, ...collection}` | Multi-field filter form with operator selection |
333
- | `Pagination` | `{...table}` | Previous/Next page controls |
359
+ All utility components read from `DataTable.Provider` context — no prop spreading needed:
334
360
 
335
361
  ```tsx
336
- <SearchFilterForm {...table} {...collection} />
337
- <ColumnSelector {...table} />
338
- <CsvButton {...table} filename="orders-export" />
339
- <Pagination {...table} />
362
+ <DataTable.Provider value={table}>
363
+ <SearchFilterForm />
364
+ <ColumnSelector />
365
+ <CsvButton filename="orders-export" />
366
+ <DataTable.Root>
367
+ <DataTable.Headers />
368
+ <DataTable.Body />
369
+ </DataTable.Root>
370
+ <Pagination />
371
+ </DataTable.Provider>
340
372
  ```
341
373
 
342
374
  ### Optimistic Updates in Cell Renderers
343
375
 
344
- Use `useDataTableContext()` inside `DataTable.Root` to access row operations from custom cell renderers.
376
+ Use `useDataTableContext()` inside `DataTable.Provider` to access row operations from custom cell renderers.
345
377
 
346
378
  ```tsx
347
379
  const StatusEditor: CellRenderer<Order> = ({ value, row }) => {
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.22",
4
+ "version": "0.2.23",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, expectTypeOf } from "vitest";
2
- import { createColumnHelper, inferColumnHelper } from "./field-helpers";
2
+ import { createColumnHelper } from "./field-helpers";
3
3
  import type { TableMetadataMap } from "../generator/metadata-generator";
4
4
  import { fieldTypeToSortConfig, fieldTypeToFilterConfig } from "./types";
5
5
  import type { NodeType, TableFieldName } from "./types";
@@ -175,7 +175,7 @@ describe("fieldTypeToFilterConfig", () => {
175
175
  });
176
176
  });
177
177
 
178
- describe("inferColumnHelper()", () => {
178
+ describe("createColumnHelper().inferColumns()", () => {
179
179
  const testMetadata = {
180
180
  task: {
181
181
  name: "task",
@@ -203,8 +203,19 @@ describe("inferColumnHelper()", () => {
203
203
  },
204
204
  } as const satisfies TableMetadataMap;
205
205
 
206
+ type TaskRow = {
207
+ id: string;
208
+ title: string;
209
+ status: string;
210
+ dueDate: string;
211
+ count: number;
212
+ isActive: boolean;
213
+ tags: string[];
214
+ };
215
+
206
216
  it("creates a column with auto-detected sort/filter", () => {
207
- const { column } = inferColumnHelper(testMetadata.task);
217
+ const { inferColumns } = createColumnHelper<TaskRow>();
218
+ const { column } = inferColumns(testMetadata.task);
208
219
 
209
220
  const titleCol = column("title");
210
221
  expect(titleCol.kind).toBe("field");
@@ -214,7 +225,8 @@ describe("inferColumnHelper()", () => {
214
225
  });
215
226
 
216
227
  it("auto-detects enum options", () => {
217
- const { column } = inferColumnHelper(testMetadata.task);
228
+ const { inferColumns } = createColumnHelper<TaskRow>();
229
+ const { column } = inferColumns(testMetadata.task);
218
230
  const statusCol = column("status");
219
231
  expect(statusCol.filter).toEqual({
220
232
  type: "enum",
@@ -229,42 +241,48 @@ describe("inferColumnHelper()", () => {
229
241
  });
230
242
 
231
243
  it("auto-detects date type", () => {
232
- const { column } = inferColumnHelper(testMetadata.task);
244
+ const { inferColumns } = createColumnHelper<TaskRow>();
245
+ const { column } = inferColumns(testMetadata.task);
233
246
  const dateCol = column("dueDate");
234
247
  expect(dateCol.sort).toEqual({ type: "date" });
235
248
  expect(dateCol.filter).toEqual({ type: "date" });
236
249
  });
237
250
 
238
251
  it("disables sort with sort: false", () => {
239
- const { column } = inferColumnHelper(testMetadata.task);
252
+ const { inferColumns } = createColumnHelper<TaskRow>();
253
+ const { column } = inferColumns(testMetadata.task);
240
254
  const col = column("title", { sort: false });
241
255
  expect(col.sort).toBeUndefined();
242
256
  expect(col.filter).toEqual({ type: "string" });
243
257
  });
244
258
 
245
259
  it("disables filter with filter: false", () => {
246
- const { column } = inferColumnHelper(testMetadata.task);
260
+ const { inferColumns } = createColumnHelper<TaskRow>();
261
+ const { column } = inferColumns(testMetadata.task);
247
262
  const col = column("title", { filter: false });
248
263
  expect(col.sort).toEqual({ type: "string" });
249
264
  expect(col.filter).toBeUndefined();
250
265
  });
251
266
 
252
267
  it("uuid has no sort, has uuid filter", () => {
253
- const { column } = inferColumnHelper(testMetadata.task);
268
+ const { inferColumns } = createColumnHelper<TaskRow>();
269
+ const { column } = inferColumns(testMetadata.task);
254
270
  const col = column("id");
255
271
  expect(col.sort).toBeUndefined();
256
272
  expect(col.filter).toEqual({ type: "uuid" });
257
273
  });
258
274
 
259
275
  it("array type has no sort/filter", () => {
260
- const { column } = inferColumnHelper(testMetadata.task);
276
+ const { inferColumns } = createColumnHelper<TaskRow>();
277
+ const { column } = inferColumns(testMetadata.task);
261
278
  const col = column("tags");
262
279
  expect(col.sort).toBeUndefined();
263
280
  expect(col.filter).toBeUndefined();
264
281
  });
265
282
 
266
283
  it("columns() creates multiple columns at once", () => {
267
- const { columns } = inferColumnHelper(testMetadata.task);
284
+ const { inferColumns } = createColumnHelper<TaskRow>();
285
+ const { columns } = inferColumns(testMetadata.task);
268
286
  const cols = columns(["title", "status", "dueDate"]);
269
287
  expect(cols).toHaveLength(3);
270
288
  expect(cols[0].dataKey).toBe("title");
@@ -273,7 +291,8 @@ describe("inferColumnHelper()", () => {
273
291
  });
274
292
 
275
293
  it("columns() applies overrides", () => {
276
- const { columns } = inferColumnHelper(testMetadata.task);
294
+ const { inferColumns } = createColumnHelper<TaskRow>();
295
+ const { columns } = inferColumns(testMetadata.task);
277
296
  const cols = columns(["title", "status"], {
278
297
  overrides: {
279
298
  title: { label: "Custom Title", width: 300 },
@@ -284,7 +303,8 @@ describe("inferColumnHelper()", () => {
284
303
  });
285
304
 
286
305
  it("columns() applies global sort/filter options", () => {
287
- const { columns } = inferColumnHelper(testMetadata.task);
306
+ const { inferColumns } = createColumnHelper<TaskRow>();
307
+ const { columns } = inferColumns(testMetadata.task);
288
308
  const cols = columns(["title", "status"], { sort: false });
289
309
  expect(cols[0].sort).toBeUndefined();
290
310
  expect(cols[1].sort).toBeUndefined();
@@ -293,7 +313,8 @@ describe("inferColumnHelper()", () => {
293
313
  });
294
314
 
295
315
  it("throws for non-existent field", () => {
296
- const { column } = inferColumnHelper(testMetadata.task);
316
+ const { inferColumns } = createColumnHelper<TaskRow>();
317
+ const { column } = inferColumns(testMetadata.task);
297
318
  // @ts-expect-error - intentionally testing invalid field name
298
319
  expect(() => column("nonExistent")).toThrow(
299
320
  'Field "nonExistent" not found in table "task" metadata',
@@ -140,37 +140,10 @@ export function createColumnHelper<TRow extends Record<string, unknown>>(): {
140
140
  }
141
141
 
142
142
  // =============================================================================
143
- // inferColumnHelper() helper
143
+ // Internal: metadata-based column inference (used by createColumnHelper)
144
144
  // =============================================================================
145
145
 
146
- /**
147
- * Create column definition helpers from generated table metadata.
148
- *
149
- * Automatically infers sort/filter configuration from field types,
150
- * including enum options.
151
- *
152
- * @typeParam TRow - The row type for type-safe renderer access.
153
- * @param tableMetadata - A single table metadata object from the generated map.
154
- *
155
- * @example
156
- * ```tsx
157
- * import { tableMetadata } from "./generated/data-viewer-metadata.generated";
158
- *
159
- * type Task = { id: string; title: string; status: string; dueDate: string };
160
- *
161
- * const { column, columns } = inferColumnHelper<Task>(tableMetadata.task);
162
- *
163
- * const taskColumns = [
164
- * column("title"), // sort/filter auto-detected
165
- * column("status"), // enum options auto-populated
166
- * column("dueDate"), // date type auto-recognized
167
- * column("title", {
168
- * renderer: ({ row }) => <span>{row.title}</span>, // row: Task
169
- * }),
170
- * ];
171
- * ```
172
- */
173
- export function inferColumnHelper<
146
+ function inferColumnHelper<
174
147
  TRow extends Record<string, unknown>,
175
148
  const TTable extends TableMetadata = TableMetadata,
176
149
  >(
@@ -65,7 +65,7 @@ export { useDataTable } from "./data-table/use-data-table";
65
65
  export { useDataTableContext } from "./data-table/data-table-context";
66
66
 
67
67
  // Field helpers
68
- export { createColumnHelper, inferColumnHelper } from "./field-helpers";
68
+ export { createColumnHelper } from "./field-helpers";
69
69
 
70
70
  // Utility components
71
71
  export { Pagination } from "./pagination";