Skip to content

Commit 63d04d0

Browse files
os-zhuangclaude
andcommitted
feat(examples): 3 kind:'react' business-scenario showcase pages (ADR-0081)
Realistic customer scenarios that compose the real ListView + ObjectForm + charts with React state — the platform's flexible-page capability shown end to end: - Inquiry Triage: status tabs with live counts filter a real ListView; row -> detail with one-click status actions (Mark Contacted / Close) that persist + refresh. - Account Cockpit: live-search account list + ObjectForm edit + a cross-object related strip (projects / invoices / open AR) via useAdapter roll-ups. - Invoice Console: status-aggregation KPIs + segmented status filter + create/edit + a Mark Paid quick action. All browser-verified (objectui dev/sdui-scenarios-preview, real plugins + mock adapter). os validate passes the new react syntax gate. 28 pages total. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cc1b388 commit 63d04d0

6 files changed

Lines changed: 313 additions & 2 deletions

File tree

examples/app-showcase/objectstack.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js';
2222
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js';
2323
import { allReports } from './src/reports/index.js';
2424
import { allActions } from './src/actions/index.js';
25-
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, PageVariablesPage, ContactFormPage } from './src/pages/index.js';
25+
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, PageVariablesPage, ContactFormPage } from './src/pages/index.js';
2626
import { allFlows } from './src/flows/index.js';
2727
import { allWebhooks } from './src/webhooks/index.js';
2828
import { allHooks } from './src/hooks/index.js';
@@ -154,7 +154,7 @@ export default defineStack({
154154
apps: [ShowcaseApp],
155155
portals: allPortals,
156156
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
157-
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, PageVariablesPage, ContactFormPage],
157+
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, PageVariablesPage, ContactFormPage],
158158
dashboards: [ChartGalleryDashboard, OpsDashboard],
159159
books: allBooks,
160160
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],

