@object-ui/app-shell 6.2.0 → 6.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/console/AppContent.js +30 -1
  3. package/dist/console/ai/AiChatPage.js +19 -22
  4. package/dist/layout/ConsoleFloatingChatbot.js +44 -24
  5. package/dist/services/builtinComponents.js +10 -7
  6. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +11 -1
  7. package/dist/views/metadata-admin/DesignerEditorWrapper.js +21 -3
  8. package/dist/views/metadata-admin/DirectoryPage.js +1 -1
  9. package/dist/views/metadata-admin/EmbeddedItemEditor.d.ts +15 -0
  10. package/dist/views/metadata-admin/EmbeddedItemEditor.js +194 -0
  11. package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
  12. package/dist/views/metadata-admin/MetadataDetailDrawer.js +28 -0
  13. package/dist/views/metadata-admin/PageShell.js +9 -2
  14. package/dist/views/metadata-admin/QuickFind.js +2 -2
  15. package/dist/views/metadata-admin/RelatedPanel.d.ts +37 -0
  16. package/dist/views/metadata-admin/RelatedPanel.js +173 -0
  17. package/dist/views/metadata-admin/ResourceEditPage.d.ts +9 -3
  18. package/dist/views/metadata-admin/ResourceEditPage.js +105 -14
  19. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +3 -3
  20. package/dist/views/metadata-admin/ResourceHistoryPage.js +6 -3
  21. package/dist/views/metadata-admin/ResourceListPage.d.ts +2 -2
  22. package/dist/views/metadata-admin/ResourceListPage.js +6 -4
  23. package/dist/views/metadata-admin/SchemaForm.js +10 -1
  24. package/dist/views/metadata-admin/anchors.d.ts +1 -0
  25. package/dist/views/metadata-admin/anchors.js +235 -0
  26. package/dist/views/metadata-admin/index.d.ts +7 -3
  27. package/dist/views/metadata-admin/index.js +14 -2
  28. package/dist/views/metadata-admin/preview-registry.d.ts +43 -0
  29. package/dist/views/metadata-admin/preview-registry.js +18 -0
  30. package/dist/views/metadata-admin/previews/AppPreview.d.ts +2 -0
  31. package/dist/views/metadata-admin/previews/AppPreview.js +101 -0
  32. package/dist/views/metadata-admin/previews/DashboardPreview.d.ts +2 -0
  33. package/dist/views/metadata-admin/previews/DashboardPreview.js +25 -0
  34. package/dist/views/metadata-admin/previews/EmailTemplatePreview.d.ts +2 -0
  35. package/dist/views/metadata-admin/previews/EmailTemplatePreview.js +65 -0
  36. package/dist/views/metadata-admin/previews/ObjectPreview.d.ts +2 -0
  37. package/dist/views/metadata-admin/previews/ObjectPreview.js +36 -0
  38. package/dist/views/metadata-admin/previews/PagePreview.d.ts +2 -0
  39. package/dist/views/metadata-admin/previews/PagePreview.js +26 -0
  40. package/dist/views/metadata-admin/previews/PreviewShell.d.ts +40 -0
  41. package/dist/views/metadata-admin/previews/PreviewShell.js +49 -0
  42. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +2 -0
  43. package/dist/views/metadata-admin/previews/ReportPreview.js +27 -0
  44. package/dist/views/metadata-admin/previews/ViewPreview.d.ts +2 -0
  45. package/dist/views/metadata-admin/previews/ViewPreview.js +113 -0
  46. package/dist/views/metadata-admin/previews/index.d.ts +1 -0
  47. package/dist/views/metadata-admin/previews/index.js +26 -0
  48. package/dist/views/metadata-admin/registry.d.ts +124 -0
  49. package/dist/views/metadata-admin/registry.js +58 -0
  50. package/package.json +26 -26
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 6.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - c5821ce: `AiChatPage` no longer PATCHes a client-side title-from-first-message
8
+ on the freshly-created conversation. The server (`@objectstack/service-ai`
9
+ ≥ next minor) now generates a concise LLM-summarised title fire-and-forget
10
+ after the first assistant turn lands, and a client-side truncated title
11
+ would race that and win — pinning every conversation row to a 40-char
12
+ substring of the first user message instead of a real summary.
13
+
14
+ Drop the PATCH; bump the sidebar list a couple of times (2.5 s + 6 s)
15
+ to pick up the LLM title whenever the model finally responds.
16
+
17
+ - 3b35084: Fix: floating chatbot now replays persisted conversation history on mount.
18
+
19
+ The right-corner floating chatbot (`ConsoleFloatingChatbot`) was passing only
20
+ `conversationId` to its inner `useObjectChat`, dropping the `initialMessages`
21
+ returned by `useChatConversation`. Backend persistence already worked — the
22
+ server-side `ai_conversation` + `ai_message` rows were created and survived a
23
+ page refresh — but the UI started each session with just the static "welcome"
24
+ bubble, making users believe their history had been lost.
25
+
26
+ Now matches the `/ai/:conversationId` full-page chat: history is hydrated
27
+ into the chat surface, and the welcome bubble is suppressed when prior turns
28
+ exist (showing it above real user/assistant turns is confusing).
29
+
30
+ - Updated dependencies [a66f788]
31
+ - @object-ui/react@6.2.2
32
+ - @object-ui/components@6.2.2
33
+ - @object-ui/fields@6.2.2
34
+ - @object-ui/layout@6.2.2
35
+ - @object-ui/plugin-editor@6.2.2
36
+ - @object-ui/types@6.2.2
37
+ - @object-ui/core@6.2.2
38
+ - @object-ui/i18n@6.2.2
39
+ - @object-ui/data-objectstack@6.2.2
40
+ - @object-ui/auth@6.2.2
41
+ - @object-ui/permissions@6.2.2
42
+ - @object-ui/collaboration@6.2.2
43
+ - @object-ui/providers@6.2.2
44
+
45
+ ## 6.2.1
46
+
47
+ ### Patch Changes
48
+
49
+ - bc269b0: fix
50
+ - @object-ui/types@6.2.1
51
+ - @object-ui/core@6.2.1
52
+ - @object-ui/i18n@6.2.1
53
+ - @object-ui/react@6.2.1
54
+ - @object-ui/components@6.2.1
55
+ - @object-ui/fields@6.2.1
56
+ - @object-ui/layout@6.2.1
57
+ - @object-ui/data-objectstack@6.2.1
58
+ - @object-ui/auth@6.2.1
59
+ - @object-ui/permissions@6.2.1
60
+ - @object-ui/plugin-editor@6.2.1
61
+ - @object-ui/collaboration@6.2.1
62
+ - @object-ui/providers@6.2.1
63
+
3
64
  ## 6.2.0
