@izumisy-tailor/tailor-data-viewer 0.1.4 → 0.1.5

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.
Files changed (47) hide show
  1. package/README.md +44 -4
  2. package/dist/generator/index.d.mts +7 -1
  3. package/docs/app-shell-module.md +151 -0
  4. package/docs/saved-view-store.md +155 -0
  5. package/package.json +27 -2
  6. package/src/app-shell/create-data-view-module.tsx +84 -0
  7. package/src/app-shell/index.ts +2 -0
  8. package/src/app-shell/types.ts +42 -0
  9. package/src/component/column-selector.tsx +4 -4
  10. package/src/component/data-table.tsx +2 -2
  11. package/src/component/data-view-tab-content.tsx +3 -3
  12. package/src/component/data-viewer.tsx +38 -9
  13. package/src/component/hooks/use-accessible-tables.ts +1 -1
  14. package/src/component/hooks/use-column-state.ts +1 -1
  15. package/src/component/hooks/use-relation-data.ts +1 -1
  16. package/src/component/hooks/use-table-access-check.ts +103 -0
  17. package/src/component/hooks/use-table-data.ts +1 -1
  18. package/src/component/index.ts +4 -1
  19. package/src/component/pagination.tsx +1 -1
  20. package/src/component/relation-content.tsx +4 -4
  21. package/src/component/saved-view-context.tsx +195 -48
  22. package/src/component/search-filter.tsx +8 -8
  23. package/src/component/single-record-tab-content.tsx +5 -5
  24. package/src/component/table-selector.tsx +2 -2
  25. package/src/component/types.ts +1 -1
  26. package/src/component/view-save-load.tsx +4 -4
  27. package/src/generator/metadata-generator.ts +7 -0
  28. package/src/store/indexeddb.ts +150 -0
  29. package/src/store/tailordb/index.ts +204 -0
  30. package/src/store/tailordb/schema.ts +114 -0
  31. package/src/store/types.ts +85 -0
  32. package/src/utils/query-builder.ts +1 -1
  33. package/src/types/table-metadata.ts +0 -72
  34. /package/{API.md → docs/API.md} +0 -0
  35. /package/src/{lib → component/lib}/utils.ts +0 -0
  36. /package/src/{ui → component/ui}/alert.tsx +0 -0
  37. /package/src/{ui → component/ui}/badge.tsx +0 -0
  38. /package/src/{ui → component/ui}/button.tsx +0 -0
  39. /package/src/{ui → component/ui}/card.tsx +0 -0
  40. /package/src/{ui → component/ui}/checkbox.tsx +0 -0
  41. /package/src/{ui → component/ui}/collapsible.tsx +0 -0
  42. /package/src/{ui → component/ui}/dialog.tsx +0 -0
  43. /package/src/{ui → component/ui}/dropdown-menu.tsx +0 -0
  44. /package/src/{ui → component/ui}/input.tsx +0 -0
  45. /package/src/{ui → component/ui}/label.tsx +0 -0
  46. /package/src/{ui → component/ui}/select.tsx +0 -0
  47. /package/src/{ui → component/ui}/table.tsx +0 -0
@@ -1,16 +1,15 @@
1
1
  import { useState, useCallback } from "react";
2
- import { Plus, X } from "lucide-react";
3
- import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
4
- import { Button } from "../ui/button";
5
- import type { TableMetadata, TableMetadataMap } from "../types/table-metadata";
6
- import { useAccessibleTables } from "./hooks/use-accessible-tables";
2
+ import { Plus, X, Loader2 } from "lucide-react";
3
+ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
4
+ import { Button } from "./ui/button";
5
+ import type { TableMetadata, TableMetadataMap } from "../generator/metadata-generator";
6
+ import { useTableAccessCheck } from "./hooks/use-table-access-check";
7
7
  import { DataViewTabContent } from "./data-view-tab-content";
