@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.
@@ -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: { hasNextPage: true, endCursor: "cursor-3" },
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 data?.pageInfo ?? { hasNextPage: false, endCursor: null };
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
- collection?.prevPage();
116
- }, [collection]);
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 { TableMetadataMap } from "../generator/metadata-generator";
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
- FieldName,
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 options - Object containing `metadata` (the generated map) and `tableName`.
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
- const TMetadata extends TableMetadataMap,
154
- TTableName extends string & keyof TMetadata,
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: FieldName<TMetadata, TTableName>,
153
+ dataKey: TableFieldName<TTable>,
161
154
  options?: MetadataFieldOptions,
162
155
  ) => FieldColumn<Record<string, unknown>>;
163
156
 
164
157
  columns: (
165
- dataKeys: FieldName<TMetadata, TTableName>[],
158
+ dataKeys: TableFieldName<TTable>[],
166
159
  options?: MetadataFieldsOptions,
167
160
  ) => FieldColumn<Record<string, unknown>>[];
168
161
  } {
169
- const { metadata, tableName } = options;
170
- const tableMeta = metadata[tableName];
171
- const fields = tableMeta.fields;
162
+ const fields = tableMetadata.fields;
172
163
 
173
164
  const column = (
174
- dataKey: FieldName<TMetadata, TTableName>,
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 "${String(tableName)}" metadata`,
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: FieldName<TMetadata, TTableName>[],
207
+ dataKeys: TableFieldName<TTable>[],
217
208
  options?: MetadataFieldsOptions,
218
209
  ): FieldColumn<Record<string, unknown>>[] => {
219
210
  return dataKeys.map((dataKey) => {
@@ -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={prevPage}
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={!pageInfo.hasNextPage}
42
+ disabled={!hasNextPage}
38
43
  >
39
44
  Next
40
45
  </button>