Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Metadata plugin tests** — Updated `MetadataPlugin` test suite in `packages/metadata/src/metadata.test.ts` to match the refactored `start()` implementation. Tests now verify `setDataEngine` (via `getService('objectql')`) instead of the removed `setDatabaseDriver` (via `getServices()`) pattern. Also added `setDataEngine` and `setRealtimeService` to the `NodeMetadataManager` mock class.

### Added
- **Claude Code integration (`CLAUDE.md`)** — Added root `CLAUDE.md` file so that [Claude Code](https://docs.anthropic.com/en/docs/claude-code) automatically loads the project's system prompt when launched in the repository. Content is synced with `.github/copilot-instructions.md` and includes build/test quick-reference commands, all prime directives, monorepo structure, protocol domains, coding patterns, and domain-specific prompt references. This complements the existing GitHub Copilot instructions and `skills/` directory.

Expand Down
14 changes: 7 additions & 7 deletions apps/server/scripts/build-vercel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ set -euo pipefail
# Steps:
# 1. Build the project with turbo (includes studio)
# 2. Bundle the API serverless function (→ api/_handler.js)
# 3. Copy studio dist files to public/_studio/ for UI serving at /_studio path
# 3. Copy studio dist files to public/ for UI serving
# 4. Install external deps in api/node_modules/ (resolve pnpm symlinks)

echo "[build-vercel] Starting server build..."
Expand All @@ -27,13 +27,13 @@ cd apps/server
# 2. Bundle API serverless function
node scripts/bundle-api.mjs

# 3. Copy studio dist files to public/_studio/ for UI serving at /_studio path
echo "[build-vercel] Copying studio dist to public/_studio/..."
# 3. Copy studio dist files to public/ for UI serving
echo "[build-vercel] Copying studio dist to public/..."
rm -rf public
mkdir -p public/_studio
mkdir -p public
if [ -d "../studio/dist" ]; then
cp -r ../studio/dist/* public/_studio/
echo "[build-vercel] ✓ Copied studio dist to public/_studio/"
cp -r ../studio/dist/* public/
echo "[build-vercel] ✓ Copied studio dist to public/"
else
echo "[build-vercel] ⚠ Studio dist not found (skipped)"
fi
Expand All @@ -60,4 +60,4 @@ rm package.json
cd ..
echo "[build-vercel] ✓ External dependencies installed in api/node_modules/"

echo "[build-vercel] Done. Static files in public/_studio/, serverless function in api/[[...route]].js → api/_handler.js"
echo "[build-vercel] Done. Static files in public/, serverless function in api/[[...route]].js → api/_handler.js"
11 changes: 3 additions & 8 deletions apps/server/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"build": {
"env": {
"VITE_RUNTIME_MODE": "server",
"VITE_SERVER_URL": "",
"VERCEL": "true"
"VITE_SERVER_URL": ""
}
},
"functions": {
Expand All @@ -18,18 +17,14 @@
},
"headers": [
{
"source": "/_studio/assets/(.*)",
"source": "/assets/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
],
"redirects": [
{ "source": "/", "destination": "/_studio", "permanent": false }
],
"rewrites": [
{ "source": "/api/:path*", "destination": "/api/[[...route]]" },
{ "source": "/_studio/(.*)", "destination": "/_studio/$1" },
{ "source": "/_studio", "destination": "/_studio/index.html" }
{ "source": "/((?!api/).*)", "destination": "/index.html" }
]
}
486 changes: 206 additions & 280 deletions apps/studio/ROADMAP.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"build": "pnpm msw:init && vite build",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:bdd": "objectstack test",
"preview": "vite preview"
},
Expand Down Expand Up @@ -41,6 +43,7 @@
"@objectstack/service-automation": "workspace:*",
"@objectstack/service-feed": "workspace:*",
"@objectstack/spec": "workspace:*",
"@tanstack/react-router": "^1.91.6",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
Expand Down Expand Up @@ -68,10 +71,16 @@
},
"devDependencies": {
"@objectstack/cli": "workspace:*",
"@tanstack/router-devtools": "^1.91.6",
"@tanstack/router-plugin": "^1.91.5",
"@tailwindcss/postcss": "^4.2.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "^4.1.4",
"autoprefixer": "^10.4.27",
"esbuild": "^0.28.0",
"happy-dom": "^20.8.9",
Expand Down
163 changes: 8 additions & 155 deletions apps/studio/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,161 +1,14 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { useState, useEffect, useCallback } from 'react';
import { ObjectStackClient } from '@objectstack/client';
import { ObjectStackProvider } from '@objectstack/client-react';
import { ErrorBoundary } from './components/ErrorBoundary';
import { AppSidebar } from "./components/app-sidebar"
import { SiteHeader } from "@/components/site-header"
import { SidebarProvider } from "@/components/ui/sidebar"
import { DeveloperOverview } from './components/DeveloperOverview';
import { PackageManager } from './components/PackageManager';
import { ApiConsolePage } from './components/ApiConsolePage';
import { Toaster } from "@/components/ui/toaster"
import { AiChatPanel } from "@/components/AiChatPanel"
import { getApiBaseUrl, config } from './lib/config';
import { PluginRegistryProvider, PluginHost } from './plugins';
import { builtInPlugins } from './plugins/built-in';
import type { InstalledPackage } from '@objectstack/spec/kernel';
/**
* App Component
*
* Main application wrapper that provides the TanStack Router instance.
*/

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

export default function App() {
const [client, setClient] = useState<ObjectStackClient | null>(null);
const [packages, setPackages] = useState<InstalledPackage[]>([]);
const [selectedPackage, setSelectedPackage] = useState<InstalledPackage | null>(null);
const [selectedObject, setSelectedObject] = useState<string | null>(null);
const [selectedMeta, setSelectedMeta] = useState<{ type: string; name: string } | null>(null);
const [selectedView, setSelectedView] = useState<ViewType>('overview');

// 1. Create client once
useEffect(() => {
const baseUrl = getApiBaseUrl();
console.log(`[App] Connecting to API: ${baseUrl} (mode: ${config.mode})`);
setClient(new ObjectStackClient({ baseUrl }));
}, []);

// 2. Fetch installed packages from the server API
useEffect(() => {
if (!client) return;
let mounted = true;

async function loadPackages() {
try {
const result = await client!.packages.list();
const all: InstalledPackage[] = result?.packages || [];
// Filter out the root dev-workspace — it's the monorepo aggregator, not a real package
const items = all.filter((p) => p.manifest?.version !== '0.0.0' && p.manifest?.id !== 'dev-workspace');
console.log('[App] Fetched packages:', items.map((p) => p.manifest?.name || p.manifest?.id));
if (mounted && items.length > 0) {
setPackages(items);
setSelectedPackage(items[0]);
}
} catch (err) {
console.error('[App] Failed to fetch packages:', err);
}
}

loadPackages();
return () => { mounted = false; };
}, [client]);

const handleSelectPackage = useCallback((pkg: InstalledPackage) => {
setSelectedPackage(pkg);
setSelectedObject(null);
setSelectedMeta(null);
setSelectedView('overview');
}, []);

const handleSelectObject = useCallback((name: string) => {
if (name) {
setSelectedObject(name);
setSelectedView('object');
} else {
setSelectedObject(null);
setSelectedView('overview');
}
}, []);

const handleSelectView = useCallback((view: ViewType) => {
setSelectedView(view);
setSelectedObject(null);
setSelectedMeta(null);
}, []);

const handleSelectMeta = useCallback((type: string, name: string) => {
setSelectedMeta({ type, name });
setSelectedObject(null);
setSelectedView('metadata');
}, []);

const handleNavigate = useCallback((view: string, detail?: string) => {
if (view === 'packages') handleSelectView('packages');
else if (detail) handleSelectObject(detail);
}, [handleSelectView, handleSelectObject]);

if (!client) return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="text-center space-y-2">
<div className="h-8 w-8 mx-auto animate-spin rounded-full border-4 border-muted border-t-primary" />
<p className="text-sm text-muted-foreground">Connecting to ObjectStack…</p>
</div>
</div>
);

return (
<ObjectStackProvider client={client}>
<PluginRegistryProvider plugins={builtInPlugins}>
<ErrorBoundary>
<SidebarProvider>
<AppSidebar
selectedObject={selectedObject}
onSelectObject={handleSelectObject}
selectedMeta={selectedMeta}
onSelectMeta={handleSelectMeta}
packages={packages}
selectedPackage={selectedPackage}
onSelectPackage={handleSelectPackage}
onSelectView={handleSelectView}
selectedView={selectedView}
/>
<main className="flex min-w-0 flex-1 flex-col h-svh overflow-hidden bg-background">
<SiteHeader
selectedObject={selectedObject}
selectedMeta={selectedMeta}
selectedView={selectedView}
packageLabel={selectedPackage?.manifest?.name || selectedPackage?.manifest?.id}
/>
<div className="flex flex-1 flex-col overflow-hidden">
{selectedView === 'object' && selectedObject ? (
<PluginHost
metadataType="object"
metadataName={selectedObject}
packageId={selectedPackage?.manifest?.id}
/>
) : selectedView === 'metadata' && selectedMeta ? (
<PluginHost
metadataType={selectedMeta.type}
metadataName={selectedMeta.name}
packageId={selectedPackage?.manifest?.id}
/>
) : selectedView === 'packages' ? (
<PackageManager />
) : selectedView === 'api-console' ? (
<ApiConsolePage />
) : (
<DeveloperOverview
packages={packages}
selectedPackage={selectedPackage}
onNavigate={handleNavigate}
/>
)}
</div>
</main>
<Toaster />
<AiChatPanel />
</SidebarProvider>
</ErrorBoundary>
</PluginRegistryProvider>
</ObjectStackProvider>
);
return <RouterProvider router={router} />;
}
5 changes: 3 additions & 2 deletions apps/studio/src/components/ObjectDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
interface ObjectDataTableProps {
objectApiName: string;
onEdit: (record: any) => void;
refreshTrigger?: number;
}

