Skip to content

Commit 6e9a947

Browse files
authored
Merge pull request #435 from objectstack-ai/copilot/optimize-console-ui-components
2 parents 75922f8 + 28a8939 commit 6e9a947

15 files changed

Lines changed: 456 additions & 198 deletions

ROADMAP.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,39 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
204204
- Widget manifest system ✅
205205
- @objectstack/client integration hardening (in progress)
206206

207+
#### 1.7 Console UX Polish (2 weeks) ✅ Completed
208+
**Target:** Enterprise-grade UX across every console component
209+
210+
**Loading & Connection (Completed):**
211+
- [x] Multi-step progress indicator on LoadingScreen (animated step transitions)
212+
- [x] Brief "Connected" flash on successful connection (auto-dismiss after 2s)
213+
214+
**Navigation & Wayfinding (Completed):**
215+
- [x] Humanized breadcrumb labels (slug → Title Case, e.g. `crm_dashboard``Crm Dashboard`)
216+
- [x] System admin route support in breadcrumbs
217+
- [x] Improved record ID display in breadcrumbs (`#abc123…` format)
218+
219+
**Empty States (Completed):**
220+
- [x] Contextual icons in all "Not Found" empty states (ObjectView, DashboardView, PageView, ReportView, RecordDetailView)
221+
- [x] Helpful descriptions guiding users on what to do next
222+
223+
**Error Recovery (Completed):**
224+
- [x] Added "Go Home" recovery action alongside "Try Again" in ErrorBoundary
225+
226+
**System Admin Pages (Completed):**
227+
- [x] ProfilePage: Migrated from raw HTML to Shadcn Card/Input/Label/Alert/Avatar/Badge components
228+
- [x] User/Org/Role/Audit pages: Shadcn Button/Card/Badge, contextual page icons, improved empty state visuals
229+
230+
**Next Steps (Q2 2026):**
231+
- [ ] Skeleton loading states for all data-heavy views (grid, dashboard, detail)
232+
- [ ] Toast notifications for CRUD operations (create/update/delete success/error)
233+
- [ ] Keyboard shortcuts help dialog (? key)
234+
- [ ] Responsive sidebar auto-collapse on tablet
235+
- [ ] Onboarding walkthrough for first-time users
236+
- [ ] Notification center with unread count badge
237+
- [ ] Global search results page (beyond command palette)
238+
- [ ] Drag-and-drop sidebar navigation reordering
239+
207240
**Q1 Milestone:**
208241
- **v0.6.0 Release (March 2026):** Infrastructure Complete + Auth Foundation + Client Integration Validated
209242

@@ -282,6 +315,20 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
282315
- [ ] Optimistic UI updates with rollback on failure
283316
- [ ] Batch operation progress tracking with connection-aware retry
284317

318+
#### 2.7 Console UX Enhancement Phase 2 (3 weeks)
319+
**Target:** Production-grade console experience
320+
321+
- [ ] Skeleton loading states for data-heavy views (grid, dashboard, detail)
322+
- [ ] Toast notifications for CRUD operations (create/update/delete)
323+
- [ ] Keyboard shortcuts help dialog (? key)
324+
- [ ] Responsive sidebar auto-collapse on tablet breakpoints
325+
- [ ] Onboarding walkthrough for first-time users
326+
- [ ] Notification center with unread count badge
327+
- [ ] Global search results page (beyond command palette)
328+
- [ ] Drag-and-drop sidebar navigation reordering
329+
- [ ] Breadcrumb-based quick navigation dropdown
330+
- [ ] Recent items / favorites in sidebar
331+
285332
**Q2 Milestone:**
286333
- **v1.0.0 Release (June 2026):** Feature Complete + Full @objectstack/client Integration
287334

apps/console/src/components/AppHeader.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ import { ModeToggle } from './mode-toggle';
2626
import { ConnectionStatus } from './ConnectionStatus';
2727
import type { ConnectionState } from '../dataSource';
2828

