@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/console/AppContent.js +30 -1
  3. package/dist/console/ai/AiChatPage.js +19 -22
  4. package/dist/layout/ConsoleFloatingChatbot.js +44 -24
  5. package/dist/services/builtinComponents.js +10 -7
  6. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +11 -1
  7. package/dist/views/metadata-admin/DesignerEditorWrapper.js +21 -3
  8. package/dist/views/metadata-admin/DirectoryPage.js +1 -1
  9. package/dist/views/metadata-admin/EmbeddedItemEditor.d.ts +15 -0
  10. package/dist/views/metadata-admin/EmbeddedItemEditor.js +194 -0
  11. package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
  12. package/dist/views/metadata-admin/MetadataDetailDrawer.js +28 -0
  13. package/dist/views/metadata-admin/PageShell.js +9 -2
  14. package/dist/views/metadata-admin/QuickFind.js +2 -2
  15. package/dist/views/metadata-admin/RelatedPanel.d.ts +37 -0
  16. package/dist/views/metadata-admin/RelatedPanel.js +173 -0
  17. package/dist/views/metadata-admin/ResourceEditPage.d.ts +9 -3
  18. package/dist/views/metadata-admin/ResourceEditPage.js +105 -14
  19. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +3 -3
  20. package/dist/views/metadata-admin/ResourceHistoryPage.js +6 -3
  21. package/dist/views/metadata-admin/ResourceListPage.d.ts +2 -2
  22. package/dist/views/metadata-admin/ResourceListPage.js +6 -4
  23. package/dist/views/metadata-admin/SchemaForm.js +10 -1
  24. package/dist/views/metadata-admin/anchors.d.ts +1 -0
  25. package/dist/views/metadata-admin/anchors.js +235 -0
  26. package/dist/views/metadata-admin/index.d.ts +7 -3
  27. package/dist/views/metadata-admin/index.js +14 -2
  28. package/dist/views/metadata-admin/preview-registry.d.ts +43 -0
  29. package/dist/views/metadata-admin/preview-registry.js +18 -0
  30. package/dist/views/metadata-admin/previews/AppPreview.d.ts +2 -0
  31. package/dist/views/metadata-admin/previews/AppPreview.js +101 -0
  32. package/dist/views/metadata-admin/previews/DashboardPreview.d.ts +2 -0
  33. package/dist/views/metadata-admin/previews/DashboardPreview.js +25 -0
  34. package/dist/views/metadata-admin/previews/EmailTemplatePreview.d.ts +2 -0
  35. package/dist/views/metadata-admin/previews/EmailTemplatePreview.js +65 -0
  36. package/dist/views/metadata-admin/previews/ObjectPreview.d.ts +2 -0
  37. package/dist/views/metadata-admin/previews/ObjectPreview.js +36 -0
  38. package/dist/views/metadata-admin/previews/PagePreview.d.ts +2 -0
  39. package/dist/views/metadata-admin/previews/PagePreview.js +26 -0
  40. package/dist/views/metadata-admin/previews/PreviewShell.d.ts +40 -0
  41. package/dist/views/metadata-admin/previews/PreviewShell.js +49 -0
  42. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +2 -0
  43. package/dist/views/metadata-admin/previews/ReportPreview.js +27 -0
  44. package/dist/views/metadata-admin/previews/ViewPreview.d.ts +2 -0
  45. package/dist/views/metadata-admin/previews/ViewPreview.js +113 -0
  46. package/dist/views/metadata-admin/previews/index.d.ts +1 -0
  47. package/dist/views/metadata-admin/previews/index.js +26 -0
  48. package/dist/views/metadata-admin/registry.d.ts +124 -0
  49. package/dist/views/metadata-admin/registry.js +58 -0
  50. package/package.json +26 -26
@@ -20,8 +20,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
20
20
  * EditPage via `registerMetadataResource()`.
21
21
  */
22
22
  import * as React from 'react';
23
- import { useNavigate } from 'react-router-dom';
24
- import { Save, RotateCcw, History, Link2, Loader2, AlertTriangle, } from 'lucide-react';
23
+ import { useNavigate, useParams } from 'react-router-dom';
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';
@@ -31,8 +31,14 @@ import { PageShell } from './PageShell';
31
31
  import { LayeredDiff } from './LayeredDiff';
