Skip to content

Commit 8ae0ff2

Browse files
committed
feat: enhance CRM example with routing, sidebar navigation, and new layout components; update dependencies and TypeScript configuration
1 parent 609ada9 commit 8ae0ff2

File tree

12 files changed

+414
-150
lines changed

12 files changed

+414
-150
lines changed

examples/crm-app/package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12-
"react": "^18.3.1",
13-
"react-dom": "^18.3.1",
14-
"@object-ui/core": "workspace:*",
1512
"@object-ui/components": "workspace:*",
13+
"@object-ui/core": "workspace:*",
1614
"@object-ui/fields": "workspace:*",
1715
"@object-ui/layout": "workspace:*",
16+
"@object-ui/react": "workspace:*",
1817
"@object-ui/types": "workspace:*",
19-
"@object-ui/react": "workspace:*"
18+
"clsx": "^2.1.1",
19+
"lucide-react": "^0.563.0",
20+
"react": "^18.3.1",
21+
"react-dom": "^18.3.1",
22+
"react-router-dom": "^7.13.0",
23+
"tailwind-merge": "^3.4.0"
2024
},
2125
"devDependencies": {
2226
"@types/react": "^18.3.3",

examples/crm-app/src/App.tsx

Lines changed: 119 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,132 @@
1-
// examples/crm-app/src/App.tsx
2-
import { useState } from 'react';
3-
import { SchemaRenderer } from '@object-ui/react';
4-
import { ComponentRegistry } from '@object-ui/core';
5-
// import { registerComponents } from '@object-ui/fields'; // Assuming this exists or we need to manually register
6-
import { Card, CardContent, CardHeader, CardTitle, Button } from '@object-ui/components';
7-
import {
8-
TextField,
9-
NumberField,
10-
BooleanField,
11-
SelectField,
12-
DateField
13-
} from '@object-ui/fields';
1+
import { useEffect } from 'react';
2+
import { BrowserRouter, Routes, Route, Outlet, Link, useLocation } from 'react-router-dom';
3+
import { SchemaRendererProvider, SchemaRenderer, useRenderer } from '@object-ui/react';
4+
import { registerFields } from '@object-ui/fields';
5+
import { registerLayout } from '@object-ui/layout';
6+
import { SidebarNav } from './components/SidebarNav';
147

15-
import { ContactObject, ContactFormPage, contactData } from './schema';
8+
// 1. Register components from packages (The "Controls Repository")
9+
registerFields();
10+
registerLayout();
1611

17-
// --- Temporary Manual Registration (Simulating what @object-ui/fields should do) ---
18-
// In a real app, this would be `import '@object-ui/fields/register';`
19-
ComponentRegistry.register('text', (props: any) => <TextField {...props} />);
20-
ComponentRegistry.register('textarea', (props: any) => <TextField {...props} />);
21-
ComponentRegistry.register('number', (props: any) => <NumberField {...props} />);
22-
ComponentRegistry.register('boolean', (props: any) => <BooleanField {...props} />);
23-
ComponentRegistry.register('select', (props: any) => <SelectField {...props} />);
24-
ComponentRegistry.register('date', (props: any) => <DateField {...props} />);
12+
// 2. Define Mock Data (In a real app, this comes from an API)
13+
const mockData = {
14+
user: { name: "Demo User", role: "admin" },
15+
stats: { revenue: 125000, leads: 45, deals: 12 },
16+
contacts: [
17+
{ id: 1, name: "Alice Johnson", email: "alice@example.com", status: "Active" },
18+
{ id: 2, name: "Bob Smith", email: "bob@tech.com", status: "Lead" },
19+
{ id: 3, name: "Charlie Brown", email: "charlie@peanuts.com", status: "Customer" }
20+
]
21+
};
2522

26-
// Simple Form Container Renderer
27-
ComponentRegistry.register('form', ({ children, className }: any) => (
28-
<form className={className} onSubmit={(e) => e.preventDefault()}>
29-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
30-
{children}
31-
</div>
32-
</form>
33-
));
34-
35-
// Simple Field Wrapper Renderer
36-
ComponentRegistry.register('field', ({ schema, data, onChange, objectSchema }: any) => {
37-
const fieldName = schema.name;
38-
const fieldConfig = objectSchema.fields[fieldName];
39-
40-
if (!fieldConfig) return <div className="text-red-500">Field {fieldName} not found</div>;
23+
// 3. Define Page Schemas (The "JSON Rendering")
4124

42-
const Widget = ComponentRegistry.get(fieldConfig.type) || ComponentRegistry.get('text');
43-
44-
// Resolve value from data binding
45-
const value = data[schema.bind] || data[fieldName];
25+
// Dashboard Page
26+
const dashboardSchema = {
27+
type: "page",
28+
props: { title: "Executive Dashboard" },
29+
children: [
30+
{
31+
type: "grid",
32+
props: { cols: 3, gap: 4, className: "mb-8" },
33+
children: [
34+
{
35+
type: "card",
36+
props: { title: "Total Revenue" },
37+
children: [{ type: "text", props: { value: "$125,000", className: "text-2xl font-bold" } }]
38+
},
39+
{
40+
type: "card",
41+
props: { title: "Active Leads" },
42+
children: [{ type: "text", props: { value: "45", className: "text-2xl font-bold" } }]
43+
},
44+
{
45+
type: "card",
46+
props: { title: "Open Deals" },
47+
children: [{ type: "text", props: { value: "12", className: "text-2xl font-bold" } }]
48+
}
49+
]
50+
},
51+
{
52+
type: "card",
53+
props: { title: "Recent Activity", className: "col-span-3" },
54+
children: [
55+
{ type: "text", props: { value: "Activity list would go here...", className: "text-gray-500" } }
56+
]
57+
}
58+
]
59+
};
4660

47-
const handleChange = (val: any) => {
48-
onChange?.(schema.bind || fieldName, val);
49-
};
61+
// Contacts List Page
62+
const contactsSchema = {
63+
type: "page",
64+
props: { title: "Contacts" },
65+
children: [
66+
{
67+
type: "page-header",
68+
props: {
69+
title: "All Contacts",
70+
description: "Manage your customer relationships"
71+
},
72+
children: [
73+
{
74+
type: "button",
75+
props: { label: "Add Contact", variant: "default" },
76+
events: { onClick: [{ action: "navigate", params: { url: "/contacts/new" } }] }
77+
}
78+
]
79+
},
80+
{
81+
type: "card",
82+
className: "mt-6",
83+
children: [
84+
{
85+
type: "table", // Note: We need to implement 'table' in plugins soon
86+
bind: "contacts",
87+
props: {
88+
columns: [
89+
{ key: "name", label: "Name" },
90+
{ key: "email", label: "Email" },
91+
{ key: "status", label: "Status" }
92+
]
93+
}
94+
}
95+
]
96+
}
97+
]
98+
};
5099

100+
// Layout Shell Component
101+
const Layout = () => {
51102
return (
52-
<div className={schema.props?.className}>
53-
<label className="block text-sm font-medium mb-1.5">
54-
{fieldConfig.label}
55-
{fieldConfig.required && <span className="text-red-500 ml-1">*</span>}
56-
</label>
57-
<Widget
58-
value={value}
59-
onChange={handleChange}
60-
field={fieldConfig}
61-
readonly={schema.readonly}
62-
/>
63-
{fieldConfig.help && <p className="text-xs text-muted-foreground mt-1">{fieldConfig.help}</p>}
103+
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
104+
<SidebarNav />
105+
<main className="flex-1 overflow-y-auto p-8">
106+
<Outlet />
107+
</main>
64108
</div>
65109
);
66-
});
67-
68-
69-
// -------------------------------------------------------------
70-
71-
export default function App() {
72-
const [data, setData] = useState(contactData);
73-
74-
const handleFieldChange = (field: string, value: any) => {
75-
console.log('Update:', field, value);
76-
setData(prev => ({
77-
...prev,
78-
[field]: value
79-
}));
80-
};
110+
};
81111

112+
function App() {
82113
return (
83-
<div className="min-h-screen bg-slate-50 p-8">
84-
<div className="max-w-4xl mx-auto space-y-8">
85-
86-
<header className="mb-8">
87-
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">ObjectUI CRM Example</h1>
88-
<p className="text-slate-500 mt-2">
89-
Demonstrating Server-Driven UI with React + Tailwind + Shadcn.
90-
</p>
91-
</header>
92-
93-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
94-
95-
{/* Main Form Area */}
96-
<div className="lg:col-span-2">
97-
<Card>
98-
<CardHeader>
99-
<CardTitle>{ContactFormPage.title}</CardTitle>
100-
</CardHeader>
101-
<CardContent>
102-
{/* Recursively render the Page Schema */}
103-
{((ContactFormPage.children as any[]) || []).map((childSchema: any) => (
104-
<SchemaRenderer
105-
key={childSchema.id || childSchema.name}
106-
schema={childSchema}
107-
// Inject Context
108-
{...{
109-
data,
110-
onChange: handleFieldChange,
111-
objectSchema: ContactObject
112-
}}
113-
/>
114-
))}
115-
116-
<div className="mt-8 flex justify-end space-x-4">
117-
<Button variant="outline">Cancel</Button>
118-
<Button onClick={() => alert(JSON.stringify(data, null, 2))}>Save Contact</Button>
119-
</div>
120-
</CardContent>
121-
</Card>
122-
</div>
123-
124-
{/* Real-time Data Debugger */}
125-
<div className="lg:col-span-1">
126-
<Card className="bg-slate-900 text-slate-50">
127-
<CardHeader>
128-
<CardTitle className="text-slate-50">Live State</CardTitle>
129-
</CardHeader>
130-
<CardContent>
131-
<pre className="text-xs font-mono overflow-auto max-h-[600px] p-2 bg-slate-950 rounded border border-slate-800">
132-
{JSON.stringify(data, null, 2)}
133-
</pre>
134-
</CardContent>
135-
</Card>
136-
</div>
137-
138-
</div>
139-
</div>
140-
</div>
114+
<SchemaRendererProvider
115+
dataSource={mockData}
116+
debug={true}
117+
>
118+
<BrowserRouter>
119+
<Routes>
120+
<Route path="/" element={<Layout />}>
121+
<Route index element={<SchemaRenderer schema={dashboardSchema} />} />
122+
<Route path="contacts" element={<SchemaRenderer schema={contactsSchema} />} />
123+
<Route path="opportunities" element={<div className="p-4">Opportunities Module (Coming Soon)</div>} />
124+
<Route path="settings" element={<div className="p-4">Settings Module (Coming Soon)</div>} />
125+
</Route>
126+
</Routes>
127+
</BrowserRouter>
128+
</SchemaRendererProvider>
141129
);
142130
}
131+
132+
export default App;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react';
2+
import { NavLink, useLocation } from 'react-router-dom';
3+
import { cn } from '@object-ui/components';
4+
import {
5+
LayoutDashboard,
6+
Users,
7+
Settings,
8+
Package,
9+
FileText
10+
} from 'lucide-react';
11+
import {
12+
SidebarGroup,
13+
SidebarGroupLabel,
14+
SidebarGroupContent,
15+
SidebarMenu,
16+
SidebarMenuItem,
17+
SidebarMenuButton,
18+
} from '@object-ui/components';
19+
20+
export interface NavItem {
21+
title: string;
22+
href: string;
23+
icon?: React.ComponentType<{ className?: string }>;
24+
}
25+
26+
export const mainNavItems: NavItem[] = [
27+
{
28+
title: "Dashboard",
29+
href: "/",
30+
icon: LayoutDashboard,
31+
},
32+
{
33+
title: "Contacts",
34+
href: "/contacts",
35+
icon: Users,
36+
},
37+
{
38+
title: "Opportunities",
39+
href: "/opportunities",
40+
icon: FileText,
41+
},
42+
{
43+
title: "Products",
44+
href: "/products",
45+
icon: Package,
46+
},
47+
{
48+
title: "Settings",
49+
href: "/settings",
50+
icon: Settings,
51+
},
52+
];
53+
54+
export function SidebarNav({ items }: { items: NavItem[] }) {
55+
const location = useLocation();
56+
57+
return (
58+
<SidebarGroup>
59+
<SidebarGroupLabel>Application</SidebarGroupLabel>
60+
<SidebarGroupContent>
61+
<SidebarMenu>
62+
{items.map((item) => (
63+
<SidebarMenuItem key={item.href}>
64+
<SidebarMenuButton asChild isActive={location.pathname === item.href}>
65+
<NavLink to={item.href}>
66+
{item.icon && <item.icon />}
67+
<span>{item.title}</span>
68+
</NavLink>
69+
</SidebarMenuButton>
70+
</SidebarMenuItem>
71+
))}
72+
</SidebarMenu>
73+
</SidebarGroupContent>
74+
</SidebarGroup>
75+
);
76+
}

packages/components/src/renderers/data-display/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ import './tree-view';
1414
import './statistic';
1515
import './breadcrumb';
1616
import './kbd';
17+
import './table';
18+

0 commit comments

Comments
 (0)