Skip to content

Commit 334afb1

Browse files
authored
Merge pull request #1169 from objectstack-ai/claude/implement-home-dashboard
2 parents 01b4164 + 72087d1 commit 334afb1

9 files changed

Lines changed: 534 additions & 39 deletions

File tree

ROADMAP.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
# ObjectUI Development Roadmap
22

3-
> **Last Updated:** March 23, 2026
3+
> **Last Updated:** April 1, 2026
44
> **Current Version:** v0.5.x
55
> **Spec Version:** @objectstack/spec v3.3.0
66
> **Client Version:** @objectstack/client v3.3.0
77
> **Target UX Benchmark:** 🎯 Airtable parity
8-
> **Current Priority:** AppShell Navigation · Designer Interaction · **View Config Live Preview Sync ✅** · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** · **System Settings & App Management ✅** · **Right-Side Visual Editor Drawer ✅** · **Object Manager & Field Designer ✅** · **AI SDUI Chatbot (service-ai + vercel/ai) ✅**
8+
> **Current Priority:** AppShell Navigation · Designer Interaction · **View Config Live Preview Sync ✅** · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** · **System Settings & App Management ✅** · **Right-Side Visual Editor Drawer ✅** · **Object Manager & Field Designer ✅** · **AI SDUI Chatbot (service-ai + vercel/ai) ✅** · **Unified Home Dashboard ✅**
99
1010
---
1111

1212
## 📋 Executive Summary
1313

1414
ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind + Shadcn. It renders JSON metadata from the @objectstack/spec protocol into pixel-perfect, accessible, and interactive enterprise interfaces.
1515

16-
**Where We Are:** Foundation is **solid and shipping** — 35 packages, 99+ components, 6,700+ tests, 80 Storybook stories, 43/43 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Console through Phase 20 (L3), **AppShell Navigation Renderer** (P0.1), **Flow Designer** (P2.4), **Feed/Chatter UI** (P1.5), **App Creation & Editing Flow** (P1.11), **System Settings & App Management** (P1.12), **Page/Dashboard Editor Console Integration** (P1.11), **Right-Side Visual Editor Drawer** (P1.11), and **Console Engine Schema Integration** (P1.14) — all ✅ complete. **ViewDesigner** has been removed — its capabilities (drag-to-reorder, undo/redo) are now provided by the ViewConfigPanel (right-side config panel).
16+
**Where We Are:** Foundation is **solid and shipping** — 35 packages, 99+ components, 6,700+ tests, 80 Storybook stories, 43/43 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Console through Phase 20 (L3), **AppShell Navigation Renderer** (P0.1), **Flow Designer** (P2.4), **Feed/Chatter UI** (P1.5), **App Creation & Editing Flow** (P1.11), **System Settings & App Management** (P1.12), **Page/Dashboard Editor Console Integration** (P1.11), **Right-Side Visual Editor Drawer** (P1.11), **Console Engine Schema Integration** (P1.14), and **Unified Home Dashboard** (P1.7.1) — all ✅ complete. **ViewDesigner** has been removed — its capabilities (drag-to-reorder, undo/redo) are now provided by the ViewConfigPanel (right-side config panel).
1717

1818
**What Remains:** The gap to **Airtable-level UX** is primarily in:
1919
1. ~~**AppShell** — No dynamic navigation renderer from spec JSON (last P0 blocker)~~ ✅ Complete
@@ -222,6 +222,22 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
222222
-`useNavigationOverlay` hook delegates `new_window` to `onNavigate` when available for app-specific URL control
223223
- ✅ plugin-view `handleRowClick` supports `split` and `popover` branches
224224