function CellValue({ value, type }: { value: any; type: string }) {
Expand Down Expand Up @@ -78,7 +79,7 @@ function TableSkeleton({ cols }: { cols: number }) {
);
}

export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps) {
export function ObjectDataTable({ objectApiName, onEdit, refreshTrigger = 0 }: ObjectDataTableProps) {
const client = useClient();
const [def, setDef] = useState<any>(null);
const [records, setRecords] = useState<any[]>([]);
Expand Down Expand Up @@ -136,7 +137,7 @@ export function ObjectDataTable({ objectApiName, onEdit }: ObjectDataTableProps)
}
loadData();
return () => { mounted = false; };
}, [client, objectApiName, page]);
}, [client, objectApiName, page, refreshTrigger]);

async function handleDelete(id: string) {
if (!confirm('Are you sure you want to delete this record?')) return;
Expand Down
11 changes: 5 additions & 6 deletions apps/studio/src/components/ObjectExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export function ObjectExplorer({ objectApiName }: ObjectExplorerProps) {
const [activeTab, setActiveTab] = useState<ObjectTab>('schema');
const [editingRecord, setEditingRecord] = useState<any>(null);
const [showForm, setShowForm] = useState(false);
// Refresh trigger: increment this to force data table to refetch
const [refreshTrigger, setRefreshTrigger] = useState(0);

function handleEdit(record: any) {
setEditingRecord(record);
Expand All @@ -27,11 +29,8 @@ export function ObjectExplorer({ objectApiName }: ObjectExplorerProps) {
function handleFormSuccess() {
setShowForm(false);
setEditingRecord(null);
// Force data tab re-fetch by toggling activeTab
if (activeTab === 'data') {
setActiveTab('schema');
setTimeout(() => setActiveTab('data'), 0);
}
// Trigger data table refresh by incrementing the refresh counter
setRefreshTrigger(prev => prev + 1);
}

const tabs: { id: ObjectTab; label: string; icon: React.ElementType }[] = [
Expand Down Expand Up @@ -78,7 +77,7 @@ export function ObjectExplorer({ objectApiName }: ObjectExplorerProps) {
<ObjectSchemaInspector objectApiName={objectApiName} />
)}
{activeTab === 'data' && (
<ObjectDataTable objectApiName={objectApiName} onEdit={handleEdit} />
<ObjectDataTable objectApiName={objectApiName} onEdit={handleEdit} refreshTrigger={refreshTrigger} />
)}
{activeTab === 'api' && (
<ObjectApiConsole objectApiName={objectApiName} />
Expand Down
Loading
Loading