32
32
  import { SchemaForm } from './SchemaForm';
33
33
  import { useMetadataClient, useMetadataTypes, } from './useMetadata';
34
- import { getMetadataResource, resolveResourceConfig, } from './registry';
35
- export function MetadataResourceEditPage({ type, name, createMode = false, }) {
34
+ import { getMetadataResource, resolveResourceConfig, listAnchorsFor, } from './registry';
35
+ import { RelatedPanel } from './RelatedPanel';
36
+ import { MetadataDetailDrawer } from './MetadataDetailDrawer';
37
+ import { getMetadataPreview } from './preview-registry';
38
+ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode = false, embedded = false, }) {
39
+ const params = useParams();
40
+ const type = typeProp ?? params.type ?? '';
41
+ const name = nameProp ?? params.name ?? '';
36
42
  const navigate = useNavigate();
37
43
  const client = useMetadataClient();
38
44
  const { entries } = useMetadataTypes(client);
@@ -57,6 +63,15 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
57
63
  const [issues, setIssues] = React.useState([]);
58
64
  const [destructiveIssues, setDestructiveIssues] = React.useState(null);
59
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);
60
75
  // Prefetch object name list once — fuels the `ref:object` widget.
61
76
  // We don't block render on it; the widget shows a "Loading…" state.
62
77
  const [objectNames, setObjectNames] = React.useState([]);
@@ -102,6 +117,7 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
102
117
  // Initial draft = effective if available, otherwise code.
103
118
  const initial = (lay.effective ?? lay.code ?? {});
104
119
  setDraft(initial);
120
+ draftSnapshotRef.current = initial;
105
121
  setLoading(false);
106
122
  }
107
123
  catch (err) {
@@ -134,6 +150,45 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
134
150
  setRefsLoading(false);
135
151
  }
136
152
  }
153
+ // Related drawer state. `null` = closed. We avoid querystring round-
154
+ // trips on every keystroke; URL state is best-effort sync via effect
155
+ // below.
156
+ const [relatedTarget, setRelatedTarget] = React.useState(null);
157
+ const hasAnchors = React.useMemo(() => !createMode && !embedded && listAnchorsFor(type).length > 0, [type, createMode, embedded]);
158
+ // Read ?tab and ?open on first mount so deep-links work. Embedded
159
+ // items are not deep-linkable (they live in the parent body and need
160
+ // the parent payload to materialise) so we only restore metadata
161
+ // targets here.
162
+ const initialTabRef = React.useRef(null);
163
+ React.useEffect(() => {
164
+ if (typeof window === 'undefined' || embedded)
165
+ return;
166
+ const sp = new URLSearchParams(window.location.search);
167
+ const tab = sp.get('tab');
168
+ if (tab)
169
+ initialTabRef.current = tab;
170
+ const open = sp.get('open');
171
+ if (open && open.includes(':')) {
172
+ const [t, n] = open.split(':', 2);
173
+ if (t && n)
174
+ setRelatedTarget({ kind: 'metadata', type: t, name: n });
175
+ }
176
+ // intentionally empty deps — first mount only
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ }, []);
179
+ // Reflect drawer target into the URL so refresh/share works.
180
+ React.useEffect(() => {
181
+ if (typeof window === 'undefined' || embedded)
182
+ return;
183
+ const url = new URL(window.location.href);
184
+ if (relatedTarget?.kind === 'metadata') {
185
+ url.searchParams.set('open', `${relatedTarget.type}:${relatedTarget.name}`);
186
+ }
187
+ else {
188
+ url.searchParams.delete('open');
189
+ }
190
+ window.history.replaceState({}, '', url.toString());
191
+ }, [relatedTarget, embedded]);
137
192
  async function doSave(force) {
138
193
  setSaving(true);
139
194
  setError(null);
@@ -153,17 +208,17 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
153
208
  // Refresh layered after save.
154
209
  const lay = await client.layered(type, savedName);
155
210
  setLayered(lay);
156
- setDraft((lay.effective ?? itemToSave));
211
+ const fresh = (lay.effective ?? itemToSave);
212
+ setDraft(fresh);
213
+ draftSnapshotRef.current = fresh;
157
214
  setDestructiveIssues(null);
158
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);
159
220
  if (createMode) {
160
- // Use absolute path — react-router resolves `../` against the
161
- // matched route pattern (`/apps/:app/component/:ns/:name/*`),
162
- // which overshoots and lands at `/apps/:app`. Anchor on the
163
- // location's pathname for predictable behaviour.
164
- const here = window.location.pathname; // .../resource/new
165
- const parent = here.replace(/\/new\/?$/, '');
166
- navigate(`${parent}/${encodeURIComponent(savedName)}?type=${encodeURIComponent(type)}`);
221
+ navigate(`../${encodeURIComponent(savedName)}`);
167
222
  }