225+
### P1.7.1 Console — Unified Home Dashboard (Workspace) ✅
226+
227+
- [x] **HomePage component** — Unified landing page displaying all available applications
228+
- [x] **Route integration**`/home` route added with proper authentication guards
229+
- [x] **App cards grid** — Responsive grid layout showing all active apps with icons, descriptions, and branding colors
230+
- [x] **QuickActions section** — Quick access cards for creating apps, managing objects, and system settings
231+
- [x] **Recent items** — Display recently accessed objects, dashboards, and pages using `useRecentItems` hook
232+
- [x] **Starred items** — Display user-favorited items using `useFavorites` hook with star/unstar toggle
233+
- [x] **Empty state** — Helpful guidance for new users with "Create First App" and "System Settings" CTAs
234+
- [x] **i18n support** — All labels support internationalization via `useObjectTranslation`
235+
- [x] **RootRedirect update** — Root path (`/`) now redirects to `/home` instead of first app
236+
- [x] **Responsive design** — Mobile-friendly grid layouts that adapt to screen size
237+
- [x] **Airtable/Notion UX pattern** — Inspired by industry-leading workspace home pages
238+
239+
**Impact:** Users now have a unified workspace dashboard that provides overview of all applications, quick actions, and recent activity. This eliminates the previous behavior of auto-redirecting to the first app, giving users better control and visibility.
240+
225241
### P1.8 Console — View Config Panel (Phase 20)
226242

227243
- [x] Inline ViewConfigPanel for all view types (Airtable-style right sidebar)

