@object-ui/app-shell 6.2.1 → 6.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/console/ai/AiChatPage.js +19 -22
  3. package/dist/layout/ConsoleFloatingChatbot.js +44 -24
  4. package/dist/views/metadata-admin/EmbeddedItemEditor.d.ts +15 -0
  5. package/dist/views/metadata-admin/EmbeddedItemEditor.js +194 -0
  6. package/dist/views/metadata-admin/MetadataDetailDrawer.js +2 -26
  7. package/dist/views/metadata-admin/RelatedPanel.d.ts +4 -0
  8. package/dist/views/metadata-admin/RelatedPanel.js +2 -0
  9. package/dist/views/metadata-admin/ResourceEditPage.js +50 -5
  10. package/dist/views/metadata-admin/anchors.js +6 -0
  11. package/dist/views/metadata-admin/index.d.ts +2 -0
  12. package/dist/views/metadata-admin/index.js +6 -0
  13. package/dist/views/metadata-admin/preview-registry.d.ts +43 -0
  14. package/dist/views/metadata-admin/preview-registry.js +18 -0
  15. package/dist/views/metadata-admin/previews/AppPreview.d.ts +2 -0
  16. package/dist/views/metadata-admin/previews/AppPreview.js +101 -0
  17. package/dist/views/metadata-admin/previews/DashboardPreview.d.ts +2 -0
  18. package/dist/views/metadata-admin/previews/DashboardPreview.js +25 -0
  19. package/dist/views/metadata-admin/previews/EmailTemplatePreview.d.ts +2 -0
  20. package/dist/views/metadata-admin/previews/EmailTemplatePreview.js +65 -0
  21. package/dist/views/metadata-admin/previews/ObjectPreview.d.ts +2 -0
  22. package/dist/views/metadata-admin/previews/ObjectPreview.js +36 -0
  23. package/dist/views/metadata-admin/previews/PagePreview.d.ts +2 -0
  24. package/dist/views/metadata-admin/previews/PagePreview.js +26 -0
  25. package/dist/views/metadata-admin/previews/PreviewShell.d.ts +40 -0
  26. package/dist/views/metadata-admin/previews/PreviewShell.js +49 -0
  27. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +2 -0
  28. package/dist/views/metadata-admin/previews/ReportPreview.js +27 -0
  29. package/dist/views/metadata-admin/previews/ViewPreview.d.ts +2 -0
  30. package/dist/views/metadata-admin/previews/ViewPreview.js +113 -0
  31. package/dist/views/metadata-admin/previews/index.d.ts +1 -0
  32. package/dist/views/metadata-admin/previews/index.js +26 -0
  33. package/dist/views/metadata-admin/registry.d.ts +17 -0
  34. package/package.json +26 -26
@@ -21,7 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
21
21
  */
22
22
  import * as React from 'react';
23
23
  import { useNavigate, useParams } from 'react-router-dom';
24
- import { Save, RotateCcw, History, Link2, Loader2, AlertTriangle, Layers3, } from 'lucide-react';
24
+ import { Save, RotateCcw, History, Link2, Loader2, AlertTriangle, Layers3, Eye, Pencil, X, } from 'lucide-react';
25
25
  import { Button } from '@object-ui/components';
26
26
  import { Badge } from '@object-ui/components';
27
27
  import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@object-ui/components';
@@ -34,6 +34,7 @@ import { useMetadataClient, useMetadataTypes, } from './useMetadata';
34
34
  import { getMetadataResource, resolveResourceConfig, listAnchorsFor, } from './registry';
35
35
  import { RelatedPanel } from './RelatedPanel';
36
36
  import { MetadataDetailDrawer } from './MetadataDetailDrawer';
37
+ import { getMetadataPreview } from './preview-registry';
37
38
  export function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode = false, embedded = false, }) {
38
39
  const params = useParams();
39
40
  const type = typeProp ?? params.type ?? '';
@@ -62,6 +63,15 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
62
63
  const [issues, setIssues] = React.useState([]);
63
64
  const [destructiveIssues, setDestructiveIssues] = React.useState(null);
64
65
  const [pendingItem, setPendingItem] = React.useState(null);
66
+ // Form edit mode. The form is read-only by default — admins land in a
67
+ // "view" state and must click Edit to mutate, mirroring the Salesforce /
68
+ // Notion convention. createMode is always editing (you can't view what
69
+ // doesn't exist yet). Truly read-only types (no allowOrgOverride) stay
70
+ // read-only regardless.
71
+ const [editing, setEditing] = React.useState(!!createMode);
72
+ // Snapshot of the last saved draft. Used by Cancel to revert in-flight
73
+ // edits, and as the source-of-truth when entering edit mode.
74
+ const draftSnapshotRef = React.useRef(null);
65
75
  // Prefetch object name list once — fuels the `ref:object` widget.
66
76
  // We don't block render on it; the widget shows a "Loading…" state.
67
77
  const [objectNames, setObjectNames] = React.useState([]);
@@ -107,6 +117,7 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
107
117
  // Initial draft = effective if available, otherwise code.
108
118
  const initial = (lay.effective ?? lay.code ?? {});
109
119
  setDraft(initial);
120
+ draftSnapshotRef.current = initial;
110
121
  setLoading(false);
111
122
  }
