|
| 1 | +/* Verifies the kind:'react' tier composing the REAL data components |
| 2 | + * (<ListView> + <ObjectForm>) with React state — a master/detail workbench, |
| 3 | + * the exact pattern of examples/app-showcase crm-workbench.page.ts. Rendered |
| 4 | + * through the real PageRenderer; a tiny in-memory adapter stands in for the |
| 5 | + * backend so the real plugins mount and the interaction is genuinely exercised. */ |
| 6 | +import './index.css'; |
| 7 | +import '@object-ui/components'; |
| 8 | +import '@object-ui/plugin-grid'; |
| 9 | +import '@object-ui/plugin-view'; |
| 10 | +import '@object-ui/plugin-list'; |
| 11 | +import '@object-ui/plugin-form'; |
| 12 | +import '@object-ui/plugin-detail'; |
| 13 | +import React from 'react'; |
| 14 | +import { createRoot } from 'react-dom/client'; |
| 15 | +import { SchemaRenderer, SchemaRendererProvider, AdapterCtx } from '@object-ui/react'; |
| 16 | +import { I18nProvider } from '@object-ui/i18n'; |
| 17 | +// NOTE: we do NOT call enableCapability here — `react-pages` is ON by default. |
| 18 | +// `?disable=1` simulates a server that set OS_PAGE_REACT=off (the runtime would |
| 19 | +// inject this global); the page then renders the "disabled" notice. |
| 20 | +if (new URLSearchParams(location.search).has('disable')) { |
| 21 | + (window as unknown as { __OBJECTUI_CAPABILITIES_DISABLED__?: string[] }).__OBJECTUI_CAPABILITIES_DISABLED__ = ['react-pages']; |
| 22 | +} |
| 23 | + |
| 24 | +// ---- in-memory adapter (just enough for ListView + ObjectForm) ---- |
| 25 | +let store: any[] = [ |
| 26 | + { id: '1', name: 'Apollo Migration', status: 'active', health: 'green', budget: 120000, owner: 'Dana Lee' }, |
| 27 | + { id: '2', name: 'Billing Revamp', status: 'planned', health: 'yellow', budget: 80000, owner: 'Sam Ortiz' }, |
| 28 | + { id: '3', name: 'Mobile App v2', status: 'active', health: 'red', budget: 210000, owner: 'Priya N.' }, |
| 29 | + { id: '4', name: 'Data Warehouse', status: 'on_hold', health: 'yellow', budget: 64000, owner: 'Chen Wu' }, |
| 30 | +]; |
| 31 | +let nextId = 5; |
| 32 | +const projectSchema = { |
| 33 | + name: 'showcase_project', |
| 34 | + label: 'Project', |
| 35 | + fields: { |
| 36 | + name: { type: 'text', label: 'Project Name', required: true }, |
| 37 | + status: { type: 'select', label: 'Status', options: [ |
| 38 | + { label: 'Planned', value: 'planned' }, { label: 'Active', value: 'active' }, |
| 39 | + { label: 'On Hold', value: 'on_hold' }, { label: 'Completed', value: 'completed' } ] }, |
| 40 | + health: { type: 'select', label: 'Health', options: [ |
| 41 | + { label: 'Green', value: 'green' }, { label: 'Yellow', value: 'yellow' }, { label: 'Red', value: 'red' } ] }, |
| 42 | + budget: { type: 'currency', label: 'Budget' }, |
| 43 | + owner: { type: 'text', label: 'Owner' }, |
| 44 | + }, |
| 45 | +}; |
| 46 | +const adapter: any = { |
| 47 | + find: async () => store.slice(), |
| 48 | + findOne: async (_o: string, id: any) => store.find((r) => String(r.id) === String(id)) || null, |
| 49 | + create: async (_o: string, data: any) => { const rec = { id: String(nextId++), ...data }; store.push(rec); return rec; }, |
| 50 | + update: async (_o: string, id: any, data: any) => { |
| 51 | + const i = store.findIndex((r) => String(r.id) === String(id)); if (i >= 0) store[i] = { ...store[i], ...data }; return store[i]; |
| 52 | + }, |
| 53 | + getObjectSchema: async () => projectSchema, |
| 54 | +}; |
| 55 | + |
| 56 | +const source = ` |
| 57 | +function Page() { |
| 58 | + const adapter = useAdapter(); |
| 59 | + const [selected, setSelected] = React.useState(null); |
| 60 | + const [mode, setMode] = React.useState('edit'); |
| 61 | + const [reloadKey, setReloadKey] = React.useState(0); |
| 62 | + const [stats, setStats] = React.useState({ total: 0, active: 0 }); |
| 63 | + const refreshStats = React.useCallback(async () => { |
| 64 | + if (!adapter) return; |
| 65 | + const all = await adapter.find('showcase_project', { top: 200 }); |
| 66 | + const rows = Array.isArray(all) ? all : (all && all.records) || []; |
| 67 | + setStats({ total: rows.length, active: rows.filter((r) => r.status === 'active').length }); |
| 68 | + }, [adapter]); |
| 69 | + React.useEffect(() => { refreshStats(); }, [refreshStats, reloadKey]); |
| 70 | + const openNew = () => { setSelected(null); setMode('create'); }; |
| 71 | + const onRowClick = (rec) => { setSelected(rec); setMode('edit'); }; |
| 72 | + const afterSave = () => { setSelected(null); setMode('edit'); setReloadKey((k) => k + 1); }; |
| 73 | + const editing = mode === 'create' || selected; |
| 74 | + return ( |
| 75 | + <div className="mx-auto max-w-6xl space-y-6 p-8"> |
| 76 | + <header className="flex items-center justify-between"> |
| 77 | + <div> |
| 78 | + <h1 className="text-2xl font-bold tracking-tight text-slate-900">CRM Workbench</h1> |
| 79 | + <p className="mt-1 text-sm text-slate-500">Real <ListView> + <ObjectForm> wired with React state.</p> |
| 80 | + </div> |
| 81 | + <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> |
| 82 | + </header> |
| 83 | + <div className="grid grid-cols-3 gap-4"> |
| 84 | + <div className="rounded-xl border border-slate-200 bg-white p-4"><div className="text-xs font-medium uppercase tracking-wide text-slate-400">Total projects</div><div className="mt-1 text-3xl font-bold text-slate-900">{stats.total}</div></div> |
| 85 | + <div className="rounded-xl border border-slate-200 bg-white p-4"><div className="text-xs font-medium uppercase tracking-wide text-slate-400">Active</div><div className="mt-1 text-3xl font-bold text-emerald-600">{stats.active}</div></div> |
| 86 | + <div className="rounded-xl border border-slate-200 bg-white p-4"><div className="text-xs font-medium uppercase tracking-wide text-slate-400">Editing</div><div className="mt-1 truncate text-lg font-semibold text-slate-700">{mode === 'create' ? 'New project' : selected ? (selected.name || selected.id) : '—'}</div></div> |
| 87 | + </div> |
| 88 | + <div className="grid grid-cols-5 gap-6"> |
| 89 | + <section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2"> |
| 90 | + <ListView key={reloadKey} objectName="showcase_project" fields={['name','status','health','budget','owner']} navigation={{ mode: 'none' }} onRowClick={onRowClick} /> |
| 91 | + </section> |
| 92 | + <section className="col-span-2 rounded-xl border border-slate-200 bg-white p-5"> |
| 93 | + {editing ? ( |
| 94 | + <ObjectForm key={(mode === 'create' ? 'new' : selected && selected.id) + ':' + reloadKey} objectName="showcase_project" mode={mode} recordId={mode === 'edit' && selected ? selected.id : undefined} onSuccess={afterSave} onCancel={() => { setSelected(null); }} /> |
| 95 | + ) : ( |
| 96 | + <div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400"><div className="text-4xl">\u{1F5C2}\u{FE0F}</div><p className="mt-2 text-sm">Select a project to edit, or create a new one.</p></div> |
| 97 | + )} |
| 98 | + </section> |
| 99 | + </div> |
| 100 | + </div> |
| 101 | + ); |
| 102 | +}`; |
| 103 | + |
| 104 | +const page = { type: 'home', kind: 'react', name: 'crm_workbench', label: 'CRM Workbench', source }; |
| 105 | + |
| 106 | +createRoot(document.getElementById('root')!).render( |
| 107 | + <React.StrictMode> |
| 108 | + <I18nProvider> |
| 109 | + <AdapterCtx.Provider value={adapter}> |
| 110 | + <SchemaRendererProvider dataSource={adapter}> |
| 111 | + <div className="bg-slate-50"><SchemaRenderer schema={page as any} /></div> |
| 112 | + </SchemaRendererProvider> |
| 113 | + </AdapterCtx.Provider> |
| 114 | + </I18nProvider> |
| 115 | + </React.StrictMode>, |
| 116 | +); |
0 commit comments