4
65
 
5
66
  ### 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
@@ -15,7 +15,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
16
  import { useNavigate, useParams } from 'react-router-dom';
17
17
  import { useAuth } from '@object-ui/auth';
18
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
18
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, } from '@object-ui/components';
19
+ import { Share2 } from 'lucide-react';
19
20
  import { ChatbotEnhanced, useAgents, useObjectChat, useHitlInChat, } from '@object-ui/plugin-chatbot';
20
21
  import { AppHeader } from '../../layout/AppHeader';
21
22
  import { useNavigationContext } from '../../context/NavigationContext';
@@ -63,6 +64,8 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
63
64
  activeId: urlConversationId,
64
65
  });
65
66
  const [refreshKey, setRefreshKey] = useState(0);
67
+ const [shareOpen, setShareOpen] = useState(false);
68
+ const restApiBase = useMemo(() => apiBase.replace(/\/v1\/ai$/, '').replace(/\/ai$/, '') || '/api', [apiBase]);
66
69
  // After the hook resolves a real id for a fresh `/ai` visit, mirror it into
67
70
  // the URL so the sidebar's active-row + share/refresh both work.
68
71
  useEffect(() => {
@@ -81,32 +84,26 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
81
84
  const handleSent = useCallback((firstUserMessage) => {
82
85
  // New user turn → bump sidebar list so the row's preview/timestamp refreshes.
83
86
  setRefreshKey((k) => k + 1);
84
- // Auto-title the conversation from the first user message. Server only
85
- // tracks `title` when we PATCH it; without this every row shows the same
86
- // "New conversation" placeholder. We mark `conversationId` as titled in a
87
- // ref so subsequent turns don't re-rename and clobber a manual rename.
87
+ // Server now generates a concise LLM-summarised title fire-and-forget
88
+ // after the first assistant turn lands (see service-ai
89
+ // `summarizeConversation`). We don't PATCH a truncated preview from the
90
+ // client anymore that races the LLM and wins, which would block the
91
+ // real title. Instead, bump the sidebar a couple of times so the new
92
+ // title is picked up whenever the model finally responds.
88
93
  if (!firstUserMessage || !conversationId)
89
94
  return;
90
95
  if (titledRef.current.has(conversationId))
91
96
  return;
92
97
  titledRef.current.add(conversationId);
93
- const text = firstUserMessage.trim();
94
- if (!text)
95
- return;
96
- const truncated = text.length > 40 ? `${text.slice(0, 40)}…` : text;
97
- fetch(`${apiBase}/conversations/${encodeURIComponent(conversationId)}`, {
98
- method: 'PATCH',
99
- credentials: 'include',
100
- headers: { 'Content-Type': 'application/json' },
101
- body: JSON.stringify({ title: truncated }),
102
- })
103
- .then(() => setRefreshKey((k) => k + 1))
104
- .catch(() => {
105
- // Leave it untitled; user can rename manually.
106
- titledRef.current.delete(conversationId);
107
- });
108
- }, [apiBase, conversationId]);
109
- return (_jsxs("div", { className: "flex h-svh w-full flex-col bg-background", "data-testid": "ai-chat-page", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsxs("div", { className: "flex flex-1 min-h-0 w-full", children: [_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, refreshKey: refreshKey, className: "hidden w-72 shrink-0 border-r md:flex" }), _jsx("main", { className: "flex flex-1 min-w-0 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, initialMessages: initialMessages, hydrating: convoLoading, onSent: handleSent }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }));
98
+ const bump = () => setRefreshKey((k) => k + 1);
99
+ const t1 = setTimeout(bump, 2500);
100
+ const t2 = setTimeout(bump, 6000);
101
+ // Best-effort: if the component unmounts before the bumps fire, the
102
+ // setRefreshKey call is a no-op so we don't bother tracking the timers.
103
+ void t1;
104
+ void t2;
105
+ }, [conversationId]);
106
+ return (_jsxs("div", { className: "flex h-svh w-full flex-col bg-background", "data-testid": "ai-chat-page", children: [_jsxs("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: [_jsx(AppHeader, { variant: "home" }), _jsx("div", { className: "ml-auto flex items-center gap-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", onClick: () => setShareOpen(true), disabled: !conversationId, "data-testid": "ai-chat-share-button", title: conversationId ? 'Share this conversation' : 'Start chatting to enable sharing', children: [_jsx(Share2, { className: "h-3.5 w-3.5" }), "Share"] }) })] }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase })), _jsxs("div", { className: "flex flex-1 min-h-0 w-full", children: [_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, refreshKey: refreshKey, className: "hidden w-72 shrink-0 border-r md:flex" }), _jsx("main", { className: "flex flex-1 min-w-0 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, initialMessages: initialMessages, hydrating: convoLoading, onSent: handleSent }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }));
110
107
  }
111
108
  function ChatPane({ agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, conversationId, initialMessages, hydrating, onSent, }) {
112
109
  const activeAgentLabel = useMemo(() => {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * ConsoleFloatingChatbot
4
4
  *
@@ -13,7 +13,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  */
14
14
  import React from 'react';
15
15
  import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, } from '@object-ui/plugin-chatbot';
16
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
16
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, } from '@object-ui/components';
17
+ import { Share2 } from 'lucide-react';
17
18
  import { useChatConversation } from '../hooks';
18
19
  const DEFAULT_AI_PATH = '/api/v1/ai';
19
20
  function resolveApiBase(explicit) {
@@ -26,8 +27,19 @@ function resolveApiBase(explicit) {
26
27
  const serverUrl = env.VITE_SERVER_URL ?? '';
27
28
  return `${serverUrl.replace(/\/$/, '')}${DEFAULT_AI_PATH}`;
28
29
  }
29
- function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, conversationId, }) {
30
+ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, conversationId, initialMessages: persistedMessages, }) {
30
31
  const objectNames = objects.map((o) => o.label || o.name).join(', ');
32
+ // Replay persisted history when present. Suppress the static "welcome"
33
+ // bubble in that case — showing it above real prior turns is confusing.
34
+ const hydratedHistory = React.useMemo(() => {
35
+ if (!persistedMessages || persistedMessages.length === 0)
36
+ return [];
37
+ return persistedMessages.map((m) => ({
38
+ id: m.id,
39
+ role: m.role,
40
+ content: m.parts.map((p) => p.text).join(''),
41
+ }));
42
+ }, [persistedMessages]);
31
43
  const activeAgentLabel = React.useMemo(() => {
32
44
  const found = agents.find((a) => a.name === activeAgent);
33
45
  return found?.label ?? activeAgent ?? appLabel;
@@ -45,13 +57,15 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
45
57
  agentName: activeAgent,
46
58
  },
47
59
  },
48
- initialMessages: [
49
- {
50
- id: 'welcome',
51
- role: 'assistant',
52
- content: welcomeContent,
53
- },
54
- ],
60
+ initialMessages: hydratedHistory.length > 0
61
+ ? hydratedHistory
62
+ : [
63
+ {
64
+ id: 'welcome',
65
+ role: 'assistant',
66
+ content: welcomeContent,
67
+ },
68
+ ],
55
69
  // Local-mode fallback: only used when `chatApi` is undefined (no agent
56
70
  // resolved yet, or no backend available). Keeps the UI usable.
57
71
  autoResponse: !chatApi,
@@ -73,18 +87,24 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
73
87
  },
74
88
  });
75
89
  const headerExtra = agents.length > 0 ? (_jsxs(Select, { value: activeAgent, onValueChange: onAgentChange, disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-[180px] text-xs", "data-testid": "floating-chatbot-agent-picker", children: _jsx(SelectValue, { placeholder: "Choose agent..." }) }), _jsx(SelectContent, { align: "end", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: agent.label }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[220px]", children: agent.description })) : null] }, agent.name))) })] })) : null;
