@object-ui/app-shell 6.2.0 → 6.2.1
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 +19 -0
- package/dist/console/AppContent.js +30 -1
- 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/MetadataDetailDrawer.d.ts +13 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.js +52 -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 +33 -0
- package/dist/views/metadata-admin/RelatedPanel.js +171 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +9 -3
- package/dist/views/metadata-admin/ResourceEditPage.js +58 -12
- 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 +229 -0
- package/dist/views/metadata-admin/index.d.ts +5 -3
- package/dist/views/metadata-admin/index.js +8 -2
- package/dist/views/metadata-admin/registry.d.ts +107 -0
- package/dist/views/metadata-admin/registry.js +58 -0
- 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, } 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,13 @@ 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
|
-
|
|
34
|
+
import { getMetadataResource, resolveResourceConfig, listAnchorsFor, } from './registry';
|
|
35
|
+
import { RelatedPanel } from './RelatedPanel';
|
|
36
|
+
import { MetadataDetailDrawer } from './MetadataDetailDrawer';
|
|
37
|
+
export function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode = false, embedded = false, }) {
|
|
38
|
+
const params = useParams();
|
|
39
|
+
const type = typeProp ?? params.type ?? '';
|
|
40
|
+
const name = nameProp ?? params.name ?? '';
|
|
36
41
|
const navigate = useNavigate();
|
|
37
42
|
const client = useMetadataClient();
|
|
38
43
|
const { entries } = useMetadataTypes(client);
|
|
@@ -134,6 +139,45 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
|
|
|
134
139
|
setRefsLoading(false);
|
|
135
140
|
}
|
|
136
141
|
}
|
|
142
|
+
// Related drawer state. `null` = closed. We avoid querystring round-
|
|
143
|
+
// trips on every keystroke; URL state is best-effort sync via effect
|
|
144
|
+
// below.
|
|
145
|
+
const [relatedTarget, setRelatedTarget] = React.useState(null);
|
|
146
|
+
const hasAnchors = React.useMemo(() => !createMode && !embedded && listAnchorsFor(type).length > 0, [type, createMode, embedded]);
|
|
147
|
+
// Read ?tab and ?open on first mount so deep-links work. Embedded
|
|
148
|
+
// items are not deep-linkable (they live in the parent body and need
|
|
149
|
+
// the parent payload to materialise) so we only restore metadata
|
|
150
|
+
// targets here.
|
|
151
|
+
const initialTabRef = React.useRef(null);
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
if (typeof window === 'undefined' || embedded)
|
|
154
|
+
return;
|
|
155
|
+
const sp = new URLSearchParams(window.location.search);
|
|
156
|
+
const tab = sp.get('tab');
|
|
157
|
+
if (tab)
|
|
158
|
+
initialTabRef.current = tab;
|
|
159
|
+
const open = sp.get('open');
|
|
160
|
+
if (open && open.includes(':')) {
|
|
161
|
+
const [t, n] = open.split(':', 2);
|
|
162
|
+
if (t && n)
|
|
163
|
+
setRelatedTarget({ kind: 'metadata', type: t, name: n });
|
|
164
|
+
}
|
|
165
|
+
// intentionally empty deps — first mount only
|
|
166
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
167
|
+
}, []);
|
|
168
|
+
// Reflect drawer target into the URL so refresh/share works.
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
if (typeof window === 'undefined' || embedded)
|
|
171
|
+
return;
|
|
172
|
+
const url = new URL(window.location.href);
|
|
173
|
+
if (relatedTarget?.kind === 'metadata') {
|
|
174
|
+
url.searchParams.set('open', `${relatedTarget.type}:${relatedTarget.name}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
url.searchParams.delete('open');
|
|
178
|
+
}
|
|
179
|
+
window.history.replaceState({}, '', url.toString());
|
|
180
|
+
}, [relatedTarget, embedded]);
|
|
137
181
|
async function doSave(force) {
|
|
138
182
|
setSaving(true);
|
|
139
183
|
setError(null);
|
|
@@ -157,13 +201,7 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
|
|
|
157
201
|
setDestructiveIssues(null);
|
|
158
202
|
setPendingItem(null);
|
|
159
203
|
if (createMode) {
|
|
160
|
-
|
|
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)}`);
|
|
204
|
+
navigate(`../${encodeURIComponent(savedName)}`);
|
|
167
205
|
}
|
|
168
206
|
}
|
|
169
207
|
catch (err) {
|
|
@@ -238,7 +276,15 @@ export function MetadataResourceEditPage({ type, name, createMode = false, }) {
|
|
|
238
276
|
const schema = entry?.schema ??
|
|
239
277
|
config.defaultSchema;
|
|
240
278
|
const readOnly = !entry?.allowOrgOverride && !createMode;
|
|
241
|
-
|
|
279
|
+
const DesignerTab = !createMode ? customConfig?.DesignerTab : undefined;
|
|
280
|
+
const designerTabLabel = customConfig?.designerTabLabel ?? 'Designer';
|
|
281
|
+
return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? 'Create new' : 'Edit overlay', actions: _jsxs(_Fragment, { children: [!createMode && entry?.allowOrgOverride && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset overlay"] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(History, { className: "h-4 w-4 mr-1" }), "History"] })), entry?.allowOrgOverride && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), "Save"] }))] }), children: [_jsxs("div", { className: "p-6 space-y-6 max-w-7xl", children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), readOnly && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3", children: ["This type is read-only. To enable runtime editing, set", ' ', _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }), " to include ", _jsx("code", { className: "font-mono", children: type }), ", or flip", ' ', _jsx("code", { className: "font-mono", children: "allowOrgOverride" }), " in the registry."] })), _jsxs(Tabs, { defaultValue: initialTabRef.current ?? (DesignerTab ? 'designer' : 'form'), className: "w-full", onValueChange: (v) => {
|
|
282
|
+
if (typeof window === 'undefined' || embedded)
|
|
283
|
+
return;
|
|
284
|
+
const url = new URL(window.location.href);
|
|
285
|
+
url.searchParams.set('tab', v);
|
|
286
|
+
window.history.replaceState({}, '', url.toString());
|
|
287
|
+
}, children: [_jsxs(TabsList, { children: [DesignerTab && (_jsx(TabsTrigger, { value: "designer", children: designerTabLabel })), _jsx(TabsTrigger, { value: "form", children: "Form" }), !createMode && (_jsxs(TabsTrigger, { value: "layers", children: ["Layers", layered?.overlay && (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: "overlay" }))] })), !createMode && (_jsxs(TabsTrigger, { value: "references", onClick: loadReferences, children: [_jsx(Link2, { className: "h-3.5 w-3.5 mr-1" }), "References", refs && (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: refs.length }))] })), hasAnchors && (_jsxs(TabsTrigger, { value: "related", children: [_jsx(Layers3, { className: "h-3.5 w-3.5 mr-1" }), "Related"] }))] }), DesignerTab && (_jsx(TabsContent, { value: "designer", className: "mt-4", children: _jsx(DesignerTab, { type: type, name: name }) })), _jsx(TabsContent, { value: "form", className: "mt-4", children: _jsx(SchemaForm, { schema: schema, form: entry?.form, value: draft, onChange: setDraft, issues: issues, hiddenFields: config.hiddenFields, fieldOrder: config.fieldOrder, readOnly: readOnly, createMode: createMode, widgetContext: widgetContext }) }), !createMode && (_jsx(TabsContent, { value: "layers", className: "mt-4", children: _jsx(LayeredDiff, { layered: layered }) })), !createMode && (_jsx(TabsContent, { value: "references", className: "mt-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })), hasAnchors && (_jsx(TabsContent, { value: "related", className: "mt-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) }))] })] }), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
|
|
242
288
|
if (!open) {
|
|
243
289
|
setDestructiveIssues(null);
|
|
244
290
|
setPendingItem(null);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface MetadataResourceHistoryPageProps {
|
|
2
|
-
type
|
|
3
|
-
name
|
|
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)}
|
|
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
|
|
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
|
|
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)}
|
|
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
|
-
|
|
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,229 @@
|
|
|
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
|
+
extract: (parent) => mapOrArrayToList(parent.fields),
|
|
39
|
+
groupLabel: 'Fields',
|
|
40
|
+
order: 5,
|
|
41
|
+
}],
|
|
42
|
+
});
|
|
43
|
+
registerMetadataResource({
|
|
44
|
+
type: '__object_index',
|
|
45
|
+
label: 'Index',
|
|
46
|
+
anchors: [{
|
|
47
|
+
anchorType: 'object',
|
|
48
|
+
source: 'embedded',
|
|
49
|
+
extract: (parent) => {
|
|
50
|
+
const raw = parent.indexes;
|
|
51
|
+
if (!Array.isArray(raw))
|
|
52
|
+
return [];
|
|
53
|
+
// Indexes have no `name`; synthesise one from their column list.
|
|
54
|
+
return raw.map((idx, i) => {
|
|
55
|
+
const fields = Array.isArray(idx.fields) ? idx.fields.join(',') : '';
|
|
56
|
+
const synthesised = fields ? `idx_${fields}` : `index_${i}`;
|
|
57
|
+
return { name: synthesised, ...idx };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
groupLabel: 'Indexes',
|
|
61
|
+
order: 10,
|
|
62
|
+
}],
|
|
63
|
+
});
|
|
64
|
+
registerMetadataResource({
|
|
65
|
+
type: '__object_validation',
|
|
66
|
+
label: 'Embedded validation',
|
|
67
|
+
anchors: [{
|
|
68
|
+
anchorType: 'object',
|
|
69
|
+
source: 'embedded',
|
|
70
|
+
extract: (parent) => mapOrArrayToList(parent.validations),
|
|
71
|
+
groupLabel: 'Embedded Validations',
|
|
72
|
+
order: 15,
|
|
73
|
+
}],
|
|
74
|
+
});
|
|
75
|
+
// ── LIST: standalone child metadata types ─────────────────────────
|
|
76
|
+
// hook.object → object (beforeInsert / afterUpdate / …)
|
|
77
|
+
registerMetadataResource({
|
|
78
|
+
type: 'hook',
|
|
79
|
+
anchors: [{
|
|
80
|
+
anchorType: 'object',
|
|
81
|
+
match: anchorByField('object'),
|
|
82
|
+
groupLabel: 'Hooks',
|
|
83
|
+
order: 20,
|
|
84
|
+
}],
|
|
85
|
+
});
|
|
86
|
+
// approval.object → object (approval processes targeting this object)
|
|
87
|
+
registerMetadataResource({
|
|
88
|
+
type: 'approval',
|
|
89
|
+
anchors: [{
|
|
90
|
+
anchorType: 'object',
|
|
91
|
+
match: anchorByField('object'),
|
|
92
|
+
groupLabel: 'Approval Processes',
|
|
93
|
+
order: 70,
|
|
94
|
+
}],
|
|
95
|
+
});
|
|
96
|
+
// page.object → object (auto-generated record pages, etc.)
|
|
97
|
+
registerMetadataResource({
|
|
98
|
+
type: 'page',
|
|
99
|
+
anchors: [{
|
|
100
|
+
anchorType: 'object',
|
|
101
|
+
match: anchorByField('object'),
|
|
102
|
+
groupLabel: 'Pages',
|
|
103
|
+
order: 40,
|
|
104
|
+
}],
|
|
105
|
+
});
|
|
106
|
+
// view binds to an object via data.object (list view / form view variants)
|
|
107
|
+
registerMetadataResource({
|
|
108
|
+
type: 'view',
|
|
109
|
+
anchors: [{
|
|
110
|
+
anchorType: 'object',
|
|
111
|
+
match: anchorByField([
|
|
112
|
+
'data.object',
|
|
113
|
+
'list.data.object',
|
|
114
|
+
'form.data.object',
|
|
115
|
+
'object',
|
|
116
|
+
]),
|
|
117
|
+
groupLabel: 'Views',
|
|
118
|
+
order: 30,
|
|
119
|
+
}],
|
|
120
|
+
});
|
|
121
|
+
// flow / workflow may reference an object at the root or under `on`
|
|
122
|
+
registerMetadataResource({
|
|
123
|
+
type: 'flow',
|
|
124
|
+
anchors: [{
|
|
125
|
+
anchorType: 'object',
|
|
126
|
+
match: anchorByField(['object', 'on.object', 'trigger.object']),
|
|
127
|
+
groupLabel: 'Flows',
|
|
128
|
+
order: 50,
|
|
129
|
+
}],
|
|
130
|
+
});
|
|
131
|
+
registerMetadataResource({
|
|
132
|
+
type: 'workflow',
|
|
133
|
+
anchors: [{
|
|
134
|
+
anchorType: 'object',
|
|
135
|
+
match: anchorByField(['object', 'on.object']),
|
|
136
|
+
groupLabel: 'Workflow Rules',
|
|
137
|
+
order: 51,
|
|
138
|
+
}],
|
|
139
|
+
});
|
|
140
|
+
// trigger.object → object (low-level DB-style triggers)
|
|
141
|
+
registerMetadataResource({
|
|
142
|
+
type: 'trigger',
|
|
143
|
+
anchors: [{
|
|
144
|
+
anchorType: 'object',
|
|
145
|
+
match: anchorByField(['object', 'on.object']),
|
|
146
|
+
groupLabel: 'Triggers',
|
|
147
|
+
order: 52,
|
|
148
|
+
}],
|
|
149
|
+
});
|
|
150
|
+
// validation: usually embedded in the object, but standalone variants
|
|
151
|
+
// do exist. Match anything whose `object` points back at us.
|
|
152
|
+
registerMetadataResource({
|
|
153
|
+
type: 'validation',
|
|
154
|
+
anchors: [{
|
|
155
|
+
anchorType: 'object',
|
|
156
|
+
match: anchorByField('object'),
|
|
157
|
+
groupLabel: 'Validations',
|
|
158
|
+
order: 25,
|
|
159
|
+
}],
|
|
160
|
+
});
|
|
161
|
+
// permission has a sparse object-keyed map under `objects`. Match by
|
|
162
|
+
// membership of the parent name in that map.
|
|
163
|
+
registerMetadataResource({
|
|
164
|
+
type: 'permission',
|
|
165
|
+
anchors: [{
|
|
166
|
+
anchorType: 'object',
|
|
167
|
+
match: (item, name) => {
|
|
168
|
+
const objs = item?.objects;
|
|
169
|
+
return !!objs && typeof objs === 'object' && name in objs;
|
|
170
|
+
},
|
|
171
|
+
groupLabel: 'Permissions',
|
|
172
|
+
order: 60,
|
|
173
|
+
}],
|
|
174
|
+
});
|
|
175
|
+
// action — both record-scoped and object-scoped actions live here.
|
|
176
|
+
// Schemas vary, so accept any of the three common shapes.
|
|
177
|
+
registerMetadataResource({
|
|
178
|
+
type: 'action',
|
|
179
|
+
anchors: [{
|
|
180
|
+
anchorType: 'object',
|
|
181
|
+
match: anchorByField([
|
|
182
|
+
'object',
|
|
183
|
+
'target.object',
|
|
184
|
+
'on.object',
|
|
185
|
+
]),
|
|
186
|
+
groupLabel: 'Actions',
|
|
187
|
+
order: 55,
|
|
188
|
+
}],
|
|
189
|
+
});
|
|
190
|
+
// dashboard / report — surface ones bound to a specific object.
|
|
191
|
+
// Many will not have an explicit anchor; those simply don't appear.
|
|
192
|
+
registerMetadataResource({
|
|
193
|
+
type: 'dashboard',
|
|
194
|
+
anchors: [{
|
|
195
|
+
anchorType: 'object',
|
|
196
|
+
match: anchorByField(['object', 'data.object']),
|
|
197
|
+
groupLabel: 'Dashboards',
|
|
198
|
+
order: 80,
|
|
199
|
+
}],
|
|
200
|
+
});
|
|
201
|
+
registerMetadataResource({
|
|
202
|
+
type: 'report',
|
|
203
|
+
anchors: [{
|
|
204
|
+
anchorType: 'object',
|
|
205
|
+
match: anchorByField(['object', 'data.object']),
|
|
206
|
+
groupLabel: 'Reports',
|
|
207
|
+
order: 81,
|
|
208
|
+
}],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Coerce an embedded "collection" into a list of `{ name, …rest }`.
|
|
213
|
+
*
|
|
214
|
+
* ObjectStack stores some embedded collections as name-keyed maps
|
|
215
|
+
* (`object.fields`) and others as arrays (`object.indexes`). Both want
|
|
216
|
+
* to render the same way in Related; this helper papers over the gap
|
|
217
|
+
* by injecting the map key as `name` when needed.
|
|
218
|
+
*/
|
|
219
|
+
function mapOrArrayToList(raw) {
|
|
220
|
+
if (Array.isArray(raw))
|
|
221
|
+
return raw;
|
|
222
|
+
if (raw && typeof raw === 'object') {
|
|
223
|
+
return Object.entries(raw).map(([k, v]) => {
|
|
224
|
+
const body = v && typeof v === 'object' ? v : {};
|
|
225
|
+
return { name: k, ...body };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
@@ -15,17 +15,19 @@ 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';
|
|
30
32
|
export { useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './useMetadata';
|
|
31
33
|
export type { RichMetadataTypeEntry } from './useMetadata';
|
|
@@ -16,15 +16,21 @@ 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.
|
|
@@ -64,11 +64,32 @@ export interface MetadataResourceConfig {
|
|
|
64
64
|
}>;
|
|
65
65
|
/**
|
|
66
66
|
* Fully custom edit page. Receives `{ type, name }`.
|
|
67
|
+
*
|
|
68
|
+
* Use this when the bespoke editor replaces the JSONSchema form entirely
|
|
69
|
+
* (e.g. PermissionMatrix grid). For visual designers that should coexist
|
|
70
|
+
* with the generic Form/Layers/References tabs, prefer `DesignerTab`.
|
|
67
71
|
*/
|
|
68
72
|
EditPage?: ComponentType<{
|
|
69
73
|
type: string;
|
|
70
74
|
name: string;
|
|
71
75
|
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Optional visual designer that renders inside its own dedicated tab on
|
|
78
|
+
* the generic edit page, alongside Form / Layers / References. Receives
|
|
79
|
+
* `{ type, name }` and should render without an outer `PageShell` — the
|
|
80
|
+
* generic engine owns the chrome.
|
|
81
|
+
*
|
|
82
|
+
* When both `EditPage` and `DesignerTab` are provided, `EditPage` wins
|
|
83
|
+
* (full takeover); the tab variant is only used when `EditPage` is unset.
|
|
84
|
+
*/
|
|
85
|
+
DesignerTab?: ComponentType<{
|
|
86
|
+
type: string;
|
|
87
|
+
name: string;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Optional label for the Designer tab. Defaults to "Designer".
|
|
91
|
+
*/
|
|
92
|
+
designerTabLabel?: string;
|
|
72
93
|
/**
|
|
73
94
|
* Fully custom create page. Receives `{ type }`.
|
|
74
95
|
*/
|
|
@@ -96,7 +117,80 @@ export interface MetadataResourceConfig {
|
|
|
96
117
|
* If the server registry DOES return a `schema`, this is ignored.
|
|
97
118
|
*/
|
|
98
119
|
defaultSchema?: Record<string, unknown>;
|
|
120
|
+
/**
|
|
121
|
+
* Declares that items of this type are "anchored" to one or more
|
|
122
|
+
* parent metadata types. The Related tab on the parent's edit page
|
|
123
|
+
* lists every item that matches the anchor predicate.
|
|
124
|
+
*
|
|
125
|
+
* For example, registering `hook` with
|
|
126
|
+
* `{ anchorType: 'object', match: (item, name) => item.object === name }`
|
|
127
|
+
* makes every `hook` whose `object` field equals `account` show up in
|
|
128
|
+
* the Account object's Related tab under a "Hooks" group.
|
|
129
|
+
*
|
|
130
|
+
* Multiple anchors are allowed (e.g. a field could anchor to both
|
|
131
|
+
* `object` and `view`). The Related panel groups by anchored type.
|
|
132
|
+
*/
|
|
133
|
+
anchors?: MetadataAnchor[];
|
|
99
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Anchor declaration — "items of type X are children of items of type Y
|
|
137
|
+
* when this predicate matches". Used to power the Related tab.
|
|
138
|
+
*
|
|
139
|
+
* The predicate runs in the client against the unwrapped item shape
|
|
140
|
+
* returned by `client.list(type)`. Keep it cheap (just property reads);
|
|
141
|
+
* we evaluate it once per item per render.
|
|
142
|
+
*/
|
|
143
|
+
export interface MetadataAnchor {
|
|
144
|
+
/** The parent metadata type whose Related tab should surface this. */
|
|
145
|
+
anchorType: string;
|
|
146
|
+
/**
|
|
147
|
+
* Where the anchored items live.
|
|
148
|
+
* - 'list' (default): query `client.list(childType)` and filter with
|
|
149
|
+
* `match()`. Used for first-class metadata types (hooks, views, …).
|
|
150
|
+
* - 'embedded': items are stored inside the parent body itself
|
|
151
|
+
* (e.g. `object.fields[]`). `extract()` plucks the array; no
|
|
152
|
+
* network call is made.
|
|
153
|
+
*/
|
|
154
|
+
source?: 'list' | 'embedded';
|
|
155
|
+
/**
|
|
156
|
+
* Returns true when the child `item` belongs to the parent identified
|
|
157
|
+
* by `anchorName`. The default helper `byField('object')` covers the
|
|
158
|
+
* common case of an explicit `object: 'foo'` reference. Required when
|
|
159
|
+
* `source === 'list'` (the default); ignored for `'embedded'`.
|
|
160
|
+
*/
|
|
161
|
+
match?: (item: Record<string, unknown>, anchorName: string) => boolean;
|
|
162
|
+
/**
|
|
163
|
+
* For `source: 'embedded'` only. Returns the embedded items array
|
|
164
|
+
* given the fully-loaded parent body. Items should already carry a
|
|
165
|
+
* `name` (or be normalised by the renderer).
|
|
166
|
+
*/
|
|
167
|
+
extract?: (parentItem: Record<string, unknown>) => Array<Record<string, unknown>>;
|
|
168
|
+
/**
|
|
169
|
+
* Optional override for the group label shown on the Related panel.
|
|
170
|
+
* Defaults to the child type's resolved label.
|
|
171
|
+
*/
|
|
172
|
+
groupLabel?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Optional icon override for the group header. Falls back to the
|
|
175
|
+
* child type's registered icon.
|
|
176
|
+
*/
|
|
177
|
+
iconName?: string;
|
|
178
|
+
/**
|
|
179
|
+
* Optional explicit ordering hint inside the Related panel. Lower
|
|
180
|
+
* numbers float to the top. Defaults to 100 if unset.
|
|
181
|
+
*/
|
|
182
|
+
order?: number;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Helper that returns a `match` reading a (possibly dotted) field path
|
|
186
|
+
* from the item and comparing it to the anchor name.
|
|
187
|
+
*
|
|
188
|
+
* anchorByField('object') // item.object === name
|
|
189
|
+
* anchorByField('data.object') // item.data?.object === name
|
|
190
|
+
* anchorByField(['list.data.object',
|
|
191
|
+
* 'form.data.object']) // either path matches
|
|
192
|
+
*/
|
|
193
|
+
export declare function anchorByField(paths: string | string[]): MetadataAnchor['match'];
|
|
100
194
|
/**
|
|
101
195
|
* Register (or merge) an entry. Idempotent — re-registering with the
|
|
102
196
|
* same `type` merges the new fields with any existing entry so that
|
|
@@ -109,6 +203,19 @@ export declare function registerMetadataResource(config: MetadataResourceConfig)
|
|
|
109
203
|
export declare function getMetadataResource(type: string): MetadataResourceConfig | undefined;
|
|
110
204
|
/** Snapshot of all registered entries (diagnostics; directory page). */
|
|
111
205
|
export declare function listMetadataResources(): MetadataResourceConfig[];
|
|
206
|
+
/**
|
|
207
|
+
* Build the list of child types whose items can be "anchored to" a
|
|
208
|
+
* parent type. Used by the Related tab to decide which `client.list`
|
|
209
|
+
* calls to make.
|
|
210
|
+
*
|
|
211
|
+
* Returns an array of `{ type, anchor }` pairs, sorted by `anchor.order`
|
|
212
|
+
* (lower first) and then by child type label for stable rendering.
|
|
213
|
+
*/
|
|
214
|
+
export declare function listAnchorsFor(parentType: string): Array<{
|
|
215
|
+
type: string;
|
|
216
|
+
config: MetadataResourceConfig;
|
|
217
|
+
anchor: MetadataAnchor;
|
|
218
|
+
}>;
|
|
112
219
|
/**
|
|
113
220
|
* Merge a registered config (if any) with server-side defaults from
|
|
114
221
|
* `/meta/types`. Server fields win for label/description/domain
|