Skip to content

Commit 37b305a

Browse files
Claudehotlong
andauthored
feat(studio): implement TanStack Router structure
- Create route tree with __root, index, and dynamic routes - Add useObjectStackClient and usePackages hooks - Migrate App.tsx from useState to RouterProvider - Routes: /, /$package, /$package/objects/$name, /$package/metadata/$type/$name, /packages, /api-console - Next: Update AppSidebar to use Link components Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 7575214 commit 37b305a

11 files changed

Lines changed: 356 additions & 155 deletions

apps/studio/src/App.tsx

Lines changed: 8 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,161 +1,14 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { useState, useEffect, useCallback } from 'react';
4-
import { ObjectStackClient } from '@objectstack/client';
5-
import { ObjectStackProvider } from '@objectstack/client-react';
6-
import { ErrorBoundary } from './components/ErrorBoundary';
7-
import { AppSidebar } from "./components/app-sidebar"
8-
import { SiteHeader } from "@/components/site-header"
9-
import { SidebarProvider } from "@/components/ui/sidebar"
10-
import { DeveloperOverview } from './components/DeveloperOverview';
11-
import { PackageManager } from './components/PackageManager';
12-
import { ApiConsolePage } from './components/ApiConsolePage';
13-
import { Toaster } from "@/components/ui/toaster"
14-
import { AiChatPanel } from "@/components/AiChatPanel"
15-
import { getApiBaseUrl, config } from './lib/config';
16-
import { PluginRegistryProvider, PluginHost } from './plugins';
17-
import { builtInPlugins } from './plugins/built-in';
18-
import type { InstalledPackage } from '@objectstack/spec/kernel';
3+
/**
4+
* App Component
5+
*
6+
* Main application wrapper that provides the TanStack Router instance.
7+
*/
198

20-
type ViewType = 'overview' | 'packages' | 'object' | 'metadata' | 'api-console';
9+
import { RouterProvider } from '@tanstack/react-router';
10+
import { router } from './router';
2111

