@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.
- package/CHANGELOG.md +61 -0
- package/dist/console/ai/AiChatPage.js +19 -22
- package/dist/layout/ConsoleFloatingChatbot.js +44 -24
- 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.js +2 -26
- package/dist/views/metadata-admin/RelatedPanel.d.ts +4 -0
- package/dist/views/metadata-admin/RelatedPanel.js +2 -0
- package/dist/views/metadata-admin/ResourceEditPage.js +50 -5
- package/dist/views/metadata-admin/anchors.js +6 -0
- package/dist/views/metadata-admin/index.d.ts +2 -0
- package/dist/views/metadata-admin/index.js +6 -0
- 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 +17 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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,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
|
+
}
|