@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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 6.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bc269b0: fix
8
+ - @object-ui/types@6.2.1
9
+ - @object-ui/core@6.2.1
10
+ - @object-ui/i18n@6.2.1
11
+ - @object-ui/react@6.2.1
12
+ - @object-ui/components@6.2.1
13
+ - @object-ui/fields@6.2.1
14
+ - @object-ui/layout@6.2.1
15
+ - @object-ui/data-objectstack@6.2.1
16
+ - @object-ui/auth@6.2.1
17
+ - @object-ui/permissions@6.2.1
18
+ - @object-ui/plugin-editor@6.2.1
19
+ - @object-ui/collaboration@6.2.1
20
+ - @object-ui/providers@6.2.1
21
+
3
22
  ## 6.2.0
4
23
 
5
24
  ### Minor Changes
@@ -40,6 +40,13 @@ const ReportView = lazy(() => import('../views/ReportView').then(m => ({ default
40
40
  const SearchResultsPage = lazy(() => import('../views/SearchResultsPage').then(m => ({ default: m.SearchResultsPage })));
41
41
  const RecordFormPage = lazy(() => import('../views/RecordFormPage').then(m => ({ default: m.RecordFormPage })));
42
42
  const ComponentNavView = lazy(() => import('../views/ComponentNavView').then(m => ({ default: m.ComponentNavView })));
43
+ // Metadata admin — mounted under /apps/:app/metadata. Lives at the top
44
+ // level so URLs read like a normal nested resource (RFC-style) instead of
45
+ // piggy-backing on the legacy ComponentRegistry fan-out.
46
+ const MetadataDirectoryPage = lazy(() => import('../views/metadata-admin').then(m => ({ default: m.MetadataDirectoryPage })));
47
+ const MetadataResourceListPage = lazy(() => import('../views/metadata-admin').then(m => ({ default: m.MetadataResourceListPage })));
48
+ const MetadataResourceEditPage = lazy(() => import('../views/metadata-admin').then(m => ({ default: m.MetadataResourceEditPage })));
49
+ const MetadataResourceHistoryPage = lazy(() => import('../views/metadata-admin').then(m => ({ default: m.MetadataResourceHistoryPage })));
43
50
  // Designer pages — sourced from @object-ui/plugin-designer so third-party hosts
44
51
  // can opt out by not registering these routes.
45
52
  const CreateAppPage = lazy(() => import('@object-ui/plugin-designer').then(m => ({ default: m.CreateAppPage })));
@@ -259,7 +266,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
259
266
  const expressionUser = user
260
267
  ? { name: user.name, email: user.email, role: user.role ?? 'user' }
261
268
  : { name: 'Anonymous', email: '', role: 'guest' };
262
- return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, features: features, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, userId: user?.id, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange, dataSource: dataSource }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsx(RouteFader, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: resolveLandingRoute(activeApp), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "component/:ns/:name/*", element: _jsx(ComponentNavView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
269
+ return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, features: features, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, userId: user?.id, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange, dataSource: dataSource }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsx(RouteFader, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: resolveLandingRoute(activeApp), replace: true }) }), _jsxs(Route, { path: "metadata", children: [_jsx(Route, { index: true, element: _jsx(MetadataDirectoryPage, {}) }), _jsx(Route, { path: ":type", element: _jsx(MetadataResourceListPage, {}) }), _jsx(Route, { path: ":type/new", element: _jsx(MetadataResourceEditPage, { createMode: true }) }), _jsx(Route, { path: ":type/:name", element: _jsx(MetadataResourceEditPage, {}) }), _jsx(Route, { path: ":type/:name/history", element: _jsx(MetadataResourceHistoryPage, {}) })] }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "component/:ns/:name/*", element: _jsx(ComponentNavView, {}) }), _jsx(Route, { path: "component/metadata/directory", element: _jsx(LegacyMetadataRedirect, { mode: "directory" }) }), _jsx(Route, { path: "component/metadata/resource/*", element: _jsx(LegacyMetadataRedirect, { mode: "resource" }) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
263
270
  type: 'object-form',
264
271
  formType: 'modal',
265
272
  objectName: currentObjectDef.name,
@@ -378,6 +385,7 @@ const RESERVED_SECOND_SEGMENTS = new Set([
378
385
  'new', 'view', 'record', 'edit',
379
386
  'dashboard', 'report', 'page',
380
387
  'design', 'search', 'create-app', 'edit-app',
388
+ 'metadata',
381
389
  ]);
382
390
  function looksLikeRecordId(segment) {
383
391
  if (!segment)
@@ -390,6 +398,27 @@ function looksLikeRecordId(segment) {
390
398
  // Most record ids are at least 6 chars (UUID, ULID, nanoid all >=8).
391
399
  return segment.length >= 6;
392
400
  }
401
+ /**
402
+ * Translates pre-refactor metadata admin URLs
403
+ * (`/apps/:app/component/metadata/resource/:name?type=:type`,
404
+ * `/apps/:app/component/metadata/directory`) into the new REST-style
405
+ * shape (`/apps/:app/metadata/:type/:name`). Keeps bookmarks and any
406
+ * still-unmigrated link producers working.
407
+ */
408
+ function LegacyMetadataRedirect({ mode }) {
409
+ const location = useLocation();
410
+ const appBase = location.pathname.replace(/\/component\/metadata\/.*$/, '');
411
+ if (mode === 'directory') {
412
+ return _jsx(Navigate, { to: `${appBase}/metadata${location.search}${location.hash}`, replace: true });
413
+ }
414
+ const sp = new URLSearchParams(location.search);
415
+ const type = sp.get('type') ?? '';
416
+ const tail = location.pathname.match(/\/component\/metadata\/resource(\/.*)?$/)?.[1] ?? '';
417
+ const target = type
418
+ ? `${appBase}/metadata/${encodeURIComponent(type)}${tail}${location.hash}`
419
+ : `${appBase}/metadata${location.hash}`;
420
+ return _jsx(Navigate, { to: target, replace: true });
421
+ }
393
422
  /**
394
423
  * Redirects `/apps/:appName/:objectName/:recordId` shorthand to the
395
424
  * canonical `/apps/:appName/:objectName/record/:recordId` so externally
@@ -21,7 +21,7 @@ import { lazy, Suspense } from 'react';
21
21
  import { registerAppComponent } from './componentRegistry';
22
22
  import { MetadataDirectoryPage, MetadataResourceRouter, registerMetadataResource, } from '../views/metadata-admin';
23
23
  import { PermissionMatrixEditPage } from '../views/metadata-admin/PermissionMatrixEditor';
24
- import { DesignerEditorWrapper } from '../views/metadata-admin/DesignerEditorWrapper';
24
+ import { DesignerEditorBody } from '../views/metadata-admin/DesignerEditorWrapper';
25
25
  /* -------------------------------------------------------------------------- */
26
26
  /* 1) Top-level admin pages — bound to `metadata:directory` + `metadata:resource` */
27
27
  /* -------------------------------------------------------------------------- */
@@ -104,7 +104,7 @@ const ObjectViewConfigurator = lazy(() => import('@object-ui/plugin-designer').t
104
104
  const DashboardEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.DashboardEditor })));
