@izumisy-tailor/tailor-data-viewer 0.2.32 → 0.2.33

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.
@@ -12,18 +12,17 @@ type TestRow = {
12
12
 
13
13
  const testColumns: Column<TestRow>[] = [
14
14
  {
15
- kind: "field",
16
- dataKey: "name",
17
15
  label: "Name",
18
- sort: { type: "string" },
19
- filter: { type: "string" },
16
+ render: (row) => row.name,
17
+ sort: { field: "name", type: "string" },
18
+ filter: { field: "name", type: "string" },
20
19
  },
21
20
  {
22
- kind: "field",
23
- dataKey: "status",
24
21
  label: "Status",
25
- sort: { type: "string" },
22
+ render: (row) => row.status,
23
+ sort: { field: "status", type: "string" },
26
24
  filter: {
25
+ field: "status",
27
26
  type: "enum",
28
27
  options: [
29
28
  { value: "ACTIVE", label: "Active" },
@@ -32,13 +31,11 @@ const testColumns: Column<TestRow>[] = [
32
31
  },
33
32
  },
34
33
  {
35
- kind: "field",
36
- dataKey: "amount",
37
34
  label: "Amount",
38
- sort: { type: "number" },
35
+ render: (row) => String(row.amount),
36
+ sort: { field: "amount", type: "number" },
39
37
  },
40
38
  {
41
- kind: "display",
42
39
  id: "actions",
43
40
  label: "Actions",
44
41
  render: () => null,
@@ -115,7 +112,7 @@ describe("useDataTable", () => {
115
112
  useDataTable<TestRow>({ columns: testColumns, data: testData }),
116
113
  );
117
114
  expect(result.current.visibleColumns).toHaveLength(4);
118
- expect(result.current.isColumnVisible("name")).toBe(true);
115
+ expect(result.current.isColumnVisible("Name")).toBe(true);
119
116
  expect(result.current.isColumnVisible("actions")).toBe(true);
120
117
  });
121
118
 
@@ -125,16 +122,16 @@ describe("useDataTable", () => {
125
122
  );
126
123
 
127
124
  act(() => {
128
- result.current.toggleColumn("status");
125
+ result.current.toggleColumn("Status");
129
126
  });
130
127
  expect(result.current.visibleColumns).toHaveLength(3);
131
- expect(result.current.isColumnVisible("status")).toBe(false);
128
+ expect(result.current.isColumnVisible("Status")).toBe(false);
132
129
 
133
130
  act(() => {
134
- result.current.toggleColumn("status");
131
+ result.current.toggleColumn("Status");
135
132
  });
136
133
  expect(result.current.visibleColumns).toHaveLength(4);
137
- expect(result.current.isColumnVisible("status")).toBe(true);
134
+ expect(result.current.isColumnVisible("Status")).toBe(true);
138
135
  });
139
136
 
140
137
  it("hideAllColumns hides all columns", () => {
@@ -93,7 +93,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
93
93
  const [hiddenColumns, setHiddenColumns] = useState<Set<string>>(new Set());
94
94
 
95
95
  const getColumnKey = useCallback((col: Column<TRow>): string => {
96
- return col.kind === "field" ? col.dataKey : col.id;
96
+ return col.id ?? col.label ?? "";
97
97
  }, []);
98
98
 
99
99
  const visibleColumns = useMemo<Column<TRow>[]>(() => {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, expectTypeOf } from "vitest";
2
- import { createColumnHelper } from "./field-helpers";
2
+ import { column, inferColumns, 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";
@@ -19,62 +19,58 @@ describe("NodeType", () => {
19
19
  type Row = NodeType<Result>;
20
20
  expectTypeOf<Row>().toEqualTypeOf<{ id: string; amount: number }>();
21
21
  });
22
-
23
- it("works with createColumnHelper", () => {
24
- type Result = { edges: { node: { id: string; title: string } }[] } | null;
25
- type Row = NodeType<Result>;
26
- const { field } = createColumnHelper<Row>();
27
- const col = field("title");
28
- expect(col.dataKey).toBe("title");
29
- });
30
22
  });
31
23
 
32
- describe("createColumnHelper()", () => {
24
+ describe("column()", () => {
33
25
  type TestRow = { name: string; age: number };
34
26
 
35
- it("returns field and display helpers with TRow bound", () => {
36
- const helpers = createColumnHelper<TestRow>();
37
- expect(typeof helpers.field).toBe("function");
38
- expect(typeof helpers.display).toBe("function");
39
- });
40
-
41
- it("field helper creates a field column without explicit type param", () => {
42
- const { field } = createColumnHelper<TestRow>();
43
- const col = field("name", {
27
+ it("creates a column with render and sort/filter", () => {
28
+ const col = column<TestRow>({
44
29
  label: "Name",
45
- sort: { type: "string" },
46
- filter: { type: "string" },
30
+ render: (row) => row.name,
31
+ sort: { field: "name", type: "string" },
32
+ filter: { field: "name", type: "string" },
47
33
  });
48
- expect(col.kind).toBe("field");
49
- expect(col.dataKey).toBe("name");
50
34
  expect(col.label).toBe("Name");
51
- expect(col.sort).toEqual({ type: "string" });
52
- expect(col.filter).toEqual({ type: "string" });
35
+ expect(col.sort).toEqual({ field: "name", type: "string" });
36
+ expect(col.filter).toEqual({ field: "name", type: "string" });
37
+ expect(typeof col.render).toBe("function");
53
38
  });
54
39
 
55
- it("field helper creates minimal column", () => {
56
- const { field } = createColumnHelper<TestRow>();
57
- const col = field("age");
58
- expect(col.kind).toBe("field");
59
- expect(col.dataKey).toBe("age");
60
- expect(col.label).toBeUndefined();
40
+ it("creates minimal column with label and render", () => {
41
+ const col = column<TestRow>({
42
+ label: "Age",
43
+ render: (row) => String(row.age),
44
+ });
45
+ expect(col.label).toBe("Age");
46
+ expect(col.sort).toBeUndefined();
47
+ expect(col.filter).toBeUndefined();
61
48
  });
62
49
 
63
- it("display helper creates a display column without explicit type param", () => {
64
- const { display } = createColumnHelper<TestRow>();
65
- const col = display("actions", {
50
+ it("supports accessor and width", () => {
51
+ const col = column<TestRow>({
66
52
  label: "Actions",
67
53
  width: 100,
68
54
  render: (row) => `${row.name}: ${row.age}`,
55
+ accessor: (row) => row.name,
69
56
  });
70
- expect(col.kind).toBe("display");
71
- expect(col.id).toBe("actions");
72
57
  expect(col.label).toBe("Actions");
73
58
  expect(col.width).toBe(100);
74
59
  expect(typeof col.render).toBe("function");
60
+ expect(typeof col.accessor).toBe("function");
61
+ });
62
+
63
+ it("allows undefined label", () => {
64
+ const col = column<TestRow>({
65
+ render: (row) => row.name,
66
+ });
67
+ expect(col.label).toBeUndefined();
68
+ expect(typeof col.render).toBe("function");
75
69
  });
70
+ });
76
71
 
77
- it("inferColumns returns column/columns helpers with TRow bound", () => {
72
+ describe("inferColumns()", () => {
73
+ it("returns a function that produces column options from metadata", () => {
78
74
  type TaskRow = { id: string; title: string; status: string };
79
75
  const metadata = {
80
76
  name: "task",
@@ -92,75 +88,103 @@ describe("createColumnHelper()", () => {
92
88
  ],
93
89
  } as const;
94
90
 
95
- const { inferColumns, display } = createColumnHelper<TaskRow>();
96
- const { column, columns } = inferColumns(metadata);
91
+ const infer = inferColumns<TaskRow>(metadata);
97
92
 
98
- // column() works with auto-detection
99
- const titleCol = column("title");
100
- expect(titleCol.dataKey).toBe("title");
101
- expect(titleCol.sort).toEqual({ type: "string" });
93
+ // infer() returns ColumnOptions that can be passed to column()
94
+ const titleOpts = infer("title");
95
+ expect(titleOpts.label).toBe("title");
96
+ expect(titleOpts.sort).toEqual({ field: "title", type: "string" });
97
+ expect(typeof titleOpts.render).toBe("function");
98
+ expect(typeof titleOpts.accessor).toBe("function");
102
99
 
103
- // columns() works
104
- const cols = columns(["title", "status"]);
105
- expect(cols).toHaveLength(2);
100
+ // Can be used with column()
101
+ const titleCol = column(infer("title"));
102
+ expect(titleCol.label).toBe("title");
103
+ expect(titleCol.sort).toEqual({ field: "title", type: "string" });
106
104
 
107
- // display still works alongside inferColumns
108
- const actionsCol = display("actions", {
109
- render: (row) => `${row.title}`,
105
+ // Can be spread and overridden
106
+ const customCol = column({
107
+ ...infer("status"),
108
+ label: "Custom Status",
110
109
  });
111
- expect(actionsCol.kind).toBe("display");
110
+ expect(customCol.label).toBe("Custom Status");
112
111
  });
113
112
  });
114
113
 
115
114
  describe("fieldTypeToSortConfig", () => {
116
115
  it("maps string to string sort", () => {
117
- expect(fieldTypeToSortConfig("string")).toEqual({ type: "string" });
116
+ expect(fieldTypeToSortConfig("name", "string")).toEqual({
117
+ field: "name",
118
+ type: "string",
119
+ });
118
120
  });
119
121
 
120
122
  it("maps number to number sort", () => {
121
- expect(fieldTypeToSortConfig("number")).toEqual({ type: "number" });
123
+ expect(fieldTypeToSortConfig("amount", "number")).toEqual({
124
+ field: "amount",
125
+ type: "number",
126
+ });
122
127
  });
123
128
 
124
129
  it("maps datetime to date sort", () => {
125
- expect(fieldTypeToSortConfig("datetime")).toEqual({ type: "date" });
130
+ expect(fieldTypeToSortConfig("createdAt", "datetime")).toEqual({
131
+ field: "createdAt",
132
+ type: "date",
133
+ });
126
134
  });
127
135
 
128
136
  it("maps date to date sort", () => {
129
- expect(fieldTypeToSortConfig("date")).toEqual({ type: "date" });
137
+ expect(fieldTypeToSortConfig("dueDate", "date")).toEqual({
138
+ field: "dueDate",
139
+ type: "date",
140
+ });
130
141
  });
131
142
 
132
143
  it("maps enum to string sort", () => {
133
- expect(fieldTypeToSortConfig("enum")).toEqual({ type: "string" });
144
+ expect(fieldTypeToSortConfig("status", "enum")).toEqual({
145
+ field: "status",
146
+ type: "string",
147
+ });
134
148
  });
135
149
 
136
150
  it("returns undefined for uuid", () => {
137
- expect(fieldTypeToSortConfig("uuid")).toBeUndefined();
151
+ expect(fieldTypeToSortConfig("id", "uuid")).toBeUndefined();
138
152
  });
139
153
 
140
154
  it("returns undefined for array", () => {
141
- expect(fieldTypeToSortConfig("array")).toBeUndefined();
155
+ expect(fieldTypeToSortConfig("tags", "array")).toBeUndefined();
142
156
  });
143
157
 
144
158
  it("returns undefined for nested", () => {
145
- expect(fieldTypeToSortConfig("nested")).toBeUndefined();
159
+ expect(fieldTypeToSortConfig("meta", "nested")).toBeUndefined();
146
160
  });
147
161
  });
148
162
 
149
163
  describe("fieldTypeToFilterConfig", () => {
150
164
  it("maps string to string filter", () => {
151
- expect(fieldTypeToFilterConfig("string")).toEqual({ type: "string" });
165
+ expect(fieldTypeToFilterConfig("name", "string")).toEqual({
166
+ field: "name",
167
+ type: "string",
168
+ });
152
169
  });
153
170
 
154
171
  it("maps number to number filter", () => {
155
- expect(fieldTypeToFilterConfig("number")).toEqual({ type: "number" });
172
+ expect(fieldTypeToFilterConfig("amount", "number")).toEqual({
173
+ field: "amount",
174
+ type: "number",
175
+ });
156
176
  });
157
177
 
158
178
  it("maps uuid to uuid filter", () => {
159
- expect(fieldTypeToFilterConfig("uuid")).toEqual({ type: "uuid" });
179
+ expect(fieldTypeToFilterConfig("id", "uuid")).toEqual({
180
+ field: "id",
181
+ type: "uuid",
182
+ });
160
183
  });
161
184
 
162
185
  it("maps enum with values to enum filter", () => {
163
- expect(fieldTypeToFilterConfig("enum", ["a", "b", "c"])).toEqual({
186
+ expect(fieldTypeToFilterConfig("status", "enum", ["a", "b", "c"])).toEqual({
187
+ field: "status",
164
188
  type: "enum",
165
189
  options: [
166
190
  { value: "a", label: "a" },
@@ -171,11 +195,11 @@ describe("fieldTypeToFilterConfig", () => {
171
195
  });
172
196
 
173
197
  it("returns undefined for array", () => {
174
- expect(fieldTypeToFilterConfig("array")).toBeUndefined();
198
+ expect(fieldTypeToFilterConfig("tags", "array")).toBeUndefined();
175
199
  });
176
200
  });
177
201
 
178
- describe("createColumnHelper().inferColumns()", () => {
202
+ describe("inferColumns() with metadata", () => {
179
203
  const testMetadata = {
180
204
  task: {
181
205
  name: "task",
@@ -213,22 +237,22 @@ describe("createColumnHelper().inferColumns()", () => {
213
237
  tags: string[];
214
238
  };
215
239
 
216
- it("creates a column with auto-detected sort/filter", () => {
217
- const { inferColumns } = createColumnHelper<TaskRow>();
218
- const { column } = inferColumns(testMetadata.task);
240
+ it("creates column options with auto-detected sort/filter", () => {
241
+ const infer = inferColumns<TaskRow>(testMetadata.task);
219
242
 
220
- const titleCol = column("title");
221
- expect(titleCol.kind).toBe("field");
222
- expect(titleCol.dataKey).toBe("title");
223
- expect(titleCol.sort).toEqual({ type: "string" });
224
- expect(titleCol.filter).toEqual({ type: "string" });
243
+ const titleOpts = infer("title");
244
+ expect(titleOpts.label).toBe("title");
245
+ expect(titleOpts.sort).toEqual({ field: "title", type: "string" });
246
+ expect(titleOpts.filter).toEqual({ field: "title", type: "string" });
247
+ expect(typeof titleOpts.render).toBe("function");
248
+ expect(typeof titleOpts.accessor).toBe("function");
225
249
  });
226
250
 
227
251
  it("auto-detects enum options", () => {
228
- const { inferColumns } = createColumnHelper<TaskRow>();
229
- const { column } = inferColumns(testMetadata.task);
230
- const statusCol = column("status");
231
- expect(statusCol.filter).toEqual({
252
+ const infer = inferColumns<TaskRow>(testMetadata.task);
253
+ const statusOpts = infer("status");
254
+ expect(statusOpts.filter).toEqual({
255
+ field: "status",
232
256
  type: "enum",
233
257
  options: [
234
258
  { value: "todo", label: "todo" },
@@ -237,88 +261,68 @@ describe("createColumnHelper().inferColumns()", () => {
237
261
  ],
238
262
  });
239
263
  // enum maps to string sort
240
- expect(statusCol.sort).toEqual({ type: "string" });
264
+ expect(statusOpts.sort).toEqual({ field: "status", type: "string" });
241
265
  });
242
266
 
243
267
  it("auto-detects date type", () => {
244
- const { inferColumns } = createColumnHelper<TaskRow>();
245
- const { column } = inferColumns(testMetadata.task);
246
- const dateCol = column("dueDate");
247
- expect(dateCol.sort).toEqual({ type: "date" });
248
- expect(dateCol.filter).toEqual({ type: "date" });
268
+ const infer = inferColumns<TaskRow>(testMetadata.task);
269
+ const dateOpts = infer("dueDate");
270
+ expect(dateOpts.sort).toEqual({ field: "dueDate", type: "date" });
271
+ expect(dateOpts.filter).toEqual({ field: "dueDate", type: "date" });
249
272
  });
250
273
 
251
274
  it("disables sort with sort: false", () => {
252
- const { inferColumns } = createColumnHelper<TaskRow>();
253
- const { column } = inferColumns(testMetadata.task);
254
- const col = column("title", { sort: false });
255
- expect(col.sort).toBeUndefined();
256
- expect(col.filter).toEqual({ type: "string" });
275
+ const infer = inferColumns<TaskRow>(testMetadata.task);
276
+ const opts = infer("title", { sort: false });
277
+ expect(opts.sort).toBeUndefined();
278
+ expect(opts.filter).toEqual({ field: "title", type: "string" });
257
279
  });
258
280
 
259
281
  it("disables filter with filter: false", () => {
260
- const { inferColumns } = createColumnHelper<TaskRow>();
261
- const { column } = inferColumns(testMetadata.task);
262
- const col = column("title", { filter: false });
263
- expect(col.sort).toEqual({ type: "string" });
264
- expect(col.filter).toBeUndefined();
282
+ const infer = inferColumns<TaskRow>(testMetadata.task);
283
+ const opts = infer("title", { filter: false });
284
+ expect(opts.sort).toEqual({ field: "title", type: "string" });
285
+ expect(opts.filter).toBeUndefined();
265
286
  });
266
287
 
267
288
  it("uuid has no sort, has uuid filter", () => {
268
- const { inferColumns } = createColumnHelper<TaskRow>();
269
- const { column } = inferColumns(testMetadata.task);
270
- const col = column("id");
271
- expect(col.sort).toBeUndefined();
272
- expect(col.filter).toEqual({ type: "uuid" });
289
+ const infer = inferColumns<TaskRow>(testMetadata.task);
290
+ const opts = infer("id");
291
+ expect(opts.sort).toBeUndefined();
292
+ expect(opts.filter).toEqual({ field: "id", type: "uuid" });
273
293
  });
274
294
 
275
295
  it("array type has no sort/filter", () => {
276
- const { inferColumns } = createColumnHelper<TaskRow>();
277
- const { column } = inferColumns(testMetadata.task);
278
- const col = column("tags");
279
- expect(col.sort).toBeUndefined();
280
- expect(col.filter).toBeUndefined();
296
+ const infer = inferColumns<TaskRow>(testMetadata.task);
297
+ const opts = infer("tags");
298
+ expect(opts.sort).toBeUndefined();
299
+ expect(opts.filter).toBeUndefined();
281
300
  });
282
301
 
283
- it("columns() creates multiple columns at once", () => {
284
- const { inferColumns } = createColumnHelper<TaskRow>();
285
- const { columns } = inferColumns(testMetadata.task);
286
- const cols = columns(["title", "status", "dueDate"]);
287
- expect(cols).toHaveLength(3);
288
- expect(cols[0].dataKey).toBe("title");
289
- expect(cols[1].dataKey).toBe("status");
290
- expect(cols[2].dataKey).toBe("dueDate");
291
- });
302
+ it("generates default render and accessor functions", () => {
303
+ const infer = inferColumns<TaskRow>(testMetadata.task);
292
304
 
293
- it("columns() applies overrides", () => {
294
- const { inferColumns } = createColumnHelper<TaskRow>();
295
- const { columns } = inferColumns(testMetadata.task);
296
- const cols = columns(["title", "status"], {
297
- overrides: {
298
- title: { label: "Custom Title", width: 300 },
299
- },
300
- });
301
- expect(cols[0].label).toBe("Custom Title");
302
- expect(cols[0].width).toBe(300);
303
- });
305
+ const opts = infer("title");
306
+ const testRow = {
307
+ id: "1",
308
+ title: "Test Task",
309
+ status: "todo",
310
+ dueDate: "2024-01-01",
311
+ count: 5,
312
+ isActive: true,
313
+ tags: ["a"],
314
+ };
304
315
 
305
- it("columns() applies global sort/filter options", () => {
306
- const { inferColumns } = createColumnHelper<TaskRow>();
307
- const { columns } = inferColumns(testMetadata.task);
308
- const cols = columns(["title", "status"], { sort: false });
309
- expect(cols[0].sort).toBeUndefined();
310
- expect(cols[1].sort).toBeUndefined();
311
- // filter should still be auto-detected
312
- expect(cols[0].filter).toEqual({ type: "string" });
316
+ expect(opts.render(testRow)).toBe("Test Task");
317
+ expect(opts.accessor!(testRow)).toBe("Test Task");
313
318
  });
314
319
 
315
320
  it("throws for non-existent field", () => {
316
- const { inferColumns } = createColumnHelper<TaskRow>();
317
- const { column } = inferColumns(testMetadata.task);
318
- // @ts-expect-error - intentionally testing invalid field name
319
- expect(() => column("nonExistent")).toThrow(
320
- 'Field "nonExistent" not found in table "task" metadata',
321
- );
321
+ const infer = inferColumns<TaskRow>(testMetadata.task);
322
+ expect(() =>
323
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
324
+ infer("nonExistent" as any),
325
+ ).toThrow('Field "nonExistent" not found in table "task" metadata');
322
326
  });
323
327
 
324
328
  it("infers TableFieldName type correctly", () => {
@@ -327,4 +331,83 @@ describe("createColumnHelper().inferColumns()", () => {
327
331
  "id" | "title" | "status" | "dueDate" | "count" | "isActive" | "tags"
328
332
  >();
329
333
  });
334
+
335
+ it("spread override works with column()", () => {
336
+ const infer = inferColumns<TaskRow>(testMetadata.task);
337
+
338
+ const col = column({
339
+ ...infer("title"),
340
+ label: "Custom Title",
341
+ width: 200,
342
+ });
343
+ expect(col.label).toBe("Custom Title");
344
+ expect(col.width).toBe(200);
345
+ expect(col.sort).toEqual({ field: "title", type: "string" });
346
+ });
347
+ });
348
+
349
+ describe("createColumnHelper()", () => {
350
+ type OrderRow = { id: string; name: string; amount: number };
351
+
352
+ it("returns column and inferColumns with TRow bound", () => {
353
+ const helper = createColumnHelper<OrderRow>();
354
+ expect(typeof helper.column).toBe("function");
355
+ expect(typeof helper.inferColumns).toBe("function");
356
+ });
357
+
358
+ it("column() works without type parameter", () => {
359
+ const { column } = createColumnHelper<OrderRow>();
360
+ const col = column({
361
+ label: "Name",
362
+ render: (row) => row.name,
363
+ sort: { field: "name", type: "string" },
364
+ });
365
+ expect(col.label).toBe("Name");
366
+ expect(col.sort).toEqual({ field: "name", type: "string" });
367
+ });
368
+
369
+ it("inferColumns() works without type parameter", () => {
370
+ const metadata = {
371
+ name: "order",
372
+ pluralForm: "orders",
373
+ readAllowedRoles: [],
374
+ fields: [
375
+ { name: "id", type: "uuid", required: true },
376
+ { name: "name", type: "string", required: true },
377
+ { name: "amount", type: "number", required: false },
378
+ ],
379
+ } as const;
380
+
381
+ const { column, inferColumns } = createColumnHelper<OrderRow>();
382
+ const infer = inferColumns(metadata);
383
+
384
+ const col = column(infer("name"));
385
+ expect(col.label).toBe("name");
386
+ expect(col.sort).toEqual({ field: "name", type: "string" });
387
+ });
388
+
389
+ it("column + inferColumns spread override", () => {
390
+ const metadata = {
391
+ name: "order",
392
+ pluralForm: "orders",
393
+ readAllowedRoles: [],
394
+ fields: [
395
+ { name: "id", type: "uuid", required: true },
396
+ { name: "name", type: "string", required: true },
397
+ { name: "amount", type: "number", required: false },
398
+ ],
399
+ } as const;
400
+
401
+ const { column, inferColumns } = createColumnHelper<OrderRow>();
402
+ const infer = inferColumns(metadata);
403
+
404
+ const col = column({
405
+ ...infer("name"),
406
+ label: "Custom Name",
407
+ render: (row) => `Name: ${row.name}`,
408
+ });
409
+ expect(col.label).toBe("Custom Name");
410
+ expect(col.sort).toEqual({ field: "name", type: "string" });
411
+ expect(col.render({ id: "1", name: "Test", amount: 0 })).toBe("Name: Test");
412
+ });
330
413
  });