@izumisy-tailor/tailor-data-viewer 0.1.8 → 0.1.10

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.1.8",
4
+ "version": "0.1.10",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,9 +1,11 @@
1
- import { useState, useCallback } from "react";
2
- import { Plus, X, Loader2 } from "lucide-react";
1
+ import { useState, useCallback, useEffect, useRef } from "react";
2
+ import { Plus, X } from "lucide-react";
3
3
  import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
4
4
  import { Button } from "./ui/button";
5
- import type { TableMetadata, TableMetadataMap } from "../generator/metadata-generator";
6
- import { useTableAccessCheck } from "./hooks/use-table-access-check";
5
+ import type {
6
+ TableMetadata,
7
+ TableMetadataMap,
8
+ } from "../generator/metadata-generator";
7
9
  import { DataViewTabContent } from "./data-view-tab-content";
8
10
  import { SingleRecordTabContent } from "./single-record-tab-content";
9
11
  import { useSavedViews, type SavedView } from "./saved-view-context";
@@ -54,33 +56,14 @@ export function DataViewer({
54
56
  appUri,
55
57
  initialViewId,
56
58
  }: DataViewerProps) {
57
- // Get tables accessible to the current user via runtime permission check
58
59
  const allTables = Object.values(tableMetadata);
59
- const { accessibleTables, isLoading, error } = useTableAccessCheck(
60
- allTables,
61
- appUri,
62
- );
63
- const { getViewById } = useSavedViews();
60
+ const { getViewById, isLoading } = useSavedViews();
61
+
62
+ // Track if we've already initialized from the saved view
63
+ const initializedFromViewRef = useRef(false);
64
64
 
65
- // Tab management - initialize with saved view if provided
65
+ // Tab management - start with empty tab, will apply saved view after loading
66
66
  const [tabs, setTabs] = useState<Tab[]>(() => {
67
- if (initialViewId) {
68
- const view = getViewById(initialViewId);
69
- if (view) {
70
- const table = tableMetadata[view.tableName];
71
- if (table) {
72
- return [
73
- {
74
- id: generateTabId(),
75
- label: view.name,
76
- table,
77
- isLocked: true,
78
- initialView: view,
79
- },
80
- ];
81
- }
82
- }
83
- }
84
67
  return [
85
68
  {
86
69
  id: generateTabId(),
@@ -92,6 +75,31 @@ export function DataViewer({
92
75
  });
93
76
  const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);
94
77
 
78
+ // Apply saved view after views are loaded from store
79
+ useEffect(() => {
80
+ // Skip if no initialViewId, already initialized, or still loading
81
+ if (!initialViewId || initializedFromViewRef.current || isLoading) {
82
+ return;
83
+ }
84
+
85
+ const view = getViewById(initialViewId);
86
+ if (view) {
87
+ const table = tableMetadata[view.tableName];
88
+ if (table) {
89
+ initializedFromViewRef.current = true;
90
+ const newTab: Tab = {
91
+ id: generateTabId(),
92
+ label: view.name,
93
+ table,
94
+ isLocked: true,
95
+ initialView: view,
96
+ };
97
+ setTabs([newTab]);
98
+ setActiveTabId(newTab.id);
99
+ }
100
+ }
101
+ }, [initialViewId, isLoading, getViewById, tableMetadata]);
102
+
95
103
  const handleAddTab = useCallback(() => {
96
104
  const newTab: Tab = {
97
105
  id: generateTabId(),
@@ -188,45 +196,6 @@ export function DataViewer({
188
196
  [tableMetadata],
189
197
  );
190
198
 
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
-
218
- if (accessibleTables.length === 0) {
219
- return (
220
- <Card>
221
- <CardContent className="py-8">
222
- <div className="text-muted-foreground text-center">
223
- アクセス可能なテーブルがありません
224
- </div>
225
- </CardContent>
226
- </Card>
227
- );
228
- }
229
-
230
199
  return (
231
200
  <Card>
232
201
  <CardHeader className="pb-2">
@@ -289,7 +258,7 @@ export function DataViewer({
289
258
  />
290
259
  ) : (
291
260
  <DataViewTabContent
292
- tables={accessibleTables}
261
+ tables={allTables}
293
262
  appUri={appUri}
294
263
  initialTable={tab.table ?? undefined}
295
264
  onTableConfirm={(table) => handleTableConfirm(tab.id, table)}
@@ -9,10 +9,6 @@ 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 {
13
- useTableAccessCheck,
14
- checkTableAccess,
15
- } from "./hooks/use-table-access-check";
16
12
  export { useSavedViews, SavedViewProvider } from "./saved-view-context";
17
13
  export type { SavedView, SaveViewInput } from "./saved-view-context";
18
14
  export type { SearchFilter, SearchFilters } from "./types";
@@ -1,22 +0,0 @@
1
- import { useMemo } from "react";
2
- import type {
3
- TableMetadata,
4
- TableMetadataMap,
5
- } from "../../generator/metadata-generator";
6
-
7
- /**
8
- * Filter tables based on user's roles
9
- * Returns only tables the user has read access to
10
- */
11
- export function useAccessibleTables(
12
- tableMetadata: TableMetadataMap,
13
- userRoles: string[],
14
- ): TableMetadata[] {
15
- return useMemo(() => {
16
- return Object.values(tableMetadata).filter((table) =>
17
- table.readAllowedRoles.some((role) =>
18
- userRoles.map((r) => r.toUpperCase()).includes(role.toUpperCase()),
19
- ),
20
- );
21
- }, [tableMetadata, userRoles]);
22
- }
@@ -1,108 +0,0 @@
1
- import { useState, useEffect, useMemo } 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
- // 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
-
31
- useEffect(() => {
32
- let cancelled = false;
33
-
34
- async function checkAccess() {
35
- setIsLoading(true);
36
- setError(null);
37
-
38
- const client = new GraphQLClient(`${appUri}/query`, {
39
- credentials: "include",
40
- headers: {
41
- "Content-Type": "application/json",
42
- "X-Tailor-Nonce": crypto.randomUUID(),
43
- },
44
- });
45
-
46
- const results = await Promise.allSettled(
47
- stableTables.map(async (table) => {
48
- // Execute a lightweight aggregate query to check access
49
- const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
50
- await client.request(query);
51
- return table;
52
- }),
53
- );
54
-
55
- if (cancelled) return;
56
-
57
- // Filter to only fulfilled results (tables with access)
58
- const accessible = results
59
- .filter(
60
- (r): r is PromiseFulfilledResult<TableMetadata> =>
61
- r.status === "fulfilled",
62
- )
63
- .map((r) => r.value);
64
-
65
- setAccessibleTables(accessible);
66
- setIsLoading(false);
67
- }
68
-
69
- checkAccess().catch((err) => {
70
- if (!cancelled) {
71
- setError(
72
- err instanceof Error ? err.message : "Failed to check table access",
73
- );
74
- setIsLoading(false);
75
- }
76
- });
77
-
78
- return () => {
79
- cancelled = true;
80
- };
81
- }, [stableTables, appUri]);
82
-
83
- return { accessibleTables, isLoading, error };
84
- }
85
-
86
- /**
87
- * Standalone function to check table access (non-hook version)
88
- * Useful for one-time checks or server-side usage
89
- */
90
- export async function checkTableAccess(
91
- client: GraphQLClient,
92
- tables: TableMetadata[],
93
- ): Promise<TableMetadata[]> {
94
- const results = await Promise.allSettled(
95
- tables.map(async (table) => {
96
- const query = `query CheckAccess { ${table.pluralForm}(query: {}) { total } }`;
97
- await client.request(query);
98
- return table;
99
- }),
100
- );
101
-
102
- return results
103
- .filter(
104
- (r): r is PromiseFulfilledResult<TableMetadata> =>
105
- r.status === "fulfilled",
106
- )
107
- .map((r) => r.value);
108
- }