112
123
  catch (err) {
@@ -197,9 +208,15 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
197
208
  // Refresh layered after save.
198
209
  const lay = await client.layered(type, savedName);
199
210
  setLayered(lay);
200
- setDraft((lay.effective ?? itemToSave));
211
+ const fresh = (lay.effective ?? itemToSave);
212
+ setDraft(fresh);
213
+ draftSnapshotRef.current = fresh;
201
214
  setDestructiveIssues(null);
202
215
  setPendingItem(null);
216
+ // Exit edit mode on successful save (unless we were creating —
217
+ // navigation to the new record's URL will reset state anyway).
218
+ if (!createMode)
219
+ setEditing(false);
203
220
  if (createMode) {
204
221
  navigate(`../${encodeURIComponent(savedName)}`);
205
222
  }
@@ -261,7 +278,10 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
261
278
  await client.reset(type, name);
262
279
  const lay = await client.layered(type, name);
263
280
  setLayered(lay);
264
- setDraft((lay.effective ?? lay.code ?? {}));
281
+ const fresh = (lay.effective ?? lay.code ?? {});
282
+ setDraft(fresh);
283
+ draftSnapshotRef.current = fresh;
284
+ setEditing(false);
265
285
  }
266
286
  catch (err) {
267
287
  setError(err?.message ?? String(err));
@@ -278,13 +298,38 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
278
298
  const readOnly = !entry?.allowOrgOverride && !createMode;
279
299
  const DesignerTab = !createMode ? customConfig?.DesignerTab : undefined;
280
300
  const designerTabLabel = customConfig?.designerTabLabel ?? 'Designer';
281
- return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? 'Create new' : 'Edit overlay', actions: _jsxs(_Fragment, { children: [!createMode && entry?.allowOrgOverride && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset overlay"] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(History, { className: "h-4 w-4 mr-1" }), "History"] })), entry?.allowOrgOverride && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), "Save"] }))] }), children: [_jsxs("div", { className: "p-6 space-y-6 max-w-7xl", children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), readOnly && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3", children: ["This type is read-only. To enable runtime editing, set", ' ', _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }), " to include ", _jsx("code", { className: "font-mono", children: type }), ", or flip", ' ', _jsx("code", { className: "font-mono", children: "allowOrgOverride" }), " in the registry."] })), _jsxs(Tabs, { defaultValue: initialTabRef.current ?? (DesignerTab ? 'designer' : 'form'), className: "w-full", onValueChange: (v) => {
301
+ // Preview tab opt-in via `registerMetadataPreview()`. Hidden in
302
+ // create mode (nothing to preview yet) and inside the embedded
303
+ // drawer (the parent context owns the preview surface).
304
+ const PreviewComponent = !createMode && !embedded ? getMetadataPreview(type) : undefined;
305
+ // Cancel edits: revert the draft to the last saved snapshot and exit
306
+ // edit mode. Safe to call even with no snapshot (no-op).
307
+ function doCancelEdit() {
308
+ if (draftSnapshotRef.current) {
309
+ setDraft(draftSnapshotRef.current);
310
+ }
311
+ setIssues([]);
312
+ setError(null);
313
+ setEditing(false);
314
+ }
315
+ // When the form is "live" but not yet in edit mode, it renders as
316
+ // read-only. createMode is always editing; truly read-only types
317
+ // (no allowOrgOverride) ignore the editing toggle entirely.
318
+ const formReadOnly = readOnly || (!editing && !createMode);
319
+ // Default tab priority:
320
+ // 1. URL ?tab= (explicit user nav / deep link)
321
+ // 2. Designer (custom rich editor present)
322
+ // 3. Preview (live preview present — most informative landing)
323
+ // 4. Form
324
+ const defaultTab = initialTabRef.current ??
325
+ (DesignerTab ? 'designer' : PreviewComponent ? 'preview' : 'form');
326
+ return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? 'Create new' : 'Edit overlay', actions: _jsxs(_Fragment, { children: [!createMode && entry?.allowOrgOverride && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset overlay"] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(History, { className: "h-4 w-4 mr-1" }), "History"] })), entry?.allowOrgOverride && !createMode && !editing && (_jsxs(Button, { size: "sm", onClick: () => setEditing(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-1" }), "Edit"] })), entry?.allowOrgOverride && !createMode && editing && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doCancelEdit, disabled: saving, children: [_jsx(X, { className: "h-4 w-4 mr-1" }), "Cancel"] })), entry?.allowOrgOverride && (editing || createMode) && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), "Save"] }))] }), children: [_jsxs("div", { className: "p-6 space-y-6 max-w-7xl", children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), readOnly && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3", children: ["This type is read-only. To enable runtime editing, set", ' ', _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }), " to include ", _jsx("code", { className: "font-mono", children: type }), ", or flip", ' ', _jsx("code", { className: "font-mono", children: "allowOrgOverride" }), " in the registry."] })), _jsxs(Tabs, { defaultValue: defaultTab, className: "w-full", onValueChange: (v) => {
282
327
  if (typeof window === 'undefined' || embedded)
283
328
  return;
284
329
  const url = new URL(window.location.href);
285
330
  url.searchParams.set('tab', v);
286
331
  window.history.replaceState({}, '', url.toString());
287
- }, children: [_jsxs(TabsList, { children: [DesignerTab && (_jsx(TabsTrigger, { value: "designer", children: designerTabLabel })), _jsx(TabsTrigger, { value: "form", children: "Form" }), !createMode && (_jsxs(TabsTrigger, { value: "layers", children: ["Layers", layered?.overlay && (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: "overlay" }))] })), !createMode && (_jsxs(TabsTrigger, { value: "references", onClick: loadReferences, children: [_jsx(Link2, { className: "h-3.5 w-3.5 mr-1" }), "References", refs && (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: refs.length }))] })), hasAnchors && (_jsxs(TabsTrigger, { value: "related", children: [_jsx(Layers3, { className: "h-3.5 w-3.5 mr-1" }), "Related"] }))] }), DesignerTab && (_jsx(TabsContent, { value: "designer", className: "mt-4", children: _jsx(DesignerTab, { type: type, name: name }) })), _jsx(TabsContent, { value: "form", className: "mt-4", children: _jsx(SchemaForm, { schema: schema, form: entry?.form, value: draft, onChange: setDraft, issues: issues, hiddenFields: config.hiddenFields, fieldOrder: config.fieldOrder, readOnly: readOnly, createMode: createMode, widgetContext: widgetContext }) }), !createMode && (_jsx(TabsContent, { value: "layers", className: "mt-4", children: _jsx(LayeredDiff, { layered: layered }) })), !createMode && (_jsx(TabsContent, { value: "references", className: "mt-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })), hasAnchors && (_jsx(TabsContent, { value: "related", className: "mt-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) }))] })] }), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
332
+ }, children: [_jsxs(TabsList, { children: [DesignerTab && (_jsx(TabsTrigger, { value: "designer", children: designerTabLabel })), PreviewComponent && (_jsxs(TabsTrigger, { value: "preview", children: [_jsx(Eye, { className: "h-3.5 w-3.5 mr-1" }), "Preview"] })), _jsx(TabsTrigger, { value: "form", children: "Detail" }), !createMode && (_jsxs(TabsTrigger, { value: "layers", children: ["Layers", layered?.overlay && (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: "overlay" }))] })), !createMode && (_jsxs(TabsTrigger, { value: "references", onClick: loadReferences, children: [_jsx(Link2, { className: "h-3.5 w-3.5 mr-1" }), "References", refs && (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: refs.length }))] })), hasAnchors && (_jsxs(TabsTrigger, { value: "related", children: [_jsx(Layers3, { className: "h-3.5 w-3.5 mr-1" }), "Related"] }))] }), DesignerTab && (_jsx(TabsContent, { value: "designer", className: "mt-4", children: _jsx(DesignerTab, { type: type, name: name }) })), _jsxs(TabsContent, { value: "form", className: "mt-4 space-y-3", children: [formReadOnly && !readOnly && entry?.allowOrgOverride && !createMode && (_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs text-muted-foreground border rounded p-2.5 bg-muted/30", children: [_jsxs("span", { children: ["Viewing in read-only mode. Click ", _jsx("strong", { children: "Edit" }), " to make changes."] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: [_jsx(Pencil, { className: "h-3.5 w-3.5 mr-1" }), "Edit"] })] })), _jsx(SchemaForm, { schema: schema, form: entry?.form, value: draft, onChange: setDraft, issues: issues, hiddenFields: config.hiddenFields, fieldOrder: config.fieldOrder, readOnly: formReadOnly, createMode: createMode, widgetContext: widgetContext })] }), PreviewComponent && (_jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsx(PreviewComponent, { type: type, name: name, draft: draft }) })), !createMode && (_jsx(TabsContent, { value: "layers", className: "mt-4", children: _jsx(LayeredDiff, { layered: layered }) })), !createMode && (_jsx(TabsContent, { value: "references", className: "mt-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })), hasAnchors && (_jsx(TabsContent, { value: "related", className: "mt-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) }))] })] }), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
288
333
  if (!open) {
289
334
  setDestructiveIssues(null);
290
335
  setPendingItem(null);
@@ -35,6 +35,8 @@ export function registerBuiltinAnchors() {
35
35
  anchors: [{
36
36
  anchorType: 'object',
37
37
  source: 'embedded',
38
+ editAs: 'field',
39
+ embeddedPath: 'fields',
38
40
  extract: (parent) => mapOrArrayToList(parent.fields),
39
41
  groupLabel: 'Fields',
40
42
  order: 5,
@@ -46,6 +48,8 @@ export function registerBuiltinAnchors() {
46
48
  anchors: [{
47
49
  anchorType: 'object',
48
50
  source: 'embedded',
51
+ editAs: 'index',
52
+ embeddedPath: 'indexes',
49
53
  extract: (parent) => {
50
54
  const raw = parent.indexes;
51
55
  if (!Array.isArray(raw))
@@ -67,6 +71,8 @@ export function registerBuiltinAnchors() {
67
71
  anchors: [{
68
72
  anchorType: 'object',
69
73
  source: 'embedded',
74
+ editAs: 'validation',
75
+ embeddedPath: 'validations',
70
76
  extract: (parent) => mapOrArrayToList(parent.validations),
71
77
  groupLabel: 'Embedded Validations',
72
78
  order: 15,
@@ -29,5 +29,7 @@ export { translateMetadataType, translateMetadataDomain, t as translateMetadataA
29
29
  export type { SupportedLocale } from './i18n';
30
30
  export { registerMetadataResource, getMetadataResource, listMetadataResources, listAnchorsFor, resolveResourceConfig, anchorByField, } from './registry';
31
31
  export type { MetadataResourceConfig, MetadataDomain, MetadataAnchor, } from './registry';
32
+ export { registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, } from './preview-registry';
33
+ export type { MetadataPreview, MetadataPreviewProps } from './preview-registry';
32
34
  export { useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './useMetadata';
33
35
  export type { RichMetadataTypeEntry } from './useMetadata';
@@ -36,4 +36,10 @@ registerBuiltinAnchors();
36
36
  // until the framework wires Zod→JSONSchema generation into /meta/types.
37
37
  import { registerDefaultMetadataSchemas } from './default-schemas';
38
38
  registerDefaultMetadataSchemas();
39
+ // Side-effect: register built-in Preview-tab renderers (page, view,
40
+ // dashboard, report, app, object, email_template). Plugins can add or
41
+ // override entries via `registerMetadataPreview()`.
42
+ import { registerBuiltinPreviews } from './previews';
43
+ registerBuiltinPreviews();
44
+ export { registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, } from './preview-registry';
39
45
  export { useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './useMetadata';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * MetadataPreviewRegistry — per-type "Preview" tab renderers for the
3
+ * metadata-admin engine.
4
+ *
5
+ * The Preview tab is opt-in: a type only gets a Preview tab when its
6
+ * type id is registered here. This matches the philosophy used for
7
+ * `DesignerTab` and the bespoke `EditPage` in `registry.ts` — generic
8
+ * by default, escape hatch when a type benefits from a richer surface.
9
+ *
10
+ * The renderer receives the **current draft** (not the saved layered
11
+ * record), so users see their unsaved edits live. Drafts can be
12
+ * incomplete or invalid — implementations must defensively read fields.
13
+ *
14
+ * registerMetadataPreview('page', PagePreview);
15
+ * const Preview = getMetadataPreview('page');
16
+ * if (Preview) <Preview type="page" name="crm_welcome" draft={draft} />;
17
+ *
18
+ * If the type isn't registered, the engine simply omits the tab — no
19
+ * empty "preview not available" surface is shown.
20
+ */
21
+ import type { ComponentType } from 'react';
22
+ export interface MetadataPreviewProps {
23
+ /** The metadata type, e.g. 'page', 'dashboard'. */
24
+ type: string;
25
+ /** The item's primary-key name. May be empty string in create mode. */
26
+ name: string;
27
+ /**
28
+ * The live draft from the Form tab. Implementations should treat this
29
+ * as immutable and untrusted (validation may be in progress).
30
+ */
31
+ draft: Record<string, unknown>;
32
+ }
33
+ export type MetadataPreview = ComponentType<MetadataPreviewProps>;
34
+ /**
35
+ * Register (or replace) the Preview tab renderer for a metadata type.
36
+ * Idempotent — re-registering overwrites the previous entry so app
37
+ * authors can swap implementations from their plugin bootstrap.
38
+ */
39
+ export declare function registerMetadataPreview(type: string, component: MetadataPreview): void;
40
+ /** Look up the registered preview for a type, if any. */
41
+ export declare function getMetadataPreview(type: string): MetadataPreview | undefined;
42
+ /** Snapshot of registered preview types (diagnostics). */
43
+ export declare function listMetadataPreviewTypes(): string[];
@@ -0,0 +1,18 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ const REGISTRY = new Map();
3
+ /**
4
+ * Register (or replace) the Preview tab renderer for a metadata type.
5
+ * Idempotent — re-registering overwrites the previous entry so app
6
+ * authors can swap implementations from their plugin bootstrap.
7
+ */
8
+ export function registerMetadataPreview(type, component) {
9
+ REGISTRY.set(type, component);
10
+ }
11
+ /** Look up the registered preview for a type, if any. */
12
+ export function getMetadataPreview(type) {
13
+ return REGISTRY.get(type);
14
+ }
15
+ /** Snapshot of registered preview types (diagnostics). */
16
+ export function listMetadataPreviewTypes() {
17
+ return Array.from(REGISTRY.keys()).sort();
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { MetadataPreviewProps } from '../preview-registry';
2
+ export declare function AppPreview({ name, draft }: MetadataPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * AppPreview — visual summary of an App metadata record's nav and
5
+ * landing route, since rendering a full nested AppShell inside the
6
+ * admin would be confusing (nav-within-nav).
7
+ *
8
+ * Shows:
9
+ * • App label/icon + landing route
10
+ * • Top-level navigation items (tabs/menu) as a clickable list —
11
+ * each link opens the runtime app in a new tab so authors can
12
+ * test the configured nav without leaving the editor.
13
+ *
14
+ * If the App schema doesn't follow the expected shape we degrade to
15
+ * a "no preview" hint rather than throw.
16
+ */
17
+ import * as React from 'react';
18
+ import { Compass, ExternalLink, LayoutDashboard, FileText, Database, BarChart3 } from 'lucide-react';
19
+ import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell';
20
+ function normalizeNav(raw) {
21
+ if (!Array.isArray(raw))
22
+ return [];
23
+ return raw
24
+ .map((it) => {
25
+ if (!it || typeof it !== 'object')
26
+ return null;
27
+ const label = String(it.label ?? it.title ?? it.name ?? it.path ?? '').trim();
28
+ if (!label && !it.children)
29
+ return null;
30
+ const path = it.path ?? it.href ?? it.route ?? it.url ?? undefined;
31
+ // Best-effort kind inference for icon selection.
32
+ let kind;
33
+ if (it.object || it.objectName)
34
+ kind = 'object';
35
+ else if (it.page || it.pageName)
36
+ kind = 'page';
37
+ else if (it.dashboard)
38
+ kind = 'dashboard';
39
+ else if (it.report)
40
+ kind = 'report';
41
+ else if (typeof path === 'string' && /^https?:/i.test(path))
42
+ kind = 'link';
43
+ else if (Array.isArray(it.children) && it.children.length)
44
+ kind = 'group';
45
+ const children = Array.isArray(it.children) ? normalizeNav(it.children) : undefined;
46
+ return { label: label || '(unnamed)', path, kind, children };
47
+ })
48
+ .filter((x) => x !== null);
49
+ }
50
+ function kindIcon(kind) {
51
+ switch (kind) {
52
+ case 'object':
53
+ return Database;
54
+ case 'page':
55
+ return FileText;
56
+ case 'dashboard':
57
+ return LayoutDashboard;
58
+ case 'report':
59
+ return BarChart3;
60
+ default:
61
+ return Compass;
62
+ }
63
+ }
64
+ export function AppPreview({ name, draft }) {
65
+ const appName = String(draft.name ?? name ?? '');
66
+ const label = draft.label ?? appName;
67
+ const landing = draft.landingRoute ?? draft.landing ?? draft.defaultRoute ?? '/';
68
+ const navItems = React.useMemo(() => {
69
+ // Accept the most common shapes used by app schemas in the wild.
70
+ const candidates = [
71
+ draft.nav,
72
+ draft.navigation,
73
+ draft.tabs,
74
+ draft.items,
75
+ draft.menu,
76
+ ];
77
+ for (const c of candidates) {
78
+ if (Array.isArray(c) && c.length)
79
+ return normalizeNav(c);
80
+ }
81
+ return [];
82
+ }, [draft]);
83
+ const baseRuntimeUrl = appName ? `/apps/${encodeURIComponent(appName)}/` : null;
84
+ return (_jsx(PreviewShell, { hint: "app", toolbar: baseRuntimeUrl && (_jsxs("a", { href: baseRuntimeUrl, target: "_blank", rel: "noreferrer", className: "text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1", title: "Open this app in a new tab", children: ["Open ", _jsx(ExternalLink, { className: "h-3 w-3" })] })), children: _jsx(PreviewErrorBoundary, { children: _jsxs("div", { className: "p-3 space-y-3", children: [_jsxs("div", { className: "rounded border bg-muted/30 p-3", children: [_jsx("div", { className: "text-sm font-medium text-foreground", children: String(label) }), _jsx("div", { className: "text-xs text-muted-foreground font-mono mt-0.5", children: appName }), _jsxs("div", { className: "text-xs text-muted-foreground mt-1", children: ["Landing: ", _jsx("code", { className: "font-mono", children: String(landing) })] })] }), navItems.length === 0 ? (_jsxs(PreviewMessage, { children: ["No top-level nav items. Add ", _jsx("code", { children: "nav" }), " / ", _jsx("code", { children: "tabs" }), " entries in the Form tab to populate the app's navigation."] })) : (_jsx("div", { className: "border rounded divide-y", children: navItems.map((item, i) => (_jsx(NavRow, { item: item, appName: appName, depth: 0 }, i))) }))] }) }) }));
85
+ }
86
+ function NavRow({ item, appName, depth }) {
87
+ const Icon = kindIcon(item.kind);
88
+ const url = buildUrl(appName, item.path);
89
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent/40", style: { paddingLeft: `${12 + depth * 16}px` }, children: [_jsx(Icon, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsx("span", { className: "font-medium truncate", children: item.label }), item.kind && (_jsx("span", { className: "text-[10px] uppercase tracking-wider opacity-60", children: item.kind })), url && (_jsxs("a", { href: url, target: "_blank", rel: "noreferrer", className: "ml-auto font-mono text-[10px] text-muted-foreground hover:text-foreground inline-flex items-center gap-1", children: [item.path, " ", _jsx(ExternalLink, { className: "h-3 w-3" })] }))] }), item.children?.map((c, i) => (_jsx(NavRow, { item: c, appName: appName, depth: depth + 1 }, i)))] }));
90
+ }
91
+ function buildUrl(appName, path) {
92
+ if (!path)
93
+ return null;
94
+ if (/^https?:/i.test(path))
95
+ return path;
96
+ if (!appName)
97
+ return null;
98
+ // Treat path as relative to /apps/<appName>/ by default.
99
+ const trimmed = path.startsWith('/') ? path.slice(1) : path;
100
+ return `/apps/${encodeURIComponent(appName)}/${trimmed}`;
101
+ }
@@ -0,0 +1,2 @@
1
+ import type { MetadataPreviewProps } from '../preview-registry';
2
+ export declare function DashboardPreview({ draft }: MetadataPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * DashboardPreview — read-only render of a Dashboard metadata draft.
5
+ *
6
+ * Uses the same DashboardRenderer the runtime DashboardView uses, with
7
+ * the adapter from app-shell's AdapterProvider so widgets can query
8
+ * live data. `designMode` is OFF — this is preview, not edit.
9
+ *
10
+ * The plugin is loaded lazily to avoid pulling its dep graph into
11
+ * every metadata-admin page load.
12
+ */
13
+ import * as React from 'react';
14
+ import { Loader2 } from 'lucide-react';
15
+ import { useAdapter } from '../../../providers/AdapterProvider';
16
+ import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
17
+ const DashboardRenderer = React.lazy(() => import('@object-ui/plugin-dashboard').then((m) => ({ default: m.DashboardRenderer })));
18
+ export function DashboardPreview({ draft }) {
19
+ const adapter = useAdapter();
20
+ const widgets = Array.isArray(draft.widgets) ? draft.widgets : [];
21
+ if (widgets.length === 0) {
22
+ return (_jsx(PreviewShell, { hint: "dashboard", children: _jsx(PreviewMessage, { children: "Add at least one widget to see a preview." }) }));
23
+ }
24
+ return (_jsx(PreviewShell, { hint: `dashboard · ${widgets.length} widget${widgets.length === 1 ? '' : 's'}`, children: _jsx(PreviewErrorBoundary, { fallbackHint: "A widget references an object or field that doesn't resolve.", children: _jsx(React.Suspense, { fallback: _jsxs("div", { className: "p-6 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading dashboard renderer\u2026"] }), children: _jsx("div", { className: "p-3 max-h-[70vh] overflow-auto", children: _jsx(DashboardRenderer, { schema: draft, dataSource: adapter, designMode: false, hideHeaderText: true }) }) }) }) }));
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { MetadataPreviewProps } from '../preview-registry';
2
+ export declare function EmailTemplatePreview({ draft }: MetadataPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * EmailTemplatePreview — sandboxed HTML preview with `{{var}}` and
5
+ * `${var}` substitution from a tiny variables editor.
6
+ *
7
+ * Why an iframe? Email HTML is full-document content (`<html>`, inline
8
+ * styles, sometimes `<head>` blocks). Injecting it inline would let it
9
+ * style the host page. `srcdoc` + `sandbox="allow-same-origin"` gives
10
+ * us a clean visual without exposing the admin UI to template CSS.
11
+ *
12
+ * Subject / from / to are surfaced above the body so authors can see
13
+ * the full envelope at a glance.
14
+ */
15
+ import * as React from 'react';
16
+ import { Mail } from 'lucide-react';
17
+ import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell';
18
+ function detectVariables(text) {
19
+ const out = new Set();
20
+ // {{ var }} — Handlebars/Mustache style
21
+ for (const m of text.matchAll(/\{\{\s*([a-zA-Z_][\w.]*)\s*\}\}/g))
22
+ out.add(m[1]);
23
+ // ${ var } — JS template style
24
+ for (const m of text.matchAll(/\$\{\s*([a-zA-Z_][\w.]*)\s*\}/g))
25
+ out.add(m[1]);
26
+ return Array.from(out).sort();
27
+ }
28
+ function resolveVar(path, scope) {
29
+ return scope[path] ?? '';
30
+ }
31
+ function substitute(text, scope) {
32
+ return text
33
+ .replace(/\{\{\s*([a-zA-Z_][\w.]*)\s*\}\}/g, (_, p) => resolveVar(p, scope))
34
+ .replace(/\$\{\s*([a-zA-Z_][\w.]*)\s*\}/g, (_, p) => resolveVar(p, scope));
35
+ }
36
+ export function EmailTemplatePreview({ draft }) {
37
+ const subject = String(draft.subject ?? '');
38
+ const from = String(draft.from ?? draft.fromAddress ?? '');
39
+ const to = String(draft.to ?? '');
40
+ const bodyHtml = String(draft.bodyHtml ?? draft.html ?? draft.body ?? '');
41
+ const bodyText = String(draft.bodyText ?? draft.text ?? '');
42
+ const variables = React.useMemo(() => {
43
+ const all = new Set();
44
+ for (const v of detectVariables(subject))
45
+ all.add(v);
46
+ for (const v of detectVariables(bodyHtml))
47
+ all.add(v);
48
+ for (const v of detectVariables(bodyText))
49
+ all.add(v);
50
+ return Array.from(all).sort();
51
+ }, [subject, bodyHtml, bodyText]);
52
+ const [scope, setScope] = React.useState({});
53
+ const resolvedSubject = substitute(subject, scope);
54
+ const resolvedHtml = bodyHtml ? substitute(bodyHtml, scope) : substitute(bodyText, scope).replace(/\n/g, '<br/>');
55
+ // Wrap the body in a minimal HTML doc so emails missing their own
56
+ // `<html>` shell still render with a sensible default font.
57
+ const srcDoc = `<!doctype html><html><head><meta charset="utf-8"><style>
58
+ html,body{margin:0;padding:16px;font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#111;background:#fff;}
59
+ a{color:#2563eb;}
60
+ </style></head><body>${resolvedHtml || '<p style="color:#888">(empty body)</p>'}</body></html>`;
61
+ if (!bodyHtml && !bodyText && !subject) {
62
+ return (_jsx(PreviewShell, { hint: "email_template", children: _jsx(PreviewMessage, { children: "Fill in the subject / body in the Form tab to see a preview." }) }));
63
+ }
64
+ return (_jsx(PreviewShell, { hint: "email_template", children: _jsx(PreviewErrorBoundary, { children: _jsxs("div", { className: "grid lg:grid-cols-[1fr_220px] gap-0", children: [_jsxs("div", { className: "p-3 space-y-3 min-w-0", children: [_jsxs("div", { className: "rounded border bg-muted/30 px-3 py-2 text-xs space-y-0.5", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "w-12 text-muted-foreground shrink-0", children: "From" }), _jsx("span", { className: "font-mono truncate", children: from || '—' })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "w-12 text-muted-foreground shrink-0", children: "To" }), _jsx("span", { className: "font-mono truncate", children: to || '—' })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "w-12 text-muted-foreground shrink-0", children: "Subject" }), _jsx("span", { className: "font-medium truncate", children: resolvedSubject || '—' })] })] }), _jsx("iframe", { title: "Email preview", srcDoc: srcDoc, sandbox: "allow-same-origin", className: "w-full min-h-[400px] max-h-[60vh] border rounded bg-white" })] }), _jsxs("div", { className: "border-l bg-muted/20 p-3 text-xs space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5 font-medium text-muted-foreground", children: [_jsx(Mail, { className: "h-3 w-3" }), " Variables"] }), variables.length === 0 ? (_jsxs("div", { className: "text-muted-foreground italic", children: ["No ", _jsx("code", { children: '{{var}}' }), " placeholders found."] })) : (_jsx("div", { className: "space-y-2", children: variables.map((v) => (_jsxs("label", { className: "block", children: [_jsx("span", { className: "block font-mono text-[10px] text-muted-foreground mb-0.5", children: v }), _jsx("input", { type: "text", value: scope[v] ?? '', onChange: (e) => setScope((s) => ({ ...s, [v]: e.target.value })), placeholder: `sample for ${v}`, className: "w-full text-xs px-2 py-1 border rounded bg-background" })] }, v))) }))] })] }) }) }));
65
+ }
@@ -0,0 +1,2 @@
1
+ import type { MetadataPreviewProps } from '../preview-registry';
2
+ export declare function ObjectPreview({ name, draft }: MetadataPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * ObjectPreview — render the object exactly the way the console does it:
5
+ * via the same `object-view` SchemaRenderer the runtime route uses, with
6
+ * the live adapter behind it. Authors see real records, real localized
7
+ * column labels, real type-aware cell formatters (booleans → checkbox,
8
+ * dates → locale string, refs → links), real search / filter chrome.
9
+ *
10
+ * This is a deliberate change from the earlier hand-rolled table: keeping
11
+ * two implementations in sync was bound to drift, and the user explicitly
12
+ * asked for preview parity with the console.
13
+ */
14
+ import * as React from 'react';
15
+ import { SchemaRenderer } from '@object-ui/react';
16
+ import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell';
17
+ export function ObjectPreview({ name, draft }) {
18
+ const objectName = String(draft.name ?? name ?? '');
19
+ if (!objectName) {
20
+ return (_jsx(PreviewShell, { hint: "object", children: _jsx(PreviewMessage, { children: "Give the object a name to enable preview." }) }));
21
+ }
22
+ // Reuse the exact same SDUI component the runtime route renders, so the
23
+ // preview inherits localized headers, type-aware cell formatters, view
24
+ // switcher, search, filter, sort and pagination chrome out of the box.
25
+ const schema = React.useMemo(() => ({
26
+ type: 'object-view',
27
+ objectName,
28
+ defaultViewType: 'grid',
29
+ showSearch: true,
30
+ showFilters: true,
31
+ showCreate: false,
32
+ showRefresh: true,
33
+ showViewSwitcher: true,
34
+ }), [objectName]);
35
+ return (_jsx(PreviewShell, { hint: "object \u00B7 live data", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The object metadata couldn't be rendered. Save the draft and reload to retry.", children: _jsx("div", { className: "max-h-[75vh] overflow-auto", children: _jsx(SchemaRenderer, { schema: schema }) }) }) }));
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { MetadataPreviewProps } from '../preview-registry';
2
+ export declare function PagePreview({ draft }: MetadataPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * PagePreview — renders a Page metadata record using the runtime
5
+ * SchemaRenderer so authors see exactly what end-users would see.
6
+ *
7
+ * Reads the live draft (not the server-saved record) so edits in the
8
+ * Form tab preview instantly. URL query params are intentionally not
9
+ * threaded in: previews run in a sandbox with no params context.
10
+ */
11
+ import * as React from 'react';
12
+ import { SchemaRenderer } from '@object-ui/react';
13
+ import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
14
+ export function PagePreview({ draft }) {
15
+ const schema = React.useMemo(() => {
16
+ // SchemaRenderer needs a `type` discriminator. Page schemas may
17
+ // omit it (Page is the implicit type at this metadata level), so
18
+ // we inject it if missing while preserving any explicit override.
19
+ const t = draft.type ?? 'page';
20
+ return { ...draft, type: t };
21
+ }, [draft]);
22
+ if (!schema || Object.keys(schema).length <= 1) {
23
+ return (_jsx(PreviewShell, { hint: "page", children: _jsx(PreviewMessage, { children: "Add components to the page to see a preview." }) }));
24
+ }
25
+ return (_jsx(PreviewShell, { hint: "page", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The Page schema is incomplete or references a component that hasn't been registered yet.", children: _jsx("div", { className: "min-h-[200px] max-h-[70vh] overflow-auto p-4", children: _jsx(SchemaRenderer, { schema: schema }) }) }) }));
26
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared chrome for Preview-tab renderers — gives every preview the
3
+ * same border, padding, header strip, and empty/error states so they
4
+ * feel like one feature instead of seven one-offs.
5
+ */
6
+ import * as React from 'react';
7
+ export interface PreviewShellProps {
8
+ /** Right-hand badge/label, e.g. the resolved view type or row count. */
9
+ hint?: React.ReactNode;
10
+ /** Optional title override. Defaults to `Preview`. */
11
+ title?: React.ReactNode;
12
+ /** Optional toolbar rendered on the right of the header. */
13
+ toolbar?: React.ReactNode;
14
+ children: React.ReactNode;
15
+ }
16
+ export declare function PreviewShell({ hint, title, toolbar, children }: PreviewShellProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function PreviewMessage({ tone, children, }: {
18
+ tone?: 'info' | 'warn' | 'error';
19
+ children: React.ReactNode;
20
+ }): import("react/jsx-runtime").JSX.Element;
21
+ /**
22
+ * Catch render errors from third-party preview renderers so a buggy
23
+ * widget can't blank the whole edit page. Keeps the rest of the tabs
24
+ * (Form / Layers / References) usable.
25
+ */
26
+ export declare class PreviewErrorBoundary extends React.Component<{
27
+ children: React.ReactNode;
28
+ fallbackHint?: string;
29
+ }, {
30
+ error: Error | null;
31
+ }> {
32
+ state: {
33
+ error: Error | null;
34
+ };
35
+ static getDerivedStateFromError(error: Error): {
36
+ error: Error;
37
+ };
38
+ componentDidCatch(error: Error): void;
39
+ render(): import("react/jsx-runtime").JSX.Element;
40
+ }
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * Shared chrome for Preview-tab renderers — gives every preview the
5
+ * same border, padding, header strip, and empty/error states so they
6
+ * feel like one feature instead of seven one-offs.
7
+ */
8
+ import * as React from 'react';
9
+ import { AlertCircle, Eye } from 'lucide-react';
10
+ export function PreviewShell({ hint, title = 'Preview', toolbar, children }) {
11
+ return (_jsxs("div", { className: "rounded-lg border bg-background overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between border-b bg-muted/30 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground", children: [_jsx(Eye, { className: "h-3.5 w-3.5" }), _jsx("span", { children: title }), hint != null && (_jsx("span", { className: "ml-1 text-[10px] uppercase tracking-wider opacity-70", children: hint }))] }), toolbar] }), _jsx("div", { className: "bg-background", children: children })] }));
12
+ }
13
+ export function PreviewMessage({ tone = 'info', children, }) {
14
+ const styles = tone === 'warn'
15
+ ? 'text-amber-800 bg-amber-50 border-amber-200'
16
+ : tone === 'error'
17
+ ? 'text-destructive bg-destructive/5 border-destructive/30'
18
+ : 'text-muted-foreground border-muted';
19
+ return (_jsxs("div", { className: `m-4 rounded border p-3 text-sm flex items-start gap-2 ${styles}`, children: [tone !== 'info' && _jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("div", { className: "flex-1", children: children })] }));
20
+ }
21
+ /**
22
+ * Catch render errors from third-party preview renderers so a buggy
23
+ * widget can't blank the whole edit page. Keeps the rest of the tabs
24
+ * (Form / Layers / References) usable.
25
+ */
26
+ export class PreviewErrorBoundary extends React.Component {
27
+ constructor() {
28
+ super(...arguments);
29
+ Object.defineProperty(this, "state", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: { error: null }
34
+ });
35
+ }
36
+ static getDerivedStateFromError(error) {
37
+ return { error };
38
+ }
39
+ componentDidCatch(error) {
40
+ // eslint-disable-next-line no-console
41
+ console.error('[MetadataPreview] render failed', error);
42
+ }
43
+ render() {
44
+ if (this.state.error) {
45
+ return (_jsxs(PreviewMessage, { tone: "error", children: [_jsx("div", { className: "font-medium", children: "Preview failed to render" }), _jsx("div", { className: "text-xs mt-1 font-mono opacity-80", children: this.state.error.message }), this.props.fallbackHint && (_jsx("div", { className: "text-xs mt-2 opacity-70", children: this.props.fallbackHint }))] }));
46
+ }
47
+ return this.props.children;
48
+ }
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { MetadataPreviewProps } from '../preview-registry';
2
+ export declare function ReportPreview({ draft }: MetadataPreviewProps): import("react/jsx-runtime").JSX.Element;