Skip to content

Commit b2e6925

Browse files
os-zhuangclaude
andcommitted
feat(examples): CRM Workbench — a kind:'react' master/detail showcase page
A complete real-React example (ADR-0081 trusted tier): composes the platform's real <ListView> + <ObjectForm> with React state to build a master/detail workbench (row click → controlled recordId; save → list refresh; live KPIs via useAdapter). Browser-verified through the real PageRenderer. Renders behind the host capability CAP_REACT_PAGES (a trusted deployment enables it); otherwise it shows the enterprise-capability notice. Also renames the JSX nav label to (HTML). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6b366e0 commit b2e6925

4 files changed

Lines changed: 121 additions & 3 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, 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, 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, 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, PageVariablesPage, ContactFormPage],
158158
dashboards: [ChartGalleryDashboard, OpsDashboard],
159159
books: allBooks,
160160
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export const ShowcaseApp = App.create({
6565
icon: 'layout',
6666
children: [
6767
{ id: 'nav_gallery', type: 'page', pageName: 'showcase_component_gallery', label: 'Component Gallery', icon: 'layout-template' },
68-
{ id: 'nav_command_center_jsx', type: 'page', pageName: 'showcase_command_center_jsx', label: 'Command Center (JSX)', icon: 'code' },
68+
{ id: 'nav_command_center_jsx', type: 'page', pageName: 'showcase_command_center_jsx', label: 'Command Center (HTML)', icon: 'code' },
69+
{ id: 'nav_crm_workbench', type: 'page', pageName: 'showcase_crm_workbench', label: 'CRM Workbench (React)', icon: 'layout-dashboard' },
6970
{ id: 'nav_styling_gallery', type: 'page', pageName: 'showcase_styling_gallery', label: 'Styling (ADR-0065)', icon: 'palette' },
7071
{ id: 'nav_page_variables', type: 'page', pageName: 'showcase_page_variables', label: 'Page Variables', icon: 'mouse-pointer-click' },
7172
{ id: 'nav_contact_form', type: 'page', pageName: 'showcase_contact_form', label: 'Contact Form', icon: 'mail-plus' },
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* CRM Workbench — a `kind:'react'` page (ADR-0081, the TRUSTED tier).
7+
*
8+
* The whole page is REAL React executed at render by `@object-ui/react-runtime`
9+
* (hooks, event handlers, arbitrary JS) — NOT the constrained parse-never-execute
10+
* `kind:'html'` tier. It demonstrates exactly what a real customer business UI
11+
* needs: a master/detail workbench that COMPOSES the platform's real data
12+
* components — `<ListView>` (the object table) and `<ObjectForm>` — with React
13+
* state to wire complex interaction the fixed page schema cannot express:
14+
*
15+
* - click a project row → load that record into the editor (controlled `recordId`)
16+
* - save the form → refresh the list (`onSuccess` bumps a remount key)
17+
* - "New project" → the same editor in create mode
18+
* - a live KPI strip → `useAdapter()` queries the object directly
19+
*
20+
* The injected scope exposes `React`, `useAdapter`, and the curated public data
21+
* blocks as PascalCase components (`ListView`, `ObjectForm`, `ObjectMetric`, …),
22+
* each a real registered renderer. Layout is plain HTML + Tailwind.
23+
*
24+
* NOTE: `kind:'react'` executes author code, so it is gated behind the host
25+
* capability `CAP_REACT_PAGES` (default OFF). A trusted/enterprise deployment
26+
* enables it from its host bootstrap — e.g. `enableCapability('react-pages')`
27+
* or `globalThis.__OBJECTUI_CAPABILITIES__ = ['react-pages']`. Until then this
28+
* page renders an "enterprise capability required" notice rather than executing.
29+
*/
30+
export const CrmWorkbenchPage = definePage({
31+
name: 'showcase_crm_workbench',
32+
label: 'CRM Workbench (React)',
33+
type: 'home',
34+
kind: 'react',
35+
source: `
36+
function Page() {
37+
const adapter = useAdapter();
38+
const [selected, setSelected] = React.useState(null);
39+
const [mode, setMode] = React.useState('edit');
40+
const [reloadKey, setReloadKey] = React.useState(0);
41+
const [stats, setStats] = React.useState({ total: 0, active: 0 });
42+
43+
const refreshStats = React.useCallback(async () => {
44+
if (!adapter) return;
45+
try {
46+
const all = await adapter.find('showcase_project', { top: 200 });
47+
const rows = Array.isArray(all) ? all : (all && all.records) || [];
48+
setStats({ total: rows.length, active: rows.filter((r) => r.status === 'active').length });
49+
} catch (e) { /* ignore in demo */ }
50+
}, [adapter]);
51+
52+
React.useEffect(() => { refreshStats(); }, [refreshStats, reloadKey]);
53+
54+
const openNew = () => { setSelected(null); setMode('create'); };
55+
const onRowClick = (rec) => { setSelected(rec); setMode('edit'); };
56+
const afterSave = () => { setSelected(null); setMode('edit'); setReloadKey((k) => k + 1); };
57+
58+
const editing = mode === 'create' || selected;
59+
60+
return (
61+
<div className="mx-auto max-w-6xl space-y-6 p-8">
62+
<header className="flex items-center justify-between">
63+
<div>
64+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">CRM Workbench</h1>
65+
<p className="mt-1 text-sm text-slate-500">Master/detail over <code>showcase_project</code> — real <code>&lt;ListView&gt;</code> + <code>&lt;ObjectForm&gt;</code> wired with React state.</p>
66+
</div>
67+
<button onClick={openNew} className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">+ New project</button>
68+
</header>
69+
70+
<div className="grid grid-cols-3 gap-4">
71+
<div className="rounded-xl border border-slate-200 bg-white p-4">
72+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">Total projects</div>
73+
<div className="mt-1 text-3xl font-bold text-slate-900">{stats.total}</div>
74+
</div>
75+
<div className="rounded-xl border border-slate-200 bg-white p-4">
76+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">Active</div>
77+
<div className="mt-1 text-3xl font-bold text-emerald-600">{stats.active}</div>
78+
</div>
79+
<div className="rounded-xl border border-slate-200 bg-white p-4">
80+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">Editing</div>
81+
<div className="mt-1 truncate text-lg font-semibold text-slate-700">{mode === 'create' ? 'New project' : selected ? (selected.name || selected.id) : '—'}</div>
82+
</div>
83+
</div>
84+
85+
<div className="grid grid-cols-5 gap-6">
86+
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
87+
<ListView
88+
key={reloadKey}
89+
objectName="showcase_project"
90+
fields={['name', 'status', 'health', 'budget', 'owner']}
91+
navigation={{ mode: 'none' }}
92+
onRowClick={onRowClick}
93+
/>
94+
</section>
95+
<section className="col-span-2 rounded-xl border border-slate-200 bg-white p-5">
96+
{editing ? (
97+
<ObjectForm
98+
key={(mode === 'create' ? 'new' : selected && selected.id) + ':' + reloadKey}
99+
objectName="showcase_project"
100+
mode={mode}
101+
recordId={mode === 'edit' && selected ? selected.id : undefined}
102+
onSuccess={afterSave}
103+
onCancel={() => { setSelected(null); }}
104+
/>
105+
) : (
106+
<div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400">
107+
<div className="text-4xl">🗂️</div>
108+
<p className="mt-2 text-sm">Select a project to edit, or create a new one.</p>
109+
</div>
110+
)}
111+
</section>
112+
</div>
113+
</div>
114+
);
115+
}`,
116+
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export { SettingsPage } from './settings.page.js';
1616
export { StylingGalleryPage } from './styling-gallery.page.js';
1717
export { CommandCenterPage } from './command-center.page.js';
1818
export { CommandCenterJsxPage } from './command-center-jsx.page.js';
19+
export { CrmWorkbenchPage } from './crm-workbench.page.js';
1920
export { PageVariablesPage } from './page-variables.page.js';
2021
export { ContactFormPage } from './contact-form.page.js';
2122
export {

0 commit comments

Comments
 (0)