Skip to content

Commit af15ff7

Browse files
committed
feat: enhance AppShell with branding support and CSS custom properties
feat: extend ObjectView component with multi-view support, including ViewSwitcher, FilterUI, and SortUI fix: resolve dataSource context in ObjectViewRenderer for better integration with SchemaRendererProvider refactor: update useActionRunner to support custom confirmation and toast handlers chore: add new methods to DataSource interface for view and app definitions feat: introduce RecordDetailView component for displaying single record details feat: create ExpressionProvider for dynamic visibility and expression evaluation context docs: add Console Architecture guide detailing data flow and routing docs: create Console App documentation for quick start and configuration details
1 parent 237a080 commit af15ff7

File tree

27 files changed

+1347
-726
lines changed

27 files changed

+1347
-726
lines changed

apps/console/src/App.tsx

Lines changed: 10 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ import { CommandPalette } from './components/CommandPalette';
1313
import { ErrorBoundary } from './components/ErrorBoundary';
1414
import { LoadingScreen } from './components/LoadingScreen';
1515
import { ObjectView } from './components/ObjectView';
16+
import { RecordDetailView } from './components/RecordDetailView';
1617
import { DashboardView } from './components/DashboardView';
1718
import { PageView } from './components/PageView';
1819
import { ReportView } from './components/ReportView';
1920
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './components/MetadataInspector';
20-
import { useBranding } from './hooks/useBranding';
21-
22-
import { DetailView } from '@object-ui/plugin-detail';
23-
import { useParams } from 'react-router-dom';
21+
import { ExpressionProvider } from './context/ExpressionProvider';
2422