examples/app-showcase/src/apps/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export const ShowcaseApp = App.create({
6767
{ id: 'nav_gallery', type: 'page', pageName: 'showcase_component_gallery', label: 'Component Gallery', icon: 'layout-template' },
6868
{ id: 'nav_command_center_jsx', type: 'page', pageName: 'showcase_command_center_jsx', label: 'Command Center (HTML)', icon: 'code' },
6969
{ id: 'nav_crm_workbench', type: 'page', pageName: 'showcase_crm_workbench', label: 'CRM Workbench (React)', icon: 'layout-dashboard' },
70+
{ id: 'nav_inquiry_triage', type: 'page', pageName: 'showcase_inquiry_triage', label: 'Inquiry Triage (React)', icon: 'inbox' },
71+
{ id: 'nav_account_cockpit', type: 'page', pageName: 'showcase_account_cockpit', label: 'Account Cockpit (React)', icon: 'satellite' },
72+
{ id: 'nav_invoice_console', type: 'page', pageName: 'showcase_invoice_console', label: 'Invoice Console (React)', icon: 'receipt' },
7073
{ id: 'nav_styling_gallery', type: 'page', pageName: 'showcase_styling_gallery', label: 'Styling (ADR-0065)', icon: 'palette' },
7174
{ id: 'nav_page_variables', type: 'page', pageName: 'showcase_page_variables', label: 'Page Variables', icon: 'mouse-pointer-click' },
7275
{ id: 'nav_contact_form', type: 'page', pageName: 'showcase_contact_form', label: 'Contact Form', icon: 'mail-plus' },
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Account Cockpit — a `kind:'react'` business scenario (ADR-0081).
7+
*
8+
* A customer-360 cockpit: a live-search account list (React text input drives
9+
* the real `<ListView>`'s `filters`), a selected account edited in a real
10+
* `<ObjectForm>`, and a related-data strip that aggregates the account's
11+
* projects and invoices via cross-object `useAdapter` queries. Demonstrates
12+
* live search, master-detail edit, and cross-object roll-ups together.
13+
*/
14+
export const AccountCockpitPage = definePage({
15+
name: 'showcase_account_cockpit',
16+
label: 'Account Cockpit (React)',
17+
type: 'home',
18+
kind: 'react',
19+
source: `
20+
function Page() {
21+
const adapter = useAdapter();
22+
const [q, setQ] = React.useState('');
23+
const [sel, setSel] = React.useState(null);
24+
const [reload, setReload] = React.useState(0);
25+
const [related, setRelated] = React.useState({ projects: 0, invoices: 0, openInvoices: 0 });
26+
27+
React.useEffect(() => {
28+
let alive = true;
29+
(async () => {
30+
if (!adapter || !sel) { setRelated({ projects: 0, invoices: 0, openInvoices: 0 }); return; }
31+
const pr = await adapter.find('showcase_project', { $filter: ['account', '=', sel.id], top: 500 });
32+
const iv = await adapter.find('showcase_invoice', { $filter: ['account', '=', sel.id], top: 500 });
33+
const projects = Array.isArray(pr) ? pr : (pr && pr.records) || [];
34+
const invoices = Array.isArray(iv) ? iv : (iv && iv.records) || [];
35+
if (alive) setRelated({ projects: projects.length, invoices: invoices.length, openInvoices: invoices.filter((r) => r.status !== 'paid' && r.status !== 'void').length });
36+
})();
37+
return () => { alive = false; };
38+
}, [adapter, sel, reload]);
39+
40+
const filters = q.trim() ? ['name', 'contains', q.trim()] : undefined;
41+
const Stat = ({ label, value, accent }) => (
42+
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3">
43+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">{label}</div>
44+
<div className={'mt-1 text-2xl font-bold ' + (accent || 'text-slate-900')}>{value}</div>
45+
</div>
46+
);
47+
48+
return (
49+
<div className="mx-auto max-w-6xl space-y-5 p-8">
50+
<header className="flex items-end justify-between gap-4">
51+
<div>
52+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Account Cockpit</h1>
53+
<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>
54+
</div>
55+
<input value={q} onChange={(e) => { setQ(e.target.value); setSel(null); }}
56+
placeholder="Search accounts…"
57+
className="w-64 rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none" />
58+
</header>
59+
60+
<div className="grid grid-cols-5 gap-6">
61+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
62+
<ListView key={q + ':' + reload} objectName="showcase_account"
63+
fields={['name', 'industry', 'status', 'annual_revenue']} filters={filters}
64+
navigation={{ mode: 'none' }} onRowClick={(r) => setSel(r)} />
65+
</section>
66+
<section className="col-span-2 space-y-4">
67+
{sel ? (
68+
<React.Fragment>
69+
<div className="grid grid-cols-3 gap-3">
70+
<Stat label="Projects" value={related.projects} />
71+
<Stat label="Invoices" value={related.invoices} />
72+
<Stat label="Open AR" value={related.openInvoices} accent="text-amber-600" />
73+
</div>
74+
<div className="rounded-xl border border-slate-200 bg-white p-5">
75+
<ObjectForm key={sel.id + ':' + reload} objectName="showcase_account" mode="edit"
76+
recordId={sel.id} onSuccess={() => { setSel(null); setReload((k) => k + 1); }}
77+
onCancel={() => setSel(null)} />
78+
</div>
79+
</React.Fragment>
80+
) : (
81+
<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">
82+
<div className="text-4xl">🛰️</div>
83+
<p className="mt-2 text-sm">Search and select an account.</p>
84+
</div>
85+
)}
86+
</section>
87+
</div>
88+
</div>
89+
);
90+
}`,
91+
});

examples/app-showcase/src/pages/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export { StylingGalleryPage } from './styling-gallery.page.js';
1717
export { CommandCenterPage } from './command-center.page.js';
1818
export { CommandCenterJsxPage } from './command-center-jsx.page.js';
1919
export { CrmWorkbenchPage } from './crm-workbench.page.js';
20+
export { InquiryTriagePage } from './inquiry-triage.page.js';
21+
export { AccountCockpitPage } from './account-cockpit.page.js';
22+
export { InvoiceConsolePage } from './invoice-console.page.js';
2023
export { PageVariablesPage } from './page-variables.page.js';
2124
export { ContactFormPage } from './contact-form.page.js';
2225
export {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Inquiry Triage Inbox — a `kind:'react'` business scenario (ADR-0081).
7+
*
8+
* Models the classic support/lead-triage queue every CRM needs: status TABS
9+
* with live counts, a real `<ListView>` filtered by the active tab, and a detail
10+
* panel with one-click status actions ("Mark Contacted" / "Close") that persist
11+
* via `useAdapter().update` and refresh the list + counts. Demonstrates
12+
* React-state-driven server filtering, cross-row aggregation, and a status
13+
* workflow — none of which the fixed page schema can express.
14+
*/
15+
export const InquiryTriagePage = definePage({
16+
name: 'showcase_inquiry_triage',
17+
label: 'Inquiry Triage (React)',
18+
type: 'home',
19+
kind: 'react',
20+
source: `
21+
function Page() {
22+
const adapter = useAdapter();
23+
const [tab, setTab] = React.useState('all');
24+
const [sel, setSel] = React.useState(null);
25+
const [reload, setReload] = React.useState(0);
26+
const [counts, setCounts] = React.useState({ all: 0, new: 0, contacted: 0, closed: 0 });
27+
28+
const refresh = React.useCallback(async () => {
29+
if (!adapter) return;
30+
const res = await adapter.find('showcase_inquiry', { top: 500 });
31+
const rows = Array.isArray(res) ? res : (res && res.records) || [];
32+
setCounts({
33+
all: rows.length,
34+
new: rows.filter((r) => r.status === 'new').length,
35+
contacted: rows.filter((r) => r.status === 'contacted').length,
36+
closed: rows.filter((r) => r.status === 'closed').length,
37+
});
38+
}, [adapter]);
39+
React.useEffect(() => { refresh(); }, [refresh, reload]);
40+
41+
const setStatus = async (status) => {
42+
if (!adapter || !sel) return;
43+
await adapter.update('showcase_inquiry', sel.id, { status });
44+
setSel(null);
45+
setReload((k) => k + 1);
46+
};
47+
48+
const TABS = [['all', 'All'], ['new', 'New'], ['contacted', 'Contacted'], ['closed', 'Closed']];
49+
const filters = tab === 'all' ? undefined : ['status', '=', tab];
50+
const STATUS_COLOR = { new: 'bg-blue-100 text-blue-700', contacted: 'bg-amber-100 text-amber-700', closed: 'bg-emerald-100 text-emerald-700' };
51+
52+
return (
53+
<div className="mx-auto max-w-6xl space-y-5 p-8">
54+
<header>
55+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Inquiry Triage</h1>
56+
<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>
57+
</header>
58+
59+
<div className="flex flex-wrap gap-2">
60+
{TABS.map(([k, label]) => (
61+
<button key={k} onClick={() => { setTab(k); setSel(null); }}
62+
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')}>
63+
{label}
64+
<span className={'rounded-full px-2 py-0.5 text-xs ' + (tab === k ? 'bg-white/20' : 'bg-white text-slate-500')}>{counts[k]}</span>
65+
</button>
66+
))}
67+
</div>
68+
69+
<div className="grid grid-cols-5 gap-6">
70+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
71+
<ListView key={tab + ':' + reload} objectName="showcase_inquiry"
72+
fields={['name', 'company', 'email', 'status']} filters={filters}
73+
navigation={{ mode: 'none' }} onRowClick={(r) => setSel(r)} />
74+
</section>
75+
<section className="col-span-2 rounded-xl border border-slate-200 bg-white p-5">
76+
{sel ? (
77+
<div className="space-y-4">
78+
<div>
79+
<div className="flex items-center justify-between">
80+
<h2 className="text-lg font-semibold text-slate-900">{sel.name}</h2>
81+
<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>
82+
</div>
83+
<p className="text-sm text-slate-500">{sel.company} · {sel.email}</p>
84+
</div>
85+
<p className="rounded-lg bg-slate-50 p-3 text-sm leading-relaxed text-slate-700">{sel.message || 'No message.'}</p>
86+
<div className="flex gap-2">
87+
<button onClick={() => setStatus('contacted')} disabled={sel.status === 'contacted'}
88+
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>
89+
<button onClick={() => setStatus('closed')} disabled={sel.status === 'closed'}
90+
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>
91+
</div>
92+
</div>
93+
) : (
94+
<div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400">
95+
<div className="text-4xl">📥</div>
96+
<p className="mt-2 text-sm">Select an inquiry to triage.</p>
97+
</div>
98+
)}
99+
</section>
100+
</div>
101+
</div>
102+
);
103+
}`,
104+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Invoice Console — a `kind:'react'` business scenario (ADR-0081).
7+
*
8+
* Accounts-receivable management: a KPI strip aggregating invoices by status
9+
* (useAdapter), a status segmented-filter driving a real `<ListView>`, a real
10+
* `<ObjectForm>` for create + edit, and a "Mark Paid" quick action on the
11+
* selected invoice. Demonstrates aggregation KPIs, segmented filtering, full
12+
* CRUD, and a one-click status transition in one screen.
13+
*/
14+
export const InvoiceConsolePage = definePage({
15+
name: 'showcase_invoice_console',
16+
label: 'Invoice Console (React)',
17+
type: 'home',
18+
kind: 'react',
19+
source: `
20+
function Page() {
21+
const adapter = useAdapter();
22+
const [status, setStatus] = React.useState('all');
23+
const [sel, setSel] = React.useState(null);
24+
const [mode, setMode] = React.useState('edit');
25+
const [reload, setReload] = React.useState(0);
26+
const [kpi, setKpi] = React.useState({ count: 0, draft: 0, sent: 0, paid: 0 });
27+
28+
React.useEffect(() => {
29+
let alive = true;
30+
(async () => {
31+
if (!adapter) return;
32+
const res = await adapter.find('showcase_invoice', { top: 500 });
33+
const rows = Array.isArray(res) ? res : (res && res.records) || [];
34+
if (alive) setKpi({
35+
count: rows.length,
36+
draft: rows.filter((r) => r.status === 'draft').length,
37+
sent: rows.filter((r) => r.status === 'sent').length,
38+
paid: rows.filter((r) => r.status === 'paid').length,
39+
});
40+
})();
41+
return () => { alive = false; };
42+
}, [adapter, reload]);
43+
44+
const afterWrite = () => { setSel(null); setMode('edit'); setReload((k) => k + 1); };
45+
const markPaid = async () => { if (!adapter || !sel) return; await adapter.update('showcase_invoice', sel.id, { status: 'paid' }); afterWrite(); };
46+
const openNew = () => { setSel(null); setMode('create'); };
47+
48+
const FILTERS = [['all', 'All'], ['draft', 'Draft'], ['sent', 'Sent'], ['paid', 'Paid'], ['void', 'Void']];
49+
const filters = status === 'all' ? undefined : ['status', '=', status];
50+
const editing = mode === 'create' || sel;
51+
const Kpi = ({ label, value, accent }) => (
52+
<div className="rounded-xl border border-slate-200 bg-white p-4">
53+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">{label}</div>
54+
<div className={'mt-1 text-3xl font-bold ' + (accent || 'text-slate-900')}>{value}</div>
55+
</div>
56+
);
57+
58+
return (
59+
<div className="mx-auto max-w-6xl space-y-5 p-8">
60+
<header className="flex items-center justify-between">
61+
<div>
62+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Invoice Console</h1>
63+
<p className="mt-1 text-sm text-slate-500">Accounts receivable over <code>showcase_invoice</code> — aggregate, filter, edit, and collect.</p>
64+
</div>
65+
<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>
66+
</header>
67+
68+
<div className="grid grid-cols-4 gap-4">
69+
<Kpi label="Total" value={kpi.count} />
70+
<Kpi label="Draft" value={kpi.draft} accent="text-slate-500" />
71+
<Kpi label="Sent" value={kpi.sent} accent="text-blue-600" />
72+
<Kpi label="Paid" value={kpi.paid} accent="text-emerald-600" />
73+
</div>
74+
75+
<div className="flex flex-wrap gap-2">
76+
{FILTERS.map(([k, label]) => (
77+
<button key={k} onClick={() => setStatus(k)}
78+
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>
79+
))}
80+
</div>
81+
82+
<div className="grid grid-cols-5 gap-6">
83+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
84+
<ListView key={status + ':' + reload} objectName="showcase_invoice"
85+
fields={['name', 'account', 'status', 'total']} filters={filters}
86+
navigation={{ mode: 'none' }} onRowClick={(r) => { setSel(r); setMode('edit'); }} />
87+
</section>
88+
<section className="col-span-2 space-y-3 rounded-xl border border-slate-200 bg-white p-5">
89+
{editing ? (
90+
<React.Fragment>
91+
<ObjectForm key={(mode === 'create' ? 'new' : sel && sel.id) + ':' + reload}
92+
objectName="showcase_invoice" mode={mode}
93+
recordId={mode === 'edit' && sel ? sel.id : undefined}
94+
onSuccess={afterWrite} onCancel={() => setSel(null)} />
95+
{mode === 'edit' && sel && sel.status !== 'paid' ? (
96+
<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>
97+
) : null}
98+
</React.Fragment>
99+
) : (
100+
<div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400">
101+
<div className="text-4xl">🧾</div>
102+
<p className="mt-2 text-sm">Select an invoice, or create one.</p>
103+
</div>
104+
)}
105+
</section>
106+
</div>
107+
</div>
108+
);
109+
}`,
110+
});

0 commit comments

Comments
 (0)