@izumisy-tailor/tailor-data-viewer 0.2.14 → 0.2.16
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 +2 -2
- package/package.json +1 -1
- package/src/component/collection/use-collection.test.ts +46 -25
- package/src/component/collection/use-collection.ts +62 -53
- package/src/component/collection/use-collection.typetest.ts +140 -21
- package/src/component/data-table/use-data-table.test.ts +8 -1
- package/src/component/data-table/use-data-table.ts +23 -4
- package/src/component/field-helpers.test.ts +19 -45
- package/src/component/field-helpers.ts +13 -22
- package/src/component/index.ts +4 -1
- package/src/component/pagination.tsx +7 -2
- package/src/component/types.ts +168 -43
|
@@ -51,7 +51,12 @@ const testData: CollectionResult<TestRow> = {
|
|
|
51
51
|
{ node: { id: "2", name: "Bob", status: "INACTIVE", amount: 200 } },
|
|
52
52
|
{ node: { id: "3", name: "Charlie", status: "ACTIVE", amount: 300 } },
|
|
53
53
|
],
|
|
54
|
-
pageInfo: {
|
|
54
|
+
pageInfo: {
|
|
55
|
+
hasNextPage: true,
|
|
56
|
+
endCursor: "cursor-3",
|
|
57
|
+
hasPreviousPage: false,
|
|
58
|
+
startCursor: "cursor-1",
|
|
59
|
+
},
|
|
55
60
|
};
|
|
56
61
|
|
|
57
62
|
describe("useDataTable", () => {
|
|
@@ -95,6 +100,8 @@ describe("useDataTable", () => {
|
|
|
95
100
|
expect(result.current.pageInfo).toEqual({
|
|
96
101
|
hasNextPage: true,
|
|
97
102
|
endCursor: "cursor-3",
|
|
103
|
+
hasPreviousPage: false,
|
|
104
|
+
startCursor: "cursor-1",
|
|
98
105
|
});
|
|
99
106
|
});
|
|
100
107
|
});
|
|
@@ -59,9 +59,23 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
59
59
|
}, [sourceRows]);
|
|
60
60
|
|
|
61
61
|
const pageInfo = useMemo<PageInfo>(() => {
|
|
62
|
-
return
|
|
62
|
+
return (
|
|
63
|
+
data?.pageInfo ?? {
|
|
64
|
+
hasNextPage: false,
|
|
65
|
+
endCursor: null,
|
|
66
|
+
hasPreviousPage: false,
|
|
67
|
+
startCursor: null,
|
|
68
|
+
}
|
|
69
|
+
);
|
|
63
70
|
}, [data]);
|
|
64
71
|
|
|
72
|
+
// Sync pageInfo to collection so hasPrevPage/hasNextPage are up-to-date
|
|
73
|
+
useMemo(() => {
|
|
74
|
+
if (data?.pageInfo) {
|
|
75
|
+
collection?.setPageInfo(data.pageInfo);
|
|
76
|
+
}
|
|
77
|
+
}, [data?.pageInfo, collection]);
|
|
78
|
+
|
|
65
79
|
// ---------------------------------------------------------------------------
|
|
66
80
|
// Column visibility management
|
|
67
81
|
// ---------------------------------------------------------------------------
|
|
@@ -111,11 +125,15 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
111
125
|
[collection],
|
|
112
126
|
);
|
|
113
127
|
|
|
114
|
-
const prevPage = useCallback(
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
const prevPage = useCallback(
|
|
129
|
+
(startCursor: string) => {
|
|
130
|
+
collection?.prevPage(startCursor);
|
|
131
|
+
},
|
|
132
|
+
[collection],
|
|
133
|
+
);
|
|
117
134
|
|
|
118
135
|
const hasPrevPage = collection?.hasPrevPage ?? false;
|
|
136
|
+
const hasNextPage = collection?.hasNextPage ?? false;
|
|
119
137
|
|
|
120
138
|
// ---------------------------------------------------------------------------
|
|
121
139
|
// Row Operations (Optimistic Updates)
|
|
@@ -240,6 +258,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
240
258
|
nextPage,
|
|
241
259
|
prevPage,
|
|
242
260
|
hasPrevPage,
|
|
261
|
+
hasNextPage,
|
|
243
262
|
|
|
244
263
|
// Column management
|
|
245
264
|
columns: allColumns,
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, expectTypeOf } from "vitest";
|
|
|
2
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
|
+
import type { NodeType, TableFieldName } from "./types";
|
|
6
6
|
|
|
7
7
|
describe("NodeType", () => {
|
|
8
8
|
it("extracts node type from a collection result", () => {
|
|
@@ -167,10 +167,7 @@ describe("inferColumnHelper()", () => {
|
|
|
167
167
|
} as const satisfies TableMetadataMap;
|
|
168
168
|
|
|
169
169
|
it("creates a column with auto-detected sort/filter", () => {
|
|
170
|
-
const { column } = inferColumnHelper(
|
|
171
|
-
metadata: testMetadata,
|
|
172
|
-
tableName: "task",
|
|
173
|
-
});
|
|
170
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
174
171
|
|
|
175
172
|
const titleCol = column("title");
|
|
176
173
|
expect(titleCol.kind).toBe("field");
|
|
@@ -180,10 +177,7 @@ describe("inferColumnHelper()", () => {
|
|
|
180
177
|
});
|
|
181
178
|
|
|
182
179
|
it("auto-detects enum options", () => {
|
|
183
|
-
const { column } = inferColumnHelper(
|
|
184
|
-
metadata: testMetadata,
|
|
185
|
-
tableName: "task",
|
|
186
|
-
});
|
|
180
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
187
181
|
const statusCol = column("status");
|
|
188
182
|
expect(statusCol.filter).toEqual({
|
|
189
183
|
type: "enum",
|
|
@@ -198,60 +192,42 @@ describe("inferColumnHelper()", () => {
|
|
|
198
192
|
});
|
|
199
193
|
|
|
200
194
|
it("auto-detects date type", () => {
|
|
201
|
-
const { column } = inferColumnHelper(
|
|
202
|
-
metadata: testMetadata,
|
|
203
|
-
tableName: "task",
|
|
204
|
-
});
|
|
195
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
205
196
|
const dateCol = column("dueDate");
|
|
206
197
|
expect(dateCol.sort).toEqual({ type: "date" });
|
|
207
198
|
expect(dateCol.filter).toEqual({ type: "date" });
|
|
208
199
|
});
|
|
209
200
|
|
|
210
201
|
it("disables sort with sort: false", () => {
|
|
211
|
-
const { column } = inferColumnHelper(
|
|
212
|
-
metadata: testMetadata,
|
|
213
|
-
tableName: "task",
|
|
214
|
-
});
|
|
202
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
215
203
|
const col = column("title", { sort: false });
|
|
216
204
|
expect(col.sort).toBeUndefined();
|
|
217
205
|
expect(col.filter).toEqual({ type: "string" });
|
|
218
206
|
});
|
|
219
207
|
|
|
220
208
|
it("disables filter with filter: false", () => {
|
|
221
|
-
const { column } = inferColumnHelper(
|
|
222
|
-
metadata: testMetadata,
|
|
223
|
-
tableName: "task",
|
|
224
|
-
});
|
|
209
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
225
210
|
const col = column("title", { filter: false });
|
|
226
211
|
expect(col.sort).toEqual({ type: "string" });
|
|
227
212
|
expect(col.filter).toBeUndefined();
|
|
228
213
|
});
|
|
229
214
|
|
|
230
215
|
it("uuid has no sort, has uuid filter", () => {
|
|
231
|
-
const { column } = inferColumnHelper(
|
|
232
|
-
metadata: testMetadata,
|
|
233
|
-
tableName: "task",
|
|
234
|
-
});
|
|
216
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
235
217
|
const col = column("id");
|
|
236
218
|
expect(col.sort).toBeUndefined();
|
|
237
219
|
expect(col.filter).toEqual({ type: "uuid" });
|
|
238
220
|
});
|
|
239
221
|
|
|
240
222
|
it("array type has no sort/filter", () => {
|
|
241
|
-
const { column } = inferColumnHelper(
|
|
242
|
-
metadata: testMetadata,
|
|
243
|
-
tableName: "task",
|
|
244
|
-
});
|
|
223
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
245
224
|
const col = column("tags");
|
|
246
225
|
expect(col.sort).toBeUndefined();
|
|
247
226
|
expect(col.filter).toBeUndefined();
|
|
248
227
|
});
|
|
249
228
|
|
|
250
229
|
it("columns() creates multiple columns at once", () => {
|
|
251
|
-
const { columns } = inferColumnHelper(
|
|
252
|
-
metadata: testMetadata,
|
|
253
|
-
tableName: "task",
|
|
254
|
-
});
|
|
230
|
+
const { columns } = inferColumnHelper(testMetadata.task);
|
|
255
231
|
const cols = columns(["title", "status", "dueDate"]);
|
|
256
232
|
expect(cols).toHaveLength(3);
|
|
257
233
|
expect(cols[0].dataKey).toBe("title");
|
|
@@ -260,10 +236,7 @@ describe("inferColumnHelper()", () => {
|
|
|
260
236
|
});
|
|
261
237
|
|
|
262
238
|
it("columns() applies overrides", () => {
|
|
263
|
-
const { columns } = inferColumnHelper(
|
|
264
|
-
metadata: testMetadata,
|
|
265
|
-
tableName: "task",
|
|
266
|
-
});
|
|
239
|
+
const { columns } = inferColumnHelper(testMetadata.task);
|
|
267
240
|
const cols = columns(["title", "status"], {
|
|
268
241
|
overrides: {
|
|
269
242
|
title: { label: "Custom Title", width: 300 },
|
|
@@ -274,10 +247,7 @@ describe("inferColumnHelper()", () => {
|
|
|
274
247
|
});
|
|
275
248
|
|
|
276
249
|
it("columns() applies global sort/filter options", () => {
|
|
277
|
-
const { columns } = inferColumnHelper(
|
|
278
|
-
metadata: testMetadata,
|
|
279
|
-
tableName: "task",
|
|
280
|
-
});
|
|
250
|
+
const { columns } = inferColumnHelper(testMetadata.task);
|
|
281
251
|
const cols = columns(["title", "status"], { sort: false });
|
|
282
252
|
expect(cols[0].sort).toBeUndefined();
|
|
283
253
|
expect(cols[1].sort).toBeUndefined();
|
|
@@ -286,13 +256,17 @@ describe("inferColumnHelper()", () => {
|
|
|
286
256
|
});
|
|
287
257
|
|
|
288
258
|
it("throws for non-existent field", () => {
|
|
289
|
-
const { column } = inferColumnHelper(
|
|
290
|
-
metadata: testMetadata,
|
|
291
|
-
tableName: "task",
|
|
292
|
-
});
|
|
259
|
+
const { column } = inferColumnHelper(testMetadata.task);
|
|
293
260
|
// @ts-expect-error - intentionally testing invalid field name
|
|
294
261
|
expect(() => column("nonExistent")).toThrow(
|
|
295
262
|
'Field "nonExistent" not found in table "task" metadata',
|
|
296
263
|
);
|
|
297
264
|
});
|
|
265
|
+
|
|
266
|
+
it("infers TableFieldName type correctly", () => {
|
|
267
|
+
type TaskFieldNames = TableFieldName<(typeof testMetadata)["task"]>;
|
|
268
|
+
expectTypeOf<TaskFieldNames>().toEqualTypeOf<
|
|
269
|
+
"id" | "title" | "status" | "dueDate" | "count" | "isActive" | "tags"
|
|
270
|
+
>();
|
|
271
|
+
});
|
|
298
272
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TableMetadata } from "../generator/metadata-generator";
|
|
2
2
|
import type {
|
|
3
3
|
CellRenderer,
|
|
4
4
|
DisplayColumn,
|
|
5
5
|
DisplayColumnOptions,
|
|
6
6
|
FieldColumn,
|
|
7
7
|
FieldColumnOptions,
|
|
8
|
-
|
|
8
|
+
TableFieldName,
|
|
9
9
|
FilterConfig,
|
|
10
10
|
MetadataFieldOptions,
|
|
11
11
|
MetadataFieldsOptions,
|
|
@@ -130,16 +130,13 @@ export function createColumnHelper<TRow extends Record<string, unknown>>(): {
|
|
|
130
130
|
* Automatically infers sort/filter configuration from field types,
|
|
131
131
|
* including enum options.
|
|
132
132
|
*
|
|
133
|
-
* @param
|
|
133
|
+
* @param tableMetadata - A single table metadata object from the generated map.
|
|
134
134
|
*
|
|
135
135
|
* @example
|
|
136
136
|
* ```tsx
|
|
137
137
|
* import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
138
138
|
*
|
|
139
|
-
* const { column, columns } = inferColumnHelper(
|
|
140
|
-
* metadata: tableMetadata,
|
|
141
|
-
* tableName: "task",
|
|
142
|
-
* });
|
|
139
|
+
* const { column, columns } = inferColumnHelper(tableMetadata.task);
|
|
143
140
|
*
|
|
144
141
|
* const taskColumns = [
|
|
145
142
|
* column("title"), // sort/filter auto-detected
|
|
@@ -149,35 +146,29 @@ export function createColumnHelper<TRow extends Record<string, unknown>>(): {
|
|
|
149
146
|
* ];
|
|
150
147
|
* ```
|
|
151
148
|
*/
|
|
152
|
-
export function inferColumnHelper<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
>(options: {
|
|
156
|
-
metadata: TMetadata;
|
|
157
|
-
tableName: TTableName;
|
|
158
|
-
}): {
|
|
149
|
+
export function inferColumnHelper<const TTable extends TableMetadata>(
|
|
150
|
+
tableMetadata: TTable,
|
|
151
|
+
): {
|
|
159
152
|
column: (
|
|
160
|
-
dataKey:
|
|
153
|
+
dataKey: TableFieldName<TTable>,
|
|
161
154
|
options?: MetadataFieldOptions,
|
|
162
155
|
) => FieldColumn<Record<string, unknown>>;
|
|
163
156
|
|
|
164
157
|
columns: (
|
|
165
|
-
dataKeys:
|
|
158
|
+
dataKeys: TableFieldName<TTable>[],
|
|
166
159
|
options?: MetadataFieldsOptions,
|
|
167
160
|
) => FieldColumn<Record<string, unknown>>[];
|
|
168
161
|
} {
|
|
169
|
-
const
|
|
170
|
-
const tableMeta = metadata[tableName];
|
|
171
|
-
const fields = tableMeta.fields;
|
|
162
|
+
const fields = tableMetadata.fields;
|
|
172
163
|
|
|
173
164
|
const column = (
|
|
174
|
-
dataKey:
|
|
165
|
+
dataKey: TableFieldName<TTable>,
|
|
175
166
|
columnOptions?: MetadataFieldOptions,
|
|
176
167
|
): FieldColumn<Record<string, unknown>> => {
|
|
177
168
|
const fieldMeta = fields.find((f) => f.name === dataKey);
|
|
178
169
|
if (!fieldMeta) {
|
|
179
170
|
throw new Error(
|
|
180
|
-
`Field "${String(dataKey)}" not found in table "${
|
|
171
|
+
`Field "${String(dataKey)}" not found in table "${tableMetadata.name}" metadata`,
|
|
181
172
|
);
|
|
182
173
|
}
|
|
183
174
|
|
|
@@ -213,7 +204,7 @@ export function inferColumnHelper<
|
|
|
213
204
|
};
|
|
214
205
|
|
|
215
206
|
const columnsHelper = (
|
|
216
|
-
dataKeys:
|
|
207
|
+
dataKeys: TableFieldName<TTable>[],
|
|
217
208
|
options?: MetadataFieldsOptions,
|
|
218
209
|
): FieldColumn<Record<string, unknown>>[] => {
|
|
219
210
|
return dataKeys.map((dataKey) => {
|
package/src/component/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ export type {
|
|
|
5
5
|
SelectOption,
|
|
6
6
|
FilterOperator,
|
|
7
7
|
Filter,
|
|
8
|
-
MetadataFilter,
|
|
9
8
|
SortState,
|
|
10
9
|
PageInfo,
|
|
11
10
|
QueryVariables,
|
|
@@ -28,7 +27,9 @@ export type {
|
|
|
28
27
|
DataTableRowProps,
|
|
29
28
|
DataTableCellProps,
|
|
30
29
|
FieldName,
|
|
30
|
+
TableFieldName,
|
|
31
31
|
OrderableFieldName,
|
|
32
|
+
TableOrderableFieldName,
|
|
32
33
|
ExtractOrderField,
|
|
33
34
|
ExtractQueryVariables,
|
|
34
35
|
ExtractQueryInputKeys,
|
|
@@ -37,6 +38,8 @@ export type {
|
|
|
37
38
|
MatchingTableName,
|
|
38
39
|
MetadataFieldOptions,
|
|
39
40
|
MetadataFieldsOptions,
|
|
41
|
+
MetadataFilter,
|
|
42
|
+
TableMetadataFilter,
|
|
40
43
|
ColumnSelectorProps,
|
|
41
44
|
CsvButtonProps,
|
|
42
45
|
SearchFilterFormProps,
|
|
@@ -15,13 +15,18 @@ export function Pagination({
|
|
|
15
15
|
nextPage,
|
|
16
16
|
prevPage,
|
|
17
17
|
hasPrevPage,
|
|
18
|
+
hasNextPage,
|
|
18
19
|
}: PaginationProps) {
|
|
19
20
|
return (
|
|
20
21
|
<div className="flex items-center justify-end gap-2 py-2">
|
|
21
22
|
<button
|
|
22
23
|
type="button"
|
|
23
24
|
className="inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
24
|
-
onClick={
|
|
25
|
+
onClick={() => {
|
|
26
|
+
if (pageInfo.startCursor) {
|
|
27
|
+
prevPage(pageInfo.startCursor);
|
|
28
|
+
}
|
|
29
|
+
}}
|
|
25
30
|
disabled={!hasPrevPage}
|
|
26
31
|
>
|
|
27
32
|
Previous
|
|
@@ -34,7 +39,7 @@ export function Pagination({
|
|
|
34
39
|
nextPage(pageInfo.endCursor);
|
|
35
40
|
}
|
|
36
41
|
}}
|
|
37
|
-
disabled={!
|
|
42
|
+
disabled={!hasNextPage}
|
|
38
43
|
>
|
|
39
44
|
Next
|
|
40
45
|
</button>
|