2523
/**
2624
* Patch: MSW discovery response uses 'routes' instead of 'endpoints'.
@@ -38,66 +36,8 @@ function patchDiscoveryEndpoints(adapter: ObjectStackAdapter) {
3836
}
3937
}
4038

41-
// Detail View Component
42-
function RecordDetailView({ dataSource, objects, onEdit }: any) {
43-
const { objectName, recordId } = useParams();
44-
const { showDebug, toggleDebug } = useMetadataInspector();
45-
const objectDef = objects.find((o: any) => o.name === objectName);
46-
47-
if (!objectDef) {
48-
return (
49-
<div className="flex h-full items-center justify-center p-4">
50-
<Empty>
51-
<EmptyTitle>Object Not Found</EmptyTitle>
52-
<p>Object "{objectName}" definition missing.</p>
53-
</Empty>
54-
</div>
55-
);
56-
}
57-
58-
const detailSchema = {
59-
type: 'detail-view',
60-
objectName: objectDef.name,
61-
resourceId: recordId,
62-
showBack: true,
63-
onBack: 'history',
64-
showEdit: true,
65-
title: objectDef.label,
66-
sections: [
67-
{
68-
title: 'Details',
69-
fields: Object.keys(objectDef.fields || {}).map(key => ({
70-
name: key,
71-
label: objectDef.fields[key].label || key,
72-
type: objectDef.fields[key].type || 'text'
73-
})),
74-
columns: 2
75-
}
76-
]
77-
};
78-
79-
return (
80-
<div className="h-full bg-background overflow-hidden flex flex-col relative">
81-
<div className="absolute top-4 right-4 z-50">
82-
<MetadataToggle open={showDebug} onToggle={toggleDebug} />
83-
</div>
84-
85-
<div className="flex-1 overflow-hidden flex flex-row">
86-
<div className="flex-1 overflow-auto p-4 lg:p-6">
87-
<DetailView
88-
schema={detailSchema}
89-
dataSource={dataSource}
90-
onEdit={() => onEdit({ _id: recordId, id: recordId })}
91-
/>
92-
</div>
93-
<MetadataPanel
94-
open={showDebug}
95-
sections={[{ title: 'View Schema', data: detailSchema }]}
96-
/>
97-
</div>
98-
</div>
99-
);
100-
}
39+
import { useParams } from 'react-router-dom';
40+
import { ThemeProvider } from './components/theme-provider';
10141

10242
export function AppContent() {
10343
const [dataSource, setDataSource] = useState<ObjectStackAdapter | null>(null);
@@ -118,8 +58,7 @@ export function AppContent() {
11858
const [editingRecord, setEditingRecord] = useState<any>(null);
11959
const [refreshKey, setRefreshKey] = useState(0);
12060

121-
// Apply app branding (primaryColor, favicon, title)
122-
useBranding(activeApp);
61+
// Branding is now applied by AppShell via ConsoleLayout
12362

12463
useEffect(() => {
12564
initializeDataSource();
@@ -202,7 +141,11 @@ export function AppContent() {
202141
</div>
203142
);
204143

144+
// Expression context for dynamic visibility/disabled/hidden expressions
145+
const expressionUser = { name: 'John Doe', email: 'admin@example.com', role: 'admin' };
146+
205147
return (
148+
<ExpressionProvider user={expressionUser} app={activeApp} data={{}}>
206149
<ConsoleLayout
207150
activeAppName={activeApp.name}
208151
activeApp={activeApp}
@@ -300,6 +243,7 @@ export function AppContent() {
300243
</Dialog>
301244
</SchemaRendererProvider>
302245
</ConsoleLayout>
246+
</ExpressionProvider>
303247
);
304248
}
305249

@@ -331,8 +275,6 @@ function RootRedirect() {
331275
return <LoadingScreen />;
332276
}
333277

334-
import { ThemeProvider } from './components/theme-provider';
335-
336278
export function App() {
337279
return (
338280
<ThemeProvider defaultTheme="system" storageKey="object-ui-theme">

apps/console/src/__tests__/ConsoleApp.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ vi.mock('@object-ui/components', async (importOriginal) => {
8585
});
8686

8787
// Mock Lucide icons to avoid rendering SVG complexity
88-
vi.mock('lucide-react', async () => {
88+
vi.mock('lucide-react', async (importOriginal) => {
89+
const actual = await importOriginal<any>();
8990
return {
91+
...actual,
9092
Database: () => <span data-testid="icon-database" />,
9193
LayoutDashboard: () => <span data-testid="icon-dashboard" />,
9294
Briefcase: () => <span data-testid="icon-briefcase" />,

apps/console/src/__tests__/ConsoleFeatures.test.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,20 @@ vi.mock('@object-ui/components', async (importOriginal) => {
4747
});
4848

4949
// Mock Icons
50-
vi.mock('lucide-react', () => ({
51-
Plus: () => <span>+</span>,
52-
Calendar: () => <span>Cal</span>,
53-
Kanban: () => <span>Kan</span>,
54-
Table: () => <span>Tab</span>,
55-
AlignLeft: () => <span>Gantt</span>,
56-
Filter: () => <span>FilterIcon</span>,
57-
X: () => <span>X</span>,
58-
Code2: () => <span>Code</span>
59-
}));
50+
vi.mock('lucide-react', async (importOriginal) => {
51+
const actual = await importOriginal<any>();
52+
return {
53+
...actual,
54+
Plus: () => <span>+</span>,
55+
Calendar: () => <span>Cal</span>,
56+
Kanban: () => <span>Kan</span>,
57+
Table: () => <span>Tab</span>,
58+
AlignLeft: () => <span>Gantt</span>,
59+
Filter: () => <span>FilterIcon</span>,
60+
X: () => <span>X</span>,
61+
Code2: () => <span>Code</span>,
62+
};
63+
});
6064

6165
describe('ObjectView Console Features', () => {
6266

apps/console/src/components/AppHeader.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
/**
2+
* AppHeader
3+
*
4+
* Top header bar for the console application. Renders breadcrumb navigation
5+
* derived from the current route, along with search, notifications, theme
6+
* toggle, and connection status indicators.
7+
* @module
8+
*/
9+
110
import { useLocation, useParams, Link } from 'react-router-dom';
211
import {
312
Breadcrumb,

apps/console/src/components/AppSidebar.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
/**
2+
* AppSidebar
3+
*
4+
* Collapsible sidebar navigation for the console. Displays the active app's
5+
* objects, dashboards, pages, and reports as grouped menu items, with an
6+
* app-switcher dropdown and user profile footer.
7+
* @module
8+
*/
9+
110
import * as React from 'react';
211
import { useLocation, Link } from 'react-router-dom';
312
import * as LucideIcons from 'lucide-react';
@@ -36,14 +45,18 @@ import {
3645
ChevronRight,
3746
} from 'lucide-react';
3847
import appConfig from '../../objectstack.shared';
48+
import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider';
3949

40-
// Helper to get icon from Lucide
41-
function getIcon(name?: string) {
50+
/**
51+
* Resolve a Lucide icon component by name string.
52+
* Supports camelCase, PascalCase, and kebab-case icon names.
53+
*/
54+
function getIconComponent(name?: string): React.ComponentType<any> {
4255
if (!name) return LucideIcons.Database;
43-
44-
// 1. Try direct match (e.g. if user passed "User")
56+
57+
// 1. Direct match (PascalCase or camelCase)
4558
if ((LucideIcons as any)[name]) {
46-
return (LucideIcons as any)[name];
59+
return (LucideIcons as any)[name];
4760
}
4861

4962
// 2. Try converting kebab-case to PascalCase (e.g. "shopping-cart" -> "ShoppingCart")
@@ -257,11 +270,11 @@ function NavigationItemRenderer({ item, activeAppName }: { item: any, activeAppN
257270
const Icon = getIcon(item.icon);
258271
const location = useLocation();
259272
const [isOpen, setIsOpen] = React.useState(item.expanded !== false);
273+
const { evaluator } = useExpressionContext();
260274

261-
// Handle visibility condition from spec (visible field)
262-
// In a real implementation, this would evaluate the expression
263-
// For now, we'll just check if it exists and is not explicitly false
264-
if (item.visible === 'false' || item.visible === false) {
275+
// Evaluate visibility expression (supports boolean, string, and ${} template expressions)
276+
const isVisible = evaluateVisibility(item.visible ?? item.visibleOn, evaluator);
277+
if (!isVisible) {
265278
return null;
266279
}
267280

apps/console/src/components/CommandPalette.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
Monitor,
3030
} from 'lucide-react';
3131
import { useTheme } from './theme-provider';
32+
import { useExpressionContext, evaluateVisibility } from '../context/ExpressionProvider';
3233

3334
/** Resolve a Lucide icon by name (kebab-case or PascalCase) */
3435
function getIcon(name?: string): React.ElementType {
@@ -54,6 +55,7 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
5455
const navigate = useNavigate();
5556
const { appName } = useParams();
5657
const { setTheme } = useTheme();
58+
const { evaluator } = useExpressionContext();
5759

5860
// ⌘+K / Ctrl+K shortcut
5961
useEffect(() => {
@@ -74,8 +76,10 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
7476
command();
7577
}, []);
7678

77-
// Extract navigation items from active app
78-
const navItems = flattenNavigation(activeApp?.navigation || []);
79+
// Extract navigation items from active app, filtering by visibility expressions
80+
const navItems = flattenNavigation(activeApp?.navigation || []).filter(
81+
(item) => evaluateVisibility(item.visible ?? item.visibleOn, evaluator)
82+
);
7983

8084
return (
8185
<CommandDialog open={open} onOpenChange={setOpen}>

apps/console/src/components/ConsoleLayout.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* ConsoleLayout
3+
*
4+
* Root layout shell for the console application. Composes the AppShell
5+
* with the sidebar, header, and main content area.
6+
* @module
7+
*/
8+
19
import React from 'react';
210
import { AppShell } from '@object-ui/layout';
311
import { AppSidebar } from './AppSidebar';
@@ -37,6 +45,19 @@ export function ConsoleLayout({
3745
/>
3846
}
3947
className="p-0 overflow-hidden bg-muted/5"
48+
branding={
49+
activeApp?.branding
50+
? {
51+
primaryColor: activeApp.branding.primaryColor,
52+
accentColor: activeApp.branding.accentColor,
53+
favicon: activeApp.branding.favicon,
54+
logo: activeApp.branding.logo,
55+
title: activeApp.label
56+
? `${activeApp.label} — ObjectStack Console`
57+
: undefined,
58+
}
59+
: undefined
60+
}
4061
>
4162
{children}
4263
</AppShell>

0 commit comments

Comments
 (0)