105
105
  const PageCanvasEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.PageCanvasEditor })));
106
106
  function ViewEditPage(props) {
107
- return (_jsx(DesignerEditorWrapper, { ...props, fromMetadata: (raw) => {
107
+ return (_jsx(DesignerEditorBody, { ...props, fromMetadata: (raw) => {
108
108
  // Normalize backend view metadata into the shape ObjectViewConfigurator expects.
109
109
  // Built-in views may omit `columns` or use legacy field names.
110
110
  const base = raw && typeof raw === 'object' ? raw : {};
@@ -119,10 +119,10 @@ function ViewEditPage(props) {
119
119
  }, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "view designer" }), children: _jsx(ObjectViewConfigurator, { config: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
120
120
  }
121
121
  function DashboardEditPage(props) {
122
- return (_jsx(DesignerEditorWrapper, { ...props, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "dashboard editor" }), children: _jsx(DashboardEditor, { schema: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
122
+ return (_jsx(DesignerEditorBody, { ...props, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "dashboard editor" }), children: _jsx(DashboardEditor, { schema: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
123
123
  }
124
124
  function PageEditPage(props) {
125
- return (_jsx(DesignerEditorWrapper, { ...props, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "page canvas" }), children: _jsx(PageCanvasEditor, { schema: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
125
+ return (_jsx(DesignerEditorBody, { ...props, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "page canvas" }), children: _jsx(PageCanvasEditor, { schema: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
126
126
  }
127
127
  function DesignerFallback({ label }) {
128
128
  return (_jsxs("div", { className: "p-6 text-sm text-muted-foreground", children: ["Loading ", label, "\u2026"] }));
@@ -132,7 +132,8 @@ registerMetadataResource({
132
132
  label: 'Views',
133
133
  description: 'Saved list / kanban / calendar / gantt configurations on top of an object.',
134
134
  domain: 'ui',
135
- EditPage: ViewEditPage,
135
+ DesignerTab: ViewEditPage,
136
+ designerTabLabel: 'View designer',
136
137
  listColumns: [
137
138
  { key: 'name', label: 'Name', width: '30%' },
138
139
  { key: 'object', label: 'Object', width: '20%' },
@@ -145,7 +146,8 @@ registerMetadataResource({
145
146
  label: 'Dashboards',
146
147
  description: 'Composed dashboards with charts, KPIs, and tables.',
147
148
  domain: 'ui',
148
- EditPage: DashboardEditPage,
149
+ DesignerTab: DashboardEditPage,
150
+ designerTabLabel: 'Dashboard designer',
149
151
  listColumns: [
150
152
  { key: 'name', label: 'Name', width: '30%' },
151
153
  { key: 'label', label: 'Label', width: '30%' },
@@ -157,7 +159,8 @@ registerMetadataResource({
157
159
  label: 'Pages',
158
160
  description: 'Visual page layouts authored in the Page Canvas editor.',
159
161
  domain: 'ui',
160
- EditPage: PageEditPage,
162
+ DesignerTab: PageEditPage,
163
+ designerTabLabel: 'Page canvas',
161
164
  listColumns: [
162
165
  { key: 'name', label: 'Name', width: '30%' },
163
166
  { key: 'label', label: 'Label', width: '30%' },
@@ -55,4 +55,14 @@ export interface DesignerEditorWrapperProps<TValue = any> {
55
55
  */
56
56
  toMetadata?: (value: TValue) => unknown;
57
57
  }
58
- export declare function DesignerEditorWrapper<TValue = any>({ type, name, renderDesigner, fromMetadata, toMetadata, }: DesignerEditorWrapperProps<TValue>): import("react/jsx-runtime").JSX.Element;
58
+ export declare function DesignerEditorWrapper<TValue = any>(props: DesignerEditorWrapperProps<TValue>): import("react/jsx-runtime").JSX.Element;
59
+ /**
60
+ * Embedded variant — same state machine, but no surrounding `PageShell`.
61
+ * Used by `ResourceEditPage` to host the designer inside a tab alongside
62
+ * the generic Form / Layers / References tabs. The action toolbar (Save /
63
+ * Reset / Refresh) is rendered inline at the top of the panel so the tab
64
+ * remains self-sufficient.
65
+ */
66
+ export declare function DesignerEditorBody<TValue = any>({ type, name, renderDesigner, fromMetadata, toMetadata, withChrome, }: DesignerEditorWrapperProps<TValue> & {
67
+ withChrome?: boolean;
68
+ }): import("react/jsx-runtime").JSX.Element;
@@ -47,7 +47,17 @@ import { PageShell } from './PageShell';
47
47
  import { useMetadataClient, useMetadataTypes } from './useMetadata';
48
48
  import { resolveResourceConfig } from './registry';
49
49
  import { t, detectLocale } from './i18n';
50
- export function DesignerEditorWrapper({ type, name, renderDesigner, fromMetadata, toMetadata, }) {
50
+ export function DesignerEditorWrapper(props) {
51
+ return _jsx(DesignerEditorBody, { ...props, withChrome: true });
52
+ }
53
+ /**
54
+ * Embedded variant — same state machine, but no surrounding `PageShell`.
55
+ * Used by `ResourceEditPage` to host the designer inside a tab alongside
56
+ * the generic Form / Layers / References tabs. The action toolbar (Save /
57
+ * Reset / Refresh) is rendered inline at the top of the panel so the tab
58
+ * remains self-sufficient.
59
+ */
60
+ export function DesignerEditorBody({ type, name, renderDesigner, fromMetadata, toMetadata, withChrome = false, }) {
51
61
  const navigate = useNavigate();
52
62
  const client = useMetadataClient();
53
63
  const { entries } = useMetadataTypes(client);
@@ -134,7 +144,15 @@ export function DesignerEditorWrapper({ type, name, renderDesigner, fromMetadata
134
144
  }
135
145
  }
136
146
  if (loading || value == null) {
137
- return (_jsx(PageShell, { entry: entry, itemName: name, children: _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" }), " ", t('engine.edit.loading', locale), " ", type, "/", name, "\u2026"] }) }));
147
+ const loadingBody = (_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" }), " ", t('engine.edit.loading', locale), " ", type, "/", name, "\u2026"] }));
148
+ return withChrome ? (_jsx(PageShell, { entry: entry, itemName: name, children: loadingBody })) : (loadingBody);
149
+ }
150
+ const toolbarButtons = (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: load, disabled: saving, children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1" }), " ", t('engine.list.refresh', locale)] }), withChrome && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(HistoryIcon, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.history', locale)] })), writable && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.reset', locale)] })), writable && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving || !dirty, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), t('engine.edit.save', locale)] }))] }));
151
+ const banners = (_jsxs(_Fragment, { children: [!writable && (_jsx("div", { className: "mx-6 mt-4 rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: t('engine.edit.readOnlyHint', locale) })), error && (_jsxs("div", { className: "mx-6 mt-4 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive flex items-start gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: error })] })), dirty && writable && (_jsxs("div", { className: "mx-6 mt-3 flex items-center gap-2 rounded-md border border-amber-300/50 bg-amber-50 dark:bg-amber-950/30 px-3 py-1.5 text-xs", children: [_jsx(Badge, { variant: "outline", children: t('engine.edit.unsaved', locale) }), _jsx("span", { children: t('engine.edit.unsavedHint', locale) })] }))] }));
152
+ const designerArea = (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: renderDesigner(value, (next) => setValue(next), !writable) }));
153
+ const destructiveDialog = (_jsx(Dialog, { open: !!destructive, onOpenChange: (open) => !open && setDestructive(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), " ", t('engine.edit.destructive', locale)] }), _jsx(DialogDescription, { children: t('engine.edit.destructiveHint', locale) })] }), _jsx("ul", { className: "text-sm space-y-1 max-h-64 overflow-auto", children: destructive?.issues.map((i, idx) => (_jsxs("li", { className: "border-l-2 border-amber-500 pl-2", children: [i.kind && _jsx(Badge, { variant: "outline", className: "mr-2", children: i.kind }), i.message ?? JSON.stringify(i)] }, idx))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setDestructive(null), children: t('engine.cancel', locale) }), _jsxs(Button, { variant: "destructive", onClick: () => destructive && doSave(true, destructive.pending), disabled: saving, children: [saving && _jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), t('engine.edit.forceSave', locale)] })] })] }) }));
154
+ if (!withChrome) {
155
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsx("div", { className: "flex items-center justify-end gap-1 px-6 pt-3", children: toolbarButtons }), banners, designerArea, destructiveDialog] }));
138
156
  }
139
- return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: t('engine.edit.bespokeDesigner', locale), actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: load, disabled: saving, children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1" }), " ", t('engine.list.refresh', locale)] }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history?type=${encodeURIComponent(type)}`), children: [_jsx(HistoryIcon, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.history', locale)] }), writable && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.reset', locale)] })), writable && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving || !dirty, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), t('engine.edit.save', locale)] }))] }), children: [_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [!writable && (_jsx("div", { className: "mx-6 mt-4 rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: t('engine.edit.readOnlyHint', locale) })), error && (_jsxs("div", { className: "mx-6 mt-4 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive flex items-start gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: error })] })), dirty && writable && (_jsxs("div", { className: "mx-6 mt-3 flex items-center gap-2 rounded-md border border-amber-300/50 bg-amber-50 dark:bg-amber-950/30 px-3 py-1.5 text-xs", children: [_jsx(Badge, { variant: "outline", children: t('engine.edit.unsaved', locale) }), _jsx("span", { children: t('engine.edit.unsavedHint', locale) })] })), _jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: renderDesigner(value, (next) => setValue(next), !writable) })] }), _jsx(Dialog, { open: !!destructive, onOpenChange: (open) => !open && setDestructive(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), " ", t('engine.edit.destructive', locale)] }), _jsx(DialogDescription, { children: t('engine.edit.destructiveHint', locale) })] }), _jsx("ul", { className: "text-sm space-y-1 max-h-64 overflow-auto", children: destructive?.issues.map((i, idx) => (_jsxs("li", { className: "border-l-2 border-amber-500 pl-2", children: [i.kind && _jsx(Badge, { variant: "outline", className: "mr-2", children: i.kind }), i.message ?? JSON.stringify(i)] }, idx))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setDestructive(null), children: t('engine.cancel', locale) }), _jsxs(Button, { variant: "destructive", onClick: () => destructive && doSave(true, destructive.pending), disabled: saving, children: [saving && _jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), t('engine.edit.forceSave', locale)] })] })] }) })] }));
157
+ return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: t('engine.edit.bespokeDesigner', locale), actions: toolbarButtons, children: [_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [banners, designerArea] }), destructiveDialog] }));
140
158
  }
@@ -126,7 +126,7 @@ function DomainChip({ domain, label, active, count, onClick, }) {
126
126
  function TypeTile({ entry, locale }) {
127
127
  // Prefer the locale-table translation; fall back to server's `label` (typically English).
128
128
  const label = translateMetadataType(entry.type, locale, entry.label);
129
- return (_jsxs(Link, { to: `../component/metadata/resource?type=${encodeURIComponent(entry.type)}`, className: "block p-4 border rounded-lg hover:border-primary hover:bg-accent transition-colors", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium truncate", children: label }), _jsx("code", { className: "text-xs text-muted-foreground font-mono", children: entry.type })] }), entry.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] shrink-0 ' +
129
+ return (_jsxs(Link, { to: `./${encodeURIComponent(entry.type)}`, className: "block p-4 border rounded-lg hover:border-primary hover:bg-accent transition-colors", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium truncate", children: label }), _jsx("code", { className: "text-xs text-muted-foreground font-mono", children: entry.type })] }), entry.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] shrink-0 ' +
130
130
  (entry.overrideSource === 'env'
131
131
  ? 'bg-amber-100 text-amber-800 hover:bg-amber-100'
132
132
  : 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), title: entry.overrideSource === 'env'
@@ -0,0 +1,13 @@
1
+ import type { RelatedTarget } from './RelatedPanel';
2
+ export interface MetadataDetailDrawerProps {
3
+ /** When non-null, drawer is open and shows this target. */
4
+ target: RelatedTarget | null;
5
+ /** Called when the drawer requests close (overlay click, esc, close btn). */
6
+ onClose: () => void;
7
+ /** Optional context: parent's type / name, shown in the title. */
8
+ parentContext?: {
9
+ type: string;
10
+ name: string;
11
+ };
12
+ }
13
+ export declare function MetadataDetailDrawer({ target, onClose, parentContext, }: MetadataDetailDrawerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,52 @@
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
+ * MetadataDetailDrawer — slide-over editor for a related metadata item.
5
+ *
6
+ * Opens from the parent's Related tab without taking the user away
7
+ * from the parent's context. Internally we mount the same
8
+ * `MetadataResourceEditPage` used by the full-page route, so all the
9
+ * Save / Reset / Validate behaviour is shared. The drawer just frames
10
+ * it and adds a "Open full page ↗" affordance.
11
+ *
12
+ * Width is wide enough for forms (max 1100px) but capped at 92vw to
13
+ * leave a thin strip of the parent visible behind, reinforcing the
14
+ * "still in the same object" feel.
15
+ */
16
+ import * as React from 'react';
17
+ import { useNavigate } from 'react-router-dom';
18
+ import { ExternalLink } from 'lucide-react';
19
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, Button, Badge, cn, } from '@object-ui/components';
20
+ import { MetadataResourceEditPage } from './ResourceEditPage';
21
+ export function MetadataDetailDrawer({ target, onClose, parentContext, }) {
22
+ const navigate = useNavigate();
23
+ const isMetadata = target?.kind === 'metadata';
24
+ const isEmbedded = target?.kind === 'embedded';
25
+ const headerType = isMetadata
26
+ ? target.type
27
+ : isEmbedded
28
+ ? target.groupLabel
29
+ : '';
30
+ const headerName = isMetadata
31
+ ? target.name
32
+ : isEmbedded
33
+ ? target.itemName
34
+ : '';
35
+ return (_jsx(Sheet, { open: target !== null, onOpenChange: (open) => {
36
+ if (!open)
37
+ onClose();
38
+ }, children: _jsxs(SheetContent, { side: "right", className: cn('w-[92vw] sm:max-w-[1100px] p-0 flex flex-col gap-0'), children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-[10px]", children: headerType }), _jsx(SheetTitle, { className: "font-mono text-base truncate", children: headerName }), _jsx("div", { className: "ml-auto flex items-center gap-1", children: isMetadata && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => {
39
+ navigate(`../../${encodeURIComponent(target.type)}/${encodeURIComponent(target.name)}`);
40
+ onClose();
41
+ }, title: "Open in full page", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5 mr-1" }), "Open full page"] })) })] }), parentContext && (_jsxs(SheetDescription, { className: "text-xs", children: [isEmbedded ? 'Embedded in ' : 'Related to ', _jsxs("span", { className: "font-mono", children: [parentContext.type, "/", parentContext.name] })] }))] }), _jsxs("div", { className: "flex-1 min-h-0 overflow-auto", children: [isMetadata && (_jsx(MetadataResourceEditPage, { type: target.type, name: target.name, embedded: true }, `${target.type}/${target.name}`)), isEmbedded && _jsx(EmbeddedItemView, { raw: target.raw })] })] }) }));
42
+ }
43
+ /**
44
+ * Read-only JSON preview for embedded items (fields, indexes, embedded
45
+ * validations). Editing happens via the parent's Form tab; jumping
46
+ * straight to that field is a future enhancement — for now the user
47
+ * can inspect the spec here and click "Edit in Form tab" in the panel.
48
+ */
49
+ function EmbeddedItemView({ raw }) {
50
+ const json = React.useMemo(() => JSON.stringify(raw, null, 2), [raw]);
51
+ return (_jsxs("div", { className: "p-4 space-y-3", children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: ["This item lives inside its parent's body. Edit it in the parent's", ' ', _jsx("span", { className: "font-medium", children: "Form" }), " tab."] }), _jsx("pre", { className: "text-xs font-mono bg-muted/40 border rounded p-3 overflow-auto whitespace-pre-wrap break-all", children: json })] }));
52
+ }
@@ -15,7 +15,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
15
15
  * page bodies stay focused on their domain logic.
16
16
  */
17
17
  import * as React from 'react';
18
- import { Link } from 'react-router-dom';
18
+ import { Link, useLocation } from 'react-router-dom';
19
19
  import { Badge } from '@object-ui/components';
20
20
  import { ChevronRight } from 'lucide-react';
21
21
  import { detectLocale, t, translateMetadataType } from './i18n';
@@ -24,7 +24,14 @@ export function PageShell({ entry, itemName, subtitle, stats, actions, children,
24
24
  const locale = React.useMemo(() => detectLocale(), []);
25
25
  // Prefer locale-table translation over server's English label.
26
26
  const label = translateMetadataType(type, locale, entry?.label);
27
- return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "px-6 pt-5 pb-4 border-b bg-background", children: [_jsxs("nav", { className: "flex items-center gap-1.5 text-xs text-muted-foreground mb-2", children: [_jsx(Link, { to: "../component/metadata/directory", className: "hover:text-foreground", children: t('engine.breadcrumb.allTypes', locale) }), _jsx(ChevronRight, { className: "h-3 w-3" }), _jsx(Link, { to: `../component/metadata/resource?type=${encodeURIComponent(type)}`, className: "hover:text-foreground", children: label }), itemName && (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { className: "h-3 w-3" }), _jsx("span", { className: "text-foreground font-medium font-mono", children: itemName })] }))] }), _jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-xl font-semibold truncate", children: label }), _jsx("code", { className: "text-xs font-mono text-muted-foreground", children: type }), entry?.domain && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase tracking-wider", children: entry.domain })), entry?.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] ' +
27
+ // Compute base path up to /metadata so breadcrumb links work regardless
28
+ // of how deep the current route is (list, edit, history, …).
29
+ const { pathname } = useLocation();
30
+ const metadataBase = React.useMemo(() => {
31
+ const idx = pathname.indexOf('/metadata');
32
+ return idx >= 0 ? pathname.slice(0, idx + '/metadata'.length) : '/metadata';
33
+ }, [pathname]);
34
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "px-6 pt-5 pb-4 border-b bg-background", children: [_jsxs("nav", { className: "flex items-center gap-1.5 text-xs text-muted-foreground mb-2", children: [_jsx(Link, { to: metadataBase, className: "hover:text-foreground", children: t('engine.breadcrumb.allTypes', locale) }), _jsx(ChevronRight, { className: "h-3 w-3" }), _jsx(Link, { to: `${metadataBase}/${encodeURIComponent(type)}`, className: "hover:text-foreground", children: label }), itemName && (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { className: "h-3 w-3" }), _jsx("span", { className: "text-foreground font-medium font-mono", children: itemName })] }))] }), _jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-xl font-semibold truncate", children: label }), _jsx("code", { className: "text-xs font-mono text-muted-foreground", children: type }), entry?.domain && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase tracking-wider", children: entry.domain })), entry?.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] ' +
28
35
  (entry.overrideSource === 'env'
29
36
  ? 'bg-amber-100 text-amber-800 hover:bg-amber-100'
30
37
  : 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), title: entry.overrideSource === 'env'
@@ -123,10 +123,10 @@ export function MetadataQuickFind({ appSlug } = {}) {
123
123
  setOpen(false);
124
124
  const base = appSlug ? `/apps/${appSlug}` : '..';
125
125
  if (r.kind === 'type') {
126
- navigate(`${base}/component/metadata/resource?type=${encodeURIComponent(r.entry.type)}`);
126
+ navigate(`${base}/metadata/${encodeURIComponent(r.entry.type)}`);
127
127
  }
128
128
  else {
129
- navigate(`${base}/component/metadata/resource/${encodeURIComponent(r.name)}?type=${encodeURIComponent(r.type)}`);
129
+ navigate(`${base}/metadata/${encodeURIComponent(r.type)}/${encodeURIComponent(r.name)}`);
130
130
  }
131
131
  }
132
132
  function onKeyDown(e) {
@@ -0,0 +1,33 @@
1
+ export interface RelatedPanelProps {
2
+ /** Parent metadata type, e.g. `object`. */
3
+ type: string;
4
+ /** Parent item name (e.g. `sys_user`). */
5
+ name: string;
6
+ /**
7
+ * Effective parent body — used to feed `source: 'embedded'` anchors
8
+ * (e.g. `object.fields[]`). Optional: if absent, embedded groups are
9
+ * skipped.
10
+ */
11
+ parentItem?: Record<string, unknown> | null;
12
+ /** Invoked when the user clicks a row. Parent should open a drawer. */
13
+ onOpen: (target: RelatedTarget) => void;
14
+ }
15
+ /**
16
+ * Target opened from a Related row. For first-class metadata items
17
+ * `kind: 'metadata'` carries the addressable (type, name). For embedded
18
+ * items we hand back the raw object plus the path it came from so the
19
+ * parent can render a focused detail view.
20
+ */
21
+ export type RelatedTarget = {
22
+ kind: 'metadata';
23
+ type: string;
24
+ name: string;
25
+ } | {
26
+ kind: 'embedded';
27
+ parentType: string;
28
+ parentName: string;
29
+ groupLabel: string;
30
+ itemName: string;
31
+ raw: Record<string, unknown>;
32
+ };
33
+ export declare function RelatedPanel({ type, name, parentItem, onOpen, }: RelatedPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,171 @@
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
+ * RelatedPanel — lists metadata items anchored to the current parent.
5
+ *
6
+ * Driven by the anchor registry (see `anchors.ts` / `registry.ts`). For
7
+ * each child type that declares it anchors at `parentType`, we issue
8
+ * one `client.list(childType)` call in parallel, filter the result with
9
+ * the anchor's match predicate, and render the survivors in collapsible
10
+ * groups.
11
+ *
12
+ * Why client-side filter? Anchoring lives in metadata bodies that are
13
+ * not indexed on the server today, and adding a parameterised list API
14
+ * for every potential anchor field would balloon the surface area. The
15
+ * Related tab is opened by humans on a specific object, so the cost of
16
+ * pulling 100-or-so items per child type is negligible — far cheaper
17
+ * than a network round-trip per row would have been.
18
+ *
19
+ * Visual model:
20
+ * - One <details> per group (default open if non-empty).
21
+ * - Rows are kebab-y: name, optional label, optional badge.
22
+ * - Click → call `onOpen({ type, name })`; parent owns the drawer.
23
+ * - A search input filters across all groups by name/label substring.
24
+ */
25
+ import * as React from 'react';
26
+ import { useNavigate } from 'react-router-dom';
27
+ import { Loader2, Search, Plus, ChevronRight, ExternalLink } from 'lucide-react';
28
+ import { Badge, Button, Input, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
29
+ import { useMetadataClient } from './useMetadata';
30
+ import { listAnchorsFor } from './registry';
31
+ export function RelatedPanel({ type, name, parentItem, onOpen, }) {
32
+ const client = useMetadataClient();
33
+ const navigate = useNavigate();
34
+ const anchors = React.useMemo(() => listAnchorsFor(type), [type]);
35
+ const [groups, setGroups] = React.useState([]);
36
+ const [search, setSearch] = React.useState('');
37
+ const [collapsed, setCollapsed] = React.useState({});
38
+ React.useEffect(() => {
39
+ if (anchors.length === 0) {
40
+ setGroups([]);
41
+ return;
42
+ }
43
+ let cancelled = false;
44
+ // Seed groups in loading state, ordered by registry order. Embedded
45
+ // groups resolve synchronously from `parentItem`; list groups dispatch
46
+ // a `client.list(childType)` and filter.
47
+ const initial = [...anchors]
48
+ .sort((a, b) => (a.anchor.order ?? 999) - (b.anchor.order ?? 999))
49
+ .map((entry) => {
50
+ const isEmbedded = entry.anchor.source === 'embedded';
51
+ if (isEmbedded) {
52
+ const raw = entry.anchor.extract && parentItem
53
+ ? entry.anchor.extract(parentItem)
54
+ : [];
55
+ return {
56
+ childType: entry.type,
57
+ anchor: entry.anchor,
58
+ loading: false,
59
+ error: null,
60
+ items: raw.map(normaliseItem),
61
+ };
62
+ }
63
+ return {
64
+ childType: entry.type,
65
+ anchor: entry.anchor,
66
+ loading: true,
67
+ error: null,
68
+ items: [],
69
+ };
70
+ });
71
+ setGroups(initial);
72
+ void Promise.all(initial.map(async (g) => {
73
+ if (g.anchor.source === 'embedded')
74
+ return g;
75
+ try {
76
+ const list = (await client.list(g.childType));
77
+ if (cancelled)
78
+ return g;
79
+ const matchFn = g.anchor.match ?? (() => false);
80
+ const filtered = list
81
+ .filter((item) => matchFn(item, name))
82
+ .map((item) => normaliseItem(item));
83
+ return { ...g, loading: false, items: filtered };
84
+ }
85
+ catch (err) {
86
+ if (cancelled)
87
+ return g;
88
+ return {
89
+ ...g,
90
+ loading: false,
91
+ error: err instanceof Error ? err.message : String(err),
92
+ };
93
+ }
94
+ })).then((finished) => {
95
+ if (cancelled)
96
+ return;
97
+ setGroups(finished);
98
+ });
99
+ return () => {
100
+ cancelled = true;
101
+ };
102
+ }, [client, type, name, anchors, parentItem]);
103
+ if (anchors.length === 0) {
104
+ return (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No related metadata" }), _jsxs(EmptyDescription, { children: ["No metadata types are configured to anchor at ", _jsx("code", { children: type }), ". You can register one via", ' ', _jsx("code", { className: "font-mono", children: "registerMetadataResource" }), "."] })] }));
105
+ }
106
+ const totalCount = groups.reduce((sum, g) => sum + g.items.length, 0);
107
+ const anyLoading = groups.some((g) => g.loading);
108
+ const q = search.trim().toLowerCase();
109
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "relative flex-1 max-w-sm", children: [_jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Search related\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-8 h-8 text-sm" })] }), _jsx("div", { className: "text-xs text-muted-foreground", children: anyLoading
110
+ ? 'Scanning…'
111
+ : `${totalCount} item${totalCount === 1 ? '' : 's'}` })] }), groups.map((g) => {
112
+ const matches = q
113
+ ? g.items.filter((it) => it.name.toLowerCase().includes(q) ||
114
+ (it.label ?? '').toLowerCase().includes(q))
115
+ : g.items;
116
+ const isCollapsed = collapsed[g.childType] ?? false;
117
+ const visible = !q || matches.length > 0;
118
+ if (!visible && !g.loading)
119
+ return null;
120
+ return (_jsxs("div", { className: "border rounded-lg overflow-hidden", children: [_jsxs("div", { className: "w-full flex items-center gap-2 px-3 py-2 bg-muted/40", children: [_jsxs("button", { type: "button", className: "flex items-center gap-2 flex-1 text-left hover:opacity-80", onClick: () => setCollapsed((s) => ({ ...s, [g.childType]: !isCollapsed })), children: [_jsx(ChevronRight, { className: cn('h-4 w-4 text-muted-foreground transition-transform', !isCollapsed && 'rotate-90') }), _jsx("div", { className: "text-sm font-medium", children: g.anchor.groupLabel ?? g.childType }), _jsx(Badge, { variant: "outline", className: "text-[10px]", children: g.loading ? '…' : matches.length })] }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-7 px-2", onClick: () => {
121
+ if (g.anchor.source === 'embedded') {
122
+ // Embedded items are edited inside the parent's Form
123
+ // tab; jump there rather than to a nonexistent route.
124
+ if (typeof window !== 'undefined') {
125
+ const url = new URL(window.location.href);
126
+ url.searchParams.set('tab', 'form');
127
+ url.searchParams.delete('open');
128
+ window.location.assign(url.toString());
129
+ }
130
+ return;
131
+ }
132
+ navigate(`../../${encodeURIComponent(g.childType)}/_new?anchor=${encodeURIComponent(`${type}:${name}`)}`);
133
+ }, title: g.anchor.source === 'embedded'
134
+ ? `Edit in Form tab`
135
+ : `New ${g.childType}`, children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })] }), !isCollapsed && (_jsxs("div", { className: "divide-y", children: [g.loading && (_jsxs("div", { className: "px-3 py-3 text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), "Loading ", g.childType, "\u2026"] })), !g.loading && g.error && (_jsxs("div", { className: "px-3 py-3 text-xs text-destructive", children: ["Failed: ", g.error] })), !g.loading && !g.error && matches.length === 0 && (_jsx("div", { className: "px-3 py-3 text-xs text-muted-foreground", children: q ? 'No matches.' : 'Nothing here yet.' })), !g.loading &&
136
+ !g.error &&
137
+ matches.map((it, idx) => (_jsxs("button", { type: "button", onClick: () => onOpen(g.anchor.source === 'embedded'
138
+ ? {
139
+ kind: 'embedded',
140
+ parentType: type,
141
+ parentName: name,
142
+ groupLabel: g.anchor.groupLabel ?? g.childType,
143
+ itemName: it.name,
144
+ raw: it.raw,
145
+ }
146
+ : {
147
+ kind: 'metadata',
148
+ type: g.childType,
149
+ name: it.name,
150
+ }), className: "w-full text-left px-3 py-2 hover:bg-accent/50 flex items-center gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-mono text-xs truncate", children: it.name }), it.label && it.label !== it.name && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: it.label }))] }), _jsx(ExternalLink, { className: "h-3.5 w-3.5 text-muted-foreground" })] }, `${it.name}-${idx}`)))] }))] }, g.childType));
151
+ })] }));
152
+ }
153
+ function normaliseItem(raw) {
154
+ const nameVal = pickString(raw, ['name', 'id', 'key']) ?? '(unnamed)';
155
+ const labelVal = pickString(raw, ['label', 'title', 'displayName']);
156
+ const descVal = pickString(raw, ['description', 'summary']);
157
+ return {
158
+ name: nameVal,
159
+ label: labelVal,
160
+ description: descVal,
161
+ raw,
162
+ };
163
+ }
164
+ function pickString(obj, keys) {
165
+ for (const k of keys) {
166
+ const v = obj[k];
167
+ if (typeof v === 'string' && v.length > 0)
168
+ return v;
169
+ }
170
+ return undefined;
171
+ }
@@ -1,7 +1,13 @@
1
1
  export interface MetadataResourceEditPageProps {
2
- type: string;
3
- name: string;
2
+ type?: string;
3
+ name?: string;
4
4
  /** When true, this is the Create flow (skip initial fetch). */
5
5
  createMode?: boolean;
6
+ /**
7
+ * When true, the editor is rendered inside another surface (e.g.
8
+ * the Related drawer). Hides Related-tab and URL-sync so the inner
9
+ * page does not fight the outer page for `?tab` / `?open`.
10
+ */
11
+ embedded?: boolean;
6
12
  }
7
- export declare function MetadataResourceEditPage({ type, name, createMode, }: MetadataResourceEditPageProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode, embedded, }: MetadataResourceEditPageProps): import("react/jsx-runtime").JSX.Element;