@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.
- package/README.md +44 -4
- package/dist/generator/index.d.mts +7 -1
- package/docs/app-shell-module.md +151 -0
- package/docs/saved-view-store.md +155 -0
- package/package.json +27 -2
- package/src/app-shell/create-data-view-module.tsx +84 -0
- package/src/app-shell/index.ts +2 -0
- package/src/app-shell/types.ts +42 -0
- package/src/component/column-selector.tsx +4 -4
- package/src/component/data-table.tsx +2 -2
- package/src/component/data-view-tab-content.tsx +3 -3
- package/src/component/data-viewer.tsx +38 -9
- package/src/component/hooks/use-accessible-tables.ts +1 -1
- package/src/component/hooks/use-column-state.ts +1 -1
- package/src/component/hooks/use-relation-data.ts +1 -1
- package/src/component/hooks/use-table-access-check.ts +103 -0
- package/src/component/hooks/use-table-data.ts +1 -1
- package/src/component/index.ts +4 -1
- package/src/component/pagination.tsx +1 -1
- package/src/component/relation-content.tsx +4 -4
- package/src/component/saved-view-context.tsx +195 -48
- package/src/component/search-filter.tsx +8 -8
- package/src/component/single-record-tab-content.tsx +5 -5
- package/src/component/table-selector.tsx +2 -2
- package/src/component/types.ts +1 -1
- package/src/component/view-save-load.tsx +4 -4
- package/src/generator/metadata-generator.ts +7 -0
- package/src/store/indexeddb.ts +150 -0
- package/src/store/tailordb/index.ts +204 -0
- package/src/store/tailordb/schema.ts +114 -0
- package/src/store/types.ts +85 -0
- package/src/utils/query-builder.ts +1 -1
- package/src/types/table-metadata.ts +0 -72
- /package/{API.md → docs/API.md} +0 -0
- /package/src/{lib → component/lib}/utils.ts +0 -0
- /package/src/{ui → component/ui}/alert.tsx +0 -0
- /package/src/{ui → component/ui}/badge.tsx +0 -0
- /package/src/{ui → component/ui}/button.tsx +0 -0
- /package/src/{ui → component/ui}/card.tsx +0 -0
- /package/src/{ui → component/ui}/checkbox.tsx +0 -0
- /package/src/{ui → component/ui}/collapsible.tsx +0 -0
- /package/src/{ui → component/ui}/dialog.tsx +0 -0
- /package/src/{ui → component/ui}/dropdown-menu.tsx +0 -0
- /package/src/{ui → component/ui}/input.tsx +0 -0
- /package/src/{ui → component/ui}/label.tsx +0 -0
- /package/src/{ui → component/ui}/select.tsx +0 -0
- /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 "
|
|
4
|
-
import { Button } from "
|
|
5
|
-
import type { TableMetadata, TableMetadataMap } from "../
|
|
6
|
-
import {
|
|
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
|
|
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>
|
|
@@ -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
|
+
}
|
package/src/component/index.ts
CHANGED
|
@@ -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 {
|
|
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";
|
|
@@ -6,14 +6,14 @@ import {
|
|
|
6
6
|
TableHead,
|
|
7
7
|
TableHeader,
|
|
8
8
|
TableRow,
|
|
9
|
-
} from "
|
|
10
|
-
import { Button } from "
|
|
11
|
-
import { Badge } from "
|
|
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 "../
|
|
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 "../
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const renameView = useCallback(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 "
|
|
11
|
-
import { Input } from "
|
|
12
|
-
import { Checkbox } from "
|
|
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 "
|
|
19
|
+
} from "./ui/select";
|
|
20
20
|
import {
|
|
21
21
|
Collapsible,
|
|
22
22
|
CollapsibleContent,
|
|
23
23
|
CollapsibleTrigger,
|
|
24
|
-
} from "
|
|
25
|
-
import { Badge } from "
|
|
26
|
-
import { Label } from "
|
|
27
|
-
import type { FieldMetadata } from "../
|
|
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 "
|
|
4
|
-
import { Button } from "
|
|
5
|
-
import { Badge } from "
|
|
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 "
|
|
13
|
+
} from "./ui/table";
|
|
14
14
|
import type {
|
|
15
15
|
TableMetadata,
|
|
16
16
|
TableMetadataMap,
|
|
17
17
|
FieldMetadata,
|
|
18
|
-
} from "../
|
|
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 "../
|
|
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 "
|
|
9
|
+
} from "./ui/select";
|
|
10
10
|
|
|
11
11
|
interface TableSelectorProps {
|
|
12
12
|
tables: TableMetadata[];
|
package/src/component/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback } from "react";
|
|
2
2
|
import { Save, Filter, Columns } from "lucide-react";
|
|
3
|
-
import { Button } from "
|
|
4
|
-
import { Input } from "
|
|
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 "
|
|
14
|
-
import type { ExpandedRelationFields } from "../
|
|
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
|
*/
|