@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
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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}/
|
|
126
|
+
navigate(`${base}/metadata/${encodeURIComponent(r.entry.type)}`);
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
|
-
navigate(`${base}/
|
|
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
|
|
3
|
-
name
|
|
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;
|