8
8
  import { SingleRecordTabContent } from "./single-record-tab-content";
9
9
  import { useSavedViews, type SavedView } from "./saved-view-context";
10
10
 
11
11
  interface DataViewerProps {
12
12
  tableMetadata: TableMetadataMap;
13
- userRoles: string[];
14
13
  appUri: string;
15
14
  /** Initial view ID to load on mount */
16
15
  initialViewId?: string;
@@ -52,12 +51,15 @@ function generateTabId(): string {
52
51
  */
53
52
  export function DataViewer({
54
53
  tableMetadata,
55
- userRoles,
56
54
  appUri,
57
55
  initialViewId,
58
56
  }: DataViewerProps) {
59
- // Get tables accessible to the current user
60
- const accessibleTables = useAccessibleTables(tableMetadata, userRoles);
57
+ // Get tables accessible to the current user via runtime permission check
58
+ const allTables = Object.values(tableMetadata);
59
+ const { accessibleTables, isLoading, error } = useTableAccessCheck(
60
+ allTables,
61
+ appUri,
62
+ );
61
63
  const { getViewById } = useSavedViews();
62
64
 
63
65
  // Tab management - initialize with saved view if provided
@@ -186,6 +188,33 @@ export function DataViewer({
186
188
  [tableMetadata],
187
189
  );
188
190
 
191
+ // Loading state during permission check
192
+ if (isLoading) {
193
+ return (
194
+ <Card>
195
+ <CardContent className="py-8">
196
+ <div className="flex flex-col items-center gap-2 text-muted-foreground">
197
+ <Loader2 className="size-6 animate-spin" />
198
+ <span>テーブルへのアクセス権限を確認中...</span>
199
+ </div>
200
+ </CardContent>
201
+ </Card>
202
+ );
203
+ }
204
+
205
+ // Error state
206
+ if (error) {
207
+ return (
208
+ <Card>
209
+ <CardContent className="py-8">
210
+ <div className="text-destructive text-center">
211
+ アクセス権限の確認中にエラーが発生しました: {error}
212
+ </div>
213
+ </CardContent>
214
+ </Card>
215
+ );
216
+ }
217
+
189
218
  if (accessibleTables.length === 0) {
190
219
  return (
191
220
  <Card>
@@ -2,7 +2,7 @@ import { useMemo } from "react";
2
2
  import type {
3
3
  TableMetadata,
4
4
  TableMetadataMap,
5
- } from "../../types/table-metadata";
5
+ } from "../../generator/metadata-generator";
6
6
 
7
7
  /**
8
8
  * Filter tables based on user's roles
@@ -3,7 +3,7 @@ import type {
3
3
  FieldMetadata,
4
4
  RelationMetadata,
5
5
  ExpandedRelationFields,
6
- } from "../../types/table-metadata";
6
+ } from "../../generator/metadata-generator";
7
7
 
8
8
  export interface ColumnState {
9
9
  selectedFields: string[];
@@ -3,7 +3,7 @@ import type {
3
3
  TableMetadata,
4
4
  RelationMetadata,
5
5
  TableMetadataMap,
6
- } from "../../types/table-metadata";
6
+ } from "../../generator/metadata-generator";
7
7
  import {
8
8
  createGraphQLClient,
9
9
  executeQuery,
@@ -0,0 +1,103 @@
1
+ import { useState, useEffect } from "react";
2
+ import { GraphQLClient } from "graphql-request";
3
+ import type { TableMetadata } from "../../generator/metadata-generator";
4
+
5
+ export interface TableAccessCheckResult {
6
+ /** Tables that the user has access to */
7
+ accessibleTables: TableMetadata[];
8
+ /** Whether the check is in progress */
9
+ isLoading: boolean;
10
+ /** Error message if the check failed entirely */
11
+ error: string | null;
12
+ }
13
+
14
+ /**
15
+ * Check which tables the user has access to by executing aggregate queries
16
+ * Uses server-side gqlPermission as the source of truth
17
+ */
18
+ export function useTableAccessCheck(
19
+ tables: TableMetadata[],
20
+ appUri: string,
21
+ ): TableAccessCheckResult {
22
+ const [accessibleTables, setAccessibleTables] = useState<TableMetadata[]>([]);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+
29
+ async function checkAccess() {
30
+ setIsLoading(true);
31
+ setError(null);
32
+
33
+ const client = new GraphQLClient(`${appUri}/query`, {
34
+ credentials: "include",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ "X-Tailor-Nonce": crypto.randomUUID(),
38
+ },
39
+ });
40
+
41
+ const results = await Promise.allSettled(
42
+ tables.map(async (table) => {
43
+ // Execute a lightweight aggregate query to check access
44
+ const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
45
+ await client.request(query);
46
+ return table;
47
+ }),
48
+ );
49
+
50
+ if (cancelled) return;
51
+
52
+ // Filter to only fulfilled results (tables with access)
53
+ const accessible = results
54
+ .filter(
55
+ (r): r is PromiseFulfilledResult<TableMetadata> =>
56
+ r.status === "fulfilled",
57
+ )
58
+ .map((r) => r.value);
59
+
60
+ setAccessibleTables(accessible);
61
+ setIsLoading(false);
62
+ }
63
+
64
+ checkAccess().catch((err) => {
65
+ if (!cancelled) {
66
+ setError(
67
+ err instanceof Error ? err.message : "Failed to check table access",
68
+ );
69
+ setIsLoading(false);
70
+ }
71
+ });
72
+
73
+ return () => {
74
+ cancelled = true;
75
+ };
76
+ }, [tables, appUri]);
77
+
78
+ return { accessibleTables, isLoading, error };
79
+ }
80
+
81
+ /**
82
+ * Standalone function to check table access (non-hook version)
83
+ * Useful for one-time checks or server-side usage
84
+ */
85
+ export async function checkTableAccess(
86
+ client: GraphQLClient,
87
+ tables: TableMetadata[],
88
+ ): Promise<TableMetadata[]> {
89
+ const results = await Promise.allSettled(
90
+ tables.map(async (table) => {
91
+ const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
92
+ await client.request(query);
93
+ return table;
94
+ }),
95
+ );
96
+
97
+ return results
98
+ .filter(
99
+ (r): r is PromiseFulfilledResult<TableMetadata> =>
100
+ r.status === "fulfilled",
101
+ )
102
+ .map((r) => r.value);
103
+ }
@@ -3,7 +3,7 @@ import type {
3
3
  TableMetadata,
4
4
  TableMetadataMap,
5
5
  ExpandedRelationFields,
6
- } from "../../types/table-metadata";
6
+ } from "../../generator/metadata-generator";
7
7
  import {
8
8
  createGraphQLClient,
9
9
  executeQuery,
@@ -9,7 +9,10 @@ export { SearchFilterForm } from "./search-filter";
9
9
  export { ViewSave } from "./view-save-load";
10
10
  export { useTableData } from "./hooks/use-table-data";
11
11
  export { useColumnState } from "./hooks/use-column-state";
12
- export { useAccessibleTables } from "./hooks/use-accessible-tables";
12
+ export {
13
+ useTableAccessCheck,
14
+ checkTableAccess,
15
+ } from "./hooks/use-table-access-check";
13
16
  export { useSavedViews, SavedViewProvider } from "./saved-view-context";
14
17
  export type { SavedView, SaveViewInput } from "./saved-view-context";
15
18
  export type { SearchFilter, SearchFilters } from "./types";
@@ -1,5 +1,5 @@
1
1
  import { ChevronLeft, ChevronRight } from "lucide-react";
2
- import { Button } from "../ui/button";
2
+ import { Button } from "./ui/button";
3
3
  import type { PaginationState } from "./hooks/use-table-data";
4
4
 
5
5
  interface PaginationProps {
@@ -6,14 +6,14 @@ import {
6
6
  TableHead,
7
7
  TableHeader,
8
8
  TableRow,
9
- } from "../ui/table";
10
- import { Button } from "../ui/button";
11
- import { Badge } from "../ui/badge";
9
+ } from "./ui/table";
10
+ import { Button } from "./ui/button";
11
+ import { Badge } from "./ui/badge";
12
12
  import type {
13
13
  TableMetadata,
14
14
  RelationMetadata,
15
15
  FieldMetadata,
16
- } from "../types/table-metadata";
16
+ } from "../generator/metadata-generator";
17
17
  import { formatFieldValue } from "../utils/query-builder";