76
- return (_jsx(FloatingChatbot, { floatingConfig: {
77
- position: 'bottom-right',
78
- defaultOpen,
79
- panelWidth: 420,
80
- panelHeight: 560,
81
- title: `${appLabel} Assistant`,
82
- triggerSize: 56,
83
- }, headerExtra: headerExtra, messages: messages, placeholder: activeAgent
84
- ? `Ask ${activeAgentLabel}...`
85
- : agentsLoading
86
- ? 'Loading agents...'
87
- : 'Ask anything...', onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true, onToolApprove: hitl.decide, toolDecisions: hitl.decisions, toolApproveLabel: "Approve & run", toolDenyLabel: "Reject", toolDenyReason: "Operator rejected from chat" }));
90
+ // Share-link control. Sits to the left of the panel's built-in
91
+ // fullscreen / close buttons so users can mint a public link without
92
+ // jumping out to the full `/ai/:id` page.
93
+ const [shareOpen, setShareOpen] = React.useState(false);
94
+ const restApiBase = React.useMemo(() => apiBase.replace(/\/v1\/ai$/, '').replace(/\/ai$/, '') || '/api', [apiBase]);
95
+ const headerActions = conversationId ? (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", "aria-label": "Share conversation", "data-testid": "floating-chatbot-share", onClick: () => setShareOpen(true), children: _jsx(Share2, { className: "h-4 w-4" }) })) : null;
96
+ return (_jsxs(_Fragment, { children: [_jsx(FloatingChatbot, { floatingConfig: {
97
+ position: 'bottom-right',
98
+ defaultOpen,
99
+ panelWidth: 420,
100
+ panelHeight: 560,
101
+ title: `${appLabel} Assistant`,
102
+ triggerSize: 56,
103
+ }, headerExtra: headerExtra, headerActions: headerActions, messages: messages, placeholder: activeAgent
104
+ ? `Ask ${activeAgentLabel}...`
105
+ : agentsLoading
106
+ ? 'Loading agents...'
107
+ : 'Ask anything...', onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true, onToolApprove: hitl.decide, toolDecisions: hitl.decisions, toolApproveLabel: "Approve & run", toolDenyLabel: "Reject", toolDenyReason: "Operator rejected from chat" }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase }))] }));
88
108
  }
