Skip to content

Commit 998c5b3

Browse files
authored
chore(dev): scenarios verification harness; drop redundant react-page PoC (#2107)
Adds dev/sdui-scenarios-preview — renders the 3 react business-scenario showcase pages (inquiry-triage / account-cockpit / invoice-console) through the real PageRenderer against an in-memory multi-object adapter (schema + data + $filter + CRUD), with a scenario switcher. Browser-verified: tab-filtered counts, live search, master-detail edit, cross-object roll-ups, status actions, KPI re-query. Removes the now-redundant raw react-page-preview PoC. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
1 parent d23d6eb commit 998c5b3

5 files changed

Lines changed: 389 additions & 47 deletions

File tree

apps/console/dev/react-page-preview.html

Lines changed: 0 additions & 2 deletions
This file was deleted.

apps/console/dev/react-page-preview.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/* Auto-extracted from examples/app-showcase react scenario pages — verification harness only. */
2+
3+
export const triageSource = `
4+
function Page() {
5+
const adapter = useAdapter();
6+
const [tab, setTab] = React.useState('all');
7+
const [sel, setSel] = React.useState(null);
8+
const [reload, setReload] = React.useState(0);
9+
const [counts, setCounts] = React.useState({ all: 0, new: 0, contacted: 0, closed: 0 });
10+
11+
const refresh = React.useCallback(async () => {
12+
if (!adapter) return;
13+
const res = await adapter.find('showcase_inquiry', { top: 500 });
14+
const rows = Array.isArray(res) ? res : (res && res.records) || [];
15+
setCounts({
16+
all: rows.length,
17+
new: rows.filter((r) => r.status === 'new').length,
18+
contacted: rows.filter((r) => r.status === 'contacted').length,
19+
closed: rows.filter((r) => r.status === 'closed').length,
20+
});
21+
}, [adapter]);
22+
React.useEffect(() => { refresh(); }, [refresh, reload]);
23+
24+
const setStatus = async (status) => {
25+
if (!adapter || !sel) return;
26+
await adapter.update('showcase_inquiry', sel.id, { status });
27+
setSel(null);
28+
setReload((k) => k + 1);
29+
};
30+
31+
const TABS = [['all', 'All'], ['new', 'New'], ['contacted', 'Contacted'], ['closed', 'Closed']];
32+
const filters = tab === 'all' ? undefined : ['status', '=', tab];
33+
const STATUS_COLOR = { new: 'bg-blue-100 text-blue-700', contacted: 'bg-amber-100 text-amber-700', closed: 'bg-emerald-100 text-emerald-700' };
34+
35+
return (
36+
<div className="mx-auto max-w-6xl space-y-5 p-8">
37+
<header>
38+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Inquiry Triage</h1>
39+
<p className="mt-1 text-sm text-slate-500">A support queue over <code>showcase_inquiry</code> — tabs filter a real <code>&lt;ListView&gt;</code>; one click moves an inquiry's status.</p>
40+
</header>
41+
42+
<div className="flex flex-wrap gap-2">
43+
{TABS.map(([k, label]) => (
44+
<button key={k} onClick={() => { setTab(k); setSel(null); }}
45+
className={'flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-semibold ' + (tab === k ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200')}>
46+
{label}
47+
<span className={'rounded-full px-2 py-0.5 text-xs ' + (tab === k ? 'bg-white/20' : 'bg-white text-slate-500')}>{counts[k]}</span>
48+
</button>
49+
))}
50+
</div>
51+
52+
<div className="grid grid-cols-5 gap-6">
53+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
54+
<ListView key={tab + ':' + reload} objectName="showcase_inquiry"
55+
fields={['name', 'company', 'email', 'status']} filters={filters}
56+
navigation={{ mode: 'none' }} onRowClick={(r) => setSel(r)} />
57+
</section>
58+
<section className="col-span-2 rounded-xl border border-slate-200 bg-white p-5">
59+
{sel ? (
60+
<div className="space-y-4">
61+
<div>
62+
<div className="flex items-center justify-between">
63+
<h2 className="text-lg font-semibold text-slate-900">{sel.name}</h2>
64+
<span className={'rounded-full px-2.5 py-0.5 text-xs font-semibold ' + (STATUS_COLOR[sel.status] || 'bg-slate-100 text-slate-600')}>{sel.status}</span>
65+
</div>
66+
<p className="text-sm text-slate-500">{sel.company} · {sel.email}</p>
67+
</div>
68+
<p className="rounded-lg bg-slate-50 p-3 text-sm leading-relaxed text-slate-700">{sel.message || 'No message.'}</p>
69+
<div className="flex gap-2">
70+
<button onClick={() => setStatus('contacted')} disabled={sel.status === 'contacted'}
71+
className="rounded-lg bg-amber-500 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-600 disabled:opacity-40">Mark Contacted</button>
72+
<button onClick={() => setStatus('closed')} disabled={sel.status === 'closed'}
73+
className="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-40">Close</button>
74+
</div>
75+
</div>
76+
) : (
77+
<div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400">
78+
<div className="text-4xl">📥</div>
79+
<p className="mt-2 text-sm">Select an inquiry to triage.</p>
80+
</div>
81+
)}
82+
</section>
83+
</div>
84+
</div>
85+
);
86+
}`;
87+
88+
export const cockpitSource = `
89+
function Page() {
90+
const adapter = useAdapter();
91+
const [q, setQ] = React.useState('');
92+
const [sel, setSel] = React.useState(null);
93+
const [reload, setReload] = React.useState(0);
94+
const [related, setRelated] = React.useState({ projects: 0, invoices: 0, openInvoices: 0 });
95+
96+
React.useEffect(() => {
97+
let alive = true;
98+
(async () => {
99+
if (!adapter || !sel) { setRelated({ projects: 0, invoices: 0, openInvoices: 0 }); return; }
100+
const pr = await adapter.find('showcase_project', { $filter: ['account', '=', sel.id], top: 500 });
101+
const iv = await adapter.find('showcase_invoice', { $filter: ['account', '=', sel.id], top: 500 });
102+
const projects = Array.isArray(pr) ? pr : (pr && pr.records) || [];
103+
const invoices = Array.isArray(iv) ? iv : (iv && iv.records) || [];
104+
if (alive) setRelated({ projects: projects.length, invoices: invoices.length, openInvoices: invoices.filter((r) => r.status !== 'paid' && r.status !== 'void').length });
105+
})();
106+
return () => { alive = false; };
107+
}, [adapter, sel, reload]);
108+
109+
const filters = q.trim() ? ['name', 'contains', q.trim()] : undefined;
110+
const Stat = ({ label, value, accent }) => (
111+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3">
112+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">{label}</div>
113+
<div className={'mt-1 text-2xl font-bold ' + (accent || 'text-slate-900')}>{value}</div>
114+
</div>
115+
);
116+
117+
return (
118+
<div className="mx-auto max-w-6xl space-y-5 p-8">
119+
<header className="flex items-end justify-between gap-4">
120+
<div>
121+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Account Cockpit</h1>
122+
<p className="mt-1 text-sm text-slate-500">Customer-360 over <code>showcase_account</code> — search, edit, and roll up related projects &amp; invoices.</p>
123+
</div>
124+
<input value={q} onChange={(e) => { setQ(e.target.value); setSel(null); }}
125+
placeholder="Search accounts…"
126+
className="w-64 rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none" />
127+
</header>
128+
129+
<div className="grid grid-cols-5 gap-6">
130+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
131+
<ListView key={q + ':' + reload} objectName="showcase_account"
132+
fields={['name', 'industry', 'status', 'annual_revenue']} filters={filters}
133+
navigation={{ mode: 'none' }} onRowClick={(r) => setSel(r)} />
134+
</section>
135+
<section className="col-span-2 space-y-4">
136+
{sel ? (
137+
<React.Fragment>
138+
<div className="grid grid-cols-3 gap-3">
139+
<Stat label="Projects" value={related.projects} />
140+
<Stat label="Invoices" value={related.invoices} />
141+
<Stat label="Open AR" value={related.openInvoices} accent="text-amber-600" />
142+
</div>
143+
<div className="rounded-xl border border-slate-200 bg-white p-5">
144+
<ObjectForm key={sel.id + ':' + reload} objectName="showcase_account" mode="edit"
145+
recordId={sel.id} onSuccess={() => { setSel(null); setReload((k) => k + 1); }}
146+
onCancel={() => setSel(null)} />
147+
</div>
148+
</React.Fragment>
149+
) : (
150+
<div className="flex h-full min-h-[240px] flex-col items-center justify-center rounded-xl border border-slate-200 bg-white text-center text-slate-400">
151+
<div className="text-4xl">🛰️</div>
152+
<p className="mt-2 text-sm">Search and select an account.</p>
153+
</div>
154+
)}
155+
</section>
156+
</div>
157+
</div>
158+
);
159+
}`;
160+
161+
export const invoiceSource = `
162+
function Page() {
163+
const adapter = useAdapter();
164+
const [status, setStatus] = React.useState('all');
165+
const [sel, setSel] = React.useState(null);
166+
const [mode, setMode] = React.useState('edit');
167+
const [reload, setReload] = React.useState(0);
168+
const [kpi, setKpi] = React.useState({ count: 0, draft: 0, sent: 0, paid: 0 });
169+
170+
React.useEffect(() => {
171+
let alive = true;
172+
(async () => {
173+
if (!adapter) return;
174+
const res = await adapter.find('showcase_invoice', { top: 500 });
175+
const rows = Array.isArray(res) ? res : (res && res.records) || [];
176+
if (alive) setKpi({
177+
count: rows.length,
178+
draft: rows.filter((r) => r.status === 'draft').length,
179+
sent: rows.filter((r) => r.status === 'sent').length,
180+
paid: rows.filter((r) => r.status === 'paid').length,
181+
});
182+
})();
183+
return () => { alive = false; };
184+
}, [adapter, reload]);
185+
186+
const afterWrite = () => { setSel(null); setMode('edit'); setReload((k) => k + 1); };
187+
const markPaid = async () => { if (!adapter || !sel) return; await adapter.update('showcase_invoice', sel.id, { status: 'paid' }); afterWrite(); };
188+
const openNew = () => { setSel(null); setMode('create'); };
189+
190+
const FILTERS = [['all', 'All'], ['draft', 'Draft'], ['sent', 'Sent'], ['paid', 'Paid'], ['void', 'Void']];
191+
const filters = status === 'all' ? undefined : ['status', '=', status];
192+
const editing = mode === 'create' || sel;
193+
const Kpi = ({ label, value, accent }) => (
194+
<div className="rounded-xl border border-slate-200 bg-white p-4">
195+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">{label}</div>
196+
<div className={'mt-1 text-3xl font-bold ' + (accent || 'text-slate-900')}>{value}</div>
197+
</div>
198+
);
199+
200+
return (
201+
<div className="mx-auto max-w-6xl space-y-5 p-8">
202+
<header className="flex items-center justify-between">
203+
<div>
204+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Invoice Console</h1>
205+
<p className="mt-1 text-sm text-slate-500">Accounts receivable over <code>showcase_invoice</code> — aggregate, filter, edit, and collect.</p>
206+
</div>
207+
<button onClick={openNew} className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500">+ New invoice</button>
208+
</header>
209+
210+
<div className="grid grid-cols-4 gap-4">
211+
<Kpi label="Total" value={kpi.count} />
212+
<Kpi label="Draft" value={kpi.draft} accent="text-slate-500" />
213+
<Kpi label="Sent" value={kpi.sent} accent="text-blue-600" />
214+
<Kpi label="Paid" value={kpi.paid} accent="text-emerald-600" />
215+
</div>
216+
217+
<div className="flex flex-wrap gap-2">
218+
{FILTERS.map(([k, label]) => (
219+
<button key={k} onClick={() => setStatus(k)}
220+
className={'rounded-full px-3.5 py-1 text-sm font-semibold ' + (status === k ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200')}>{label}</button>
221+
))}
222+
</div>
223+
224+
<div className="grid grid-cols-5 gap-6">
225+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
226+
<ListView key={status + ':' + reload} objectName="showcase_invoice"
227+
fields={['name', 'account', 'status', 'total']} filters={filters}
228+
navigation={{ mode: 'none' }} onRowClick={(r) => { setSel(r); setMode('edit'); }} />
229+
</section>
230+
<section className="col-span-2 space-y-3 rounded-xl border border-slate-200 bg-white p-5">
231+
{editing ? (
232+
<React.Fragment>
233+
<ObjectForm key={(mode === 'create' ? 'new' : sel && sel.id) + ':' + reload}
234+
objectName="showcase_invoice" mode={mode}
235+
recordId={mode === 'edit' && sel ? sel.id : undefined}
236+
onSuccess={afterWrite} onCancel={() => setSel(null)} />
237+
{mode === 'edit' && sel && sel.status !== 'paid' ? (
238+
<button onClick={markPaid} className="w-full rounded-lg border border-emerald-600 px-4 py-2 text-sm font-semibold text-emerald-700 hover:bg-emerald-50">✓ Mark Paid</button>
239+
) : null}
240+
</React.Fragment>
241+
) : (
242+
<div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400">
243+
<div className="text-4xl">🧾</div>
244+
<p className="mt-2 text-sm">Select an invoice, or create one.</p>
245+
</div>
246+
)}
247+
</section>
248+
</div>
249+
</div>
250+
);
251+
}`;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />
4+
<title>SDUI react business scenarios (real ListView + ObjectForm)</title></head>
5+
<body><div id="root"></div><script type="module" src="./sdui-scenarios-preview.tsx"></script></body>
6+
</html>

0 commit comments

Comments
 (0)