18
18
  import type { RelationDataResult } from "./hooks/use-relation-data";
19
19
 
@@ -3,13 +3,19 @@ import {
3
3
  use,
4
4
  useState,
5
5
  useCallback,
6
+ useEffect,
6
7
  type ReactNode,
7
8
  } from "react";
8
- import type { ExpandedRelationFields } from "../types/table-metadata";
9
+ import type { ExpandedRelationFields } from "../generator/metadata-generator";
9
10
  import type { SearchFilters } from "./types";
11
+ import type {
12
+ SavedViewStore,
13
+ SavedView as StoreSavedView,
14
+ SavedViewInput as StoreSavedViewInput,
15
+ } from "../store/types";
10
16
 
11
17
  /**
12
- * Saved view configuration
18
+ * Saved view configuration (UI format - maintains backward compatibility)
13
19
  */
14
20
  export interface SavedView {
15
21
  /** Unique identifier */
@@ -31,6 +37,8 @@ export interface SavedView {
31
37
  }
32
38
 
33
39
  export interface SaveViewInput {
40
+ /** ID for updating existing view */
41
+ id?: string;
34
42
  name: string;
35
43
  tableName: string;
36
44
  filters: SearchFilters;
@@ -41,70 +49,207 @@ export interface SaveViewInput {
41
49
 
42
50
  interface SavedViewContextValue {
43
51
  views: SavedView[];
44
- saveView: (input: SaveViewInput) => SavedView;
45
- deleteView: (id: string) => boolean;
46
- renameView: (id: string, newName: string) => boolean;
52
+ isLoading: boolean;
53
+ saveView: (input: SaveViewInput) => Promise<SavedView>;
54
+ deleteView: (id: string) => Promise<boolean>;
55
+ renameView: (id: string, newName: string) => Promise<boolean>;
47
56
  getViewById: (id: string) => SavedView | undefined;
48
57
  getViewsByTable: (tableName: string) => SavedView[];
58
+ refreshViews: () => Promise<void>;
49
59
  }
50
60
 
51
61
  const SavedViewContext = createContext<SavedViewContextValue | null>(null);
52
62
 
63
+ interface SavedViewProviderProps {
64
+ children: ReactNode;
65
+ /** Store implementation for persisting views */
66
+ store?: SavedViewStore;
67
+ }
68
+
69
+ /**
70
+ * Convert store format to UI format
71
+ */
72
+ function fromStoreView(storeView: StoreSavedView): SavedView {
73
+ return {
74
+ id: storeView.id,
75
+ name: storeView.name,
76
+ tableName: storeView.tableName,
77
+ filters: storeView.filters,
78
+ selectedFields: storeView.columns,
79
+ selectedRelations: storeView.selectedRelations,
80
+ expandedRelationFields: storeView.expandedRelationFields,
81
+ createdAt: storeView.createdAt,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Convert UI format to store format
87
+ */
88
+ function toStoreInput(input: SaveViewInput): StoreSavedViewInput {
89
+ return {
90
+ id: input.id,
91
+ name: input.name,
92
+ tableName: input.tableName,
93
+ columns: input.selectedFields,
94
+ filters: input.filters,
95
+ sortOrder: [],
96
+ selectedRelations: input.selectedRelations,
97
+ expandedRelationFields: input.expandedRelationFields,
98
+ };
99
+ }
100
+
53
101
  let idCounter = 0;
54
102
  function generateId(): string {
55
103
  return `saved-view-${++idCounter}`;
56
104
  }
57
105
 
58
- interface SavedViewProviderProps {
59
- children: ReactNode;
60
- }
61
-
62
- export function SavedViewProvider({ children }: SavedViewProviderProps) {
106
+ export function SavedViewProvider({ children, store }: SavedViewProviderProps) {
63
107
  const [views, setViews] = useState<SavedView[]>([]);
108
+ const [isLoading, setIsLoading] = useState(!!store);
109
+
110
+ // Load views from store on mount
111
+ useEffect(() => {
112
+ if (!store) return;
113
+
114
+ let cancelled = false;
115
+ const currentStore = store; // Capture for closure
116
+
117
+ async function loadViews() {
118
+ setIsLoading(true);
119
+ try {
120
+ const storeViews = await currentStore.listViews();
121
+ if (!cancelled) {
122
+ setViews(storeViews.map(fromStoreView));
123
+ }
124
+ } catch (error) {
125
+ console.error("Failed to load views:", error);
126
+ } finally {
127
+ if (!cancelled) {
128
+ setIsLoading(false);
129
+ }
130
+ }
131
+ }
132
+
133
+ loadViews();
64
134
 
65
- const saveView = useCallback((input: SaveViewInput): SavedView => {
66
- const newView: SavedView = {
67
- id: generateId(),
68
- name: input.name,
69
- tableName: input.tableName,
70
- filters: [...input.filters],
71
- selectedFields: [...input.selectedFields],
72
- selectedRelations: [...input.selectedRelations],
73
- expandedRelationFields: { ...input.expandedRelationFields },
74
- createdAt: new Date(),
135
+ return () => {
136
+ cancelled = true;
75
137
  };
76
- setViews((prev) => [newView, ...prev]);
77
- return newView;
78
- }, []);
79
-
80
- const deleteView = useCallback((id: string): boolean => {
81
- let deleted = false;
82
- setViews((prev) => {
83
- const newViews = prev.filter((v) => {
84
- if (v.id === id) {
85
- deleted = true;
138
+ }, [store]);
139
+
140
+ const refreshViews = useCallback(async () => {
141
+ if (!store) return;
142
+
143
+ setIsLoading(true);
144
+ try {
145
+ const storeViews = await store.listViews();
146
+ setViews(storeViews.map(fromStoreView));
147
+ } catch (error) {
148
+ console.error("Failed to refresh views:", error);
149
+ } finally {
150
+ setIsLoading(false);
151
+ }
152
+ }, [store]);
153
+
154
+ const saveView = useCallback(
155
+ async (input: SaveViewInput): Promise<SavedView> => {
156
+ if (store) {
157
+ const saved = await store.saveView(toStoreInput(input));
158
+ const view = fromStoreView(saved);
159
+ setViews((prev) => {
160
+ const existing = prev.findIndex((v) => v.id === view.id);
161
+ if (existing >= 0) {
162
+ const newViews = [...prev];
163
+ newViews[existing] = view;
164
+ return newViews;
165
+ }
166
+ return [view, ...prev];
167
+ });
168
+ return view;
169
+ }
170
+
171
+ // In-memory fallback
172
+ const newView: SavedView = {
173
+ id: input.id ?? generateId(),
174
+ name: input.name,
175
+ tableName: input.tableName,
176
+ filters: [...input.filters],
177
+ selectedFields: [...input.selectedFields],
178
+ selectedRelations: [...input.selectedRelations],
179
+ expandedRelationFields: { ...input.expandedRelationFields },
180
+ createdAt: new Date(),
181
+ };
182
+ setViews((prev) => {
183
+ if (input.id) {
184
+ return prev.map((v) => (v.id === input.id ? newView : v));
185
+ }
186
+ return [newView, ...prev];
187
+ });
188
+ return newView;
189
+ },
190
+ [store],
191
+ );
192
+
193
+ const deleteView = useCallback(
194
+ async (id: string): Promise<boolean> => {
195
+ if (store) {
196
+ try {
197
+ await store.deleteView(id);
198
+ setViews((prev) => prev.filter((v) => v.id !== id));
199
+ return true;
200
+ } catch {
86
201
  return false;
87
202
  }
88
- return true;
203
+ }
204
+
205
+ // In-memory fallback
206
+ let deleted = false;
207
+ setViews((prev) => {
208
+ const newViews = prev.filter((v) => {
209
+ if (v.id === id) {
210
+ deleted = true;
211
+ return false;
212
+ }
213
+ return true;
214
+ });
215
+ return newViews;
89
216
  });
90
- return newViews;
91
- });
92
- return deleted;
93
- }, []);
94
-
95
- const renameView = useCallback((id: string, newName: string): boolean => {
96
- let renamed = false;
97
- setViews((prev) =>
98
- prev.map((v) => {
99
- if (v.id === id) {
100
- renamed = true;
101
- return { ...v, name: newName };
217
+ return deleted;
218
+ },
219
+ [store],
220
+ );
221
+
222
+ const renameView = useCallback(
223
+ async (id: string, newName: string): Promise<boolean> => {
224
+ const view = views.find((v) => v.id === id);
225
+ if (!view) return false;
226
+
227
+ if (store) {
228
+ try {
229
+ await store.saveView(
230
+ toStoreInput({
231
+ ...view,
232
+ id,
233
+ name: newName,
234
+ }),
235
+ );
236
+ setViews((prev) =>
237
+ prev.map((v) => (v.id === id ? { ...v, name: newName } : v)),
238
+ );
239
+ return true;
240
+ } catch {
241
+ return false;
102
242
  }
103
- return v;
104
- }),
105
- );
106
- return renamed;
107
- }, []);
243
+ }
244
+
245
+ // In-memory fallback
246
+ setViews((prev) =>
247
+ prev.map((v) => (v.id === id ? { ...v, name: newName } : v)),
248
+ );
249
+ return true;
250
+ },
251
+ [store, views],
252
+ );
108
253
 
109
254
  const getViewById = useCallback(
110
255
  (id: string): SavedView | undefined => {
@@ -124,11 +269,13 @@ export function SavedViewProvider({ children }: SavedViewProviderProps) {
124
269
  <SavedViewContext
125
270
  value={{
126
271
  views,
272
+ isLoading,
127
273
  saveView,
128
274
  deleteView,
129
275
  renameView,
130
276
  getViewById,
131
277
  getViewsByTable,
278
+ refreshViews,
132
279
  }}
133
280
  >
134
281
  {children}
@@ -7,24 +7,24 @@ import {
7
7
  ChevronDown,
8
8
  ChevronRight,
9
9
  } from "lucide-react";
10
- import { Button } from "../ui/button";
11
- import { Input } from "../ui/input";
12
- import { Checkbox } from "../ui/checkbox";
10
+ import { Button } from "./ui/button";
11
+ import { Input } from "./ui/input";
12
+ import { Checkbox } from "./ui/checkbox";
13
13
  import {
14
14
  Select,
15
15
  SelectContent,
16
16
  SelectItem,
17
17
  SelectTrigger,
18
18
  SelectValue,
19
- } from "../ui/select";
19
+ } from "./ui/select";
20
20
  import {
21
21
  Collapsible,
22
22
  CollapsibleContent,
23
23
  CollapsibleTrigger,
24
- } from "../ui/collapsible";
25
- import { Badge } from "../ui/badge";
26
- import { Label } from "../ui/label";
27
- import type { FieldMetadata } from "../types/table-metadata";
24
+ } from "./ui/collapsible";
25
+ import { Badge } from "./ui/badge";
26
+ import { Label } from "./ui/label";
27
+ import type { FieldMetadata } from "../generator/metadata-generator";
28
28
  import type { SearchFilter, SearchFilters } from "./types";
29
29
 
30
30
  interface SearchFilterProps {
@@ -1,8 +1,8 @@
1
1
  import { useState, useCallback, useMemo, useEffect } from "react";
2
2
  import { RefreshCw, Loader2, ChevronDown, ExternalLink } from "lucide-react";
3
- import { Alert, AlertDescription } from "../ui/alert";
4
- import { Button } from "../ui/button";
5
- import { Badge } from "../ui/badge";
3
+ import { Alert, AlertDescription } from "./ui/alert";
4
+ import { Button } from "./ui/button";
5
+ import { Badge } from "./ui/badge";
6
6
  import {
7
7
  Table,
8
8
  TableBody,
@@ -10,12 +10,12 @@ import {
10
10
  TableHead,
11
11
  TableHeader,
12
12
  TableRow,
13
- } from "../ui/table";
13
+ } from "./ui/table";
14
14
  import type {
15
15
  TableMetadata,
16
16
  TableMetadataMap,
17
17
  FieldMetadata,
18
- } from "../types/table-metadata";
18
+ } from "../generator/metadata-generator";
19
19
  import { createGraphQLClient, executeQuery } from "../providers/graphql-client";
20
20
  import { formatFieldValue } from "../utils/query-builder";
21
21
  import { ColumnSelector } from "./column-selector";
@@ -1,12 +1,12 @@
1
1
  import { Database } from "lucide-react";
2
- import type { TableMetadata } from "../types/table-metadata";
2
+ import type { TableMetadata } from "../generator/metadata-generator";
3
3
  import {
4
4
  Select,
5
5
  SelectContent,
6
6
  SelectItem,
7
7
  SelectTrigger,
8
8
  SelectValue,
9
- } from "../ui/select";
9
+ } from "./ui/select";
10
10
 
11
11
  interface TableSelectorProps {
12
12
  tables: TableMetadata[];
@@ -1,4 +1,4 @@
1
- import type { FieldType } from "../types/table-metadata";
1
+ import type { FieldType } from "../generator/metadata-generator";
2
2
 
3
3
  /**
4
4
  * Search filter condition for a single field
@@ -1,7 +1,7 @@
1
1
  import { useState, useCallback } from "react";
2
2
  import { Save, Filter, Columns } from "lucide-react";
3
- import { Button } from "../ui/button";
4
- import { Input } from "../ui/input";
3
+ import { Button } from "./ui/button";
4
+ import { Input } from "./ui/input";
5
5
  import {
6
6
  Dialog,
7
7
  DialogContent,
@@ -10,8 +10,8 @@ import {
10
10
  DialogHeader,
11
11
  DialogTitle,
12
12
  DialogTrigger,
13
- } from "../ui/dialog";
14
- import type { ExpandedRelationFields } from "../types/table-metadata";
13
+ } from "./ui/dialog";
14
+ import type { ExpandedRelationFields } from "../generator/metadata-generator";
15
15
  import { useSavedViews, type SaveViewInput } from "./saved-view-context";
16
16
  import type { SearchFilters } from "./types";
17
17
 
@@ -68,6 +68,13 @@ export interface TableMetadata {
68
68
  */
69
69
  export type TableMetadataMap = Record<string, TableMetadata>;
70
70
 
71
+ /**
72
+ * Expanded relation fields configuration
73
+ * Key: relation field name (e.g., "task")
74
+ * Value: array of selected field names from the related table (e.g., ["name", "status"])
75
+ */
76
+ export type ExpandedRelationFields = Record<string, string[]>;
77
+
71
78
  /**
72
79
  * Intermediate type for processed table data
73
80
  */