@object-ui/app-shell 6.2.0 → 6.2.2
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.
- package/CHANGELOG.md +61 -0
- package/dist/console/AppContent.js +30 -1
- package/dist/console/ai/AiChatPage.js +19 -22
- package/dist/layout/ConsoleFloatingChatbot.js +44 -24
- package/dist/services/builtinComponents.js +10 -7
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +11 -1
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +21 -3
- package/dist/views/metadata-admin/DirectoryPage.js +1 -1
- package/dist/views/metadata-admin/EmbeddedItemEditor.d.ts +15 -0
- package/dist/views/metadata-admin/EmbeddedItemEditor.js +194 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.js +28 -0
- package/dist/views/metadata-admin/PageShell.js +9 -2
- package/dist/views/metadata-admin/QuickFind.js +2 -2
- package/dist/views/metadata-admin/RelatedPanel.d.ts +37 -0
- package/dist/views/metadata-admin/RelatedPanel.js +173 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +9 -3
- package/dist/views/metadata-admin/ResourceEditPage.js +105 -14
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +3 -3
- package/dist/views/metadata-admin/ResourceHistoryPage.js +6 -3
- package/dist/views/metadata-admin/ResourceListPage.d.ts +2 -2
- package/dist/views/metadata-admin/ResourceListPage.js +6 -4
- package/dist/views/metadata-admin/SchemaForm.js +10 -1
- package/dist/views/metadata-admin/anchors.d.ts +1 -0
- package/dist/views/metadata-admin/anchors.js +235 -0
- package/dist/views/metadata-admin/index.d.ts +7 -3
- package/dist/views/metadata-admin/index.js +14 -2
- package/dist/views/metadata-admin/preview-registry.d.ts +43 -0
- package/dist/views/metadata-admin/preview-registry.js +18 -0
- package/dist/views/metadata-admin/previews/AppPreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/AppPreview.js +101 -0
- package/dist/views/metadata-admin/previews/DashboardPreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/DashboardPreview.js +25 -0
- package/dist/views/metadata-admin/previews/EmailTemplatePreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/EmailTemplatePreview.js +65 -0
- package/dist/views/metadata-admin/previews/ObjectPreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/ObjectPreview.js +36 -0
- package/dist/views/metadata-admin/previews/PagePreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/PagePreview.js +26 -0
- package/dist/views/metadata-admin/previews/PreviewShell.d.ts +40 -0
- package/dist/views/metadata-admin/previews/PreviewShell.js +49 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/ReportPreview.js +27 -0
- package/dist/views/metadata-admin/previews/ViewPreview.d.ts +2 -0
- package/dist/views/metadata-admin/previews/ViewPreview.js +113 -0
- package/dist/views/metadata-admin/previews/index.d.ts +1 -0
- package/dist/views/metadata-admin/previews/index.js +26 -0
- package/dist/views/metadata-admin/registry.d.ts +124 -0
- package/dist/views/metadata-admin/registry.js +58 -0
- package/package.json +26 -26
|
@@ -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,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,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,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,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,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,27 @@
|
|
|
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
|
+
* ReportPreview — runs the live Report draft through the same
|
|
5
|
+
* ReportRenderer the runtime ReportView uses.
|
|
6
|
+
*
|
|
7
|
+
* Uses the app-shell AdapterProvider's data source so previews see
|
|
8
|
+
* actual rows. Lazy-loaded to keep the metadata-admin bundle small.
|
|
9
|
+
*/
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
import { Loader2 } from 'lucide-react';
|
|
12
|
+
import { useAdapter } from '../../../providers/AdapterProvider';
|
|
13
|
+
import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
|
|
14
|
+
const ReportRenderer = React.lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportRenderer })));
|
|
15
|
+
export function ReportPreview({ draft }) {
|
|
16
|
+
const adapter = useAdapter();
|
|
17
|
+
// Different fixture sets use different keys for the source object:
|
|
18
|
+
// • new schema: `object`
|
|
19
|
+
// • legacy: `objectName`
|
|
20
|
+
// • some reports embed it under `data.object`
|
|
21
|
+
const objectName = draft.object ?? draft.objectName ?? draft.data?.object;
|
|
22
|
+
const visualization = draft.visualization?.type ?? draft.type;
|
|
23
|
+
if (!objectName) {
|
|
24
|
+
return (_jsx(PreviewShell, { hint: "report", children: _jsx(PreviewMessage, { tone: "warn", children: "Pick an Object in the Form tab \u2014 Reports need a source object before they can render." }) }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsx(PreviewShell, { hint: `report · ${visualization ?? 'table'}`, children: _jsx(PreviewErrorBoundary, { fallbackHint: "The Report references an object/field that doesn't resolve, or its visualization config is incomplete.", 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 report renderer\u2026"] }), children: _jsx("div", { className: "p-3 min-h-[300px] max-h-[70vh] overflow-auto", children: _jsx(ReportRenderer, { schema: draft, dataSource: adapter }) }) }) }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
* ViewPreview — renders a View metadata draft using the same
|
|
5
|
+
* `object-view` SchemaRenderer the runtime ObjectView route uses,
|
|
6
|
+
* with the current draft's variants injected as `listViews` so the
|
|
7
|
+
* draft drives what authors see (not the saved version).
|
|
8
|
+
*
|
|
9
|
+
* If the draft is a "single-schema legacy" view (no list/form/kanban
|
|
10
|
+
* wrappers, just one top-level `type`), we pass the schema straight
|
|
11
|
+
* to SchemaRenderer.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import { SchemaRenderer } from '@object-ui/react';
|
|
15
|
+
import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
|
|
16
|
+
const VIEW_VARIANT_KEYS = [
|
|
17
|
+
'list',
|
|
18
|
+
'form',
|
|
19
|
+
'kanban',
|
|
20
|
+
'calendar',
|
|
21
|
+
'gantt',
|
|
22
|
+
'map',
|
|
23
|
+
'gallery',
|
|
24
|
+
'timeline',
|
|
25
|
+
'feed',
|
|
26
|
+
'detail',
|
|
27
|
+
];
|
|
28
|
+
function detectVariants(draft) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const k of VIEW_VARIANT_KEYS) {
|
|
31
|
+
const v = draft[k];
|
|
32
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
33
|
+
out.push({ key: k, schema: v });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function resolveObjectName(draft, variantSchema) {
|
|
39
|
+
const candidates = [
|
|
40
|
+
variantSchema?.object,
|
|
41
|
+
variantSchema?.data?.object,
|
|
42
|
+
variantSchema?.objectName,
|
|
43
|
+
draft.object,
|
|
44
|
+
draft.objectName,
|
|
45
|
+
draft.data?.object,
|
|
46
|
+
draft.list?.data?.object,
|
|
47
|
+
draft.list?.object,
|
|
48
|
+
];
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
if (typeof c === 'string' && c)
|
|
51
|
+
return c;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export function ViewPreview({ name, draft }) {
|
|
56
|
+
const variants = React.useMemo(() => detectVariants(draft), [draft]);
|
|
57
|
+
const objectName = React.useMemo(() => resolveObjectName(draft, variants[0]?.schema), [draft, variants]);
|
|
58
|
+
// Compose the listViews map: the draft IS the "default" — surface it as a
|
|
59
|
+
// primary named view FIRST so the view switcher picks it as default. Then
|
|
60
|
+
// append any saved sibling named listViews.
|
|
61
|
+
const { listViews, defaultViewId } = React.useMemo(() => {
|
|
62
|
+
const out = {};
|
|
63
|
+
const primaryVariant = variants.find((v) => v.key === 'list') ?? variants[0];
|
|
64
|
+
const primaryId = String(name) || primaryVariant?.key || 'default';
|
|
65
|
+
if (primaryVariant) {
|
|
66
|
+
out[primaryId] = {
|
|
67
|
+
...primaryVariant.schema,
|
|
68
|
+
label: primaryVariant.schema.label ?? draft.label ?? name,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const saved = draft.listViews;
|
|
72
|
+
if (saved && typeof saved === 'object' && !Array.isArray(saved)) {
|
|
73
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
74
|
+
if (v && typeof v === 'object' && k !== primaryId)
|
|
75
|
+
out[k] = v;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { listViews: out, defaultViewId: primaryVariant ? primaryId : undefined };
|
|
79
|
+
}, [draft, variants, name]);
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
// Path A — single-schema legacy view: render directly.
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
if (!variants.length && draft.type) {
|
|
84
|
+
const schema = { ...draft };
|
|
85
|
+
return (_jsx(PreviewShell, { hint: `view · ${schema.type}`, children: _jsx(PreviewErrorBoundary, { fallbackHint: "The view's `type` may not be registered, or required fields are missing.", children: _jsx("div", { className: "min-h-[300px] max-h-[75vh] overflow-auto", children: _jsx(SchemaRenderer, { schema: schema }) }) }) }));
|
|
86
|
+
}
|
|
87
|
+
if (!objectName) {
|
|
88
|
+
return (_jsx(PreviewShell, { hint: "view", children: _jsxs(PreviewMessage, { tone: "warn", children: ["This view has no object binding yet. Set ", _jsx("code", { children: "list.data.object" }), " in the Form tab to fetch live data."] }) }));
|
|
89
|
+
}
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
// Path B — multi-variant view: delegate to `object-view`, which is what
|
|
92
|
+
// the runtime route uses. Inject the draft's listViews so the preview
|
|
93
|
+
// reflects unsaved edits.
|
|
94
|
+
// -------------------------------------------------------------------------
|
|
95
|
+
const defaultViewType = variants[0]?.schema?.type ?? 'grid';
|
|
96
|
+
const schema = React.useMemo(() => ({
|
|
97
|
+
type: 'object-view',
|
|
98
|
+
objectName,
|
|
99
|
+
defaultViewType,
|
|
100
|
+
defaultListView: defaultViewId,
|
|
101
|
+
listViews,
|
|
102
|
+
showSearch: true,
|
|
103
|
+
showFilters: true,
|
|
104
|
+
showCreate: false,
|
|
105
|
+
showRefresh: true,
|
|
106
|
+
showViewSwitcher: true,
|
|
107
|
+
}), [objectName, defaultViewType, defaultViewId, listViews]);
|
|
108
|
+
const variantHint = variants
|
|
109
|
+
.map((v) => v.key)
|
|
110
|
+
.slice(0, 3)
|
|
111
|
+
.join(' · ');
|
|
112
|
+
return (_jsx(PreviewShell, { hint: `view · ${variantHint || 'list'}`, children: _jsx(PreviewErrorBoundary, { fallbackHint: "The view references an object or field that doesn't resolve.", children: _jsx("div", { className: "min-h-[300px] max-h-[75vh] overflow-auto", children: _jsx(SchemaRenderer, { schema: schema }) }) }) }));
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerBuiltinPreviews(): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Built-in Preview-tab registrations.
|
|
4
|
+
*
|
|
5
|
+
* Each registration is one line; new types are trivially added without
|
|
6
|
+
* touching `ResourceEditPage.tsx`. To opt OUT of a built-in preview in
|
|
7
|
+
* a downstream app, call `registerMetadataPreview(type, MyVersion)`
|
|
8
|
+
* after this module runs — `registerMetadataPreview` is last-write-wins.
|
|
9
|
+
*/
|
|
10
|
+
import { registerMetadataPreview } from '../preview-registry';
|
|
11
|
+
import { PagePreview } from './PagePreview';
|
|
12
|
+
import { ViewPreview } from './ViewPreview';
|
|
13
|
+
import { DashboardPreview } from './DashboardPreview';
|
|
14
|
+
import { ReportPreview } from './ReportPreview';
|
|
15
|
+
import { AppPreview } from './AppPreview';
|
|
16
|
+
import { ObjectPreview } from './ObjectPreview';
|
|
17
|
+
import { EmailTemplatePreview } from './EmailTemplatePreview';
|
|
18
|
+
export function registerBuiltinPreviews() {
|
|
19
|
+
registerMetadataPreview('page', PagePreview);
|
|
20
|
+
registerMetadataPreview('view', ViewPreview);
|
|
21
|
+
registerMetadataPreview('dashboard', DashboardPreview);
|
|
22
|
+
registerMetadataPreview('report', ReportPreview);
|
|
23
|
+
registerMetadataPreview('app', AppPreview);
|
|
24
|
+
registerMetadataPreview('object', ObjectPreview);
|
|
25
|
+
registerMetadataPreview('email_template', EmailTemplatePreview);
|
|
26
|
+
}
|