From 63d04d0a03ffff3350c8d0b61f3c1ec9c2a3a499 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:05:48 +0800 Subject: [PATCH] feat(examples): 3 kind:'react' business-scenario showcase pages (ADR-0081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/app-showcase/objectstack.config.ts | 4 +- examples/app-showcase/src/apps/index.ts | 3 + .../src/pages/account-cockpit.page.ts | 91 +++++++++++++++ examples/app-showcase/src/pages/index.ts | 3 + .../src/pages/inquiry-triage.page.ts | 104 +++++++++++++++++ .../src/pages/invoice-console.page.ts | 110 ++++++++++++++++++ 6 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 examples/app-showcase/src/pages/account-cockpit.page.ts create mode 100644 examples/app-showcase/src/pages/inquiry-triage.page.ts create mode 100644 examples/app-showcase/src/pages/invoice-console.page.ts diff --git a/examples/app-showcase/objectstack.config.ts b/examples/app-showcase/objectstack.config.ts index b67dcab765..6a6e139efe 100644 --- a/examples/app-showcase/objectstack.config.ts +++ b/examples/app-showcase/objectstack.config.ts @@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js'; import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js'; import { allReports } from './src/reports/index.js'; import { allActions } from './src/actions/index.js'; -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'; +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'; import { allFlows } from './src/flows/index.js'; import { allWebhooks } from './src/webhooks/index.js'; import { allHooks } from './src/hooks/index.js'; @@ -154,7 +154,7 @@ export default defineStack({ apps: [ShowcaseApp], portals: allPortals, views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews], - 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], + 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], dashboards: [ChartGalleryDashboard, OpsDashboard], books: allBooks, datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset], diff --git a/examples/app-showcase/src/apps/index.ts b/examples/app-showcase/src/apps/index.ts index 38bc657768..91aadc65cc 100644 --- a/examples/app-showcase/src/apps/index.ts +++ b/examples/app-showcase/src/apps/index.ts @@ -67,6 +67,9 @@ export const ShowcaseApp = App.create({ { id: 'nav_gallery', type: 'page', pageName: 'showcase_component_gallery', label: 'Component Gallery', icon: 'layout-template' }, { id: 'nav_command_center_jsx', type: 'page', pageName: 'showcase_command_center_jsx', label: 'Command Center (HTML)', icon: 'code' }, { id: 'nav_crm_workbench', type: 'page', pageName: 'showcase_crm_workbench', label: 'CRM Workbench (React)', icon: 'layout-dashboard' }, + { id: 'nav_inquiry_triage', type: 'page', pageName: 'showcase_inquiry_triage', label: 'Inquiry Triage (React)', icon: 'inbox' }, + { id: 'nav_account_cockpit', type: 'page', pageName: 'showcase_account_cockpit', label: 'Account Cockpit (React)', icon: 'satellite' }, + { id: 'nav_invoice_console', type: 'page', pageName: 'showcase_invoice_console', label: 'Invoice Console (React)', icon: 'receipt' }, { id: 'nav_styling_gallery', type: 'page', pageName: 'showcase_styling_gallery', label: 'Styling (ADR-0065)', icon: 'palette' }, { id: 'nav_page_variables', type: 'page', pageName: 'showcase_page_variables', label: 'Page Variables', icon: 'mouse-pointer-click' }, { id: 'nav_contact_form', type: 'page', pageName: 'showcase_contact_form', label: 'Contact Form', icon: 'mail-plus' }, diff --git a/examples/app-showcase/src/pages/account-cockpit.page.ts b/examples/app-showcase/src/pages/account-cockpit.page.ts new file mode 100644 index 0000000000..523b4432cd --- /dev/null +++ b/examples/app-showcase/src/pages/account-cockpit.page.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { definePage } from '@objectstack/spec/ui'; + +/** + * Account Cockpit — a `kind:'react'` business scenario (ADR-0081). + * + * A customer-360 cockpit: a live-search account list (React text input drives + * the real ``'s `filters`), a selected account edited in a real + * ``, and a related-data strip that aggregates the account's + * projects and invoices via cross-object `useAdapter` queries. Demonstrates + * live search, master-detail edit, and cross-object roll-ups together. + */ +export const AccountCockpitPage = definePage({ + name: 'showcase_account_cockpit', + label: 'Account Cockpit (React)', + type: 'home', + kind: 'react', + source: ` +function Page() { + const adapter = useAdapter(); + const [q, setQ] = React.useState(''); + const [sel, setSel] = React.useState(null); + const [reload, setReload] = React.useState(0); + const [related, setRelated] = React.useState({ projects: 0, invoices: 0, openInvoices: 0 }); + + React.useEffect(() => { + let alive = true; + (async () => { + if (!adapter || !sel) { setRelated({ projects: 0, invoices: 0, openInvoices: 0 }); return; } + const pr = await adapter.find('showcase_project', { $filter: ['account', '=', sel.id], top: 500 }); + const iv = await adapter.find('showcase_invoice', { $filter: ['account', '=', sel.id], top: 500 }); + const projects = Array.isArray(pr) ? pr : (pr && pr.records) || []; + const invoices = Array.isArray(iv) ? iv : (iv && iv.records) || []; + if (alive) setRelated({ projects: projects.length, invoices: invoices.length, openInvoices: invoices.filter((r) => r.status !== 'paid' && r.status !== 'void').length }); + })(); + return () => { alive = false; }; + }, [adapter, sel, reload]); + + const filters = q.trim() ? ['name', 'contains', q.trim()] : undefined; + const Stat = ({ label, value, accent }) => ( +
+
{label}
+
{value}
+
+ ); + + return ( +
+
+
+

Account Cockpit

+

Customer-360 over showcase_account — search, edit, and roll up related projects & invoices.

+
+ { setQ(e.target.value); setSel(null); }} + placeholder="Search accounts…" + className="w-64 rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none" /> +
+ +
+
+ setSel(r)} /> +
+
+ {sel ? ( + +
+ + + +
+
+ { setSel(null); setReload((k) => k + 1); }} + onCancel={() => setSel(null)} /> +
+
+ ) : ( +
+
🛰️
+

Search and select an account.

+
+ )} +
+
+
+ ); +}`, +}); diff --git a/examples/app-showcase/src/pages/index.ts b/examples/app-showcase/src/pages/index.ts index 83be3b380c..ec48f39cd4 100644 --- a/examples/app-showcase/src/pages/index.ts +++ b/examples/app-showcase/src/pages/index.ts @@ -17,6 +17,9 @@ export { StylingGalleryPage } from './styling-gallery.page.js'; export { CommandCenterPage } from './command-center.page.js'; export { CommandCenterJsxPage } from './command-center-jsx.page.js'; export { CrmWorkbenchPage } from './crm-workbench.page.js'; +export { InquiryTriagePage } from './inquiry-triage.page.js'; +export { AccountCockpitPage } from './account-cockpit.page.js'; +export { InvoiceConsolePage } from './invoice-console.page.js'; export { PageVariablesPage } from './page-variables.page.js'; export { ContactFormPage } from './contact-form.page.js'; export { diff --git a/examples/app-showcase/src/pages/inquiry-triage.page.ts b/examples/app-showcase/src/pages/inquiry-triage.page.ts new file mode 100644 index 0000000000..315be1bbcc --- /dev/null +++ b/examples/app-showcase/src/pages/inquiry-triage.page.ts @@ -0,0 +1,104 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { definePage } from '@objectstack/spec/ui'; + +/** + * Inquiry Triage Inbox — a `kind:'react'` business scenario (ADR-0081). + * + * Models the classic support/lead-triage queue every CRM needs: status TABS + * with live counts, a real `` filtered by the active tab, and a detail + * panel with one-click status actions ("Mark Contacted" / "Close") that persist + * via `useAdapter().update` and refresh the list + counts. Demonstrates + * React-state-driven server filtering, cross-row aggregation, and a status + * workflow — none of which the fixed page schema can express. + */ +export const InquiryTriagePage = definePage({ + name: 'showcase_inquiry_triage', + label: 'Inquiry Triage (React)', + type: 'home', + kind: 'react', + source: ` +function Page() { + const adapter = useAdapter(); + const [tab, setTab] = React.useState('all'); + const [sel, setSel] = React.useState(null); + const [reload, setReload] = React.useState(0); + const [counts, setCounts] = React.useState({ all: 0, new: 0, contacted: 0, closed: 0 }); + + const refresh = React.useCallback(async () => { + if (!adapter) return; + const res = await adapter.find('showcase_inquiry', { top: 500 }); + const rows = Array.isArray(res) ? res : (res && res.records) || []; + setCounts({ + all: rows.length, + new: rows.filter((r) => r.status === 'new').length, + contacted: rows.filter((r) => r.status === 'contacted').length, + closed: rows.filter((r) => r.status === 'closed').length, + }); + }, [adapter]); + React.useEffect(() => { refresh(); }, [refresh, reload]); + + const setStatus = async (status) => { + if (!adapter || !sel) return; + await adapter.update('showcase_inquiry', sel.id, { status }); + setSel(null); + setReload((k) => k + 1); + }; + + const TABS = [['all', 'All'], ['new', 'New'], ['contacted', 'Contacted'], ['closed', 'Closed']]; + const filters = tab === 'all' ? undefined : ['status', '=', tab]; + const STATUS_COLOR = { new: 'bg-blue-100 text-blue-700', contacted: 'bg-amber-100 text-amber-700', closed: 'bg-emerald-100 text-emerald-700' }; + + return ( +
+
+

Inquiry Triage

+

A support queue over showcase_inquiry — tabs filter a real <ListView>; one click moves an inquiry's status.

+
+ +
+ {TABS.map(([k, label]) => ( + + ))} +
+ +
+
+ setSel(r)} /> +
+
+ {sel ? ( +
+
+
+

{sel.name}

+ {sel.status} +
+

{sel.company} · {sel.email}

+
+

{sel.message || 'No message.'}

+
+ + +
+
+ ) : ( +
+
📥
+

Select an inquiry to triage.

+
+ )} +
+
+
+ ); +}`, +}); diff --git a/examples/app-showcase/src/pages/invoice-console.page.ts b/examples/app-showcase/src/pages/invoice-console.page.ts new file mode 100644 index 0000000000..29a2594d03 --- /dev/null +++ b/examples/app-showcase/src/pages/invoice-console.page.ts @@ -0,0 +1,110 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { definePage } from '@objectstack/spec/ui'; + +/** + * Invoice Console — a `kind:'react'` business scenario (ADR-0081). + * + * Accounts-receivable management: a KPI strip aggregating invoices by status + * (useAdapter), a status segmented-filter driving a real ``, a real + * `` for create + edit, and a "Mark Paid" quick action on the + * selected invoice. Demonstrates aggregation KPIs, segmented filtering, full + * CRUD, and a one-click status transition in one screen. + */ +export const InvoiceConsolePage = definePage({ + name: 'showcase_invoice_console', + label: 'Invoice Console (React)', + type: 'home', + kind: 'react', + source: ` +function Page() { + const adapter = useAdapter(); + const [status, setStatus] = React.useState('all'); + const [sel, setSel] = React.useState(null); + const [mode, setMode] = React.useState('edit'); + const [reload, setReload] = React.useState(0); + const [kpi, setKpi] = React.useState({ count: 0, draft: 0, sent: 0, paid: 0 }); + + React.useEffect(() => { + let alive = true; + (async () => { + if (!adapter) return; + const res = await adapter.find('showcase_invoice', { top: 500 }); + const rows = Array.isArray(res) ? res : (res && res.records) || []; + if (alive) setKpi({ + count: rows.length, + draft: rows.filter((r) => r.status === 'draft').length, + sent: rows.filter((r) => r.status === 'sent').length, + paid: rows.filter((r) => r.status === 'paid').length, + }); + })(); + return () => { alive = false; }; + }, [adapter, reload]); + + const afterWrite = () => { setSel(null); setMode('edit'); setReload((k) => k + 1); }; + const markPaid = async () => { if (!adapter || !sel) return; await adapter.update('showcase_invoice', sel.id, { status: 'paid' }); afterWrite(); }; + const openNew = () => { setSel(null); setMode('create'); }; + + const FILTERS = [['all', 'All'], ['draft', 'Draft'], ['sent', 'Sent'], ['paid', 'Paid'], ['void', 'Void']]; + const filters = status === 'all' ? undefined : ['status', '=', status]; + const editing = mode === 'create' || sel; + const Kpi = ({ label, value, accent }) => ( +
+
{label}
+
{value}
+
+ ); + + return ( +
+
+
+

Invoice Console

+

Accounts receivable over showcase_invoice — aggregate, filter, edit, and collect.

+
+ +
+ +
+ + + + +
+ +
+ {FILTERS.map(([k, label]) => ( + + ))} +
+ +
+
+ { setSel(r); setMode('edit'); }} /> +
+
+ {editing ? ( + + setSel(null)} /> + {mode === 'edit' && sel && sel.status !== 'paid' ? ( + + ) : null} + + ) : ( +
+
🧾
+

Select an invoice, or create one.

+
+ )} +
+
+
+ ); +}`, +});