@izumisy-tailor/tailor-data-viewer 0.3.0-preview.0 → 0.3.1

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
@@ -5,8 +5,8 @@ A low-level React component library for building data table interfaces with Tail
5
5
  ## Features
6
6
 
7
7
  - **Separation of Concerns**: Data fetching, query parameter management, and UI are fully decoupled
8
- - **`useCollection` Hook**: Manages filter, sort, and pagination state; outputs Tailor Platform-compatible GraphQL variables
9
- - **`Collection.Provider`**: Shares query parameters via React Context across sibling components
8
+ - **`useCollectionVariables` Hook**: Manages filter, sort, and pagination state; outputs Tailor Platform-compatible GraphQL variables
9
+ - **`CollectionVariablesProvider`**: Shares query parameters via React Context across sibling components
10
10
  - **`Table.*` Compound Components**: Static, unstyled table primitives (`<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`)
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
@@ -15,7 +15,7 @@ A low-level React component library for building data table interfaces with Tail
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
18
- - **Presentation Agnostic**: Same `useCollection` can drive tables, kanbans, calendars, etc.
18
+ - **Presentation Agnostic**: Same `useCollectionVariables` can drive tables, kanbans, calendars, etc.
19
19
 
20
20
  ## Installation
21
21
 
@@ -47,7 +47,7 @@ npm install react react-dom
47
47
 
48
48
  ```tsx
49
49
  import {
50
- useCollection,
50
+ useCollectionVariables,
51
51
  useDataTable,
52
52
  DataTable,
53
53
  Pagination,
@@ -95,8 +95,15 @@ const columns = [
95
95
 
96
96
  // 2. Build a page
97
97
  function OrdersPage() {
98
- const collection = useCollection({ params: { pageSize: 20 } });
99
- const [result] = useQuery({ query: GET_ORDERS, variables: { ...collection.variables.pagination, query: collection.variables.query, order: collection.variables.order } });
98
+ const { variables } = useCollectionVariables({ params: { pageSize: 20 } });
99
+ const [result] = useQuery({
100
+ query: GET_ORDERS,
101
+ variables: {
102
+ ...variables.pagination,
103
+ query: variables.query,
104
+ order: variables.order
105
+ }
106
+ });
100
107
 
101
108
  const table = useDataTable<Order>({
102
109
  columns,
@@ -119,12 +126,12 @@ function OrdersPage() {
119
126
 
120
127
  ## API Overview
121
128
 
122
- ### `useCollection(options)`
129
+ ### `useCollectionVariables(options)`
123
130
 
124
131
  Manages filter, sort, and pagination state. Returns `variables` containing `query`, `order`, and `pagination` sub-properties in Tailor Platform-compatible format.
125
132
 
126
133
  ```tsx