2212
export default function App() {
23-
const [client, setClient] = useState<ObjectStackClient | null>(null);
24-
const [packages, setPackages] = useState<InstalledPackage[]>([]);
25-
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);
26-
const [selectedObject, setSelectedObject] = useState<string | null>(null);
27-
const [selectedMeta, setSelectedMeta] = useState<{ type: string; name: string } | null>(null);
28-
const [selectedView, setSelectedView] = useState<ViewType>('overview');
29-
30-
// 1. Create client once
31-
useEffect(() => {
32-
const baseUrl = getApiBaseUrl();
33-
console.log(`[App] Connecting to API: ${baseUrl} (mode: ${config.mode})`);
34-
setClient(new ObjectStackClient({ baseUrl }));
35-
}, []);
36-
37-
// 2. Fetch installed packages from the server API
38-
useEffect(() => {
39-
if (!client) return;
40-
let mounted = true;
41-
42-
async function loadPackages() {
43-
try {
44-
const result = await client!.packages.list();
45-
const all: InstalledPackage[] = result?.packages || [];
46-
// Filter out the root dev-workspace — it's the monorepo aggregator, not a real package
47-
const items = all.filter((p) => p.manifest?.version !== '0.0.0' && p.manifest?.id !== 'dev-workspace');
48-
console.log('[App] Fetched packages:', items.map((p) => p.manifest?.name || p.manifest?.id));
49-
if (mounted && items.length > 0) {
50-
setPackages(items);
51-
setSelectedPackage(items[0]);
52-
}
53-
} catch (err) {
54-
console.error('[App] Failed to fetch packages:', err);
55-
}
56-
}
57-
58-
loadPackages();
59-
return () => { mounted = false; };
60-
}, [client]);
61-
62-
const handleSelectPackage = useCallback((pkg: InstalledPackage) => {
63-
setSelectedPackage(pkg);
64-
setSelectedObject(null);
65-
setSelectedMeta(null);
66-
setSelectedView('overview');
67-
}, []);
68-
69-
const handleSelectObject = useCallback((name: string) => {
70-
if (name) {
71-
setSelectedObject(name);
72-
setSelectedView('object');
73-
} else {
74-
setSelectedObject(null);
75-
setSelectedView('overview');
76-
}
77-
}, []);
78-
79-
const handleSelectView = useCallback((view: ViewType) => {
80-
setSelectedView(view);
81-
setSelectedObject(null);
82-
setSelectedMeta(null);
83-
}, []);
84-
85-
const handleSelectMeta = useCallback((type: string, name: string) => {
86-
setSelectedMeta({ type, name });
87-
setSelectedObject(null);
88-
setSelectedView('metadata');
89-
}, []);
90-
91-
const handleNavigate = useCallback((view: string, detail?: string) => {
92-
if (view === 'packages') handleSelectView('packages');
93-
else if (detail) handleSelectObject(detail);
94-
}, [handleSelectView, handleSelectObject]);
95-
96-
if (!client) return (
97-
<div className="flex min-h-screen items-center justify-center bg-background">
98-
<div className="text-center space-y-2">
99-
<div className="h-8 w-8 mx-auto animate-spin rounded-full border-4 border-muted border-t-primary" />
100-
<p className="text-sm text-muted-foreground">Connecting to ObjectStack…</p>
101-
</div>
102-
</div>
103-
);
104-
105-
return (
106-
<ObjectStackProvider client={client}>
107-
<PluginRegistryProvider plugins={builtInPlugins}>
108-
<ErrorBoundary>
109-
<SidebarProvider>
110-
<AppSidebar
111-
selectedObject={selectedObject}
112-
onSelectObject={handleSelectObject}
113-
selectedMeta={selectedMeta}
114-
onSelectMeta={handleSelectMeta}
115-
packages={packages}
116-
selectedPackage={selectedPackage}
117-
onSelectPackage={handleSelectPackage}
118-
onSelectView={handleSelectView}
119-
selectedView={selectedView}
120-
/>
121-
<main className="flex min-w-0 flex-1 flex-col h-svh overflow-hidden bg-background">
122-
<SiteHeader
123-
selectedObject={selectedObject}
124-
selectedMeta={selectedMeta}
125-
selectedView={selectedView}
126-
packageLabel={selectedPackage?.manifest?.name || selectedPackage?.manifest?.id}
127-
/>
128-
<div className="flex flex-1 flex-col overflow-hidden">
129-
{selectedView === 'object' && selectedObject ? (
130-
<PluginHost
131-
metadataType="object"
132-
metadataName={selectedObject}
133-
packageId={selectedPackage?.manifest?.id}
134-
/>
135-
) : selectedView === 'metadata' && selectedMeta ? (
136-
<PluginHost
137-
metadataType={selectedMeta.type}
138-
metadataName={selectedMeta.name}
139-
packageId={selectedPackage?.manifest?.id}
140-
/>
141-
) : selectedView === 'packages' ? (
142-
<PackageManager />
143-
) : selectedView === 'api-console' ? (
144-
<ApiConsolePage />
145-
) : (
146-
<DeveloperOverview
147-
packages={packages}
148-
selectedPackage={selectedPackage}
149-
onNavigate={handleNavigate}
150-
/>
151-
)}
152-
</div>
153-
</main>
154-
<Toaster />
155-
<AiChatPanel />
156-
</SidebarProvider>
157-
</ErrorBoundary>
158-
</PluginRegistryProvider>
159-
</ObjectStackProvider>
160-
);
13+
return <RouterProvider router={router} />;
16114
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { useState, useEffect } from 'react';
4+
import { ObjectStackClient } from '@objectstack/client';
5+
import { getApiBaseUrl, config } from '../lib/config';
6+
7+
/**
8+
* Hook to create and manage ObjectStack client instance
9+
*/
10+
export function useObjectStackClient() {
11+
const [client, setClient] = useState<ObjectStackClient | null>(null);
12+
13+
useEffect(() => {
14+
const baseUrl = getApiBaseUrl();
15+
console.log(`[App] Connecting to API: ${baseUrl} (mode: ${config.mode})`);
16+
setClient(new ObjectStackClient({ baseUrl }));
17+
}, []);
18+
19+
return client;
20+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { useState, useEffect } from 'react';
4+
import { useObjectStackClient as useClient } from '@objectstack/client-react';
5+
import type { InstalledPackage } from '@objectstack/spec/kernel';
6+
7+
/**
8+
* Hook to fetch and manage installed packages
9+
*/
10+
export function usePackages() {
11+
const client = useClient();
12+
const [packages, setPackages] = useState<InstalledPackage[]>([]);
13+
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);
14+
15+
useEffect(() => {
16+
if (!client) return;
17+
let mounted = true;
18+
19+
async function loadPackages() {
20+
try {
21+
const result = await client.packages.list();
22+
const all: InstalledPackage[] = result?.packages || [];
23+
// Filter out the root dev-workspace — it's the monorepo aggregator, not a real package
24+
const items = all.filter((p) => p.manifest?.version !== '0.0.0' && p.manifest?.id !== 'dev-workspace');
25+
console.log('[App] Fetched packages:', items.map((p) => p.manifest?.name || p.manifest?.id));
26+
if (mounted && items.length > 0) {
27+
setPackages(items);
28+
setSelectedPackage(items[0]);
29+
}
30+
} catch (err) {
31+
console.error('[App] Failed to fetch packages:', err);
32+
}
33+
}
34+
35+
loadPackages();
36+
return () => { mounted = false; };
37+
}, [client]);
38+
39+
return { packages, selectedPackage, setSelectedPackage };
40+
}

