@izumisy-tailor/tailor-data-viewer 0.1.6 → 0.1.8
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/package.json
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
defineModule,
|
|
3
|
+
defineResource,
|
|
4
|
+
useNavigate,
|
|
5
|
+
useSearchParams,
|
|
6
|
+
Link,
|
|
7
|
+
} from "@tailor-platform/app-shell";
|
|
8
|
+
import {
|
|
9
|
+
Database,
|
|
10
|
+
Trash2,
|
|
11
|
+
Calendar,
|
|
12
|
+
Filter,
|
|
13
|
+
Columns,
|
|
14
|
+
LayoutGrid,
|
|
15
|
+
} from "lucide-react";
|
|
3
16
|
import type { DataViewModuleConfig } from "./types";
|
|
4
17
|
import { isStoreFactory } from "./types";
|
|
5
18
|
import { DataViewer } from "../component/data-viewer";
|
|
6
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
SavedViewProvider,
|
|
21
|
+
useSavedViews,
|
|
22
|
+
} from "../component/saved-view-context";
|
|
23
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../component/ui/card";
|
|
24
|
+
import { Button } from "../component/ui/button";
|
|
25
|
+
import { Badge } from "../component/ui/badge";
|
|
7
26
|
|
|
8
27
|
/**
|
|
9
28
|
* Create a DataView module for use with AppShell
|
|
@@ -42,22 +61,130 @@ export function createDataViewModule(config: DataViewModuleConfig) {
|
|
|
42
61
|
|
|
43
62
|
// Create the explorer resource page component
|
|
44
63
|
const ExplorerPage = () => {
|
|
64
|
+
const [searchParams] = useSearchParams();
|
|
65
|
+
const viewId = searchParams.get("viewId") ?? undefined;
|
|
66
|
+
|
|
45
67
|
return (
|
|
46
68
|
<SavedViewProvider store={store}>
|
|
47
|
-
<DataViewer
|
|
69
|
+
<DataViewer
|
|
70
|
+
tableMetadata={tableMetadata}
|
|
71
|
+
appUri={appUri}
|
|
72
|
+
initialViewId={viewId}
|
|
73
|
+
/>
|
|
48
74
|
</SavedViewProvider>
|
|
49
75
|
);
|
|
50
76
|
};
|
|
51
77
|
|
|
78
|
+
// Saved views list component (used inside ModulePage)
|
|
79
|
+
const SavedViewsList = ({
|
|
80
|
+
explorerFullPath,
|
|
81
|
+
}: {
|
|
82
|
+
explorerFullPath: string;
|
|
83
|
+
}) => {
|
|
84
|
+
const { views, deleteView } = useSavedViews();
|
|
85
|
+
const navigate = useNavigate();
|
|
86
|
+
|
|
87
|
+
const handleOpenView = (viewId: string) => {
|
|
88
|
+
navigate(`${explorerFullPath}?viewId=${viewId}`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Card>
|
|
93
|
+
<CardHeader>
|
|
94
|
+
<CardTitle className="flex items-center gap-2">
|
|
95
|
+
<LayoutGrid className="size-5" />
|
|
96
|
+
保存済みビュー
|
|
97
|
+
</CardTitle>
|
|
98
|
+
</CardHeader>
|
|
99
|
+
<CardContent>
|
|
100
|
+
{views.length === 0 ? (
|
|
101
|
+
<div className="py-8 text-center">
|
|
102
|
+
<div className="text-muted-foreground mb-4">
|
|
103
|
+
保存されたビューはありません
|
|
104
|
+
</div>
|
|
105
|
+
<Link to={explorerFullPath}>
|
|
106
|
+
<Button variant="outline">
|
|
107
|
+
<Database className="mr-2 size-4" />
|
|
108
|
+
Data View Explorer を開く
|
|
109
|
+
</Button>
|
|
110
|
+
</Link>
|
|
111
|
+
</div>
|
|
112
|
+
) : (
|
|
113
|
+
<div className="space-y-3">
|
|
114
|
+
{views.map((savedView) => (
|
|
115
|
+
<div
|
|
116
|
+
key={savedView.id}
|
|
117
|
+
className="hover:bg-muted flex cursor-pointer items-center justify-between rounded-lg border p-4 transition-colors"
|
|
118
|
+
onClick={() => handleOpenView(savedView.id)}
|
|
119
|
+
>
|
|
120
|
+
<div className="flex-1">
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
<span className="font-medium">{savedView.name}</span>
|
|
123
|
+
<Badge variant="secondary">{savedView.tableName}</Badge>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="text-muted-foreground mt-1 flex items-center gap-4 text-sm">
|
|
126
|
+
<span className="flex items-center gap-1">
|
|
127
|
+
<Filter className="size-3" />
|
|
128
|
+
{savedView.filters.length} フィルター
|
|
129
|
+
</span>
|
|
130
|
+
<span className="flex items-center gap-1">
|
|
131
|
+
<Columns className="size-3" />
|
|
132
|
+
{savedView.selectedFields.length} カラム
|
|
133
|
+
</span>
|
|
134
|
+
<span className="flex items-center gap-1">
|
|
135
|
+
<Calendar className="size-3" />
|
|
136
|
+
{savedView.createdAt.toLocaleString()}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
{savedView.filters.length > 0 && (
|
|
140
|
+
<div className="mt-2 flex flex-wrap gap-1">
|
|
141
|
+
{savedView.filters.map((filter) => (
|
|
142
|
+
<Badge
|
|
143
|
+
key={filter.field}
|
|
144
|
+
variant="outline"
|
|
145
|
+
className="text-xs"
|
|
146
|
+
>
|
|
147
|
+
{filter.field}=
|
|
148
|
+
{typeof filter.value === "boolean"
|
|
149
|
+
? filter.value
|
|
150
|
+
? "true"
|
|
151
|
+
: "false"
|
|
152
|
+
: filter.value}
|
|
153
|
+
</Badge>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
<Button
|
|
159
|
+
variant="ghost"
|
|
160
|
+
size="sm"
|
|
161
|
+
className="text-destructive hover:text-destructive"
|
|
162
|
+
onClick={(e) => {
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
deleteView(savedView.id);
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<Trash2 className="size-4" />
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</CardContent>
|
|
174
|
+
</Card>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
52
178
|
// Create the module page component
|
|
179
|
+
const explorerFullPath = `/${basePath}/${explorerPath}`;
|
|
180
|
+
|
|
53
181
|
const ModulePage = () => {
|
|
54
182
|
return (
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
</div>
|
|
183
|
+
<SavedViewProvider store={store}>
|
|
184
|
+
<div className="space-y-6">
|
|
185
|
+
<SavedViewsList explorerFullPath={explorerFullPath} />
|
|
186
|
+
</div>
|
|
187
|
+
</SavedViewProvider>
|
|
61
188
|
);
|
|
62
189
|
};
|
|
63
190
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { GraphQLClient } from "graphql-request";
|
|
3
3
|
import type { TableMetadata } from "../../generator/metadata-generator";
|
|
4
4
|
|
|
@@ -23,6 +23,11 @@ export function useTableAccessCheck(
|
|
|
23
23
|
const [isLoading, setIsLoading] = useState(true);
|
|
24
24
|
const [error, setError] = useState<string | null>(null);
|
|
25
25
|
|
|
26
|
+
// Memoize tables to prevent infinite loops
|
|
27
|
+
// when the parent component passes a new array reference on each render
|
|
28
|
+
const tableKey = tables.map((t) => t.name).join(",");
|
|
29
|
+
const stableTables = useMemo(() => tables, [tableKey]);
|
|
30
|
+
|
|
26
31
|
useEffect(() => {
|
|
27
32
|
let cancelled = false;
|
|
28
33
|
|
|
@@ -39,7 +44,7 @@ export function useTableAccessCheck(
|
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
const results = await Promise.allSettled(
|
|
42
|
-
|
|
47
|
+
stableTables.map(async (table) => {
|
|
43
48
|
// Execute a lightweight aggregate query to check access
|
|
44
49
|
const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
|
|
45
50
|
await client.request(query);
|
|
@@ -73,7 +78,7 @@ export function useTableAccessCheck(
|
|
|
73
78
|
return () => {
|
|
74
79
|
cancelled = true;
|
|
75
80
|
};
|
|
76
|
-
}, [
|
|
81
|
+
}, [stableTables, appUri]);
|
|
77
82
|
|
|
78
83
|
return { accessibleTables, isLoading, error };
|
|
79
84
|
}
|