127
- const collection = useCollection({
134
+ const { variables, ...collection } = useCollectionVariables({
128
135
  params: {
129
136
  pageSize: 20,
130
137
  initialSort: [{ field: "createdAt", direction: "Desc" }],
@@ -135,9 +142,9 @@ const collection = useCollection({
135
142
  const [result] = useQuery({
136
143
  query: GET_ORDERS,
137
144
  variables: {
138
- ...collection.variables.pagination,
139
- query: collection.variables.query,
140
- order: collection.variables.order,
145
+ ...variables.pagination,
146
+ query: variables.query,
147
+ order: variables.order,
141
148
  },
142
149
  });
143
150
 
@@ -163,15 +170,15 @@ collection.hasPrevPage; // boolean
163
170
  collection.hasNextPage; // boolean
164
171
  ```
165
172
 
166
- ### `DataTable.Provider` / `useDataTableContext()` / `useCollectionContext()`
173
+ ### `DataTable.Provider` / `useDataTableContext()` / `useCollectionVariablesContext()`
167
174
 
168
175
  `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.
169
176
 
170
- When `collection` is passed to `useDataTable`, `DataTable.Provider` automatically wraps a `Collection.Provider` so child components can use `useCollectionContext()`.
177
+ When `collection` is passed to `useDataTable`, `DataTable.Provider` automatically wraps a `CollectionVariablesProvider` so child components can use `useCollectionVariablesContext()`.
171
178
 
172
179
  ```tsx
173
180
  <DataTable.Provider value={table}>
174
- <StatusFilter /> {/* useCollectionContext() inside */}
181
+ <StatusFilter /> {/* useCollectionVariablesContext() inside */}
175
182
  <DataTable.Root>
176
183
  <DataTable.Headers />
177
184
  <DataTable.Body />
@@ -180,13 +187,13 @@ When `collection` is passed to `useDataTable`, `DataTable.Provider` automaticall
180
187
  </DataTable.Provider>
181
188
  ```
182
189
 
183
- For cases where you need `Collection.Provider` without `DataTable.Provider` (e.g., non-table UIs), you can use it standalone:
190
+ For cases where you need `CollectionVariablesProvider` without `DataTable.Provider` (e.g., non-table UIs), you can use it standalone:
184
191
 
185
192
  ```tsx
186
- <Collection.Provider value={collection}>
193
+ <CollectionVariablesProvider value={collection}>
187
194
  <StatusFilter />
188
195
  <CustomKanbanBoard />
189
- </Collection.Provider>
196
+ </CollectionVariablesProvider>
190
197
  ```
191
198
 
192
199
  ### Column Definition Helper
@@ -64,7 +64,6 @@ interface TableMetadata {
64
64
  readonly name: string;
65
65
  readonly pluralForm: string;
66
66
  readonly description?: string;
67
- readonly readAllowedRoles: readonly string[];
68
67
  readonly fields: readonly FieldMetadata[];
69
68
  /** Relations (manyToOne, oneToOne, and oneToMany) */
70
69
  readonly relations?: readonly RelationMetadata[];
@@ -89,7 +88,6 @@ interface ProcessedTable {
89
88
  pluralForm: string;
90
89
  originalName: string;
91
90
  description?: string;
92
- readAllowedRoles: string[];
93
91
  fields: FieldMetadata[];
94
92
  relations: RelationMetadata[];
95
93
  }
@@ -29,26 +29,6 @@ function toCamelCase(str) {
29
29
  return str.charAt(0).toLowerCase() + str.slice(1);
30
30
  }
31
31
  /**
32
- * Extract allowed roles from gql permission policies
33
- * Only extracts roles from 'read' action policies with 'allow' permit
34
- */
35
- function extractReadAllowedRoles(gqlPermission) {
36
- if (!gqlPermission) return [];
37
- const roles = /* @__PURE__ */ new Set();
38
- for (const policy of gqlPermission) {
39
- if (policy.permit !== "allow") continue;
40
- const actions = policy.actions;
41
- if (!actions.includes("all") && !actions.includes("read")) continue;
42
- for (const condition of policy.conditions) {
43
- if (!Array.isArray(condition) || condition.length < 3) continue;
44
- const [left, operator, right] = condition;
45
- if (typeof left === "string" && operator === "in" && typeof right === "object" && right !== null && "user" in right && right.user === "roles") roles.add(left);
46
- if (typeof right === "string" && operator === "in" && typeof left === "object" && left !== null && "user" in left && left.user === "roles") roles.add(right);
47
- }
48
- }
49
- return Array.from(roles);
50
- }
51
- /**
52
32
  * Creates a custom generator that extracts table metadata for Data View
53
33
  */
54
34
  function dataViewerMetadataGenerator(options = {}) {
@@ -100,13 +80,11 @@ function dataViewerMetadataGenerator(options = {}) {
100
80
  };
101
81
  fields.push(fieldMetadata);
102
82
  }
103
- const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
104
83
  return {
105
84
  name: toCamelCase(type.name),
106
85
  pluralForm: toCamelCase(type.pluralForm),
107
86
  originalName: type.name,
108
87
  description: type.description,
109
- readAllowedRoles,
110
88
  fields,
111
89
  relations
112
90
  };
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.3.0-preview.0",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext, type ReactNode } from "react";
2
2
  import type { UseCollectionReturn } from "../types";
3
3
 
4
- const CollectionContext = createContext<UseCollectionReturn<
4
+ const CollectionVariablesContext = createContext<UseCollectionReturn<
5
5
  string,
6
6
  unknown
7
7
  > | null>(null);
@@ -9,14 +9,18 @@ const CollectionContext = createContext<UseCollectionReturn<
9
9
  /**
10
10
  * Provider that shares collection query parameters via React Context.
11
11
  *
12
- * @example\n * ```tsx\n * const collection = useCollection({ params: { pageSize: 20 } });\n *\n * <CollectionProvider value={collection}>", "oldString": " * @example\n * ```tsx\n * const collection = useCollection({ params: { pageSize: 20 } });\n *\n * <CollectionProvider value={collection}>
12
+ * @example
13
+ * ```tsx
14
+ * const { variables, ...collection } = useCollectionVariables({ params: { pageSize: 20 } });
15
+ *
16
+ * <CollectionVariablesProvider value={collection}>
13
17
  * <FilterPanel />
14
18
  * <DataTable.Root>...</DataTable.Root>
15
- * <Pagination {...table} />
16
- * </CollectionProvider>
19
+ * <Pagination />
20
+ * </CollectionVariablesProvider>
17
21
  * ```
18
22
  */
19
- export function CollectionProvider({
23
+ export function CollectionVariablesProvider({
20
24
  value,
21
25
  children,
22
26
  }: {
@@ -24,56 +28,42 @@ export function CollectionProvider({
24
28
  children: ReactNode;
25
29
  }) {
26
30
  return (
27
- <CollectionContext.Provider value={value}>
31
+ <CollectionVariablesContext.Provider value={value}>
28
32
  {children}
29
- </CollectionContext.Provider>
33
+ </CollectionVariablesContext.Provider>
30
34
  );
31
35
  }
32
36
 
33
37
  /**
34
- * Hook to access collection state from the nearest `Collection.Provider`.
38
+ * Hook to access collection state from the nearest `CollectionVariablesProvider`.
35
39
  *
36
- * Returns the same interface as `useCollection()`. Pass a `TFieldName`
40
+ * Returns the same interface as `useCollectionVariables()`. Pass a `TFieldName`
37
41
  * type parameter to narrow method arguments like `addFilter` / `setSort`.
38
42
  *
39
43
  * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
40
44
  *
41
- * @throws Error if used outside of `Collection.Provider`.
45
+ * @throws Error if used outside of `CollectionVariablesProvider`.
42
46
  *
43
47
  * @example
44
48
  * ```tsx
45
49
  * function StatusFilter() {
46
- * const { filters, addFilter, removeFilter } = useCollectionContext();
50
+ * const { filters, addFilter, removeFilter } = useCollectionVariablesContext();
47
51
  * // ...
48
52
  * }
49
53
  *
50
54
  * // With typed field names:
51
55
  * type TaskField = FieldName<typeof tableMetadata, "task">;
52
- * const { addFilter } = useCollectionContext<TaskField>();
56
+ * const { addFilter } = useCollectionVariablesContext<TaskField>();
53
57
  * ```
54
58
  */
55
- export function useCollectionContext<
59
+ export function useCollectionVariablesContext<
56
60
  TFieldName extends string = string,
57
61
  >(): UseCollectionReturn<TFieldName> {
58
- const ctx = useContext(CollectionContext);
62
+ const ctx = useContext(CollectionVariablesContext);
59
63
  if (!ctx) {
60
64
  throw new Error(
61
- "useCollectionContext must be used within <Collection.Provider>",
65
+ "useCollectionVariablesContext must be used within <CollectionVariablesProvider>",
62
66
  );
63
67
  }
64
68
  return ctx as UseCollectionReturn<TFieldName>;
65
69
  }
66
-
67
- /**
68
- * `Collection` namespace object providing the Provider component.
69
- *
70
- * @example
71
- * ```tsx
72
- * <Collection.Provider value={collection}>
73
- * ...
74
- * </Collection.Provider>
75
- * ```
76
- */
77
- export const Collection = {
78
- Provider: CollectionProvider,
79
- } as const;
@@ -1,15 +1,15 @@
1
1
  import { renderHook, act } from "@testing-library/react";
2
2
  import { describe, it, expect } from "vitest";
3
3
  import type { TableMetadataMap } from "../../generator/metadata-generator";
4
- import { useCollection } from "./use-collection";
4
+ import { useCollectionVariables } from "./use-collection";
5
5
 
6
- describe("useCollection", () => {
6
+ describe("useCollectionVariables", () => {
7
7
  // ---------------------------------------------------------------------------
8
8
  // Initial state
9
9
  // ---------------------------------------------------------------------------
10
10
  describe("initial state", () => {
11
11
  it("returns default variables with pageSize 20", () => {
12
- const { result } = renderHook(() => useCollection({}));
12
+ const { result } = renderHook(() => useCollectionVariables({}));
13
13
  expect(result.current.variables.pagination).toEqual({ first: 20 });
14
14
  expect(result.current.variables.query).toBeUndefined();
15
15
  expect(result.current.variables.order).toBeUndefined();
@@ -21,14 +21,14 @@ describe("useCollection", () => {
21
21
 
22
22
  it("uses custom pageSize", () => {
23
23
  const { result } = renderHook(() =>
24
- useCollection({ params: { pageSize: 50 } }),
24
+ useCollectionVariables({ params: { pageSize: 50 } }),
25
25
  );
26
26
  expect(result.current.variables.pagination.first).toBe(50);
27
27
  });
28
28
 
29
29
  it("applies initial sort", () => {
30
30
  const { result } = renderHook(() =>
31
- useCollection({
31
+ useCollectionVariables({
32
32
  params: {
33
33
  initialSort: [{ field: "createdAt", direction: "Desc" }],
34
34
  },
@@ -44,7 +44,7 @@ describe("useCollection", () => {
44
44
 
45
45
  it("applies initial filters", () => {
46
46
  const { result } = renderHook(() =>
47
- useCollection({
47
+ useCollectionVariables({
48
48
  params: {
49
49
  initialFilters: [
50
50
  {
@@ -68,7 +68,7 @@ describe("useCollection", () => {
68
68
  // ---------------------------------------------------------------------------
69
69
  describe("filter operations", () => {
70
70
  it("adds a filter", () => {
71
- const { result } = renderHook(() => useCollection({}));
71
+ const { result } = renderHook(() => useCollectionVariables({}));
72
72
 
73
73
  act(() => {
74
74
  result.current.addFilter("status", "eq", "ACTIVE");
@@ -86,7 +86,7 @@ describe("useCollection", () => {
86
86
  });
87
87
 
88
88
  it("replaces filter for same field", () => {
89
- const { result } = renderHook(() => useCollection({}));
89
+ const { result } = renderHook(() => useCollectionVariables({}));
90
90
 
91
91
  act(() => {
92
92
  result.current.addFilter("status", "eq", "ACTIVE");
@@ -100,7 +100,7 @@ describe("useCollection", () => {
100
100
  });
101
101
 
102
102
  it("sets filters in bulk", () => {
103
- const { result } = renderHook(() => useCollection({}));
103
+ const { result } = renderHook(() => useCollectionVariables({}));
104
104
 
105
105
  act(() => {
106
106
  result.current.setFilters([
@@ -125,7 +125,7 @@ describe("useCollection", () => {
125
125
  });
126
126
 
127
127
  it("removes a filter", () => {
128
- const { result } = renderHook(() => useCollection({}));
128
+ const { result } = renderHook(() => useCollectionVariables({}));
129
129
 
130
130
  act(() => {
131
131
  result.current.addFilter("status", "eq", "ACTIVE");
@@ -140,7 +140,7 @@ describe("useCollection", () => {
140
140
  });
141
141
 
142
142
  it("clears all filters", () => {
143
- const { result } = renderHook(() => useCollection({}));
143
+ const { result } = renderHook(() => useCollectionVariables({}));
144
144
 
145
145
  act(() => {
146
146
  result.current.addFilter("status", "eq", "ACTIVE");
@@ -155,7 +155,7 @@ describe("useCollection", () => {
155
155
  });
156
156
 
157
157
  it("resets pagination when filters change", () => {
158
- const { result } = renderHook(() => useCollection({}));
158
+ const { result } = renderHook(() => useCollectionVariables({}));
159
159
 
160
160
  // Navigate to next page
161
161
  act(() => {
@@ -177,7 +177,7 @@ describe("useCollection", () => {
177
177
  // ---------------------------------------------------------------------------
178
178
  describe("sort operations", () => {
179
179
  it("sets sort", () => {
180
- const { result } = renderHook(() => useCollection({}));
180
+ const { result } = renderHook(() => useCollectionVariables({}));
181
181
 
182
182
  act(() => {
183
183
  result.current.setSort("createdAt", "Desc");
@@ -192,7 +192,7 @@ describe("useCollection", () => {
192
192
  });
193
193
 
194
194
  it("appends sort for different fields", () => {
195
- const { result } = renderHook(() => useCollection({}));
195
+ const { result } = renderHook(() => useCollectionVariables({}));
196
196
 
197
197
  act(() => {
198
198
  result.current.setSort("createdAt", "Desc");
@@ -208,7 +208,7 @@ describe("useCollection", () => {
208
208
  });
209
209
 
210
210
  it("replaces direction for existing field", () => {
211
- const { result } = renderHook(() => useCollection({}));
211
+ const { result } = renderHook(() => useCollectionVariables({}));
212
212
 
213
213
  act(() => {
214
214
  result.current.setSort("createdAt", "Desc");
@@ -227,7 +227,7 @@ describe("useCollection", () => {
227
227
  });
228
228
 
229
229
  it("removes sort when direction is undefined", () => {
230
- const { result } = renderHook(() => useCollection({}));
230
+ const { result } = renderHook(() => useCollectionVariables({}));
231
231
 
232
232
  act(() => {
233
233
  result.current.setSort("createdAt", "Desc");
@@ -245,7 +245,7 @@ describe("useCollection", () => {
245
245
  });
246
246
 
247
247
  it("clears sort", () => {
248
- const { result } = renderHook(() => useCollection({}));
248
+ const { result } = renderHook(() => useCollectionVariables({}));
249
249
 
250
250
  act(() => {
251
251
  result.current.setSort("createdAt", "Desc");
@@ -264,7 +264,7 @@ describe("useCollection", () => {
264
264
  // ---------------------------------------------------------------------------
265
265
  describe("pagination operations", () => {
266
266
  it("navigates to next page (forward)", () => {
267
- const { result } = renderHook(() => useCollection({}));
267
+ const { result } = renderHook(() => useCollectionVariables({}));
268
268
 
269
269
  act(() => {
270
270
  result.current.nextPage("cursor1");
@@ -279,7 +279,7 @@ describe("useCollection", () => {
279
279
  });
280
280
 
281
281
  it("navigates to previous page (backward)", () => {
282
- const { result } = renderHook(() => useCollection({}));
282
+ const { result } = renderHook(() => useCollectionVariables({}));
283
283
 
284
284
  act(() => {
285
285
  result.current.prevPage("cursor1");
@@ -294,7 +294,7 @@ describe("useCollection", () => {
294
294
  });
295
295
 
296
296
  it("switches direction on nextPage after prevPage", () => {
297
- const { result } = renderHook(() => useCollection({}));
297
+ const { result } = renderHook(() => useCollectionVariables({}));
298
298
 
299
299
  act(() => {
300
300
  result.current.prevPage("cursorB");
@@ -310,7 +310,7 @@ describe("useCollection", () => {
310
310
  });
311
311
 
312
312
  it("resets page to forward direction", () => {
313
- const { result } = renderHook(() => useCollection({}));
313
+ const { result } = renderHook(() => useCollectionVariables({}));
314
314
 
315
315
  act(() => {
316
316
  result.current.prevPage("cursor1");
@@ -324,7 +324,7 @@ describe("useCollection", () => {
324
324
  });
325
325
 
326
326
  it("tracks hasPrevPage from currentPage and hasNextPage from setPageInfo", () => {
327
- const { result } = renderHook(() => useCollection({}));
327
+ const { result } = renderHook(() => useCollectionVariables({}));
328
328
 
329
329
  // Initially on page 1: no prev, no next
330
330
  expect(result.current.hasPrevPage).toBe(false);
@@ -357,7 +357,7 @@ describe("useCollection", () => {
357
357
  describe("variables", () => {
358
358
  it("generates complete variables with filters, sort, and cursor", () => {
359
359
  const { result } = renderHook(() =>
360
- useCollection({
360
+ useCollectionVariables({
361
361
  params: {
362
362
  pageSize: 10,
363
363
  initialFilters: [
@@ -387,7 +387,7 @@ describe("useCollection", () => {
387
387
  });
388
388
 
389
389
  it("omits undefined fields from pagination", () => {
390
- const { result } = renderHook(() => useCollection({}));
390
+ const { result } = renderHook(() => useCollectionVariables({}));
391
391
  const { pagination } = result.current.variables;
392
392
  expect(pagination).toEqual({ first: 20 });
393
393
  expect("after" in pagination).toBe(false);
@@ -397,7 +397,7 @@ describe("useCollection", () => {
397
397
 
398
398
  it("returns undefined for query and order when empty", () => {
399
399
  const { result } = renderHook(() =>
400
- useCollection({ params: { pageSize: 10 } }),
400
+ useCollectionVariables({ params: { pageSize: 10 } }),
401
401
  );
402
402
 
403
403
  expect(result.current.variables.query).toBeUndefined();
@@ -414,7 +414,6 @@ describe("useCollection", () => {
414
414
  task: {
415
415
  name: "task",
416
416
  pluralForm: "tasks",
417
- readAllowedRoles: [],
418
417
  fields: [
419
418
  { name: "id", type: "uuid", required: true },
420
419
  { name: "title", type: "string", required: true },
@@ -432,7 +431,7 @@ describe("useCollection", () => {
432
431
 
433
432
  it("works with tableMetadata", () => {
434
433
  const { result } = renderHook(() =>
435
- useCollection({
434
+ useCollectionVariables({
436
435
  tableMetadata: testMetadata.task,
437
436
  params: { pageSize: 10 },
438
437
  }),
@@ -442,7 +441,7 @@ describe("useCollection", () => {
442
441
 
443
442
  it("applies typed initialSort", () => {
444
443
  const { result } = renderHook(() =>
445
- useCollection({
444
+ useCollectionVariables({
446
445
  tableMetadata: testMetadata.task,
447
446
  params: {
448
447
  initialSort: [{ field: "dueDate", direction: "Desc" }],
@@ -29,18 +29,18 @@ import type { TableFieldName, TableOrderableFieldName } from "../types";
29
29
  * ```tsx
30
30
  * import { tableMetadata } from "./generated/data-viewer-metadata.generated";
31
31
  *
32
- * const collection = useCollection({
32
+ * const { variables } = useCollectionVariables({
33
33
  * tableMetadata: tableMetadata.task,
34
34
  * params: { pageSize: 20 },
35
35
  * });
36
- * const { query, order, pagination } = collection.variables;
36
+ * const { query, order, pagination } = variables;
37
37
  * const [result] = useQuery({
38
38
  * query: GET_TASKS,
39
39
  * variables: { ...pagination, query, order },
40
40
  * });
41
41
  * ```
42
42
  */
43
- export function useCollection<const TTable extends TableMetadata>(
43
+ export function useCollectionVariables<const TTable extends TableMetadata>(
44
44
  options: UseCollectionOptions<
45
45
  TableFieldName<TTable>,
46
46
  TableMetadataFilter<TTable>
@@ -70,15 +70,15 @@ export function useCollection<const TTable extends TableMetadata>(
70
70
  *
71
71
  * @example
72
72
  * ```tsx
73
- * const collection = useCollection({ params: { pageSize: 20 } });
74
- * const { query, order, pagination } = collection.variables;
73
+ * const { variables } = useCollectionVariables({ params: { pageSize: 20 } });
74
+ * const { query, order, pagination } = variables;
75
75
  * const [result] = useQuery({
76
76
  * query: GET_ORDERS,
77
77
  * variables: { ...pagination, query, order },
78
78
  * });
79
79
  * ```
80
80
  */
81
- export function useCollection(
81
+ export function useCollectionVariables(
82
82
  options: UseCollectionOptions & {
83
83
  tableMetadata?: never;
84
84
  },
@@ -87,7 +87,7 @@ export function useCollection(
87
87
  // -----------------------------------------------------------------------------
88
88
  // Implementation
89
89
  // -----------------------------------------------------------------------------
90
- export function useCollection(
90
+ export function useCollectionVariables(
91
91
  options: UseCollectionOptions & {
92
92
  tableMetadata?: TableMetadata;
93
93
  },
@@ -22,7 +22,6 @@ import type { TableMetadata } from "../../generator/metadata-generator";
22
22
  type TestTable = {
23
23
  readonly name: "test";
24
24
  readonly pluralForm: "tests";
25
- readonly readAllowedRoles: readonly [];
26
25
  readonly fields: readonly [
27
26
  { readonly name: "id"; readonly type: "uuid"; readonly required: true },
28
27
  {
@@ -82,9 +81,13 @@ type AssertNumberField = TestQuery extends {
82
81
  : never;
83
82
  export const assertNumberField: AssertNumberField = true;
84
83
 
85
- // ✅ Enum fields produce in/nin with string arrays
84
+ // ✅ Enum fields produce literal union types from enumValues
86
85
  type AssertEnumField = TestQuery extends {
87
- status?: { eq?: string; in?: string[]; nin?: string[] };
86
+ status?: {
87
+ eq?: "active" | "inactive";
88
+ in?: ("active" | "inactive")[];
89
+ nin?: ("active" | "inactive")[];
90
+ };
88
91
  }
89
92
  ? true
90
93
  : never;
@@ -8,7 +8,7 @@ import {
8
8
  type ReactNode,
9
9
  } from "react";
10
10
  import { cn } from "../lib/utils";
11
- import { CollectionProvider } from "../collection/collection-provider";
11
+ import { CollectionVariablesProvider } from "../collection/collection-provider";
12
12
  import { Table } from "../table";
13
13
  import type { RowAction, SortConfig, UseDataTableReturn } from "../types";
14
14
  import {
@@ -53,14 +53,14 @@ function DataTableRoot({
53
53
  /**
54
54
  * Provider that shares `useDataTable()` state via React Context.
55
55
  *
56
- * Internally provides both `DataTableContext` and `CollectionContext`
56
+ * Internally provides both `DataTableContext` and `CollectionVariablesContext`
57
57
  * so that utility components (`Pagination`, `ColumnSelector`,
58
58
  * `SearchFilterForm`, `CsvButton`) can consume data without explicit props.
59
59
  *
60
60
  * @example
61
61
  * ```tsx
62
- * const collection = useCollection({ params: { pageSize: 20 } });
63
- * const [result] = useQuery({ query: GET_ORDERS, variables: { ...collection.variables.pagination, query: collection.variables.query, order: collection.variables.order } });
62
+ * const { variables, ...collection } = useCollectionVariables({ params: { pageSize: 20 } });
63
+ * const [result] = useQuery({ query: GET_ORDERS, variables: { ...variables.pagination, query: variables.query, order: variables.order } });
64
64
  * const table = useDataTable({ columns, data: result.data?.orders, collection });
65
65
  *
66
66
  * <DataTable.Provider value={table}>
@@ -110,10 +110,12 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
110
110
  </DataTableContext.Provider>
111
111
  );
112
112
 
113
- // Wrap with CollectionContext when collection is available
113
+ // Wrap with CollectionVariablesContext when collection is available
114
114
  if (collectionValue) {
115
115
  return (
116
- <CollectionProvider value={collectionValue}>{inner}</CollectionProvider>
116
+ <CollectionVariablesProvider value={collectionValue}>
117
+ {inner}
118
+ </CollectionVariablesProvider>
117
119
  );
118
120
  }
119
121
 
@@ -1,5 +1,5 @@
1
1
  import { useDataTableContext } from "./data-table-context";
2
- import { useCollectionContext } from "../collection/collection-provider";
2
+ import { useCollectionVariablesContext } from "../collection/collection-provider";
3
3
  import { getLabels } from "./i18n";
4
4
 
5
5
  // =============================================================================
@@ -112,7 +112,7 @@ const btnClass =
112
112
  /**
113
113
  * Pagination controls with first/prev/next/last navigation and page indicator.
114
114
  *
115
- * Reads pagination state from `DataTableContext` and `CollectionContext`.
115
+ * Reads pagination state from `DataTableContext` and `CollectionVariablesContext`.
116
116
  * Must be rendered inside `DataTable.Provider` but **outside** `DataTable.Root`.
117
117
  *
118
118
  * @example
@@ -139,7 +139,7 @@ export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
139
139
  goToLastPage,
140
140
  pageSize,
141
141
  setPageSize,
142
- } = useCollectionContext();
142
+ } = useCollectionVariablesContext();
143
143
 
144
144
  const pl = getLabels(locale).pagination;
145
145
  const firstLabel = labels?.first ?? pl.first;
@@ -14,7 +14,7 @@ import type {
14
14
  } from "../types";
15
15
  import { OPERATORS_BY_FILTER_TYPE } from "../types";
16
16
  import { useDataTableContext } from "./data-table-context";
17
- import { useCollectionContext } from "../collection/collection-provider";
17
+ import { useCollectionVariablesContext } from "../collection/collection-provider";
18
18
  import { getLabels } from "./i18n";
19
19
 
20
20
  /**
@@ -22,7 +22,7 @@ import { getLabels } from "./i18n";
22
22
  *
23
23
  * Renders a dropdown panel with type-specific filter inputs, operator
24
24
  * selectors, and active filter badges. Reads `columns` from
25
- * `DataTableContext` and filter state from `CollectionContext`.
25
+ * `DataTableContext` and filter state from `CollectionVariablesContext`.
26
26
  * Must be rendered inside `DataTable.Provider`.
27
27
  *
28
28
  * All text is customisable through the optional `labels` prop (defaults
@@ -46,7 +46,7 @@ export function SearchFilterForm({
46
46
  } = {}) {
47
47
  const { columns, locale } = useDataTableContext();
48
48
  const { filters, addFilter, removeFilter, clearFilters } =
49
- useCollectionContext();
49
+ useCollectionVariablesContext();
50
50
  const sf = getLabels(locale).searchFilter;
51
51
  const filterableColumns = columns.filter((col) => !!col.filter);
52
52
 
@@ -13,8 +13,8 @@ import type {
13
13
  *
14
14
  * @example
15
15
  * ```tsx
16
- * const collection = useCollection({ params: { pageSize: 20 } });
17
- * const { query, order, pagination } = collection.variables;
16
+ * const { variables, ...collection } = useCollectionVariables({ params: { pageSize: 20 } });
17
+ * const { query, order, pagination } = variables;
18
18
  * const [result] = useQuery({
19
19
  * query: GET_ORDERS,
20
20
  * variables: { ...pagination, query, order },
@@ -75,7 +75,6 @@ describe("inferColumns()", () => {
75
75
  const metadata = {
76
76
  name: "task",
77
77
  pluralForm: "tasks",
78
- readAllowedRoles: [],
79
78
  fields: [
80
79
  { name: "id", type: "uuid", required: true },
81
80
  { name: "title", type: "string", required: true },
@@ -204,7 +203,6 @@ describe("inferColumns() with metadata", () => {
204
203
  task: {
205
204
  name: "task",
206
205
  pluralForm: "tasks",
207
- readAllowedRoles: [],
208
206
  fields: [
209
207
  { name: "id", type: "uuid", required: true },
210
208
  { name: "title", type: "string", required: true },
@@ -370,7 +368,6 @@ describe("createColumnHelper()", () => {
370
368
  const metadata = {
371
369
  name: "order",
372
370
  pluralForm: "orders",
373
- readAllowedRoles: [],
374
371
  fields: [
375
372
  { name: "id", type: "uuid", required: true },
376
373
  { name: "name", type: "string", required: true },
@@ -390,7 +387,6 @@ describe("createColumnHelper()", () => {
390
387
  const metadata = {
391
388
  name: "order",
392
389
  pluralForm: "orders",
393
- readAllowedRoles: [],
394
390
  fields: [
395
391
  { name: "id", type: "uuid", required: true },
396
392
  { name: "name", type: "string", required: true },
@@ -42,11 +42,10 @@ export {
42
42
  } from "./types";
43
43
 
44
44
  // Collection
45
- export { useCollection } from "./collection/use-collection";
45
+ export { useCollectionVariables } from "./collection/use-collection";
46
46
  export {
47
- Collection,
48
- CollectionProvider,
49
- useCollectionContext,
47
+ CollectionVariablesProvider,
48
+ useCollectionVariablesContext,
50
49
  } from "./collection/collection-provider";
51
50
 
52
51
  // Table (static)
@@ -219,13 +219,13 @@ export interface PageInfo {
219
219
  * GraphQL query variables in Tailor Platform format.
220
220
  *
221
221
  * Field-level type safety for `query` and `order` is not enforced here.
222
- * When `useCollection` is called with metadata, `BuildQueryVariables`
222
+ * When `useCollectionVariables` is called with metadata, `BuildQueryVariables`
223
223
  * produces precise per-field filter types at compile time.
224
224
  */
225
225
  export interface QueryVariables {
226
- /** Filter object built at runtime by `useCollection`. */
226
+ /** Filter object built at runtime by `useCollectionVariables`. */
227
227
  query?: Record<string, unknown>;
228
- /** Sort order built at runtime by `useCollection`. */
228
+ /** Sort order built at runtime by `useCollectionVariables`. */
229
229
  order?: { field: string; direction: "Asc" | "Desc" }[];
230
230
  /** Forward pagination: number of items to fetch */
231
231
  first?: number | null;
@@ -274,6 +274,23 @@ type FieldTypeToTSType = {
274
274
  enum: string;
275
275
  };
276
276
 
277
+ /**
278
+ * Resolves the value type for a field, narrowing enum fields to their
279
+ * literal union when `enumValues` is present in the metadata.
280
+ *
281
+ * - enum field with `enumValues: readonly ["OPEN", "CLOSED"]` → `"OPEN" | "CLOSED"`
282
+ * - enum field without `enumValues` → `string` (fallback)
283
+ * - all other field types → `FieldTypeToTSType[T]` (unchanged)
284
+ */
285
+ type ResolveFieldValueType<
286
+ F,
287
+ T extends keyof FieldTypeToTSType,
288
+ > = T extends "enum"
289
+ ? F extends { readonly enumValues: readonly (infer V extends string)[] }
290
+ ? V
291
+ : string
292
+ : FieldTypeToTSType[T];
293
+
277
294
  /**
278
295
  * Resolves the value type for a filter operator.
279
296
  * `in`/`nin` operators accept arrays, `between` accepts `{ min, max }`,
@@ -324,7 +341,7 @@ export type BuildQueryVariables<TTable extends TableMetadata> = {
324
341
  }
325
342
  ? FieldTypeToFilterConfigType[T] extends infer FCT extends
326
343
  FilterConfig["type"]
327
- ? FilterInputForFieldType<FCT, FieldTypeToTSType[T]>
344
+ ? FilterInputForFieldType<FCT, ResolveFieldValueType<F, T>>
328
345
  : never
329
346
  : never;
330
347
  };
@@ -425,11 +442,11 @@ export type ColumnDefinition<TRow extends Record<string, unknown>> =
425
442
  Column<TRow>;
426
443
 
427
444
  // =============================================================================
428
- // useCollection Types
445
+ // useCollectionVariables Types
429
446
  // =============================================================================
430
447
 
431
448
  /**
432
- * Options for `useCollection` hook.
449
+ * Options for `useCollectionVariables` hook.
433
450
  *
434
451
  * @typeParam TFieldName - Union of allowed field name strings (default: `string`).
435
452
  */
@@ -449,7 +466,7 @@ export interface UseCollectionOptions<
449
466
  }
450
467
 
451
468
  /**
452
- * Return type of `useCollection` hook.
469
+ * Return type of `useCollectionVariables` hook.
453
470
  *
454
471
  * Methods that accept a field name are typed with `TFieldName` so that
455
472
  * auto-completion works when a concrete union is supplied.
@@ -86,7 +86,6 @@ export interface TableMetadata {
86
86
  readonly name: string;
87
87
  readonly pluralForm: string;
88
88
  readonly description?: string;
89
- readonly readAllowedRoles: readonly string[];
90
89
  readonly fields: readonly FieldMetadata[];
91
90
  /** Relations (manyToOne, oneToOne, and oneToMany) */
92
91
  readonly relations?: readonly RelationMetadata[];
@@ -112,7 +111,6 @@ interface ProcessedTable {
112
111
  pluralForm: string;
113
112
  originalName: string; // PascalCase name for relation lookup
114
113
  description?: string;
115
- readAllowedRoles: string[];
116
114
  fields: FieldMetadata[];
117
115
  relations: RelationMetadata[];
118
116
  }
@@ -154,63 +152,6 @@ function toCamelCase(str: string): string {
154
152
  return str.charAt(0).toLowerCase() + str.slice(1);
155
153
  }
156
154
 
157
- /**
158
- * Extract allowed roles from gql permission policies
159
- * Only extracts roles from 'read' action policies with 'allow' permit
160
- */
161
- function extractReadAllowedRoles(
162
- gqlPermission?: readonly {
163
- conditions: readonly unknown[];
164
- actions: readonly ["all"] | readonly string[];
165
- permit: "allow" | "deny";
166
- description?: string;
167
- }[],
168
- ): string[] {
169
- if (!gqlPermission) return [];
170
-
171
- const roles = new Set<string>();
172
-
173
- for (const policy of gqlPermission) {
174
- // Only process 'allow' policies that include 'read' action
175
- if (policy.permit !== "allow") continue;
176
- const actions = policy.actions as readonly string[];
177
- if (!actions.includes("all") && !actions.includes("read")) continue;
178
-
179
- // Extract roles from conditions
180
- for (const condition of policy.conditions) {
181
- if (!Array.isArray(condition) || condition.length < 3) continue;
182
-
183
- const [left, operator, right] = condition;
184
-
185
- // Check for pattern: ["ROLE_NAME", "in", { user: "roles" }]
186
- if (
187
- typeof left === "string" &&
188
- operator === "in" &&
189
- typeof right === "object" &&
190
- right !== null &&
191
- "user" in right &&
192
- (right as { user: string }).user === "roles"
193
- ) {
194
- roles.add(left);
195
- }
196
-
197
- // Check for pattern: [{ user: "roles" }, "in", "ROLE_NAME"] (reversed)
198
- if (
199
- typeof right === "string" &&
200
- operator === "in" &&
201
- typeof left === "object" &&
202
- left !== null &&
203
- "user" in left &&
204
- (left as { user: string }).user === "roles"
205
- ) {
206
- roles.add(right);
207
- }
208
- }
209
- }
210
-
211
- return Array.from(roles);
212
- }
213
-
214
155
  /**
215
156
  * Parsed field type from TailorDB
216
157
  */
@@ -402,15 +343,11 @@ export function dataViewerMetadataGenerator(
402
343
  fields.push(fieldMetadata);
403
344
  }
404
345
 
405
- // Extract read allowed roles from gql permission
406
- const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
407
-
408
346
  return {
409
347
  name: toCamelCase(type.name),
410
348
  pluralForm: toCamelCase(type.pluralForm),
411
349
  originalName: type.name,
412
350
  description: type.description,
413
- readAllowedRoles,
414
351
  fields,
415
352
  relations,
416
353
  };
@@ -3,7 +3,7 @@ import { vi } from "vitest";
3
3
  import type { Column, UseCollectionReturn } from "../component/types";
4
4
  import { DataTableContext } from "../component/data-table/data-table-context";
5
5
  import type { DataTableContextValue } from "../component/data-table/data-table-context";
6
- import { CollectionProvider } from "../component/collection/collection-provider";
6
+ import { CollectionVariablesProvider } from "../component/collection/collection-provider";
7
7
 
8
8
  // =============================================================================
9
9
  // Mock factory: DataTableContext
@@ -43,7 +43,7 @@ export function createMockDataTableContext<T extends Record<string, unknown>>(
43
43
  }
44
44
 
45
45
  // =============================================================================
46
- // Mock factory: CollectionContext
46
+ // Mock factory: CollectionVariablesContext
47
47
  // =============================================================================
48
48
 
49
49
  export function createMockCollectionContext(
@@ -122,7 +122,7 @@ export function createTestProviders<
122
122
  collection?: Partial<UseCollectionReturn<string, unknown>>;
123
123
  }) {
124
124
  return (
125
- <CollectionProvider
125
+ <CollectionVariablesProvider
126
126
  value={createMockCollectionContext({
127
127
  ...defaults.collectionDefaults,
128
128
  ...collection,
@@ -136,7 +136,7 @@ export function createTestProviders<
136
136
  >
137
137
  {children}
138
138
  </DataTableContext.Provider>
139
- </CollectionProvider>
139
+ </CollectionVariablesProvider>
140
140
  );
141
141
  };
142
142
  }