apps/console/src/App.tsx

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const PermissionManagementPage = lazy(() => import('./pages/system/PermissionMan
5555
const AuditLogPage = lazy(() => import('./pages/system/AuditLogPage').then(m => ({ default: m.AuditLogPage })));
5656
const ProfilePage = lazy(() => import('./pages/system/ProfilePage').then(m => ({ default: m.ProfilePage })));
5757

58+
// Home Page (lazy — landing page)
59+
const HomePage = lazy(() => import('./pages/home/HomePage').then(m => ({ default: m.HomePage })));
60+
5861
import { useParams } from 'react-router-dom';
5962
import { ThemeProvider } from './components/theme-provider';
6063
import { ConsoleToaster } from './components/ConsoleToaster';
@@ -456,44 +459,14 @@ function findFirstRoute(items: any[]): string {
456459
return '';
457460
}
458461

459-
// Redirect root to default app
462+
// Redirect root to home page
460463
function RootRedirect() {
461-
const { apps, loading, error } = useMetadata();
462-
const navigate = useNavigate();
463-
const activeApps = apps.filter((a: any) => a.active !== false);
464-
const defaultApp = activeApps.find((a: any) => a.isDefault === true) || activeApps[0];
465-
464+
const { loading } = useMetadata();
465+
466466
if (loading) return <LoadingScreen />;
467-
if (defaultApp) {
468-
return <Navigate to={`/apps/${defaultApp.name}`} replace />;
469-
}
470-
return (
471-
<div className="h-screen flex items-center justify-center">
472-
<Empty>
473-
<EmptyTitle>{error ? 'Failed to Load Configuration' : 'No Apps Configured'}</EmptyTitle>
474-
<EmptyDescription>
475-
{error
476-
? 'There was an error loading the configuration. You can still create an app or access System Settings.'
477-
: 'No applications have been registered. Create your first app or configure your system.'}
478-
</EmptyDescription>
479-
<div className="mt-4 flex flex-col sm:flex-row items-center gap-3">
480-
<Button
481-
onClick={() => navigate('/create-app')}
482-
data-testid="create-first-app-btn"
483-
>
484-
Create Your First App
485-
</Button>
486-
<Button
487-
variant="outline"
488-
onClick={() => navigate('/system')}
489-
data-testid="go-to-settings-btn"
490-
>
491-
System Settings
492-
</Button>
493-
</div>
494-
</Empty>
495-
</div>
496-
);
467+
468+
// Always redirect to home page
469+
return <Navigate to="/home" replace />;
497470
}
498471

499472
/**
@@ -531,6 +504,16 @@ export function App() {
531504
<Route path="/login" element={<LoginPage />} />
532505
<Route path="/register" element={<RegisterPage />} />
533506
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
507+
{/* Home Dashboard — unified workspace landing page */}
508+
<Route path="/home" element={
509+
<AuthGuard fallback={<Navigate to="/login" />} loadingFallback={<LoadingScreen />}>
510+
<ConnectedShell>
511+
<Suspense fallback={<LoadingScreen />}>
512+
<HomePage />
513+
</Suspense>
514+
</ConnectedShell>
515+
</AuthGuard>
516+
} />
534517
{/* Top-level system routes — accessible without any app */}
535518
<Route path="/system/*" element={
536519
<AuthGuard fallback={<Navigate to="/login" />} loadingFallback={<LoadingScreen />}>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* AppCard
3+
*
4+
* Display card for an application with icon, name, description, and favorite toggle.
5+
*
6+
* @module
7+
*/
8+
9+
import { Star, StarOff } from 'lucide-react';
10+
import { Card, CardContent, Button } from '@object-ui/components';
11+
import { useObjectTranslation } from '@object-ui/i18n';
12+
import { resolveI18nLabel } from '../../utils';
13+
import { useFavorites } from '../../hooks/useFavorites';
14+
import { getIcon } from '../../utils/getIcon';
15+
import { cn } from '@object-ui/components';
16+
17+
interface AppCardProps {
18+
app: any;
19+
onClick: () => void;
20+
isFavorite: boolean;
21+
}
22+
23+
export function AppCard({ app, onClick, isFavorite }: AppCardProps) {
24+
const { t } = useObjectTranslation();
25+
const { toggleFavorite } = useFavorites();
26+
27+
const Icon = getIcon(app.icon);
28+
const label = resolveI18nLabel(app.label, t) || app.name;
29+
const description = resolveI18nLabel(app.description, t);
30+
const primaryColor = app.branding?.primaryColor;
31+
32+
const handleToggleFavorite = (e: React.MouseEvent) => {
33+
e.stopPropagation();
34+
toggleFavorite({
35+
id: `app:${app.name}`,
36+
label,
37+
href: `/apps/${app.name}`,
38+
type: 'object',
39+
});
40+
};
41+
42+
return (
43+
<Card
44+
className="cursor-pointer hover:shadow-lg transition-all group relative"
45+
onClick={onClick}
46+
data-testid={`app-card-${app.name}`}
47+
>
48+
<CardContent className="p-6">
49+
{/* Favorite Button */}
50+
<Button
51+
variant="ghost"
52+
size="sm"
53+
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
54+
onClick={handleToggleFavorite}
55+
data-testid={`favorite-btn-${app.name}`}
56+
>
57+
{isFavorite ? (
58+
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
59+
) : (
60+
<StarOff className="h-4 w-4" />
61+
)}
62+
</Button>
63+
64+
{/* App Icon */}
65+
<div
66+
className={cn('inline-flex p-3 rounded-lg mb-4', primaryColor ? '' : 'bg-primary/10')}
67+
style={primaryColor ? { backgroundColor: `${primaryColor}20` } : {}}
68+
>
69+
<Icon
70+
className="h-8 w-8"
71+
style={primaryColor ? { color: primaryColor } : {}}
72+
/>
73+
</div>
74+
75+
{/* App Info */}
76+
<div>
77+
<h3 className="font-semibold text-lg mb-1">{label}</h3>
78+
{description && (
79+
<p className="text-sm text-muted-foreground line-clamp-2">{description}</p>
80+
)}
81+
{!description && (
82+
<p className="text-sm text-muted-foreground">
83+
{t('home.appCard.noDescription', { defaultValue: 'No description' })}
84+
</p>
85+
)}
86+
</div>
87+
88+
{/* App Badge (if default) */}
89+
{app.isDefault && (
90+
<div className="mt-3">
91+
<span className="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">
92+
{t('home.appCard.default', { defaultValue: 'Default' })}
93+
</span>
94+
</div>
95+
)}
96+
</CardContent>
97+
</Card>
98+
);
99+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* HomePage
3+
*
4+
* Unified Home Dashboard (Workspace) that displays all available applications,
5+
* quick actions, recent items, and favorites. Inspired by Airtable/Notion home pages.
6+
*
7+
* Features:
8+
* - Display all active applications as cards
9+
* - Quick actions for creating apps, importing data, etc.
10+
* - Recent apps section (from useRecentItems)
11+
* - Starred/Favorite apps section (from useFavorites)
12+
* - Empty state guidance for new users
13+
* - Responsive grid layout
14+
* - i18n support
15+
*
16+
* @module
17+
*/
18+
19+
import { useNavigate } from 'react-router-dom';
20+
import { useMetadata } from '../../context/MetadataProvider';
21+
import { useRecentItems } from '../../hooks/useRecentItems';
22+
import { useFavorites } from '../../hooks/useFavorites';
23+
import { useObjectTranslation } from '@object-ui/i18n';
24+
import { resolveI18nLabel } from '../../utils';
25+
import { QuickActions } from './QuickActions';
26+
import { AppCard } from './AppCard';
27+
import { RecentApps } from './RecentApps';
28+
import { StarredApps } from './StarredApps';
29+
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
30+
import { Plus, Settings } from 'lucide-react';
31+
32+
export function HomePage() {
33+
const navigate = useNavigate();
34+
const { t } = useObjectTranslation();
35+
const { apps, loading } = useMetadata();
36+
const { recentItems } = useRecentItems();
37+
const { favorites } = useFavorites();
38+
39+
// Filter active apps
40+
const activeApps = apps.filter((a: any) => a.active !== false);
41+
42+
// Get recent apps (only apps, not objects/dashboards)
43+
const recentApps = recentItems
44+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
45+
.slice(0, 6);
46+
47+
// Get starred apps
48+
const starredApps = favorites
49+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
50+
.slice(0, 8);
51+
52+
if (loading) {
53+
return (
54+
<div className="min-h-screen flex items-center justify-center">
55+
<div className="text-muted-foreground">Loading workspace...</div>
56+
</div>
57+
);
58+
}
59+
60+
// Empty state - no apps configured
61+
if (activeApps.length === 0) {
62+
return (
63+
<div className="min-h-screen flex items-center justify-center p-6">
64+
<Empty>
65+
<EmptyTitle>Welcome to ObjectUI</EmptyTitle>
66+
<EmptyDescription>
67+
Get started by creating your first application or configure your system settings.
68+
</EmptyDescription>
69+
<div className="mt-6 flex flex-col sm:flex-row items-center gap-3">
70+
<Button
71+
onClick={() => navigate('/create-app')}
72+
data-testid="create-first-app-btn"
73+
>
74+
<Plus className="mr-2 h-4 w-4" />
75+
Create Your First App
76+
</Button>
77+
<Button
78+
variant="outline"
79+
onClick={() => navigate('/system')}
80+
data-testid="go-to-settings-btn"
81+
>
82+
<Settings className="mr-2 h-4 w-4" />
83+
System Settings
84+
</Button>
85+
</div>
86+
</Empty>
87+
</div>
88+
);
89+
}
90+
91+
return (
92+
<div className="min-h-screen bg-background">
93+
{/* Header */}
94+
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
95+
<div className="container mx-auto px-6 py-6">
96+
<div className="flex items-center justify-between">
97+
<div>
98+
<h1 className="text-3xl font-bold tracking-tight">
99+
{t('home.title', { defaultValue: 'Home' })}
100+
</h1>
101+
<p className="text-muted-foreground mt-1">
102+
{t('home.subtitle', { defaultValue: 'Your workspace dashboard' })}
103+
</p>
104+
</div>
105+
<div className="flex items-center gap-2">
106+
<Button
107+
variant="outline"
108+
onClick={() => navigate('/system')}
109+
data-testid="home-settings-btn"
110+
>
111+
<Settings className="mr-2 h-4 w-4" />
112+
{t('common.settings', { defaultValue: 'Settings' })}
113+
</Button>
114+
</div>
115+
</div>
116+
</div>
117+
</div>
118+
119+
{/* Main Content */}
120+
<div className="container mx-auto px-6 py-8 space-y-8">
121+
{/* Quick Actions */}
122+
<QuickActions />
123+
124+
{/* Starred/Favorite Apps */}
125+
{starredApps.length > 0 && (
126+
<StarredApps items={starredApps} />
127+
)}
128+
129+
{/* Recent Apps */}
130+
{recentApps.length > 0 && (
131+
<RecentApps items={recentApps} />
132+
)}
133+
134+
{/* All Applications */}
135+
<section>
136+
<h2 className="text-2xl font-semibold tracking-tight mb-4">
137+
{t('home.allApps', { defaultValue: 'All Applications' })}
138+
</h2>
139+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
140+
{activeApps.map((app: any) => (
141+
<AppCard
142+
key={app.name}
143+
app={app}
144+
onClick={() => navigate(`/apps/${app.name}`)}
145+
isFavorite={favorites.some(f => f.id === `app:${app.name}`)}
146+
/>
147+
))}
148+
</div>
149+
</section>
150+
</div>
151+
</div>
152+
);
153+
}

0 commit comments

Comments
 (0)