168
223
  }
169
224
  catch (err) {
@@ -223,7 +278,10 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
223
278
  await client.reset(type, name);
224
279
  const lay = await client.layered(type, name);
225
280
  setLayered(lay);
226
- setDraft((lay.effective ?? lay.code ?? {}));
281
+ const fresh = (lay.effective ?? lay.code ?? {});
282
+ setDraft(fresh);
283
+ draftSnapshotRef.current = fresh;
284
+ setEditing(false);
227
285
  }
228
286
  catch (err) {
229
287
  setError(err?.message ?? String(err));
@@ -238,7 +296,40 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
238
296
  const schema = entry?.schema ??
239
297
  config.defaultSchema;
240
298
  const readOnly = !entry?.allowOrgOverride && !createMode;
241
- 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?type=${encodeURIComponent(type)}`), 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: "form", className: "w-full", children: [_jsxs(TabsList, { children: [_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 }))] }))] }), _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 }) }))] })] }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
299
+ const DesignerTab = !createMode ? customConfig?.DesignerTab : undefined;
300
+ const designerTabLabel = customConfig?.designerTabLabel ?? 'Designer';
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) => {
327
+ if (typeof window === 'undefined' || embedded)
328
+ return;
329
+ const url = new URL(window.location.href);
330
+ url.searchParams.set('tab', v);
331
+ window.history.replaceState({}, '', url.toString());
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) => {
242
333
  if (!open) {
243
334
  setDestructiveIssues(null);
244
335
  setPendingItem(null);
@@ -1,5 +1,5 @@
1
1
  export interface MetadataResourceHistoryPageProps {
2
- type: string;
3
- name: string;
2
+ type?: string;
3
+ name?: string;
4
4
  }
5
- export declare function MetadataResourceHistoryPage({ type, name, }: MetadataResourceHistoryPageProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare function MetadataResourceHistoryPage({ type: typeProp, name: nameProp, }: MetadataResourceHistoryPageProps): import("react/jsx-runtime").JSX.Element;
@@ -15,14 +15,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
15
15
  * once admins ask for it.
16
16
  */
17
17
  import * as React from 'react';
18
- import { useNavigate } from 'react-router-dom';
18
+ import { useNavigate, useParams } from 'react-router-dom';
19
19
  import { ArrowLeft, RefreshCw, Loader2 } from 'lucide-react';
20
20
  import { Button } from '@object-ui/components';
21
21
  import { Badge } from '@object-ui/components';
22
22
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
23
23
  import { PageShell } from './PageShell';
24
24
  import { useMetadataClient, useMetadataTypes, } from './useMetadata';
25
- export function MetadataResourceHistoryPage({ type, name, }) {
25
+ export function MetadataResourceHistoryPage({ type: typeProp, name: nameProp, }) {
26
+ const params = useParams();
27
+ const type = typeProp ?? params.type ?? '';
28
+ const name = nameProp ?? params.name ?? '';
26
29
  const navigate = useNavigate();
27
30
  const client = useMetadataClient();
28
31
  const { entries } = useMetadataTypes(client);
@@ -62,7 +65,7 @@ export function MetadataResourceHistoryPage({ type, name, }) {
62
65
  cancelled = true;
63
66
  };
64
67
  }, [client, type, name, refreshKey]);
65
- return (_jsx(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: "Version history", stats: [{ label: 'Events', value: events.length }], actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`../${encodeURIComponent(name)}?type=${encodeURIComponent(type)}`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Back to item"] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), children: _jsx(RefreshCw, { className: "h-4 w-4" }) })] }), children: _jsxs("div", { className: "p-6 max-w-4xl", children: [loading && (_jsxs("div", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading history\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && events.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No history yet" }), _jsx(EmptyDescription, { children: "This item has never been edited via an overlay. The first save will create the initial history record." })] })), !loading && events.length > 0 && (_jsx("ol", { className: "space-y-2 relative pl-6 border-l", children: events.map((ev, i) => {
68
+ return (_jsx(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: "Version history", stats: [{ label: 'Events', value: events.length }], actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`../${encodeURIComponent(name)}`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Back to item"] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), children: _jsx(RefreshCw, { className: "h-4 w-4" }) })] }), children: _jsxs("div", { className: "p-6 max-w-4xl", children: [loading && (_jsxs("div", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading history\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && events.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No history yet" }), _jsx(EmptyDescription, { children: "This item has never been edited via an overlay. The first save will create the initial history record." })] })), !loading && events.length > 0 && (_jsx("ol", { className: "space-y-2 relative pl-6 border-l", children: events.map((ev, i) => {
66
69
  const isOpen = expanded === i;
67
70
  return (_jsxs("li", { className: "relative", children: [_jsx("span", { className: "absolute -left-[27px] top-2 w-3 h-3 rounded-full bg-primary ring-4 ring-background" }), _jsxs("div", { role: "button", tabIndex: 0, onClick: () => setExpanded(isOpen ? null : i), onKeyDown: (e) => {
68
71
  if (e.key === 'Enter' || e.key === ' ') {
@@ -1,4 +1,4 @@
1
1
  export interface MetadataResourceListPageProps {
2
- type: string;
2
+ type?: string;
3
3
  }
4
- export declare function MetadataResourceListPage({ type }: MetadataResourceListPageProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare function MetadataResourceListPage({ type: typeProp }: MetadataResourceListPageProps): import("react/jsx-runtime").JSX.Element;
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
12
12
  * per type, well under the threshold where it'd matter.
13
13
  */
14
14
  import * as React from 'react';
15
- import { Link, useNavigate } from 'react-router-dom';
15
+ import { Link, useNavigate, useParams } from 'react-router-dom';
16
16
  import { Plus, Search, RefreshCw } from 'lucide-react';
17
17
  import { Button } from '@object-ui/components';
18
18
  import { Input } from '@object-ui/components';
@@ -23,7 +23,9 @@ import { PageShell } from './PageShell';
23
23
  import { useMetadataClient, useMetadataTypes, matchesQuery, } from './useMetadata';
24
24
  import { getMetadataResource, resolveResourceConfig, } from './registry';
25
25
  import { t, detectLocale } from './i18n';
26
- export function MetadataResourceListPage({ type }) {
26
+ export function MetadataResourceListPage({ type: typeProp }) {
27
+ const params = useParams();
28
+ const type = typeProp ?? params.type ?? '';
27
29
  const navigate = useNavigate();
28
30
  const client = useMetadataClient();
29
31
  const { entries: typesEntries } = useMetadataTypes(client);
@@ -101,7 +103,7 @@ export function MetadataResourceListPage({ type }) {
101
103
  return (_jsx(PageShell, { entry: entry ?? { type, label: type }, stats: [
102
104
  { label: t('engine.list.items', locale), value: items.length },
103
105
  { label: t('engine.list.filtered', locale), value: filtered.length },
104
- ], actions: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), title: t('engine.list.refresh', locale), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), entry?.allowOrgOverride && (_jsxs(Button, { size: "sm", onClick: () => navigate(`./new?type=${encodeURIComponent(type)}`), children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), t('engine.list.create', locale)] }))] }), children: _jsxs("div", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[200px] max-w-md", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }), _jsx(Input, { className: "pl-8", placeholder: t('engine.list.search', locale), value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsxs(Select, { value: sourceFilter, onValueChange: setSourceFilter, children: [_jsx(SelectTrigger, { className: "w-[180px]", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsxs(SelectItem, { value: "all", children: [t('engine.list.allSources', locale), " (", sourceCounts.all, ")"] }), _jsxs(SelectItem, { value: "code", children: ["Code (", sourceCounts.code, ")"] }), _jsxs(SelectItem, { value: "overlay", children: ["Overlay (", sourceCounts.overlay, ")"] }), _jsxs(SelectItem, { value: "effective", children: ["Effective (", sourceCounts.effective, ")"] })] })] })] }), loading && (_jsxs("div", { className: "text-sm text-muted-foreground", children: [t('engine.edit.loading', locale), " ", type, "\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && filtered.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: items.length === 0
106
+ ], actions: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), title: t('engine.list.refresh', locale), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), entry?.allowOrgOverride && (_jsxs(Button, { size: "sm", onClick: () => navigate(`./new`), children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), t('engine.list.create', locale)] }))] }), children: _jsxs("div", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[200px] max-w-md", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }), _jsx(Input, { className: "pl-8", placeholder: t('engine.list.search', locale), value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsxs(Select, { value: sourceFilter, onValueChange: setSourceFilter, children: [_jsx(SelectTrigger, { className: "w-[180px]", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsxs(SelectItem, { value: "all", children: [t('engine.list.allSources', locale), " (", sourceCounts.all, ")"] }), _jsxs(SelectItem, { value: "code", children: ["Code (", sourceCounts.code, ")"] }), _jsxs(SelectItem, { value: "overlay", children: ["Overlay (", sourceCounts.overlay, ")"] }), _jsxs(SelectItem, { value: "effective", children: ["Effective (", sourceCounts.effective, ")"] })] })] })] }), loading && (_jsxs("div", { className: "text-sm text-muted-foreground", children: [t('engine.edit.loading', locale), " ", type, "\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && filtered.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: items.length === 0
105
107
  ? `No ${type} items registered`
106
108
  : `No matches for "${query}"` }), _jsx(EmptyDescription, { children: config.emptyStateHint ??
107
109
  (entry?.allowOrgOverride
@@ -112,7 +114,7 @@ export function MetadataResourceListPage({ type }) {
112
114
  return (_jsxs("tr", { className: "hover:bg-accent/50", children: [columns.map((c, ci) => {
113
115
  const value = row.item[c.key];
114
116
  const cell = c.render ? c.render(value, row.item) : defaultCell(value);
115
- return (_jsx("td", { className: "px-3 py-2 align-top", children: ci === 0 ? (_jsx(Link, { to: `./${encodeURIComponent(name)}?type=${encodeURIComponent(type)}`, className: "text-primary hover:underline font-mono", children: cell })) : (cell) }, c.key));
117
+ return (_jsx("td", { className: "px-3 py-2 align-top", children: ci === 0 ? (_jsx(Link, { to: `./${encodeURIComponent(name)}`, className: "text-primary hover:underline font-mono", children: cell })) : (cell) }, c.key));
116
118
  }), _jsx("td", { className: "px-3 py-2 text-right align-top", children: row.source ? (_jsx(Badge, { variant: "outline", className: 'text-[10px] ' +
117
119
  (row.source === 'overlay'
118
120
  ? 'border-emerald-500 text-emerald-700'
@@ -321,7 +321,16 @@ function FieldControl({ id, schema, value, onChange, readOnly, widget, fieldSpec
321
321
  }
322
322
  // Boolean → Switch (no redundant "true/false" text; the toggle state
323
323
  // already conveys the value).
324
- if (schema?.type === 'boolean') {
324
+ //
325
+ // We must also honor `widget === 'switch'` (resolved by inferWidget from
326
+ // `fieldSpec.type === 'boolean'` / `'toggle'`), because for composite
327
+ // sub-fields the JSON schema fragment is often `{}` — the parent declares
328
+ // `additionalProperties: true` and no per-property `properties`, so
329
+ // `schema?.type` is undefined even though the form spec clearly marks
330
+ // the sub-field as boolean. Without this, capability toggles inside the
331
+ // Object editor's "Capabilities" section fell through to RawJsonEditor
332
+ // and rendered as empty textareas.
333
+ if (schema?.type === 'boolean' || widget === 'switch' || fieldSpec?.type === 'boolean' || fieldSpec?.type === 'toggle') {
325
334
  return (_jsx(Switch, { id: id, checked: !!value, onCheckedChange: (c) => onChange(c), disabled: readOnly }));
326
335
  }
327
336
  // Number / integer → numeric input with min/max from fieldSpec.
@@ -0,0 +1 @@
1
+ export declare function registerBuiltinAnchors(): void;
@@ -0,0 +1,235 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Built-in anchor relationships for the Related tab.
4
+ *
5
+ * Each entry tells the metadata-admin engine "items of type X belong to
6
+ * (are anchored at) items of type Y when this predicate matches". The
7
+ * Related panel on the parent's edit page uses this registry to decide
8
+ * which `client.list(type)` calls to make and how to group the results.
9
+ *
10
+ * Adding a new relationship is intentionally a one-liner — declare it
11
+ * here (or in your own plugin's bootstrap) and the UI picks it up with
12
+ * no further wiring. Predicates are kept tiny because they run client-
13
+ * side over every item returned from `list()`.
14
+ *
15
+ * Why not parse the schema and infer this? Some types nest the anchor
16
+ * deep in `data.object` or `list.data.object`; some types live on the
17
+ * parent (e.g. `fields` is embedded inside the object item itself).
18
+ * Hand-rolled declarations are clearer than schema-walking heuristics.
19
+ */
20
+ import { registerMetadataResource, anchorByField } from './registry';
21
+ export function registerBuiltinAnchors() {
22
+ // ── EMBEDDED: items stored inside the object body itself ──────────
23
+ //
24
+ // Fields, indexes, and embedded validations don't have a top-level
25
+ // metadata type; they live under `object.fields`, `object.indexes[]`,
26
+ // `object.validations[]`. We surface them with `source: 'embedded'`
27
+ // so the Related panel can list them without a separate API call.
28
+ //
29
+ // `fields` is stored as a NAME-KEYED MAP (`{ email: {...}, name: {...} }`)
30
+ // — not an array — so we adapt it into an array carrying the key as
31
+ // `name` for the panel to render.
32
+ registerMetadataResource({
33
+ type: '__object_field',
34
+ label: 'Field',
35
+ anchors: [{
36
+ anchorType: 'object',
37
+ source: 'embedded',
38
+ editAs: 'field',
39
+ embeddedPath: 'fields',
40
+ extract: (parent) => mapOrArrayToList(parent.fields),
41
+ groupLabel: 'Fields',
42
+ order: 5,
43
+ }],
44
+ });
45
+ registerMetadataResource({
46
+ type: '__object_index',
47
+ label: 'Index',
48
+ anchors: [{
49
+ anchorType: 'object',
50
+ source: 'embedded',
51
+ editAs: 'index',
52
+ embeddedPath: 'indexes',
53
+ extract: (parent) => {
54
+ const raw = parent.indexes;
55
+ if (!Array.isArray(raw))
56
+ return [];
57
+ // Indexes have no `name`; synthesise one from their column list.
58
+ return raw.map((idx, i) => {
59
+ const fields = Array.isArray(idx.fields) ? idx.fields.join(',') : '';
60
+ const synthesised = fields ? `idx_${fields}` : `index_${i}`;
61
+ return { name: synthesised, ...idx };
62
+ });
63
+ },
64
+ groupLabel: 'Indexes',
65
+ order: 10,
66
+ }],
67
+ });
68
+ registerMetadataResource({
69
+ type: '__object_validation',
70
+ label: 'Embedded validation',
71
+ anchors: [{
72
+ anchorType: 'object',
73
+ source: 'embedded',
74
+ editAs: 'validation',
75
+ embeddedPath: 'validations',
76
+ extract: (parent) => mapOrArrayToList(parent.validations),
77
+ groupLabel: 'Embedded Validations',
78
+ order: 15,
79
+ }],
80
+ });
81
+ // ── LIST: standalone child metadata types ─────────────────────────
82
+ // hook.object → object (beforeInsert / afterUpdate / …)
83
+ registerMetadataResource({
84
+ type: 'hook',
85
+ anchors: [{
86
+ anchorType: 'object',
87
+ match: anchorByField('object'),
88
+ groupLabel: 'Hooks',
89
+ order: 20,
90
+ }],
91
+ });
92
+ // approval.object → object (approval processes targeting this object)
93
+ registerMetadataResource({
94
+ type: 'approval',
95
+ anchors: [{
96
+ anchorType: 'object',
97
+ match: anchorByField('object'),
98
+ groupLabel: 'Approval Processes',
99
+ order: 70,
100
+ }],
101
+ });
102
+ // page.object → object (auto-generated record pages, etc.)
103
+ registerMetadataResource({
104
+ type: 'page',
105
+ anchors: [{
106
+ anchorType: 'object',
107
+ match: anchorByField('object'),
108
+ groupLabel: 'Pages',
109
+ order: 40,
110
+ }],
111
+ });
112
+ // view binds to an object via data.object (list view / form view variants)
113
+ registerMetadataResource({
114
+ type: 'view',
115
+ anchors: [{
116
+ anchorType: 'object',
117
+ match: anchorByField([
118
+ 'data.object',
119
+ 'list.data.object',
120
+ 'form.data.object',
121
+ 'object',
122
+ ]),
123
+ groupLabel: 'Views',
124
+ order: 30,
125
+ }],
126
+ });
127
+ // flow / workflow may reference an object at the root or under `on`
128
+ registerMetadataResource({
129
+ type: 'flow',
130
+ anchors: [{
131
+ anchorType: 'object',
132
+ match: anchorByField(['object', 'on.object', 'trigger.object']),
133
+ groupLabel: 'Flows',
134
+ order: 50,
135
+ }],
136
+ });
137
+ registerMetadataResource({
138
+ type: 'workflow',
139
+ anchors: [{
140
+ anchorType: 'object',
141
+ match: anchorByField(['object', 'on.object']),
142
+ groupLabel: 'Workflow Rules',
143
+ order: 51,
144
+ }],
145
+ });
146
+ // trigger.object → object (low-level DB-style triggers)
147
+ registerMetadataResource({
148
+ type: 'trigger',
149
+ anchors: [{
150
+ anchorType: 'object',
151
+ match: anchorByField(['object', 'on.object']),
152
+ groupLabel: 'Triggers',
153
+ order: 52,
154
+ }],
155
+ });
156
+ // validation: usually embedded in the object, but standalone variants
157
+ // do exist. Match anything whose `object` points back at us.
158
+ registerMetadataResource({
159
+ type: 'validation',
160
+ anchors: [{
161
+ anchorType: 'object',
162
+ match: anchorByField('object'),
163
+ groupLabel: 'Validations',
164
+ order: 25,
165
+ }],
166
+ });
167
+ // permission has a sparse object-keyed map under `objects`. Match by
168
+ // membership of the parent name in that map.
169
+ registerMetadataResource({
170
+ type: 'permission',
171
+ anchors: [{
172
+ anchorType: 'object',
173
+ match: (item, name) => {
174
+ const objs = item?.objects;
175
+ return !!objs && typeof objs === 'object' && name in objs;
176
+ },
177
+ groupLabel: 'Permissions',
178
+ order: 60,
179
+ }],
180
+ });
181
+ // action — both record-scoped and object-scoped actions live here.
182
+ // Schemas vary, so accept any of the three common shapes.
183
+ registerMetadataResource({
184
+ type: 'action',
185
+ anchors: [{
186
+ anchorType: 'object',
187
+ match: anchorByField([
188
+ 'object',
189
+ 'target.object',
190
+ 'on.object',
191
+ ]),
192
+ groupLabel: 'Actions',
193
+ order: 55,
194
+ }],
195
+ });
196
+ // dashboard / report — surface ones bound to a specific object.
197
+ // Many will not have an explicit anchor; those simply don't appear.
198
+ registerMetadataResource({
199
+ type: 'dashboard',
200
+ anchors: [{
201
+ anchorType: 'object',
202
+ match: anchorByField(['object', 'data.object']),
203
+ groupLabel: 'Dashboards',
204
+ order: 80,
205
+ }],
206
+ });
207
+ registerMetadataResource({
208
+ type: 'report',
209
+ anchors: [{
210
+ anchorType: 'object',
211
+ match: anchorByField(['object', 'data.object']),
212
+ groupLabel: 'Reports',
213
+ order: 81,
214
+ }],
215
+ });
216
+ }
217
+ /**
218
+ * Coerce an embedded "collection" into a list of `{ name, …rest }`.
219
+ *
220
+ * ObjectStack stores some embedded collections as name-keyed maps
221
+ * (`object.fields`) and others as arrays (`object.indexes`). Both want
222
+ * to render the same way in Related; this helper papers over the gap
223
+ * by injecting the map key as `name` when needed.
224
+ */
225
+ function mapOrArrayToList(raw) {
226
+ if (Array.isArray(raw))
227
+ return raw;
228
+ if (raw && typeof raw === 'object') {
229
+ return Object.entries(raw).map(([k, v]) => {
230
+ const body = v && typeof v === 'object' ? v : {};
231
+ return { name: k, ...body };
232
+ });
233
+ }
234
+ return [];
235
+ }
@@ -15,17 +15,21 @@ export { MetadataDirectoryPage } from './DirectoryPage';
15
15
  export { MetadataResourceRouter } from './ResourceRouter';
16
16
  export { MetadataResourceListPage } from './ResourceListPage';
17
17
  export { MetadataResourceEditPage } from './ResourceEditPage';
18
+ export { RelatedPanel } from './RelatedPanel';
19
+ export { MetadataDetailDrawer } from './MetadataDetailDrawer';
18
20
  export { MetadataResourceHistoryPage } from './ResourceHistoryPage';
19
21
  export { MetadataQuickFind } from './QuickFind';
20
22
  export { PageShell as MetadataPageShell } from './PageShell';
21
23
  export { SchemaForm } from './SchemaForm';
22
24
  export { LayeredDiff } from './LayeredDiff';
23
25
  export { PermissionMatrixEditPage } from './PermissionMatrixEditor';
24
- export { DesignerEditorWrapper } from './DesignerEditorWrapper';
26
+ export { DesignerEditorWrapper, DesignerEditorBody } from './DesignerEditorWrapper';
25
27
  export type { DesignerEditorWrapperProps } from './DesignerEditorWrapper';
26
28
  export { translateMetadataType, translateMetadataDomain, t as translateMetadataAdmin, detectLocale, } from './i18n';
27
29
  export type { SupportedLocale } from './i18n';
28
- export { registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, } from './registry';
29
- export type { MetadataResourceConfig, MetadataDomain, } from './registry';
30
+ export { registerMetadataResource, getMetadataResource, listMetadataResources, listAnchorsFor, resolveResourceConfig, anchorByField, } from './registry';
31
+ export type { MetadataResourceConfig, MetadataDomain, MetadataAnchor, } from './registry';
32
+ export { registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, } from './preview-registry';
33
+ export type { MetadataPreview, MetadataPreviewProps } from './preview-registry';
30
34
  export { useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './useMetadata';
31
35
  export type { RichMetadataTypeEntry } from './useMetadata';
@@ -16,18 +16,30 @@ export { MetadataDirectoryPage } from './DirectoryPage';
16
16
  export { MetadataResourceRouter } from './ResourceRouter';
17
17
  export { MetadataResourceListPage } from './ResourceListPage';
18
18
  export { MetadataResourceEditPage } from './ResourceEditPage';
19
+ export { RelatedPanel } from './RelatedPanel';
20
+ export { MetadataDetailDrawer } from './MetadataDetailDrawer';
19
21
  export { MetadataResourceHistoryPage } from './ResourceHistoryPage';
20
22
  export { MetadataQuickFind } from './QuickFind';
21
23
  export { PageShell as MetadataPageShell } from './PageShell';
22
24
  export { SchemaForm } from './SchemaForm';
23
25
  export { LayeredDiff } from './LayeredDiff';
24
26
  export { PermissionMatrixEditPage } from './PermissionMatrixEditor';
25
- export { DesignerEditorWrapper } from './DesignerEditorWrapper';
27
+ export { DesignerEditorWrapper, DesignerEditorBody } from './DesignerEditorWrapper';
26
28
  export { translateMetadataType, translateMetadataDomain, t as translateMetadataAdmin, detectLocale, } from './i18n';
27
- export { registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, } from './registry';
29
+ export { registerMetadataResource, getMetadataResource, listMetadataResources, listAnchorsFor, resolveResourceConfig, anchorByField, } from './registry';
30
+ // Side-effect: register the built-in anchor relationships so the Related
31
+ // tab works out of the box for objects (hooks, views, pages, …).
32
+ import { registerBuiltinAnchors } from './anchors';
33
+ registerBuiltinAnchors();
28
34
  // Side-effect: register fallback JSONSchemas for the 12 writable types
29
35
  // so the generic SchemaForm renders a real form (vs raw-JSON fallback)
30
36
  // until the framework wires Zod→JSONSchema generation into /meta/types.
31
37
  import { registerDefaultMetadataSchemas } from './default-schemas';
32
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';
33
45
  export { useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './useMetadata';