@izumisy-tailor/tailor-data-viewer 0.2.0 → 0.2.2
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 +315 -189
- package/package.json +1 -1
- package/src/component/collection-params/collection-params-provider.tsx +12 -3
- package/src/component/{use-collection-params.test.ts → collection-params/use-collection-params.test.ts} +127 -1
- package/src/component/collection-params/use-collection-params.ts +67 -4
- package/src/component/{use-data-table.test.ts → data-table/use-data-table.test.ts} +2 -2
- package/src/component/field-helpers.test.ts +99 -33
- package/src/component/field-helpers.ts +60 -19
- package/src/component/index.ts +2 -1
- package/src/component/types.ts +47 -15
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { renderHook, act } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import type { TableMetadataMap } from "../../generator/metadata-generator";
|
|
4
|
+
import { useCollectionParams } from "./use-collection-params";
|
|
4
5
|
|
|
5
6
|
describe("useCollectionParams", () => {
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -337,4 +338,129 @@ describe("useCollectionParams", () => {
|
|
|
337
338
|
expect("after" in vars).toBe(false);
|
|
338
339
|
});
|
|
339
340
|
});
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Metadata-typed overload
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
describe("metadata-typed overload", () => {
|
|
346
|
+
const testMetadata = {
|
|
347
|
+
task: {
|
|
348
|
+
name: "task",
|
|
349
|
+
pluralForm: "tasks",
|
|
350
|
+
readAllowedRoles: [],
|
|
351
|
+
fields: [
|
|
352
|
+
{ name: "id", type: "uuid", required: true },
|
|
353
|
+
{ name: "title", type: "string", required: true },
|
|
354
|
+
{
|
|
355
|
+
name: "status",
|
|
356
|
+
type: "enum",
|
|
357
|
+
required: true,
|
|
358
|
+
enumValues: ["todo", "in_progress", "done"],
|
|
359
|
+
},
|
|
360
|
+
{ name: "dueDate", type: "date", required: false },
|
|
361
|
+
{ name: "count", type: "number", required: false },
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
} as const satisfies TableMetadataMap;
|
|
365
|
+
|
|
366
|
+
it("works with metadata and tableName", () => {
|
|
367
|
+
const { result } = renderHook(() =>
|
|
368
|
+
useCollectionParams({
|
|
369
|
+
metadata: testMetadata,
|
|
370
|
+
tableName: "task",
|
|
371
|
+
pageSize: 10,
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
expect(result.current.variables).toEqual({ first: 10 });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("auto-detects fieldType from metadata for string field", () => {
|
|
378
|
+
const { result } = renderHook(() =>
|
|
379
|
+
useCollectionParams({
|
|
380
|
+
metadata: testMetadata,
|
|
381
|
+
tableName: "task",
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
act(() => {
|
|
386
|
+
result.current.addFilter("title", "foo");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(result.current.filters[0].fieldType).toBe("string");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("auto-detects fieldType from metadata for enum field", () => {
|
|
393
|
+
const { result } = renderHook(() =>
|
|
394
|
+
useCollectionParams({
|
|
395
|
+
metadata: testMetadata,
|
|
396
|
+
tableName: "task",
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
act(() => {
|
|
401
|
+
result.current.addFilter("status", "todo");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(result.current.filters[0].fieldType).toBe("enum");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("auto-detects fieldType from metadata for date field", () => {
|
|
408
|
+
const { result } = renderHook(() =>
|
|
409
|
+
useCollectionParams({
|
|
410
|
+
metadata: testMetadata,
|
|
411
|
+
tableName: "task",
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
act(() => {
|
|
416
|
+
result.current.addFilter("dueDate", "2026-01-01");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(result.current.filters[0].fieldType).toBe("date");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("auto-detects fieldType from metadata for uuid field", () => {
|
|
423
|
+
const { result } = renderHook(() =>
|
|
424
|
+
useCollectionParams({
|
|
425
|
+
metadata: testMetadata,
|
|
426
|
+
tableName: "task",
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
act(() => {
|
|
431
|
+
result.current.addFilter("id", "some-uuid");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(result.current.filters[0].fieldType).toBe("uuid");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("auto-detects fieldType from metadata for number field", () => {
|
|
438
|
+
const { result } = renderHook(() =>
|
|
439
|
+
useCollectionParams({
|
|
440
|
+
metadata: testMetadata,
|
|
441
|
+
tableName: "task",
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
act(() => {
|
|
446
|
+
result.current.addFilter("count", 42);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(result.current.filters[0].fieldType).toBe("number");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("applies typed initialSort", () => {
|
|
453
|
+
const { result } = renderHook(() =>
|
|
454
|
+
useCollectionParams({
|
|
455
|
+
metadata: testMetadata,
|
|
456
|
+
tableName: "task",
|
|
457
|
+
initialSort: [{ field: "dueDate", direction: "Desc" }],
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(result.current.sortStates).toEqual([
|
|
462
|
+
{ field: "dueDate", direction: "Desc" },
|
|
463
|
+
]);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
340
466
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
FieldType,
|
|
4
|
+
TableMetadataMap,
|
|
5
|
+
} from "../../generator/metadata-generator";
|
|
2
6
|
import type {
|
|
3
7
|
Filter,
|
|
4
8
|
FilterOperator,
|
|
@@ -7,6 +11,40 @@ import type {
|
|
|
7
11
|
UseCollectionParamsOptions,
|
|
8
12
|
UseCollectionParamsReturn,
|
|
9
13
|
} from "../types";
|
|
14
|
+
import { fieldTypeToFilterConfig } from "../types";
|
|
15
|
+
import type { FieldName } from "../types";
|
|
16
|
+
|
|
17
|
+
// -----------------------------------------------------------------------------
|
|
18
|
+
// Overload signatures
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook for managing collection query parameters (filters, sort, pagination)
|
|
23
|
+
* with metadata-based field name typing and automatic `fieldType` detection.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
28
|
+
*
|
|
29
|
+
* const params = useCollectionParams({
|
|
30
|
+
* metadata: tableMetadata,
|
|
31
|
+
* tableName: "task",
|
|
32
|
+
* pageSize: 20,
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* params.addFilter("title", "foo"); // ✅ field name auto-completed
|
|
36
|
+
* params.addFilter("nonExistent", "foo"); // ❌ type error
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useCollectionParams<
|
|
40
|
+
const TMetadata extends TableMetadataMap,
|
|
41
|
+
TTableName extends string & keyof TMetadata,
|
|
42
|
+
>(
|
|
43
|
+
options: UseCollectionParamsOptions<FieldName<TMetadata, TTableName>> & {
|
|
44
|
+
metadata: TMetadata;
|
|
45
|
+
tableName: TTableName;
|
|
46
|
+
},
|
|
47
|
+
): UseCollectionParamsReturn<FieldName<TMetadata, TTableName>>;
|
|
10
48
|
|
|
11
49
|
/**
|
|
12
50
|
* Hook for managing collection query parameters (filters, sort, pagination).
|
|
@@ -16,19 +54,36 @@ import type {
|
|
|
16
54
|
*
|
|
17
55
|
* @example
|
|
18
56
|
* ```tsx
|
|
19
|
-
* const params = useCollectionParams
|
|
57
|
+
* const params = useCollectionParams({ pageSize: 20 });
|
|
20
58
|
* const [result] = useQuery({ query: GET_ORDERS, variables: params.variables });
|
|
21
59
|
* ```
|
|
22
60
|
*/
|
|
23
61
|
export function useCollectionParams(
|
|
24
|
-
options
|
|
62
|
+
options?: UseCollectionParamsOptions,
|
|
63
|
+
): UseCollectionParamsReturn;
|
|
64
|
+
|
|
65
|
+
// -----------------------------------------------------------------------------
|
|
66
|
+
// Implementation
|
|
67
|
+
// -----------------------------------------------------------------------------
|
|
68
|
+
export function useCollectionParams(
|
|
69
|
+
options: UseCollectionParamsOptions & {
|
|
70
|
+
metadata?: TableMetadataMap;
|
|
71
|
+
tableName?: string;
|
|
72
|
+
} = {},
|
|
25
73
|
): UseCollectionParamsReturn {
|
|
26
74
|
const {
|
|
27
75
|
initialFilters = [],
|
|
28
76
|
initialSort = [],
|
|
29
77
|
pageSize: initialPageSize = 20,
|
|
78
|
+
metadata,
|
|
79
|
+
tableName,
|
|
30
80
|
} = options;
|
|
31
81
|
|
|
82
|
+
// Keep a ref to the resolved fields list so callbacks can look up fieldType.
|
|
83
|
+
const fieldsRef = useRef(
|
|
84
|
+
metadata && tableName ? metadata[tableName]?.fields : undefined,
|
|
85
|
+
);
|
|
86
|
+
|
|
32
87
|
// ---------------------------------------------------------------------------
|
|
33
88
|
// State
|
|
34
89
|
// ---------------------------------------------------------------------------
|
|
@@ -45,9 +100,17 @@ export function useCollectionParams(
|
|
|
45
100
|
(field: string, value: unknown, operator: FilterOperator = "eq") => {
|
|
46
101
|
setFiltersState((prev) => {
|
|
47
102
|
const existing = prev.findIndex((f) => f.field === field);
|
|
103
|
+
// Auto-detect fieldType from metadata when available
|
|
104
|
+
const fieldMeta = fieldsRef.current?.find((f) => f.name === field);
|
|
105
|
+
const detectedType: Filter["fieldType"] = fieldMeta
|
|
106
|
+
? (fieldTypeToFilterConfig(
|
|
107
|
+
fieldMeta.type as FieldType,
|
|
108
|
+
fieldMeta.enumValues as readonly string[] | undefined,
|
|
109
|
+
)?.type ?? "string")
|
|
110
|
+
: "string";
|
|
48
111
|
const newFilter: Filter = {
|
|
49
112
|
field,
|
|
50
|
-
fieldType:
|
|
113
|
+
fieldType: detectedType,
|
|
51
114
|
operator,
|
|
52
115
|
value,
|
|
53
116
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { renderHook, act } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
|
-
import { useDataTable } from "./
|
|
4
|
-
import type { Column, CollectionResult } from "
|
|
3
|
+
import { useDataTable } from "./use-data-table";
|
|
4
|
+
import type { Column, CollectionResult } from "../types";
|
|
5
5
|
|
|
6
6
|
type TestRow = {
|
|
7
7
|
id: string;
|
|
@@ -1,43 +1,76 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from "vitest";
|
|
2
|
+
import { createColumnHelper, inferColumnHelper } from "./field-helpers";
|
|
3
3
|
import type { TableMetadataMap } from "../generator/metadata-generator";
|
|
4
4
|
import { fieldTypeToSortConfig, fieldTypeToFilterConfig } from "./types";
|
|
5
|
+
import type { NodeType } from "./types";
|
|
5
6
|
|
|
6
|
-
describe("
|
|
7
|
-
it("
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
describe("NodeType", () => {
|
|
8
|
+
it("extracts node type from a collection result", () => {
|
|
9
|
+
type Result = { edges: { node: { id: string; name: string } }[] };
|
|
10
|
+
type Row = NodeType<Result>;
|
|
11
|
+
expectTypeOf<Row>().toEqualTypeOf<{ id: string; name: string }>();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("handles nullable collection (e.g. gql-tada ResultOf)", () => {
|
|
15
|
+
type Result =
|
|
16
|
+
| { edges: { node: { id: string; amount: number } }[] }
|
|
17
|
+
| null
|
|
18
|
+
| undefined;
|
|
19
|
+
type Row = NodeType<Result>;
|
|
20
|
+
expectTypeOf<Row>().toEqualTypeOf<{ id: string; amount: number }>();
|
|
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
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("createColumnHelper()", () => {
|
|
33
|
+
type TestRow = { name: string; age: number };
|
|
34
|
+
|
|
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");
|
|
14
39
|
});
|
|
15
40
|
|
|
16
|
-
it("creates a field column
|
|
17
|
-
const
|
|
41
|
+
it("field helper creates a field column without explicit type param", () => {
|
|
42
|
+
const { field } = createColumnHelper<TestRow>();
|
|
43
|
+
const col = field("name", {
|
|
18
44
|
label: "Name",
|
|
19
|
-
width: 200,
|
|
20
45
|
sort: { type: "string" },
|
|
21
46
|
filter: { type: "string" },
|
|
22
47
|
});
|
|
48
|
+
expect(col.kind).toBe("field");
|
|
49
|
+
expect(col.dataKey).toBe("name");
|
|
23
50
|
expect(col.label).toBe("Name");
|
|
24
|
-
expect(col.width).toBe(200);
|
|
25
51
|
expect(col.sort).toEqual({ type: "string" });
|
|
26
52
|
expect(col.filter).toEqual({ type: "string" });
|
|
27
53
|
});
|
|
28
|
-
});
|
|
29
54
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const col =
|
|
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();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("display helper creates a display column without explicit type param", () => {
|
|
64
|
+
const { display } = createColumnHelper<TestRow>();
|
|
65
|
+
const col = display("actions", {
|
|
33
66
|
label: "Actions",
|
|
34
|
-
width:
|
|
35
|
-
render: () =>
|
|
67
|
+
width: 100,
|
|
68
|
+
render: (row) => `${row.name}: ${row.age}`,
|
|
36
69
|
});
|
|
37
70
|
expect(col.kind).toBe("display");
|
|
38
71
|
expect(col.id).toBe("actions");
|
|
39
72
|
expect(col.label).toBe("Actions");
|
|
40
|
-
expect(col.width).toBe(
|
|
73
|
+
expect(col.width).toBe(100);
|
|
41
74
|
expect(typeof col.render).toBe("function");
|
|
42
75
|
});
|
|
43
76
|
});
|
|
@@ -105,7 +138,7 @@ describe("fieldTypeToFilterConfig", () => {
|
|
|
105
138
|
});
|
|
106
139
|
});
|
|
107
140
|
|
|
108
|
-
describe("
|
|
141
|
+
describe("inferColumnHelper()", () => {
|
|
109
142
|
const testMetadata = {
|
|
110
143
|
task: {
|
|
111
144
|
name: "task",
|
|
@@ -134,7 +167,10 @@ describe("inferColumns()", () => {
|
|
|
134
167
|
} as const satisfies TableMetadataMap;
|
|
135
168
|
|
|
136
169
|
it("creates a column with auto-detected sort/filter", () => {
|
|
137
|
-
const { column } =
|
|
170
|
+
const { column } = inferColumnHelper({
|
|
171
|
+
metadata: testMetadata,
|
|
172
|
+
tableName: "task",
|
|
173
|
+
});
|
|
138
174
|
|
|
139
175
|
const titleCol = column("title");
|
|
140
176
|
expect(titleCol.kind).toBe("field");
|
|
@@ -144,7 +180,10 @@ describe("inferColumns()", () => {
|
|
|
144
180
|
});
|
|
145
181
|
|
|
146
182
|
it("auto-detects enum options", () => {
|
|
147
|
-
const { column } =
|
|
183
|
+
const { column } = inferColumnHelper({
|
|
184
|
+
metadata: testMetadata,
|
|
185
|
+
tableName: "task",
|
|
186
|
+
});
|
|
148
187
|
const statusCol = column("status");
|
|
149
188
|
expect(statusCol.filter).toEqual({
|
|
150
189
|
type: "enum",
|
|
@@ -159,42 +198,60 @@ describe("inferColumns()", () => {
|
|
|
159
198
|
});
|
|
160
199
|
|
|
161
200
|
it("auto-detects date type", () => {
|
|
162
|
-
const { column } =
|
|
201
|
+
const { column } = inferColumnHelper({
|
|
202
|
+
metadata: testMetadata,
|
|
203
|
+
tableName: "task",
|
|
204
|
+
});
|
|
163
205
|
const dateCol = column("dueDate");
|
|
164
206
|
expect(dateCol.sort).toEqual({ type: "date" });
|
|
165
207
|
expect(dateCol.filter).toEqual({ type: "date" });
|
|
166
208
|
});
|
|
167
209
|
|
|
168
210
|
it("disables sort with sort: false", () => {
|
|
169
|
-
const { column } =
|
|
211
|
+
const { column } = inferColumnHelper({
|
|
212
|
+
metadata: testMetadata,
|
|
213
|
+
tableName: "task",
|
|
214
|
+
});
|
|
170
215
|
const col = column("title", { sort: false });
|
|
171
216
|
expect(col.sort).toBeUndefined();
|
|
172
217
|
expect(col.filter).toEqual({ type: "string" });
|
|
173
218
|
});
|
|
174
219
|
|
|
175
220
|
it("disables filter with filter: false", () => {
|
|
176
|
-
const { column } =
|
|
221
|
+
const { column } = inferColumnHelper({
|
|
222
|
+
metadata: testMetadata,
|
|
223
|
+
tableName: "task",
|
|
224
|
+
});
|
|
177
225
|
const col = column("title", { filter: false });
|
|
178
226
|
expect(col.sort).toEqual({ type: "string" });
|
|
179
227
|
expect(col.filter).toBeUndefined();
|
|
180
228
|
});
|
|
181
229
|
|
|
182
230
|
it("uuid has no sort, has uuid filter", () => {
|
|
183
|
-
const { column } =
|
|
231
|
+
const { column } = inferColumnHelper({
|
|
232
|
+
metadata: testMetadata,
|
|
233
|
+
tableName: "task",
|
|
234
|
+
});
|
|
184
235
|
const col = column("id");
|
|
185
236
|
expect(col.sort).toBeUndefined();
|
|
186
237
|
expect(col.filter).toEqual({ type: "uuid" });
|
|
187
238
|
});
|
|
188
239
|
|
|
189
240
|
it("array type has no sort/filter", () => {
|
|
190
|
-
const { column } =
|
|
241
|
+
const { column } = inferColumnHelper({
|
|
242
|
+
metadata: testMetadata,
|
|
243
|
+
tableName: "task",
|
|
244
|
+
});
|
|
191
245
|
const col = column("tags");
|
|
192
246
|
expect(col.sort).toBeUndefined();
|
|
193
247
|
expect(col.filter).toBeUndefined();
|
|
194
248
|
});
|
|
195
249
|
|
|
196
250
|
it("columns() creates multiple columns at once", () => {
|
|
197
|
-
const { columns } =
|
|
251
|
+
const { columns } = inferColumnHelper({
|
|
252
|
+
metadata: testMetadata,
|
|
253
|
+
tableName: "task",
|
|
254
|
+
});
|
|
198
255
|
const cols = columns(["title", "status", "dueDate"]);
|
|
199
256
|
expect(cols).toHaveLength(3);
|
|
200
257
|
expect(cols[0].dataKey).toBe("title");
|
|
@@ -203,7 +260,10 @@ describe("inferColumns()", () => {
|
|
|
203
260
|
});
|
|
204
261
|
|
|
205
262
|
it("columns() applies overrides", () => {
|
|
206
|
-
const { columns } =
|
|
263
|
+
const { columns } = inferColumnHelper({
|
|
264
|
+
metadata: testMetadata,
|
|
265
|
+
tableName: "task",
|
|
266
|
+
});
|
|
207
267
|
const cols = columns(["title", "status"], {
|
|
208
268
|
overrides: {
|
|
209
269
|
title: { label: "Custom Title", width: 300 },
|
|
@@ -214,7 +274,10 @@ describe("inferColumns()", () => {
|
|
|
214
274
|
});
|
|
215
275
|
|
|
216
276
|
it("columns() applies global sort/filter options", () => {
|
|
217
|
-
const { columns } =
|
|
277
|
+
const { columns } = inferColumnHelper({
|
|
278
|
+
metadata: testMetadata,
|
|
279
|
+
tableName: "task",
|
|
280
|
+
});
|
|
218
281
|
const cols = columns(["title", "status"], { sort: false });
|
|
219
282
|
expect(cols[0].sort).toBeUndefined();
|
|
220
283
|
expect(cols[1].sort).toBeUndefined();
|
|
@@ -223,7 +286,10 @@ describe("inferColumns()", () => {
|
|
|
223
286
|
});
|
|
224
287
|
|
|
225
288
|
it("throws for non-existent field", () => {
|
|
226
|
-
const { column } =
|
|
289
|
+
const { column } = inferColumnHelper({
|
|
290
|
+
metadata: testMetadata,
|
|
291
|
+
tableName: "task",
|
|
292
|
+
});
|
|
227
293
|
// @ts-expect-error - intentionally testing invalid field name
|
|
228
294
|
expect(() => column("nonExistent")).toThrow(
|
|
229
295
|
'Field "nonExistent" not found in table "task" metadata',
|
|
@@ -83,7 +83,45 @@ export function display<TRow extends Record<string, unknown>>(
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// =============================================================================
|
|
86
|
-
//
|
|
86
|
+
// createColumnHelper() factory
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create `field` and `display` helpers with TRow bound once.
|
|
91
|
+
*
|
|
92
|
+
* This avoids repeating the row type parameter on every `field()` / `display()` call.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* type Order = { id: string; title: string; amount: number };
|
|
97
|
+
*
|
|
98
|
+
* const { field, display } = createColumnHelper<Order>();
|
|
99
|
+
*
|
|
100
|
+
* const columns = [
|
|
101
|
+
* field("title", { label: "Title", sort: { type: "string" } }),
|
|
102
|
+
* field("amount", { label: "Amount", sort: { type: "number" } }),
|
|
103
|
+
* display("actions", { render: (row) => <ActionMenu row={row} /> }),
|
|
104
|
+
* ];
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function createColumnHelper<TRow extends Record<string, unknown>>(): {
|
|
108
|
+
field: (
|
|
109
|
+
dataKey: keyof TRow & string,
|
|
110
|
+
options?: FieldColumnOptions<TRow>,
|
|
111
|
+
) => FieldColumn<TRow>;
|
|
112
|
+
display: (
|
|
113
|
+
id: string,
|
|
114
|
+
options: DisplayColumnOptions<TRow>,
|
|
115
|
+
) => DisplayColumn<TRow>;
|
|
116
|
+
} {
|
|
117
|
+
return {
|
|
118
|
+
field: (dataKey, options) => field<TRow>(dataKey, options),
|
|
119
|
+
display: (id, options) => display<TRow>(id, options),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// inferColumnHelper() helper
|
|
87
125
|
// =============================================================================
|
|
88
126
|
|
|
89
127
|
/**
|
|
@@ -92,14 +130,16 @@ export function display<TRow extends Record<string, unknown>>(
|
|
|
92
130
|
* Automatically infers sort/filter configuration from field types,
|
|
93
131
|
* including enum options.
|
|
94
132
|
*
|
|
95
|
-
* @param
|
|
96
|
-
* @param tableName - Key of the table in the metadata map.
|
|
133
|
+
* @param options - Object containing `metadata` (the generated map) and `tableName`.
|
|
97
134
|
*
|
|
98
135
|
* @example
|
|
99
136
|
* ```tsx
|
|
100
137
|
* import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
101
138
|
*
|
|
102
|
-
* const { column, columns } =
|
|
139
|
+
* const { column, columns } = inferColumnHelper({
|
|
140
|
+
* metadata: tableMetadata,
|
|
141
|
+
* tableName: "task",
|
|
142
|
+
* });
|
|
103
143
|
*
|
|
104
144
|
* const taskColumns = [
|
|
105
145
|
* column("title"), // sort/filter auto-detected
|
|
@@ -109,13 +149,13 @@ export function display<TRow extends Record<string, unknown>>(
|
|
|
109
149
|
* ];
|
|
110
150
|
* ```
|
|
111
151
|
*/
|
|
112
|
-
export function
|
|
152
|
+
export function inferColumnHelper<
|
|
113
153
|
const TMetadata extends TableMetadataMap,
|
|
114
154
|
TTableName extends string & keyof TMetadata,
|
|
115
|
-
>(
|
|
116
|
-
metadata: TMetadata
|
|
117
|
-
tableName: TTableName
|
|
118
|
-
): {
|
|
155
|
+
>(options: {
|
|
156
|
+
metadata: TMetadata;
|
|
157
|
+
tableName: TTableName;
|
|
158
|
+
}): {
|
|
119
159
|
column: (
|
|
120
160
|
dataKey: FieldName<TMetadata, TTableName>,
|
|
121
161
|
options?: MetadataFieldOptions,
|
|
@@ -126,12 +166,13 @@ export function inferColumns<
|
|
|
126
166
|
options?: MetadataFieldsOptions,
|
|
127
167
|
) => FieldColumn<Record<string, unknown>>[];
|
|
128
168
|
} {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
169
|
+
const { metadata, tableName } = options;
|
|
170
|
+
const tableMeta = metadata[tableName];
|
|
171
|
+
const fields = tableMeta.fields;
|
|
131
172
|
|
|
132
173
|
const column = (
|
|
133
174
|
dataKey: FieldName<TMetadata, TTableName>,
|
|
134
|
-
|
|
175
|
+
columnOptions?: MetadataFieldOptions,
|
|
135
176
|
): FieldColumn<Record<string, unknown>> => {
|
|
136
177
|
const fieldMeta = fields.find((f) => f.name === dataKey);
|
|
137
178
|
if (!fieldMeta) {
|
|
@@ -142,30 +183,30 @@ export function inferColumns<
|
|
|
142
183
|
|
|
143
184
|
// Auto-detect sort config
|
|
144
185
|
let sort: SortConfig | undefined;
|
|
145
|
-
if (
|
|
186
|
+
if (columnOptions?.sort !== false) {
|
|
146
187
|
sort = fieldTypeToSortConfig(fieldMeta.type);
|
|
147
188
|
}
|
|
148
|
-
if (
|
|
189
|
+
if (columnOptions?.sort === false) {
|
|
149
190
|
sort = undefined;
|
|
150
191
|
}
|
|
151
192
|
|
|
152
193
|
// Auto-detect filter config
|
|
153
194
|
let filter: FilterConfig | undefined;
|
|
154
|
-
if (
|
|
195
|
+
if (columnOptions?.filter !== false) {
|
|
155
196
|
filter = fieldTypeToFilterConfig(fieldMeta.type, fieldMeta.enumValues);
|
|
156
197
|
}
|
|
157
|
-
if (
|
|
198
|
+
if (columnOptions?.filter === false) {
|
|
158
199
|
filter = undefined;
|
|
159
200
|
}
|
|
160
201
|
|
|
161
202
|
return {
|
|
162
203
|
kind: "field",
|
|
163
204
|
dataKey: fieldMeta.name,
|
|
164
|
-
label:
|
|
165
|
-
width:
|
|
205
|
+
label: columnOptions?.label ?? fieldMeta.description ?? fieldMeta.name,
|
|
206
|
+
width: columnOptions?.width,
|
|
166
207
|
sort,
|
|
167
208
|
filter,
|
|
168
|
-
renderer:
|
|
209
|
+
renderer: columnOptions?.renderer as
|
|
169
210
|
| CellRenderer<Record<string, unknown>>
|
|
170
211
|
| undefined,
|
|
171
212
|
};
|
package/src/component/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type {
|
|
|
9
9
|
PageInfo,
|
|
10
10
|
QueryVariables,
|
|
11
11
|
CollectionResult,
|
|
12
|
+
NodeType,
|
|
12
13
|
CellRendererProps,
|
|
13
14
|
CellRenderer,
|
|
14
15
|
FieldColumnOptions,
|
|
@@ -59,7 +60,7 @@ export { useDataTable } from "./data-table/use-data-table";
|
|
|
59
60
|
export { useDataTableContext } from "./data-table/data-table-context";
|
|
60
61
|
|
|
61
62
|
// Field helpers
|
|
62
|
-
export {
|
|
63
|
+
export { createColumnHelper, inferColumnHelper } from "./field-helpers";
|
|
63
64
|
|
|
64
65
|
// Utility components
|
|
65
66
|
export { Pagination } from "./pagination";
|