apps/studio/src/router.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Route Tree Configuration
5+
*
6+
* TanStack Router auto-generates this file from routes/ directory.
7+
* This import is required for the router to work.
8+
*/
9+
10+
import { createRouter } from '@tanstack/react-router';
11+
import { routeTree } from './routeTree.gen';
12+
13+
export const router = createRouter({ routeTree });
14+
15+
// Register things for type-safety
16+
declare module '@tanstack/react-router' {
17+
interface Register {
18+
router: typeof router;
19+
}
20+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { createFileRoute } from '@tanstack/react-router';
4+
import { AppSidebar } from '../components/app-sidebar';
5+
import { SiteHeader } from '@/components/site-header';
6+
import { PluginHost } from '../plugins';
7+
import { usePackages } from '../hooks/usePackages';
8+
9+
function MetadataViewComponent() {
10+
const { type, name } = Route.useParams();
11+
const { packages, selectedPackage } = usePackages();
12+
13+
return (
14+
<>
15+
<AppSidebar
16+
packages={packages}
17+
selectedPackage={selectedPackage}
18+
/>
19+
<main className="flex min-w-0 flex-1 flex-col h-svh overflow-hidden bg-background">
20+
<SiteHeader
21+
selectedMeta={{ type, name }}
22+
selectedView="metadata"
23+
packageLabel={selectedPackage?.manifest?.name || selectedPackage?.manifest?.id}
24+
/>
25+
<div className="flex flex-1 flex-col overflow-hidden">
26+
<PluginHost
27+
metadataType={type}
28+
metadataName={name}
29+
packageId={selectedPackage?.manifest?.id}
30+
/>
31+
</div>
32+
</main>
33+
</>
34+
);
35+
}
36+
37+
export const Route = createFileRoute('/$package/metadata/$type/$name')({
38+
component: MetadataViewComponent,
39+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { createFileRoute } from '@tanstack/react-router';
4+
import { AppSidebar } from '../components/app-sidebar';
5+
import { SiteHeader } from '@/components/site-header';
6+
import { PluginHost } from '../plugins';
7+
import { usePackages } from '../hooks/usePackages';
8+
9+
function ObjectViewComponent() {
10+
const { name } = Route.useParams();
11+
const { packages, selectedPackage } = usePackages();
12+
13+
return (
14+
<>
15+
<AppSidebar
16+
packages={packages}
17+
selectedPackage={selectedPackage}
18+
/>
19+
<main className="flex min-w-0 flex-1 flex-col h-svh overflow-hidden bg-background">
20+
<SiteHeader
21+
selectedObject={name}
22+
selectedView="object"
23+
packageLabel={selectedPackage?.manifest?.name || selectedPackage?.manifest?.id}
24+
/>
25+
<div className="flex flex-1 flex-col overflow-hidden">
26+
<PluginHost
27+
metadataType="object"
28+
metadataName={name}
29+
packageId={selectedPackage?.manifest?.id}
30+
/>
31+
</div>
32+
</main>
33+
</>
34+
);
35+
}
36+
37+
export const Route = createFileRoute('/$package/objects/$name')({
38+
component: ObjectViewComponent,
39+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { createFileRoute, Outlet } from '@tanstack/react-router';
4+
import { AppSidebar } from '../components/app-sidebar';
5+
import { SiteHeader } from '@/components/site-header';
6+
import { usePackages } from '../hooks/usePackages';
7+
import { useEffect } from 'react';
8+
9+
function PackageLayoutComponent() {
10+
const { packageId } = Route.useParams();
11+
const { packages, selectedPackage, setSelectedPackage } = usePackages();
12+
13+
// Update selected package when route param changes
14+
useEffect(() => {
15+
const pkg = packages.find(p => p.manifest?.id === packageId);
16+
if (pkg && pkg !== selectedPackage) {
17+
setSelectedPackage(pkg);
18+
}
19+
}, [packageId, packages, selectedPackage, setSelectedPackage]);
20+
21+
return (
22+
<>
23+
<AppSidebar
24+
packages={packages}
25+
selectedPackage={selectedPackage}
26+
/>
27+
<main className="flex min-w-0 flex-1 flex-col h-svh overflow-hidden bg-background">
28+
<SiteHeader
29+
selectedView="overview"
30+
packageLabel={selectedPackage?.manifest?.name || selectedPackage?.manifest?.id}
31+
/>
32+
<div className="flex flex-1 flex-col overflow-hidden">
33+
<Outlet />
34+
</div>
35+
</main>
36+
</>
37+
);
38+
}
39+
40+
export const Route = createFileRoute('/$package')({
41+
component: PackageLayoutComponent,
42+
});

0 commit comments

Comments
 (0)