89
109
  export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen = false, userId, }) {
90
110
  const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
@@ -105,7 +125,7 @@ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: api
105
125
  // Server-backed conversation. Scoped by agent so each agent gets its own
106
126
  // persistent history. Hook is inert until `userId` is provided; without it
107
127
  // the FAB continues to work in local-only mode (no persistence).
108
- const { conversationId } = useChatConversation({
128
+ const { conversationId, initialMessages } = useChatConversation({
109
129
  userId,
110
130
  scope: activeAgent,
111
131
  apiBase,
@@ -113,5 +133,5 @@ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: api
113
133
  // `key` forces a clean remount whenever the chat endpoint OR the resolved
114
134
  // conversation id changes — required because `useObjectChat` locks its mode
115
135
  // (api vs local) and its `conversationId` on first render.
116
- return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, defaultOpen: defaultOpen, conversationId: conversationId }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`));
136
+ return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, defaultOpen: defaultOpen, conversationId: conversationId, initialMessages: initialMessages }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`));
117
137
  }
@@ -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,15 @@
1
+ export interface EmbeddedItemEditorProps {
2
+ parentType: string;
3
+ parentName: string;
4
+ /** Dotted path inside parent body where the collection lives. */
5
+ embeddedPath?: string;
6
+ /** Key of the item within the collection (e.g. field name). */
7
+ itemName: string;
8
+ /** Metadata type whose schema/form drives the form. */
9
+ editAs?: string;
10
+ /** Snapshot of the item at the moment of opening (initial draft). */
11
+ initialRaw: Record<string, unknown>;
12
+ /** Called after a successful save with the freshly-saved item. */
13
+ onSaved?: (item: Record<string, unknown>) => void;
14
+ }
15
+ export declare function EmbeddedItemEditor({ parentType, parentName, embeddedPath, itemName, editAs, initialRaw, onSaved, }: EmbeddedItemEditorProps): import("react/jsx-runtime").JSX.Element;