Skip to content

Commit 548d2da

Browse files
committed
feat: register custom widgets including ListView in the registry
1 parent 5bf10d9 commit 548d2da

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

examples/crm-app/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import { contactListSchema, contactDetailSchema } from './schemas/contacts';
1414
import { opportunityListSchema } from './schemas/opportunities';
1515
import { opportunityDetailSchema } from './schemas/opportunity-detail';
1616

17+
import { registerCustomWidgets } from './components/registry';
18+
1719
// 1. Register components
1820
registerFields();
1921
registerLayout();
2022
registerPlaceholders();
23+
registerCustomWidgets();
2124

2225
// Generic Layout Shell
2326
const Layout = () => {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import React from 'react';
2+
import { registerComponent } from '@object-ui/core';
3+
import { useDataScope } from '@object-ui/react';
4+
import { cn } from '@object-ui/components';
5+
6+
// ... existing code ...
7+
8+
// 4. List View (Simple)
9+
const ListView = ({ schema }: any) => {
10+
// If 'bind' is present, useDataScope will resolve the data relative to the current scope
11+
// But useDataScope returns the *entire* scope usually?
12+
// Actually, usually SchemaRenderer handles the `bind` resolution and passes `data` prop?
13+
// Let's assume looking at the architecture: "SchemaRenderer ... Bridges Core and Components."
14+
// If I use `useDataScope`, I should be able to get the list.
15+
16+
// However, if the protocol says:
17+
// "Data binding path (e.g., 'user.address.city')"
18+
// usually the architecture allows the component to access data.
19+
20+
const { scope } = useDataScope();
21+
let data = schema.data; // direct data
22+
23+
if (schema.bind) {
24+
// Simple binding resolution for this demo
25+
// In a real implementation this would be more robust
26+
data = scope ? scope[schema.bind] : [];
27+
}
28+
29+
// Fallback or empty
30+
if (!data || !Array.isArray(data)) return <div className="p-4 text-muted-foreground">No data</div>;
31+
32+
return (
33+
<div className="space-y-2">
34+
{data.slice(0, schema.props?.limit || 10).map((item: any, i: number) => (
35+
<div key={i} className="flex items-center justify-between p-3 border rounded bg-card text-card-foreground">
36+
<div className="flex flex-col">
37+
<span className="font-medium text-sm">{renderTemplate(schema.props?.render?.title, item) || item.name}</span>
38+
<span className="text-xs text-muted-foreground">{renderTemplate(schema.props?.render?.description, item)}</span>
39+
</div>
40+
{schema.props?.render?.extra && (
41+
<div className="text-xs font-semibold px-2 py-1 bg-secondary rounded">
42+
{renderTemplate(schema.props?.render?.extra, item)}
43+
</div>
44+
)}
45+
</div>
46+
))}
47+
</div>
48+
);
49+
};
50+
51+
// Simple string template helper: "Hello ${name}" -> "Hello World"
52+
function renderTemplate(template: string, data: any) {
53+
if (!template) return "";
54+
return template.replace(/\$\{(.*?)\}/g, (match, key) => {
55+
return data[key] !== undefined ? data[key] : match;
56+
});
57+
}
58+
59+
export function registerCustomWidgets() {
60+
registerComponent("widget:metric", MetricWidget);
61+
registerComponent("widget:chart", ChartWidget);
62+
registerComponent("view:timeline", TimelineView);
63+
registerComponent("view:list", ListView);
64+
}
65+
const MetricWidget = ({ schema }: any) => {
66+
const { value, label, trend, format } = schema.props || {};
67+
const isPositive = trend?.startsWith('+');
68+
69+
return (
70+
<div className="flex flex-col gap-1">
71+
<div className="text-2xl font-bold">{value}</div>
72+
<div className="text-xs text-muted-foreground">
73+
{trend && (
74+
<span className={cn("mr-1 font-medium", isPositive ? "text-green-600" : "text-red-600")}>
75+
{trend}
76+
</span>
77+
)}
78+
{label}
79+
</div>
80+
</div>
81+
);
82+
};
83+
84+
// 2. Simple CSS Bar Chart
85+
const ChartWidget = ({ schema }: any) => {
86+
const { title, data, height = 200 } = schema.props || {};
87+
// Extract max value for scaling
88+
const maxValue = Math.max(...(data?.map((d: any) => d.value) || [100]));
89+
90+
return (
91+
<div className="flex flex-col h-full w-full">
92+
{title && <h3 className="text-sm font-medium mb-4">{title}</h3>}
93+
<div className="flex items-end justify-around w-full" style={{ height: `${height}px` }}>
94+
{data?.map((item: any, i: number) => {
95+
const percent = (item.value / maxValue) * 100;
96+
return (
97+
<div key={i} className="flex flex-col items-center gap-2 group w-full px-1">
98+
<div className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
99+
{item.value}
100+
</div>
101+
<div
102+
className="w-full rounded-t hover:opacity-80 transition-all"
103+
style={{
104+
height: `${percent}%`,
105+
backgroundColor: item.fill || '#3b82f6'
106+
}}
107+
/>
108+
<div className="text-xs text-center text-muted-foreground truncate w-full">
109+
{item.name}
110+
</div>
111+
</div>
112+
);
113+
})}
114+
</div>
115+
</div>
116+
);
117+
};
118+
119+
// 3. Timeline View
120+
const TimelineView = ({ schema }: any) => {
121+
const { items } = schema.props || {};
122+
123+
return (
124+
<div className={cn("space-y-8", schema.className)}>
125+
{items?.map((item: any, i: number) => (
126+
<div key={i} className="flex gap-4">
127+
<div className="relative mt-1">
128+
<div className="absolute top-8 left-1/2 h-full w-px -translate-x-1/2 bg-gray-200" />
129+
<div className="relative flex h-8 w-8 items-center justify-center rounded-full border bg-background shadow-sm">
130+
{/* Simple icon mapping or fallback */}
131+
<span className="text-xs"></span>
132+
</div>
133+
</div>
134+
<div className="flex flex-col gap-1 pb-8">
135+
<p className="text-sm font-medium leading-none">{item.title}</p>
136+
<p className="text-xs text-muted-foreground">{item.description}</p>
137+
<p className="text-xs text-muted-foreground">{item.date}</p>
138+
</div>
139+
</div>
140+
))}
141+
</div>
142+
);
143+
};
144+
145+
// 4. List View (Simple)
146+
const ListView = ({ schema, data }: any) => {
147+
// If bind is used, the renderer might pass resolved data, but here we assume rudimentary binding access
148+
// In a real scenario, useDataScope or similar hook would be used.
149+
// For this simple registry, let's assume the parent resolved it or we just render props.
150+
// Actually, bind logic is handled by the SchemaRenderer usually?
151+
// If this component supports binding, it should receive `data` if the parent handles it
152+
// OR it should use `useDataScope`.
153+
154+
// Simplification: We will just render what is passed in props for now or handle simple binding manualy if needed.
155+
// But wait, the schema used `bind: "opportunities"`.
156+
// The SchemaRenderer logic should resolve this if wrapped correctly?
157+
// Let's assume standard prop passing for now.
158+
159+
// For the bound data, we need to access `schema.data` if the engine injects it.
160+
// Or we use a hook.
161+
162+
// NOTE: Since I cannot easily import `useDataScope` without checking where it is exported from (likely @object-ui/react),
163+
// I will try to import it.
164+
165+
return (
166+
<div className="space-y-4">
167+
{(schema.data || []).map((item: any, i: number) => (
168+
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
169+
<div>
170+
<div className="font-medium">{item.name || item.title || "Unknown"}</div>
171+
<div className="text-sm text-gray-500">{item.description}</div>
172+
</div>
173+
<div className="text-sm font-bold">{item.amount || item.status}</div>
174+
</div>
175+
))}
176+
</div>
177+
);
178+
}
179+
180+
export function registerCustomWidgets() {
181+
registerComponent("widget:metric", MetricWidget);
182+
registerComponent("widget:chart", ChartWidget);
183+
registerComponent("view:timeline", TimelineView);
184+
// Note: view:list might already exist or handled by plugin-view.
185+
// If I want to override or add it:
186+
registerComponent("view:list", ListView);
187+
}

0 commit comments

Comments
 (0)