29+
/** Convert a slug like "crm_dashboard" or "audit-log" to "Crm Dashboard" / "Audit Log" */
30+
function humanizeSlug(slug: string): string {
31+
return slug
32+
.replace(/[-_]/g, ' ')
33+
.replace(/\b\w/g, (c) => c.toUpperCase());
34+
}
35+
2936
export function AppHeader({ appName, objects, connectionState }: { appName: string, objects: any[], connectionState?: ConnectionState }) {
3037
const location = useLocation();
3138
const params = useParams();
@@ -43,19 +50,24 @@ export function AppHeader({ appName, objects, connectionState }: { appName: stri
4350
];
4451

4552
if (routeType === 'dashboard') {
46-
breadcrumbItems.push({ label: 'Dashboard' });
53+
breadcrumbItems.push({ label: 'Dashboards', href: `${breadcrumbItems[0].href}` });
4754
if (pathParts[3]) {
48-
breadcrumbItems.push({ label: pathParts[3] });
55+
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
4956
}
5057
} else if (routeType === 'page') {
51-
breadcrumbItems.push({ label: 'Page' });
58+
breadcrumbItems.push({ label: 'Pages', href: `${breadcrumbItems[0].href}` });
5259
if (pathParts[3]) {
53-
breadcrumbItems.push({ label: pathParts[3] });
60+
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
5461
}
5562
} else if (routeType === 'report') {
56-
breadcrumbItems.push({ label: 'Report' });
63+
breadcrumbItems.push({ label: 'Reports', href: `${breadcrumbItems[0].href}` });
64+
if (pathParts[3]) {
65+
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
66+
}
67+
} else if (routeType === 'system') {
68+
breadcrumbItems.push({ label: 'System' });
5769
if (pathParts[3]) {
58-
breadcrumbItems.push({ label: pathParts[3] });
70+
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
5971
}
6072
} else if (routeType) {
6173
// Object route
@@ -68,9 +80,10 @@ export function AppHeader({ appName, objects, connectionState }: { appName: stri
6880

6981
// Check if viewing a specific record
7082
if (pathParts[3] === 'record' && pathParts[4]) {
71-
breadcrumbItems.push({ label: `Record ${pathParts[4].slice(0, 8)}...` });
83+
const shortId = pathParts[4].length > 12 ? `${pathParts[4].slice(0, 8)}…` : pathParts[4];
84+
breadcrumbItems.push({ label: `#${shortId}` });
7285
} else if (pathParts[3] === 'view' && pathParts[4]) {
73-
breadcrumbItems.push({ label: pathParts[4] });
86+
breadcrumbItems.push({ label: humanizeSlug(pathParts[4]) });
7487
}
7588
}
7689
}

apps/console/src/components/ConnectionStatus.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@
22
* ConnectionStatus
33
*
44
* Displays the current ObjectStack connection state in the header area.
5-
* Shows a subtle indicator when connected, and a prominent one when
6-
* reconnecting or in error state.
5+
* Shows a subtle indicator when connected (auto-hides after a moment),
6+
* and a prominent one when reconnecting or in error state.
77
*/
88

9+
import { useState, useEffect } from 'react';
910
import type { ConnectionState } from '../dataSource';
1011
import { cn } from '@object-ui/components';
11-
import { Wifi, WifiOff, Loader2 } from 'lucide-react';
12+
import { Wifi, WifiOff, Loader2, CheckCircle2 } from 'lucide-react';
1213

1314
interface ConnectionStatusProps {
1415
state: ConnectionState;
1516
className?: string;
1617
}
1718

1819
const statusConfig: Record<ConnectionState, { label: string; color: string; icon: typeof Wifi }> = {
19-
connected: { label: 'Connected', color: 'text-green-500', icon: Wifi },
20+
connected: { label: 'Connected', color: 'text-green-500', icon: CheckCircle2 },
2021
connecting: { label: 'Connecting...', color: 'text-yellow-500', icon: Loader2 },
2122
reconnecting: { label: 'Reconnecting...', color: 'text-yellow-500', icon: Loader2 },
2223
disconnected: { label: 'Disconnected', color: 'text-muted-foreground', icon: WifiOff },
@@ -26,15 +27,27 @@ const statusConfig: Record<ConnectionState, { label: string; color: string; icon
2627
export function ConnectionStatus({ state, className }: ConnectionStatusProps) {
2728
const config = statusConfig[state];
2829
const Icon = config.icon;
30+
const [showConnected, setShowConnected] = useState(false);
2931

30-
// Don't show anything when connected (clean UI)
31-
if (state === 'connected') return null;
32+
// Briefly show "Connected" when transitioning to connected state
33+
useEffect(() => {
34+
if (state === 'connected') {
35+
setShowConnected(true);
36+
const timer = setTimeout(() => setShowConnected(false), 2000);
37+
return () => clearTimeout(timer);
38+
}
39+
setShowConnected(false);
40+
}, [state]);
41+
42+
// Hide when connected (after the brief flash)
43+
if (state === 'connected' && !showConnected) return null;
3244

3345
return (
3446
<div
3547
className={cn(
36-
'flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded-md',
48+
'flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded-md transition-opacity duration-300',
3749
config.color,
50+
state === 'connected' && 'bg-green-500/10',
3851
state === 'error' && 'bg-destructive/10',
3952
state === 'reconnecting' && 'bg-yellow-500/10',
4053
className

apps/console/src/components/DashboardView.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { useParams } from 'react-router-dom';
77
import { DashboardRenderer } from '@object-ui/plugin-dashboard';
88
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
9+
import { LayoutDashboard } from 'lucide-react';
910
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
1011
import appConfig from '../../objectstack.shared';
1112

@@ -21,8 +22,14 @@ export function DashboardView({ dataSource }: { dataSource?: any }) {
2122
return (
2223
<div className="h-full flex items-center justify-center p-8">
2324
<Empty>
25+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
26+
<LayoutDashboard className="h-6 w-6 text-muted-foreground" />
27+
</div>
2428
<EmptyTitle>Dashboard Not Found</EmptyTitle>
25-
<EmptyDescription>The dashboard "{dashboardName}" could not be found.</EmptyDescription>
29+
<EmptyDescription>
30+
The dashboard &quot;{dashboardName}&quot; could not be found.
31+
It may have been removed or renamed.
32+
</EmptyDescription>
2633
</Empty>
2734
</div>
2835
);

apps/console/src/components/ErrorBoundary.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import { Component, type ErrorInfo, type ReactNode } from 'react';
1919
import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
20-
import { AlertTriangle, RotateCcw } from 'lucide-react';
20+
import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
2121

2222
interface ErrorBoundaryProps {
2323
children: ReactNode;
@@ -76,15 +76,26 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
7676
<EmptyDescription className="mb-4">
7777
{this.state.error.message || 'An unexpected error occurred while rendering this view.'}
7878
</EmptyDescription>
79-
<Button
80-
variant="outline"
81-
size="sm"
82-
onClick={this.resetErrorBoundary}
83-
className="gap-2"
84-
>
85-
<RotateCcw className="h-4 w-4" />
86-
Try Again
87-
</Button>
79+
<div className="flex items-center justify-center gap-2">
80+
<Button
81+
variant="outline"
82+
size="sm"
83+
onClick={this.resetErrorBoundary}
84+
className="gap-2"
85+
>
86+
<RotateCcw className="h-4 w-4" />
87+
Try Again
88+
</Button>
89+
<Button
90+
variant="ghost"
91+
size="sm"
92+
onClick={() => { window.location.href = '/'; }}
93+
className="gap-2"
94+
>
95+
<Home className="h-4 w-4" />
96+
Go Home
97+
</Button>
98+
</div>
8899
</Empty>
89100
{import.meta.env.DEV && (
90101
<details className="mt-6 text-left">

apps/console/src/components/LoadingScreen.tsx

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11

22
import { Spinner } from '@object-ui/components';
3-
import { Database } from 'lucide-react';
3+
import { Database, CheckCircle2, Loader2 } from 'lucide-react';
4+
import { useState, useEffect } from 'react';
5+
6+
interface LoadingScreenProps {
7+
/** Optional message override */
8+
message?: string;
9+
}
10+
11+
const LOADING_STEPS = [
12+
'Connecting to data source',
13+
'Loading configuration',
14+
'Preparing workspace',
15+
];
16+
17+
export function LoadingScreen({ message }: LoadingScreenProps) {
18+
const [currentStep, setCurrentStep] = useState(0);
19+
20+
useEffect(() => {
21+
if (message) return; // skip auto-progression when message is overridden
22+
const timer = setInterval(() => {
23+
setCurrentStep((prev) => Math.min(prev + 1, LOADING_STEPS.length - 1));
24+
}, 1200);
25+
return () => clearInterval(timer);
26+
}, [message]);
427

5-
export function LoadingScreen() {
628
return (
729
<div className="flex flex-col items-center justify-center h-screen bg-background">
830
<div className="flex flex-col items-center gap-6">
@@ -20,11 +42,34 @@ export function LoadingScreen() {
2042
<p className="text-sm text-muted-foreground">Initializing application...</p>
2143
</div>
2244

23-
{/* Loading indicator */}
24-
<div className="flex items-center gap-3 px-4 py-2 bg-muted/50 rounded-full">
25-
<Spinner className="h-4 w-4 text-primary" />
26-
<span className="text-sm text-muted-foreground">Connecting to data source</span>
27-
</div>
45+
{/* Progress steps */}
46+
{message ? (
47+
<div className="flex items-center gap-3 px-4 py-2 bg-muted/50 rounded-full">
48+
<Spinner className="h-4 w-4 text-primary" />
49+
<span className="text-sm text-muted-foreground">{message}</span>
50+
</div>
51+
) : (
52+
<div className="flex flex-col gap-2 w-64">
53+
{LOADING_STEPS.map((step, index) => (
54+
<div
55+
key={step}
56+
className="flex items-center gap-2.5 text-sm transition-opacity duration-300"
57+
style={{ opacity: index <= currentStep ? 1 : 0.3 }}
58+
>
59+
{index < currentStep ? (
60+
<CheckCircle2 className="h-4 w-4 text-primary shrink-0" />
61+
) : index === currentStep ? (
62+
<Loader2 className="h-4 w-4 text-primary shrink-0 animate-spin" />
63+
) : (
64+
<div className="h-4 w-4 rounded-full border border-muted-foreground/30 shrink-0" />
65+
)}
66+
<span className={index <= currentStep ? 'text-foreground' : 'text-muted-foreground'}>
67+
{step}
68+
</span>
69+
</div>
70+
))}
71+
</div>
72+
)}
2873
</div>
2974
</div>
3075
);

apps/console/src/components/ObjectView.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
3838
return (
3939
<div className="h-full p-4 flex items-center justify-center">
4040
<Empty>
41+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
42+
<TableIcon className="h-6 w-6 text-muted-foreground" />
43+
</div>
4144
<EmptyTitle>Object Not Found</EmptyTitle>
42-
<EmptyDescription>The object &quot;{objectName}&quot; does not exist in the current configuration.</EmptyDescription>
45+
<EmptyDescription>
46+
The object &quot;{objectName}&quot; does not exist in the current configuration.
47+
Check your app navigation settings or select a different object from the sidebar.
48+
</EmptyDescription>
4349
</Empty>
4450
</div>
4551
);

apps/console/src/components/PageView.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { useParams, useSearchParams } from 'react-router-dom';
77
import { SchemaRenderer } from '@object-ui/react';
88
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
9+
import { FileText } from 'lucide-react';
910
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
1011
import appConfig from '../../objectstack.shared';
1112

@@ -22,8 +23,14 @@ export function PageView() {
2223
return (
2324
<div className="h-full flex items-center justify-center p-8">
2425
<Empty>
26+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
27+
<FileText className="h-6 w-6 text-muted-foreground" />
28+
</div>
2529
<EmptyTitle>Page Not Found</EmptyTitle>
26-
<EmptyDescription>The page "{pageName}" could not be found.</EmptyDescription>
30+
<EmptyDescription>
31+
The page &quot;{pageName}&quot; could not be found.
32+
It may have been removed or renamed.
33+
</EmptyDescription>
2734
</Empty>
2835
</div>
2936
);

apps/console/src/components/RecordDetailView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import { useParams } from 'react-router-dom';
1010
import { DetailView } from '@object-ui/plugin-detail';
11-
import { Empty, EmptyTitle } from '@object-ui/components';
11+
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
12+
import { Database } from 'lucide-react';
1213
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
1314

1415
interface RecordDetailViewProps {
@@ -26,8 +27,14 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
2627
return (
2728
<div className="flex h-full items-center justify-center p-4">
2829
<Empty>
30+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
31+
<Database className="h-6 w-6 text-muted-foreground" />
32+
</div>
2933
<EmptyTitle>Object Not Found</EmptyTitle>
30-
<p>Object "{objectName}" definition missing.</p>
34+
<EmptyDescription>
35+
Object &quot;{objectName}&quot; definition missing.
36+
Check your configuration or navigate back to select a valid object.
37+
</EmptyDescription>
3138
</Empty>
3239
</div>
3340
);

0 commit comments

Comments
 (0)