@object-ui/app-shell 6.2.3 → 7.0.0
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 +948 -0
- package/README.md +292 -0
- package/dist/assistant/assistantBus.d.ts +72 -0
- package/dist/assistant/assistantBus.js +133 -0
- package/dist/chrome/CommandPalette.d.ts +1 -1
- package/dist/chrome/CommandPalette.js +26 -22
- package/dist/chrome/ConditionalAuthWrapper.d.ts +1 -1
- package/dist/chrome/ConsoleToaster.d.ts +1 -1
- package/dist/chrome/ConsoleToaster.js +3 -1
- package/dist/chrome/ErrorBoundary.d.ts +1 -1
- package/dist/chrome/KeyboardShortcutsDialog.d.ts +1 -1
- package/dist/chrome/KeyboardShortcutsDialog.js +16 -5
- package/dist/chrome/LoadingScreen.d.ts +1 -1
- package/dist/chrome/LoadingScreen.js +22 -26
- package/dist/chrome/RouteFader.d.ts +1 -1
- package/dist/chrome/ThemeProvider.d.ts +1 -1
- package/dist/components/ManagedByBadge.d.ts +1 -1
- package/dist/console/AppContent.d.ts +1 -1
- package/dist/console/AppContent.js +170 -37
- package/dist/console/ConsoleShell.d.ts +7 -7
- package/dist/console/ConsoleShell.js +32 -3
- package/dist/console/ai/AiChatPage.d.ts +88 -1
- package/dist/console/ai/AiChatPage.js +743 -66
- package/dist/console/ai/ConversationsSidebar.d.ts +26 -1
- package/dist/console/ai/ConversationsSidebar.js +149 -34
- package/dist/console/ai/LiveCanvas.d.ts +22 -0
- package/dist/console/ai/LiveCanvas.js +78 -0
- package/dist/console/ai/reconcileTurn.d.ts +8 -0
- package/dist/console/ai/reconcileTurn.js +20 -0
- package/dist/console/auth/AuthPageLayout.d.ts +1 -1
- package/dist/console/auth/ForgotPasswordPage.d.ts +1 -1
- package/dist/console/auth/LoginPage.d.ts +1 -1
- package/dist/console/auth/RegisterPage.d.ts +1 -1
- package/dist/console/auth/RegisterPage.js +23 -3
- package/dist/console/cloud-connection/CloudConnectionPanel.d.ts +1 -0
- package/dist/console/cloud-connection/CloudConnectionPanel.js +169 -0
- package/dist/console/home/AppCard.d.ts +1 -1
- package/dist/console/home/AppCard.js +6 -12
- package/dist/console/home/HomeAppsStrip.d.ts +8 -0
- package/dist/console/home/HomeAppsStrip.js +61 -0
- package/dist/console/home/HomeLayout.d.ts +1 -1
- package/dist/console/home/HomeLayout.js +3 -1
- package/dist/console/home/HomePage.d.ts +1 -2
- package/dist/console/home/HomePage.js +149 -21
- package/dist/console/home/HomeRail.d.ts +22 -0
- package/dist/console/home/HomeRail.js +62 -0
- package/dist/console/home/QuickActions.d.ts +1 -1
- package/dist/console/home/QuickActions.js +3 -11
- package/dist/console/home/RecentApps.d.ts +1 -1
- package/dist/console/home/RecentApps.js +2 -2
- package/dist/console/home/StarredApps.d.ts +1 -1
- package/dist/console/home/StarredApps.js +2 -2
- package/dist/console/marketplace/InstalledListWidget.d.ts +1 -0
- package/dist/console/marketplace/InstalledListWidget.js +93 -0
- package/dist/console/marketplace/MarkdownText.d.ts +1 -1
- package/dist/console/marketplace/MarketplaceAccessDenied.d.ts +1 -1
- package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +8 -14
- package/dist/console/marketplace/MarketplaceInstalledPage.js +14 -66
- package/dist/console/marketplace/MarketplacePackagePage.d.ts +1 -1
- package/dist/console/marketplace/MarketplacePackagePage.js +249 -8
- package/dist/console/marketplace/MarketplacePage.d.ts +1 -1
- package/dist/console/marketplace/MarketplacePage.js +60 -3
- package/dist/console/marketplace/PackageIcon.d.ts +1 -1
- package/dist/console/marketplace/PluginDisclosure.d.ts +14 -0
- package/dist/console/marketplace/PluginDisclosure.js +38 -0
- package/dist/console/marketplace/marketplaceApi.d.ts +123 -0
- package/dist/console/marketplace/marketplaceApi.js +254 -1
- package/dist/console/organizations/CreateWorkspaceDialog.d.ts +1 -1
- package/dist/console/organizations/OrganizationsLayout.d.ts +1 -1
- package/dist/console/organizations/OrganizationsPage.d.ts +1 -1
- package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +1 -1
- package/dist/console/organizations/manage/InvitationsPage.d.ts +1 -1
- package/dist/console/organizations/manage/InviteMemberDialog.d.ts +1 -1
- package/dist/console/organizations/manage/MembersPage.d.ts +1 -1
- package/dist/console/organizations/manage/OrganizationLayout.d.ts +1 -1
- package/dist/console/organizations/manage/SettingsPage.d.ts +1 -1
- package/dist/context/CommandPaletteProvider.d.ts +44 -0
- package/dist/context/CommandPaletteProvider.js +71 -0
- package/dist/context/FavoritesProvider.d.ts +1 -1
- package/dist/context/NavigationContext.d.ts +1 -1
- package/dist/context/RecentItemsProvider.d.ts +2 -2
- package/dist/context/UserStateAdapters.d.ts +1 -1
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +1 -0
- package/dist/hooks/index.d.ts +5 -2
- package/dist/hooks/index.js +4 -1
- package/dist/hooks/useActionModal.d.ts +53 -0
- package/dist/hooks/useActionModal.js +111 -0
- package/dist/hooks/useChatConversation.d.ts +107 -4
- package/dist/hooks/useChatConversation.js +253 -25
- package/dist/hooks/useConsoleActionRuntime.d.ts +70 -0
- package/dist/hooks/useConsoleActionRuntime.js +560 -0
- package/dist/hooks/useConversationList.js +61 -3
- package/dist/hooks/useHomeInbox.d.ts +13 -0
- package/dist/hooks/useHomeInbox.js +142 -0
- package/dist/hooks/useNavPins.js +17 -23
- package/dist/hooks/useNavigationSync.d.ts +33 -0
- package/dist/hooks/useNavigationSync.js +98 -12
- package/dist/hooks/useReconcileOnError.d.ts +40 -0
- package/dist/hooks/useReconcileOnError.js +37 -0
- package/dist/hooks/useRecordApprovals.d.ts +18 -19
- package/dist/hooks/useRecordApprovals.js +24 -40
- package/dist/hooks/useResponsiveSidebar.js +14 -5
- package/dist/hooks/useSettleSignal.d.ts +19 -0
- package/dist/hooks/useSettleSignal.js +20 -0
- package/dist/hooks/useTrackRouteAsRecent.js +35 -0
- package/dist/hooks/useUrlOverlay.d.ts +62 -0
- package/dist/hooks/useUrlOverlay.js +88 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +12 -4
- package/dist/layout/ActivityFeed.d.ts +1 -1
- package/dist/layout/AppHeader.d.ts +3 -2
- package/dist/layout/AppHeader.js +237 -72
- package/dist/layout/AppSidebar.d.ts +2 -1
- package/dist/layout/AppSidebar.js +26 -46
- package/dist/layout/AppSwitcher.d.ts +2 -1
- package/dist/layout/AppSwitcher.js +9 -5
- package/dist/layout/AuthPageLayout.d.ts +1 -1
- package/dist/layout/ConnectionStatus.d.ts +1 -1
- package/dist/layout/ConnectionStatus.js +9 -6
- package/dist/layout/ConsoleChatbotFab.d.ts +19 -1
- package/dist/layout/ConsoleChatbotFab.js +16 -2
- package/dist/layout/ConsoleFloatingChatbot.d.ts +32 -2
- package/dist/layout/ConsoleFloatingChatbot.js +374 -41
- package/dist/layout/ConsoleLayout.d.ts +1 -1
- package/dist/layout/ConsoleLayout.js +27 -11
- package/dist/layout/ContextSelectors.d.ts +44 -0
- package/dist/layout/ContextSelectors.js +218 -0
- package/dist/layout/InboxPopover.d.ts +6 -1
- package/dist/layout/InboxPopover.js +25 -6
- package/dist/layout/LocaleSwitcher.d.ts +1 -1
- package/dist/layout/LocalizedSidebarTrigger.d.ts +2 -0
- package/dist/layout/LocalizedSidebarTrigger.js +15 -0
- package/dist/layout/MobileViewSwitcherContext.d.ts +1 -1
- package/dist/layout/ModeToggle.d.ts +1 -1
- package/dist/layout/PageHeader.d.ts +1 -1
- package/dist/layout/UnifiedSidebar.d.ts +2 -1
- package/dist/layout/UnifiedSidebar.js +116 -15
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/settleSignal.d.ts +64 -0
- package/dist/observability/settleSignal.js +131 -0
- package/dist/preview/DraftChangesPanel.d.ts +19 -0
- package/dist/preview/DraftChangesPanel.js +114 -0
- package/dist/preview/DraftPreviewBar.d.ts +8 -0
- package/dist/preview/DraftPreviewBar.js +86 -0
- package/dist/preview/PreviewDraftEmptyState.d.ts +16 -0
- package/dist/preview/PreviewDraftEmptyState.js +47 -0
- package/dist/preview/PreviewModeContext.d.ts +57 -0
- package/dist/preview/PreviewModeContext.js +99 -0
- package/dist/preview/UnpublishedAppBar.d.ts +8 -0
- package/dist/preview/UnpublishedAppBar.js +79 -0
- package/dist/preview/draftStatus.d.ts +20 -0
- package/dist/preview/draftStatus.js +27 -0
- package/dist/preview/usePublishAllDrafts.d.ts +18 -0
- package/dist/preview/usePublishAllDrafts.js +106 -0
- package/dist/providers/AdapterProvider.d.ts +1 -1
- package/dist/providers/AdapterProvider.js +6 -1
- package/dist/providers/ExpressionProvider.d.ts +1 -1
- package/dist/providers/MetadataProvider.d.ts +17 -2
- package/dist/providers/MetadataProvider.js +183 -12
- package/dist/runtime-config.d.ts +46 -2
- package/dist/runtime-config.js +39 -2
- package/dist/services/builtinComponents.js +68 -59
- package/dist/skeletons/SkeletonDashboard.d.ts +1 -1
- package/dist/skeletons/SkeletonDetail.d.ts +1 -1
- package/dist/skeletons/SkeletonGrid.d.ts +1 -1
- package/dist/utils/appRoute.d.ts +21 -0
- package/dist/utils/appRoute.js +25 -0
- package/dist/utils/deriveRelatedLists.d.ts +54 -0
- package/dist/utils/deriveRelatedLists.js +91 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/managedByEmptyState.d.ts +8 -1
- package/dist/utils/managedByEmptyState.js +13 -7
- package/dist/utils/preferLocal.d.ts +18 -0
- package/dist/utils/preferLocal.js +24 -0
- package/dist/views/ActionConfirmDialog.d.ts +1 -1
- package/dist/views/ActionConfirmDialog.js +3 -1
- package/dist/views/ActionParamDialog.d.ts +6 -1
- package/dist/views/ActionParamDialog.js +9 -3
- package/dist/views/ActionResultDialog.d.ts +13 -0
- package/dist/views/ActionResultDialog.js +134 -0
- package/dist/views/ComponentNavView.d.ts +14 -1
- package/dist/views/CreateViewDialog.d.ts +1 -1
- package/dist/views/DashboardConfigPanel.d.ts +28 -0
- package/dist/views/DashboardConfigPanel.js +81 -0
- package/dist/views/DashboardView.d.ts +4 -3
- package/dist/views/DashboardView.js +38 -239
- package/dist/views/FlowRunner.d.ts +59 -0
- package/dist/views/FlowRunner.js +153 -0
- package/dist/views/InterfaceListPage.d.ts +49 -0
- package/dist/views/InterfaceListPage.js +347 -0
- package/dist/views/MetadataInspector.d.ts +2 -2
- package/dist/views/ObjectView.d.ts +1 -1
- package/dist/views/ObjectView.js +209 -532
- package/dist/views/PageView.d.ts +8 -3
- package/dist/views/PageView.js +45 -32
- package/dist/views/RecordDetailView.d.ts +1 -1
- package/dist/views/RecordDetailView.js +363 -148
- package/dist/views/RecordFormPage.d.ts +1 -1
- package/dist/views/RecordFormPage.js +26 -1
- package/dist/views/ReportConfigPanel.d.ts +37 -0
- package/dist/views/ReportConfigPanel.js +85 -0
- package/dist/views/ReportView.d.ts +1 -1
- package/dist/views/ReportView.js +116 -7
- package/dist/views/RuntimeDraftBar.d.ts +30 -0
- package/dist/views/RuntimeDraftBar.js +112 -0
- package/dist/views/SearchResultsPage.d.ts +1 -1
- package/dist/views/SearchResultsPage.js +8 -18
- package/dist/views/ViewConfigPanel.d.ts +24 -17
- package/dist/views/ViewConfigPanel.js +121 -77
- package/dist/views/index.d.ts +1 -1
- package/dist/views/index.js +1 -1
- package/dist/views/metadata-admin/AuditPanel.d.ts +28 -0
- package/dist/views/metadata-admin/AuditPanel.js +79 -0
- package/dist/views/metadata-admin/DiagnosticsPage.d.ts +20 -0
- package/dist/views/metadata-admin/DiagnosticsPage.js +69 -0
- package/dist/views/metadata-admin/DirectoryPage.d.ts +16 -1
- package/dist/views/metadata-admin/DirectoryPage.js +113 -24
- package/dist/views/metadata-admin/DraftReviewPanel.d.ts +33 -0
- package/dist/views/metadata-admin/DraftReviewPanel.js +77 -0
- package/dist/views/metadata-admin/EmbeddedItemEditor.d.ts +17 -1
- package/dist/views/metadata-admin/EmbeddedItemEditor.js +15 -8
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +37 -0
- package/dist/views/metadata-admin/JsonSourceEditor.js +178 -0
- package/dist/views/metadata-admin/LayeredDiff.d.ts +39 -1
- package/dist/views/metadata-admin/LayeredDiff.js +171 -5
- package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +15 -1
- package/dist/views/metadata-admin/MetadataTypeActions.d.ts +48 -0
- package/dist/views/metadata-admin/MetadataTypeActions.js +165 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +18 -0
- package/dist/views/metadata-admin/PackagesPage.js +395 -0
- package/dist/views/metadata-admin/PageShell.d.ts +1 -1
- package/dist/views/metadata-admin/PageShell.js +9 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +35 -1
- package/dist/views/metadata-admin/QuickFind.d.ts +21 -1
- package/dist/views/metadata-admin/QuickFind.js +6 -3
- package/dist/views/metadata-admin/RelatedPanel.d.ts +24 -1
- package/dist/views/metadata-admin/RelatedPanel.js +20 -18
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +40 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +1223 -60
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +39 -1
- package/dist/views/metadata-admin/ResourceHistoryPage.js +66 -16
- package/dist/views/metadata-admin/ResourceListPage.d.ts +13 -1
- package/dist/views/metadata-admin/ResourceListPage.js +266 -30
- package/dist/views/metadata-admin/ResourceRouter.d.ts +23 -1
- package/dist/views/metadata-admin/SchemaForm.d.ts +34 -1
- package/dist/views/metadata-admin/SchemaForm.js +559 -49
- package/dist/views/metadata-admin/StudioHomePage.d.ts +22 -0
- package/dist/views/metadata-admin/StudioHomePage.js +213 -0
- package/dist/views/metadata-admin/anchors.js +237 -24
- package/dist/views/metadata-admin/clientValidation.d.ts +50 -0
- package/dist/views/metadata-admin/clientValidation.js +169 -0
- package/dist/views/metadata-admin/color-variant-field.d.ts +30 -0
- package/dist/views/metadata-admin/color-variant-field.js +38 -0
- package/dist/views/metadata-admin/createDerive.d.ts +75 -0
- package/dist/views/metadata-admin/createDerive.js +179 -0
- package/dist/views/metadata-admin/dashboard-schema.d.ts +12 -0
- package/dist/views/metadata-admin/dashboard-schema.js +80 -0
- package/dist/views/metadata-admin/datasource/DatasourceResourcePage.d.ts +35 -0
- package/dist/views/metadata-admin/datasource/DatasourceResourcePage.js +327 -0
- package/dist/views/metadata-admin/datasource/register.d.ts +1 -0
- package/dist/views/metadata-admin/datasource/register.js +24 -0
- package/dist/views/metadata-admin/default-inspector-registry.d.ts +49 -0
- package/dist/views/metadata-admin/default-inspector-registry.js +8 -0
- package/dist/views/metadata-admin/default-schemas.js +115 -10
- package/dist/views/metadata-admin/external/ExternalDatasourcePanel.d.ts +27 -0
- package/dist/views/metadata-admin/external/ExternalDatasourcePanel.js +69 -0
- package/dist/views/metadata-admin/external/ImportObjectDialog.d.ts +27 -0
- package/dist/views/metadata-admin/external/ImportObjectDialog.js +77 -0
- package/dist/views/metadata-admin/external/SchemaBrowser.d.ts +16 -0
- package/dist/views/metadata-admin/external/SchemaBrowser.js +74 -0
- package/dist/views/metadata-admin/external/ValidationPanel.d.ts +16 -0
- package/dist/views/metadata-admin/external/ValidationPanel.js +68 -0
- package/dist/views/metadata-admin/external/api.d.ts +100 -0
- package/dist/views/metadata-admin/external/api.js +124 -0
- package/dist/views/metadata-admin/i18n.d.ts +1 -0
- package/dist/views/metadata-admin/i18n.js +1166 -2
- package/dist/views/metadata-admin/index.d.ts +8 -5
- package/dist/views/metadata-admin/index.js +12 -2
- package/dist/views/metadata-admin/inspector-registry.d.ts +51 -0
- package/dist/views/metadata-admin/inspector-registry.js +11 -0
- package/dist/views/metadata-admin/inspectors/ActionDefaultInspector.d.ts +30 -0
- package/dist/views/metadata-admin/inspectors/ActionDefaultInspector.js +180 -0
- package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +16 -0
- package/dist/views/metadata-admin/inspectors/AppNavInspector.js +110 -0
- package/dist/views/metadata-admin/inspectors/ConditionBuilder.d.ts +29 -0
- package/dist/views/metadata-admin/inspectors/ConditionBuilder.js +154 -0
- package/dist/views/metadata-admin/inspectors/DashboardDefaultInspector.d.ts +28 -0
- package/dist/views/metadata-admin/inspectors/DashboardDefaultInspector.js +110 -0
- package/dist/views/metadata-admin/inspectors/DashboardWidgetInspector.d.ts +18 -0
- package/dist/views/metadata-admin/inspectors/DashboardWidgetInspector.js +139 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +107 -0
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.d.ts +16 -0
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +45 -0
- package/dist/views/metadata-admin/inspectors/FlowInspector.d.ts +12 -0
- package/dist/views/metadata-admin/inspectors/FlowInspector.js +9 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +30 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +125 -0
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +18 -0
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +40 -0
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.d.ts +14 -0
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +140 -0
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +26 -0
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +105 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +83 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +181 -0
- package/dist/views/metadata-admin/inspectors/FlowStringListField.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowStringListField.js +60 -0
- package/dist/views/metadata-admin/inspectors/InspectorComboField.d.ts +40 -0
- package/dist/views/metadata-admin/inspectors/InspectorComboField.js +61 -0
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +54 -0
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +330 -0
- package/dist/views/metadata-admin/inspectors/PageBlockInspector.d.ts +48 -0
- package/dist/views/metadata-admin/inspectors/PageBlockInspector.js +332 -0
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +58 -0
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +160 -0
- package/dist/views/metadata-admin/inspectors/ViewColumnInspector.d.ts +19 -0
- package/dist/views/metadata-admin/inspectors/ViewColumnInspector.js +144 -0
- package/dist/views/metadata-admin/inspectors/ViewInspector.d.ts +19 -0
- package/dist/views/metadata-admin/inspectors/ViewInspector.js +21 -0
- package/dist/views/metadata-admin/inspectors/ViewVariantInspector.d.ts +54 -0
- package/dist/views/metadata-admin/inspectors/ViewVariantInspector.js +191 -0
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +124 -0
- package/dist/views/metadata-admin/inspectors/_shared.js +113 -0
- package/dist/views/metadata-admin/inspectors/expression-validate.d.ts +26 -0
- package/dist/views/metadata-admin/inspectors/expression-validate.js +66 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +143 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +461 -0
- package/dist/views/metadata-admin/inspectors/index.d.ts +1 -0
- package/dist/views/metadata-admin/inspectors/index.js +45 -0
- package/dist/views/metadata-admin/inspectors/json-schema-to-fields.d.ts +40 -0
- package/dist/views/metadata-admin/inspectors/json-schema-to-fields.js +227 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +72 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/mergeServerFields.d.ts +65 -0
- package/dist/views/metadata-admin/mergeServerFields.js +56 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +55 -0
- package/dist/views/metadata-admin/previews/ActionPreview.d.ts +25 -0
- package/dist/views/metadata-admin/previews/ActionPreview.js +238 -0
- package/dist/views/metadata-admin/previews/AddWidgetPicker.d.ts +12 -0
- package/dist/views/metadata-admin/previews/AddWidgetPicker.js +56 -0
- package/dist/views/metadata-admin/previews/AgentPreview.d.ts +24 -0
- package/dist/views/metadata-admin/previews/AgentPreview.js +100 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.d.ts +31 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +260 -0
- package/dist/views/metadata-admin/previews/AppPreview.d.ts +16 -1
- package/dist/views/metadata-admin/previews/AppPreview.js +23 -14
- package/dist/views/metadata-admin/previews/BookPreview.d.ts +20 -0
- package/dist/views/metadata-admin/previews/BookPreview.js +132 -0
- package/dist/views/metadata-admin/previews/DashboardPreview.d.ts +16 -1
- package/dist/views/metadata-admin/previews/DashboardPreview.js +110 -8
- package/dist/views/metadata-admin/previews/DatasetPreview.d.ts +18 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +89 -0
- package/dist/views/metadata-admin/previews/DatasourcePreview.d.ts +23 -0
- package/dist/views/metadata-admin/previews/DatasourcePreview.js +68 -0
- package/dist/views/metadata-admin/previews/EmailTemplatePreview.d.ts +14 -1
- package/dist/views/metadata-admin/previews/FieldStub.d.ts +30 -0
- package/dist/views/metadata-admin/previews/FieldStub.js +104 -0
- package/dist/views/metadata-admin/previews/FieldsListEditor.d.ts +50 -0
- package/dist/views/metadata-admin/previews/FieldsListEditor.js +97 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +43 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.js +328 -0
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +20 -0
- package/dist/views/metadata-admin/previews/FlowPreview.js +92 -0
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +46 -0
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +97 -0
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.d.ts +25 -0
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +170 -0
- package/dist/views/metadata-admin/previews/JobPreview.d.ts +28 -0
- package/dist/views/metadata-admin/previews/JobPreview.js +290 -0
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.d.ts +30 -0
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +547 -0
- package/dist/views/metadata-admin/previews/ObjectPreview.d.ts +14 -1
- package/dist/views/metadata-admin/previews/ObjectPreview.js +5 -30
- package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +32 -0
- package/dist/views/metadata-admin/previews/OutlineStrip.js +8 -0
- package/dist/views/metadata-admin/previews/PageBlockCanvas.d.ts +49 -0
- package/dist/views/metadata-admin/previews/PageBlockCanvas.js +510 -0
- package/dist/views/metadata-admin/previews/PagePreview.d.ts +10 -1
- package/dist/views/metadata-admin/previews/PagePreview.js +90 -4
- package/dist/views/metadata-admin/previews/PermissionPreview.d.ts +27 -0
- package/dist/views/metadata-admin/previews/PermissionPreview.js +115 -0
- package/dist/views/metadata-admin/previews/PreviewShell.d.ts +29 -6
- package/dist/views/metadata-admin/previews/PreviewShell.js +16 -3
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +18 -1
- package/dist/views/metadata-admin/previews/ReportPreview.js +23 -15
- package/dist/views/metadata-admin/previews/RolePreview.d.ts +19 -0
- package/dist/views/metadata-admin/previews/RolePreview.js +14 -0
- package/dist/views/metadata-admin/previews/SkillPreview.d.ts +22 -0
- package/dist/views/metadata-admin/previews/SkillPreview.js +34 -0
- package/dist/views/metadata-admin/previews/ToolPreview.d.ts +25 -0
- package/dist/views/metadata-admin/previews/ToolPreview.js +122 -0
- package/dist/views/metadata-admin/previews/TranslationPreview.d.ts +25 -0
- package/dist/views/metadata-admin/previews/TranslationPreview.js +52 -0
- package/dist/views/metadata-admin/previews/ValidationPreview.d.ts +27 -0
- package/dist/views/metadata-admin/previews/ValidationPreview.js +110 -0
- package/dist/views/metadata-admin/previews/ViewColumnPanes.d.ts +62 -0
- package/dist/views/metadata-admin/previews/ViewColumnPanes.js +140 -0
- package/dist/views/metadata-admin/previews/ViewPreview.d.ts +23 -1
- package/dist/views/metadata-admin/previews/ViewPreview.js +101 -73
- package/dist/views/metadata-admin/previews/block-config.d.ts +82 -0
- package/dist/views/metadata-admin/previews/block-config.js +324 -0
- package/dist/views/metadata-admin/previews/block-types.d.ts +40 -0
- package/dist/views/metadata-admin/previews/block-types.js +110 -0
- package/dist/views/metadata-admin/previews/field-types.d.ts +53 -0
- package/dist/views/metadata-admin/previews/field-types.js +97 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +88 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +190 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +88 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +358 -0
- package/dist/views/metadata-admin/previews/form-preview.d.ts +24 -0
- package/dist/views/metadata-admin/previews/form-preview.js +29 -0
- package/dist/views/metadata-admin/previews/index.js +43 -0
- package/dist/views/metadata-admin/previews/object-fields-bridge.d.ts +66 -0
- package/dist/views/metadata-admin/previews/object-fields-bridge.js +171 -0
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +109 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +208 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +91 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.js +2 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +8 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +113 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +44 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +316 -0
- package/dist/views/metadata-admin/previews/useDatasetCatalog.d.ts +47 -0
- package/dist/views/metadata-admin/previews/useDatasetCatalog.js +133 -0
- package/dist/views/metadata-admin/previews/useFlowNodePalette.d.ts +44 -0
- package/dist/views/metadata-admin/previews/useFlowNodePalette.js +124 -0
- package/dist/views/metadata-admin/previews/useMetaOptions.d.ts +8 -0
- package/dist/views/metadata-admin/previews/useMetaOptions.js +50 -0
- package/dist/views/metadata-admin/previews/useObjectFields.d.ts +23 -0
- package/dist/views/metadata-admin/previews/useObjectFields.js +79 -0
- package/dist/views/metadata-admin/previews/useObjectOptions.d.ts +8 -0
- package/dist/views/metadata-admin/previews/useObjectOptions.js +43 -0
- package/dist/views/metadata-admin/previews/view-column-io.d.ts +42 -0
- package/dist/views/metadata-admin/previews/view-column-io.js +73 -0
- package/dist/views/metadata-admin/previews/widget-types.d.ts +24 -0
- package/dist/views/metadata-admin/previews/widget-types.js +40 -0
- package/dist/views/metadata-admin/registry.d.ts +140 -19
- package/dist/views/metadata-admin/report-schema.d.ts +26 -0
- package/dist/views/metadata-admin/report-schema.js +121 -0
- package/dist/views/metadata-admin/useMetadata.d.ts +100 -2
- package/dist/views/metadata-admin/useMetadata.js +155 -4
- package/dist/views/metadata-admin/view-item-normalize.d.ts +20 -0
- package/dist/views/metadata-admin/view-item-normalize.js +68 -0
- package/dist/views/metadata-admin/view-schema.d.ts +16 -0
- package/dist/views/metadata-admin/view-schema.js +107 -0
- package/dist/views/metadata-admin/view-variant-model.d.ts +23 -0
- package/dist/views/metadata-admin/view-variant-model.js +64 -0
- package/dist/views/metadata-admin/widgets.d.ts +89 -1
- package/dist/views/metadata-admin/widgets.js +491 -17
- package/dist/views/runtime-metadata-persistence.d.ts +78 -0
- package/dist/views/runtime-metadata-persistence.js +89 -0
- package/dist/views/useOpenRecordList.d.ts +18 -0
- package/dist/views/useOpenRecordList.js +36 -0
- package/dist/views/userFilterUrlState.d.ts +15 -0
- package/dist/views/userFilterUrlState.js +53 -0
- package/dist/views/view-config-adapter.d.ts +38 -0
- package/dist/views/view-config-adapter.js +80 -0
- package/package.json +52 -34
- package/dist/views/DesignDrawer.d.ts +0 -28
- package/dist/views/DesignDrawer.js +0 -51
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +0 -68
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +0 -158
|
@@ -20,31 +20,112 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
20
20
|
* EditPage via `registerMetadataResource()`.
|
|
21
21
|
*/
|
|
22
22
|
import * as React from 'react';
|
|
23
|
-
import { useNavigate, useParams } from 'react-router-dom';
|
|
24
|
-
import { Save, RotateCcw, History, Link2, Loader2, AlertTriangle, Layers3, Eye, Pencil, X, } from 'lucide-react';
|
|
23
|
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
24
|
+
import { Save, RotateCcw, Trash2, History, Link2, Loader2, AlertTriangle, Layers3, GitCompareArrows, Boxes, Eye, Pencil, X, PanelRightClose, PanelRightOpen, Maximize2, Minimize2, MousePointer2, SlidersHorizontal, FileCode2, Zap, ZapOff, Send, Undo2, Lock, ShieldCheck, } from 'lucide-react';
|
|
25
25
|
import { Button } from '@object-ui/components';
|
|
26
26
|
import { Badge } from '@object-ui/components';
|
|
27
|
-
import {
|
|
27
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from '@object-ui/components';
|
|
28
|
+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from '@object-ui/components';
|
|
28
29
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@object-ui/components';
|
|
29
30
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
30
31
|
import { PageShell } from './PageShell';
|
|
31
|
-
import {
|
|
32
|
+
import { MetadataTypeActions } from './MetadataTypeActions';
|
|
33
|
+
import { LayeredDiff, countOverlaidFields } from './LayeredDiff';
|
|
34
|
+
import { DraftReviewPanel, computeDraftChangeCount } from './DraftReviewPanel';
|
|
32
35
|
import { SchemaForm } from './SchemaForm';
|
|
33
36
|
import { useMetadataClient, useMetadataTypes, } from './useMetadata';
|
|
34
37
|
import { getMetadataResource, resolveResourceConfig, listAnchorsFor, } from './registry';
|
|
38
|
+
import { useCreateDerive, deriveDefaultCreateFields } from './createDerive';
|
|
35
39
|
import { RelatedPanel } from './RelatedPanel';
|
|
36
40
|
import { MetadataDetailDrawer } from './MetadataDetailDrawer';
|
|
41
|
+
import { HistoryPanel } from './ResourceHistoryPage';
|
|
42
|
+
import { AuditPanel } from './AuditPanel';
|
|
37
43
|
import { getMetadataPreview } from './preview-registry';
|
|
44
|
+
import { readFields } from './previews/object-fields-io';
|
|
45
|
+
import { useRegisterAssistantEditor } from '../../assistant/assistantBus';
|
|
46
|
+
import { getMetadataInspector } from './inspector-registry';
|
|
47
|
+
import { getMetadataDefaultInspector } from './default-inspector-registry';
|
|
48
|
+
import { detectLocale, t, tFormat, translateValidationMessage } from './i18n';
|
|
49
|
+
import { JsonSourceEditor } from './JsonSourceEditor';
|
|
50
|
+
import { validateMetadataDraft, hasClientValidator } from './clientValidation';
|
|
51
|
+
// react-resizable-panels' `direction` prop type does not always narrow
|
|
52
|
+
// cleanly in our TS config; cast at the boundary (precedent:
|
|
53
|
+
// packages/components/src/custom/navigation-overlay.tsx).
|
|
54
|
+
const PanelGroup = ResizablePanelGroup;
|
|
55
|
+
/**
|
|
56
|
+
* Metadata types whose canvas IS the primary create-time authoring
|
|
57
|
+
* surface, so we render the preview/inspector split during create
|
|
58
|
+
* instead of the centered basic-info form. Object-level basics stay
|
|
59
|
+
* editable via the no-selection default inspector. Other types keep
|
|
60
|
+
* the conventional "name it first, design after save" create flow.
|
|
61
|
+
*/
|
|
62
|
+
const CREATE_MODE_CANVAS_TYPES = new Set(['object']);
|
|
63
|
+
/**
|
|
64
|
+
* Top-level metadata keys that a type's canvas PreviewComponent owns and
|
|
65
|
+
* edits visually (e.g. the object designer owns `fields` + `fieldGroups`).
|
|
66
|
+
* These must never surface in the inspector's fallback SchemaForm — the
|
|
67
|
+
* no-selection panel would otherwise render a raw JSON editor for data
|
|
68
|
+
* the user is already editing on the canvas.
|
|
69
|
+
*/
|
|
70
|
+
const CANVAS_OWNED_KEYS = {
|
|
71
|
+
object: ['fields', 'fieldGroups'],
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Normalize the framework's draft envelope into either the draft body or
|
|
75
|
+
* `null` (no pending draft). The envelope is:
|
|
76
|
+
*
|
|
77
|
+
* - `{ type, name, item: {...} }` when a draft exists,
|
|
78
|
+
* - `{ type, name, label }` when no draft exists (HTTP 200, item absent).
|
|
79
|
+
*
|
|
80
|
+
* The presence of the `item` key is the single signal; we do NOT fall back
|
|
81
|
+
* to using the envelope itself as the body — doing so would mis-identify the
|
|
82
|
+
* "no draft" stub (which still has `type`/`name`/`label` keys) as a real
|
|
83
|
+
* pending draft and would corrupt the editor baseline.
|
|
84
|
+
*/
|
|
85
|
+
function extractDraftBody(draftResp) {
|
|
86
|
+
if (!draftResp || typeof draftResp !== 'object')
|
|
87
|
+
return null;
|
|
88
|
+
const env = draftResp;
|
|
89
|
+
if (!('item' in env))
|
|
90
|
+
return null;
|
|
91
|
+
const body = env.item;
|
|
92
|
+
if (!body || typeof body !== 'object')
|
|
93
|
+
return null;
|
|
94
|
+
return Object.keys(body).length > 0
|
|
95
|
+
? body
|
|
96
|
+
: null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Decide whether the validation-diagnostics banner should render at all.
|
|
100
|
+
*
|
|
101
|
+
* The gate has two reasons to stay hidden:
|
|
102
|
+
* - `loadFailed` — the layered/draft fetch itself failed, so the form is
|
|
103
|
+
* sitting on empty defaults. Any required-field issues the client
|
|
104
|
+
* validator produces are an artefact of the empty form, not a verdict on
|
|
105
|
+
* the item; the explicit "failed to load" banner already tells the real
|
|
106
|
+
* story. Suppress so a transport failure never masquerades as a broken
|
|
107
|
+
* item.
|
|
108
|
+
* - no diagnostics source — there is neither a server `_diagnostics`
|
|
109
|
+
* payload nor a client-side validator for this type, so there is nothing
|
|
110
|
+
* to show.
|
|
111
|
+
*/
|
|
112
|
+
export function shouldRenderDiagnostics(opts) {
|
|
113
|
+
if (opts.loadFailed)
|
|
114
|
+
return false;
|
|
115
|
+
return opts.hasDiag || opts.hasClientValidator;
|
|
116
|
+
}
|
|
38
117
|
export function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode = false, embedded = false, }) {
|
|
118
|
+
// Tiny dispatcher: a registered Custom EditPage / CreatePage is a
|
|
119
|
+
// different component type than MetadataResourceEditPageImpl, so React
|
|
120
|
+
// will unmount/remount when the registry-driven branch wins or loses
|
|
121
|
+
// (e.g. navigating from `/object/new` → `/object/sales_order`). Doing
|
|
122
|
+
// the dispatch INSIDE the impl below would leak hooks between
|
|
123
|
+
// branches and trigger "Rendered more hooks than during the previous
|
|
124
|
+
// render". We therefore keep this outer dispatcher hook-free apart
|
|
125
|
+
// from `useParams`, which is unconditional.
|
|
39
126
|
const params = useParams();
|
|
40
127
|
const type = typeProp ?? params.type ?? '';
|
|
41
128
|
const name = nameProp ?? params.name ?? '';
|
|
42
|
-
const navigate = useNavigate();
|
|
43
|
-
const client = useMetadataClient();
|
|
44
|
-
const { entries } = useMetadataTypes(client);
|
|
45
|
-
const entry = entries.find((t) => t.type === type);
|
|
46
|
-
const config = resolveResourceConfig(type, entry);
|
|
47
|
-
// Custom editor takes over.
|
|
48
129
|
const customConfig = getMetadataResource(type);
|
|
49
130
|
if (customConfig?.EditPage && !createMode) {
|
|
50
131
|
const Custom = customConfig.EditPage;
|
|
@@ -54,24 +135,238 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
54
135
|
const Custom = customConfig.CreatePage;
|
|
55
136
|
return _jsx(Custom, { type: type });
|
|
56
137
|
}
|
|
138
|
+
return (_jsx(MetadataResourceEditPageImpl, { type: type, name: name, createMode: createMode, embedded: embedded }));
|
|
139
|
+
}
|
|
140
|
+
function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
141
|
+
const navigate = useNavigate();
|
|
142
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
143
|
+
// ADR-0048 — the owning package of the item being edited, carried on the
|
|
144
|
+
// edit URL as `?package=` (emitted by the metadata list links). Scopes the
|
|
145
|
+
// layered/draft read so a same-name collision resolves to the right
|
|
146
|
+
// package's item. NOT the active Studio app's package — Studio edits items
|
|
147
|
+
// across all installed packages.
|
|
148
|
+
const ownerPackageId = searchParams.get('package') ?? undefined;
|
|
149
|
+
const client = useMetadataClient();
|
|
150
|
+
const { entries } = useMetadataTypes(client);
|
|
151
|
+
const entry = entries.find((t) => t.type === type);
|
|
152
|
+
const config = resolveResourceConfig(type, entry);
|
|
153
|
+
// Hoist `schema` to the top: it's a pure derivation of entry/config
|
|
154
|
+
// and several create-mode hooks below need it. Keeping it down here
|
|
155
|
+
// would put those hooks *after* the loading early-return, which
|
|
156
|
+
// breaks the rules of hooks when navigating new→edit (a different
|
|
157
|
+
// number of hooks runs across renders of the same instance).
|
|
158
|
+
const schema = (createMode && config.createSchema
|
|
159
|
+
? config.createSchema
|
|
160
|
+
: entry?.schema) ??
|
|
161
|
+
config.defaultSchema;
|
|
162
|
+
const locale = React.useMemo(() => detectLocale(), []);
|
|
57
163
|
const [layered, setLayered] = React.useState(null);
|
|
58
|
-
const
|
|
164
|
+
const identityField = config.identityField ?? 'name';
|
|
165
|
+
const [draft, setDraft] = React.useState(() => createMode ? { ...(config.createDefaults ?? {}), [identityField]: '' } : {});
|
|
59
166
|
const [refs, setRefs] = React.useState(null);
|
|
60
167
|
const [loading, setLoading] = React.useState(!createMode);
|
|
61
168
|
const [saving, setSaving] = React.useState(false);
|
|
62
169
|
const [error, setError] = React.useState(null);
|
|
170
|
+
// Distinguishes "the layered/draft fetch itself failed" (network/500/
|
|
171
|
+
// timeout) from "we loaded an item that fails validation". Without it a
|
|
172
|
+
// failed load renders the form with empty defaults and the client
|
|
173
|
+
// validator fires spurious "name/label/regions required" diagnostics,
|
|
174
|
+
// making a transport failure look like a structurally broken item. Set
|
|
175
|
+
// in the load catch block, reset at the start of each load.
|
|
176
|
+
const [loadFailed, setLoadFailed] = React.useState(false);
|
|
63
177
|
const [issues, setIssues] = React.useState([]);
|
|
178
|
+
// In create mode, hold back validation noise until the author has actually
|
|
179
|
+
// edited a field. A blank new-item form firing 3 red "required" errors before
|
|
180
|
+
// the user types anything reads as broken, not helpful (the save path still
|
|
181
|
+
// validates). Flips true on the first real edit.
|
|
182
|
+
const [createDirty, setCreateDirty] = React.useState(false);
|
|
183
|
+
// Wrap setDraft so that editing a field clears any *server-side*
|
|
184
|
+
// diagnostic issues whose path begins with that field. The user
|
|
185
|
+
// gets immediate visual feedback — the red ring disappears as
|
|
186
|
+
// they type — and the form re-validates on save. We diff at the
|
|
187
|
+
// top-level segment, which matches how Zod's `issue.path[0]`
|
|
188
|
+
// identifies the offending field.
|
|
189
|
+
const handleDraftChange = React.useCallback((next) => {
|
|
190
|
+
setDraft((prev) => {
|
|
191
|
+
const resolved = typeof next === 'function' ? next(prev) : next;
|
|
192
|
+
const changed = new Set();
|
|
193
|
+
const keys = new Set([...Object.keys(prev ?? {}), ...Object.keys(resolved ?? {})]);
|
|
194
|
+
for (const k of keys) {
|
|
195
|
+
if (!Object.is(prev?.[k], resolved?.[k]))
|
|
196
|
+
changed.add(k);
|
|
197
|
+
}
|
|
198
|
+
if (changed.size > 0) {
|
|
199
|
+
setCreateDirty(true);
|
|
200
|
+
setIssues((prevIssues) => prevIssues.filter((i) => {
|
|
201
|
+
const head = (i.path ?? '').split('.')[0];
|
|
202
|
+
return !changed.has(head);
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
return resolved;
|
|
206
|
+
});
|
|
207
|
+
}, []);
|
|
64
208
|
const [destructiveIssues, setDestructiveIssues] = React.useState(null);
|
|
65
209
|
const [pendingItem, setPendingItem] = React.useState(null);
|
|
210
|
+
// ── Create-mode form harness ──────────────────────────────────────
|
|
211
|
+
//
|
|
212
|
+
// Apply the registry's `createDerive` rules live (label→name slug,
|
|
213
|
+
// singular→plural, etc.). The hook is a no-op when not in create
|
|
214
|
+
// mode or when no rules are declared, so we always mount it.
|
|
215
|
+
const onCreatePatch = React.useCallback((patch) => {
|
|
216
|
+
handleDraftChange((d) => ({ ...d, ...patch }));
|
|
217
|
+
}, [handleDraftChange]);
|
|
218
|
+
const { markTouched: markCreateFieldTouched } = useCreateDerive({
|
|
219
|
+
rules: config.createDerive,
|
|
220
|
+
draft,
|
|
221
|
+
onPatch: onCreatePatch,
|
|
222
|
+
enabled: !!createMode,
|
|
223
|
+
});
|
|
224
|
+
// Effective hidden-fields for create mode: collapse the form to just
|
|
225
|
+
// the identity inputs declared by the type (or required-fields ∪
|
|
226
|
+
// label/name as a sensible default). Edit mode keeps the full form.
|
|
227
|
+
//
|
|
228
|
+
// The complement-set is what SchemaForm consumes (it hides paths
|
|
229
|
+
// listed in `hiddenFields`), so we invert the allowlist here.
|
|
230
|
+
const createFieldList = React.useMemo(() => {
|
|
231
|
+
if (!createMode)
|
|
232
|
+
return undefined;
|
|
233
|
+
if (config.createFields && config.createFields.length > 0)
|
|
234
|
+
return config.createFields;
|
|
235
|
+
const props = schema?.properties ?? undefined;
|
|
236
|
+
const required = schema?.required ?? undefined;
|
|
237
|
+
return deriveDefaultCreateFields(props, required);
|
|
238
|
+
}, [createMode, config.createFields, schema]);
|
|
239
|
+
const effectiveHiddenFields = React.useMemo(() => {
|
|
240
|
+
// Keys edited on the canvas (fields, fieldGroups) are never shown in
|
|
241
|
+
// the inspector's SchemaForm fallback — otherwise deselecting reveals
|
|
242
|
+
// a raw JSON editor for data the canvas already owns.
|
|
243
|
+
const canvasOwned = CANVAS_OWNED_KEYS[type] ?? [];
|
|
244
|
+
if (!createMode || !createFieldList) {
|
|
245
|
+
if (canvasOwned.length === 0)
|
|
246
|
+
return config.hiddenFields;
|
|
247
|
+
return Array.from(new Set([...(config.hiddenFields ?? []), ...canvasOwned]));
|
|
248
|
+
}
|
|
249
|
+
const props = schema?.properties ?? {};
|
|
250
|
+
const allow = new Set(createFieldList);
|
|
251
|
+
const hidden = Object.keys(props).filter((k) => !allow.has(k));
|
|
252
|
+
// Preserve any registry-declared `hiddenFields` too — they remain
|
|
253
|
+
// hidden in create mode even if they appeared in `createFields`.
|
|
254
|
+
if (config.hiddenFields) {
|
|
255
|
+
for (const k of config.hiddenFields)
|
|
256
|
+
if (!hidden.includes(k))
|
|
257
|
+
hidden.push(k);
|
|
258
|
+
}
|
|
259
|
+
// Canvas-owned keys are hidden regardless of the create allowlist.
|
|
260
|
+
for (const k of canvasOwned)
|
|
261
|
+
if (!hidden.includes(k))
|
|
262
|
+
hidden.push(k);
|
|
263
|
+
return hidden;
|
|
264
|
+
}, [createMode, createFieldList, schema, config.hiddenFields, type]);
|
|
265
|
+
const effectiveFieldOrder = React.useMemo(() => {
|
|
266
|
+
if (createMode && createFieldList)
|
|
267
|
+
return createFieldList;
|
|
268
|
+
return config.fieldOrder;
|
|
269
|
+
}, [createMode, createFieldList, config.fieldOrder]);
|
|
270
|
+
// Mark a top-level field as user-touched so create-mode derivations
|
|
271
|
+
// (label→name slug, etc.) leave it alone going forward. Wraps the
|
|
272
|
+
// standard onChange so the rest of the form is unaffected.
|
|
273
|
+
const handleCreateAwareChange = React.useCallback((next) => {
|
|
274
|
+
if (createMode) {
|
|
275
|
+
const before = draft;
|
|
276
|
+
const resolved = typeof next === 'function' ? next(before) : next;
|
|
277
|
+
const keys = new Set([...Object.keys(before ?? {}), ...Object.keys(resolved ?? {})]);
|
|
278
|
+
for (const k of keys) {
|
|
279
|
+
if (!Object.is(before?.[k], resolved?.[k]))
|
|
280
|
+
markCreateFieldTouched(k);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
handleDraftChange(next);
|
|
284
|
+
}, [createMode, draft, handleDraftChange, markCreateFieldTouched]);
|
|
285
|
+
// Live client-side Zod validation. Debounced 200ms so we don't run
|
|
286
|
+
// on every keystroke through a complex AutoForm tree. When a client
|
|
287
|
+
// schema exists for `type` (spec 7.x exports per-type schemas under
|
|
288
|
+
// /data, /ui, /automation, /ai, /system, /kernel), we replace the
|
|
289
|
+
// `issues` state with Zod's output — same schemas the server runs,
|
|
290
|
+
// so behavior matches the post-save diagnostics but appears live.
|
|
291
|
+
// Types without a client schema keep the existing server-only flow.
|
|
292
|
+
React.useEffect(() => {
|
|
293
|
+
if (!hasClientValidator(type))
|
|
294
|
+
return;
|
|
295
|
+
let cancelled = false;
|
|
296
|
+
const handle = window.setTimeout(() => {
|
|
297
|
+
// Pass the live server schema so the client never flags fields the
|
|
298
|
+
// running server now treats as optional (cross-repo spec-skew root-cure).
|
|
299
|
+
void validateMetadataDraft(type, draft, entry?.schema).then((res) => {
|
|
300
|
+
if (cancelled)
|
|
301
|
+
return;
|
|
302
|
+
setIssues(res.issues);
|
|
303
|
+
});
|
|
304
|
+
}, 200);
|
|
305
|
+
return () => {
|
|
306
|
+
cancelled = true;
|
|
307
|
+
window.clearTimeout(handle);
|
|
308
|
+
};
|
|
309
|
+
}, [type, draft, entry?.schema]);
|
|
310
|
+
// Issues to DISPLAY (banner + inline). Suppressed on a pristine create form
|
|
311
|
+
// so a blank new item doesn't open covered in required-field errors.
|
|
312
|
+
const displayIssues = React.useMemo(() => (createMode && !createDirty ? [] : issues), [createMode, createDirty, issues]);
|
|
313
|
+
// Per-item draft pending publish (mode=draft saves land here).
|
|
314
|
+
// When non-null, the editor is "viewing the draft" and we surface
|
|
315
|
+
// Publish / Discard-draft actions.
|
|
316
|
+
const [hasDraft, setHasDraft] = React.useState(false);
|
|
317
|
+
const [publishing, setPublishing] = React.useState(false);
|
|
318
|
+
// Bumped by destructive operations (rollback / discard-draft) to
|
|
319
|
+
// force the load effect to refetch layered + draft state.
|
|
320
|
+
const [reloadKey, setReloadKey] = React.useState(0);
|
|
66
321
|
// Form edit mode. The form is read-only by default — admins land in a
|
|
67
322
|
// "view" state and must click Edit to mutate, mirroring the Salesforce /
|
|
68
323
|
// Notion convention. createMode is always editing (you can't view what
|
|
69
324
|
// doesn't exist yet). Truly read-only types (no allowOrgOverride) stay
|
|
70
325
|
// read-only regardless.
|
|
71
326
|
const [editing, setEditing] = React.useState(!!createMode);
|
|
327
|
+
// Currently selected sub-element (e.g. a dashboard widget). The
|
|
328
|
+
// preview emits this; the inspector consumes it. Must live above
|
|
329
|
+
// any early returns to preserve hook order — reset on item
|
|
330
|
+
// navigation or when leaving edit mode below.
|
|
331
|
+
const [selection, setSelection] = React.useState(null);
|
|
332
|
+
React.useEffect(() => {
|
|
333
|
+
setSelection(null);
|
|
334
|
+
}, [type, name]);
|
|
335
|
+
React.useEffect(() => {
|
|
336
|
+
if (!editing)
|
|
337
|
+
setSelection(null);
|
|
338
|
+
}, [editing]);
|
|
72
339
|
// Snapshot of the last saved draft. Used by Cancel to revert in-flight
|
|
73
340
|
// edits, and as the source-of-truth when entering edit mode.
|
|
74
341
|
const draftSnapshotRef = React.useRef(null);
|
|
342
|
+
// Last successful save timestamp — surfaced as "Saved HH:MM" indicator
|
|
343
|
+
// next to the icon-only Save button.
|
|
344
|
+
const [lastSavedAt, setLastSavedAt] = React.useState(null);
|
|
345
|
+
// Auto-save toggle, persisted per-browser. Defaults to on for an
|
|
346
|
+
// "it just works" experience; users can disable it from the toolbar.
|
|
347
|
+
const [autoSaveEnabled, setAutoSaveEnabled] = React.useState(() => {
|
|
348
|
+
if (typeof window === 'undefined')
|
|
349
|
+
return true;
|
|
350
|
+
try {
|
|
351
|
+
const v = window.localStorage.getItem('metadata-admin:autosave');
|
|
352
|
+
return v === null ? true : v === '1';
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
React.useEffect(() => {
|
|
359
|
+
try {
|
|
360
|
+
window.localStorage.setItem('metadata-admin:autosave', autoSaveEnabled ? '1' : '0');
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
/* ignore */
|
|
364
|
+
}
|
|
365
|
+
}, [autoSaveEnabled]);
|
|
366
|
+
// Tracks the last draft snapshot we attempted to auto-save, so a
|
|
367
|
+
// validation failure does not loop on the same payload — auto-save
|
|
368
|
+
// only retries once the user mutates the draft again.
|
|
369
|
+
const lastAutoSaveSnapshotRef = React.useRef(null);
|
|
75
370
|
// Prefetch object name list once — fuels the `ref:object` widget.
|
|
76
371
|
// We don't block render on it; the widget shows a "Loading…" state.
|
|
77
372
|
const [objectNames, setObjectNames] = React.useState([]);
|
|
@@ -98,7 +393,99 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
98
393
|
cancelled = true;
|
|
99
394
|
};
|
|
100
395
|
}, [client]);
|
|
101
|
-
|
|
396
|
+
// Field catalog of the draft's bound/source object — fuels field-picker
|
|
397
|
+
// widgets (e.g. the interface-page filter-mode selector). For a page the
|
|
398
|
+
// source is `interfaceConfig.source` (interface mode) or the bound
|
|
399
|
+
// `object`; other types fall back to their own `object`/`objectName`.
|
|
400
|
+
const sourceObjectName = draft?.interfaceConfig?.source ||
|
|
401
|
+
draft?.object ||
|
|
402
|
+
draft?.objectName;
|
|
403
|
+
const [objectFields, setObjectFields] = React.useState([]);
|
|
404
|
+
const [objectFieldsLoading, setObjectFieldsLoading] = React.useState(false);
|
|
405
|
+
// Action catalog of the source object — fuels the `action-multi` picker so
|
|
406
|
+
// interface-page `buttons` reference the object's real actions.
|
|
407
|
+
const [objectActions, setObjectActions] = React.useState([]);
|
|
408
|
+
React.useEffect(() => {
|
|
409
|
+
let cancelled = false;
|
|
410
|
+
if (!sourceObjectName) {
|
|
411
|
+
setObjectFields([]);
|
|
412
|
+
setObjectActions([]);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
setObjectFieldsLoading(true);
|
|
416
|
+
(async () => {
|
|
417
|
+
try {
|
|
418
|
+
const obj = (await client.get('object', sourceObjectName));
|
|
419
|
+
if (cancelled)
|
|
420
|
+
return;
|
|
421
|
+
const raw = obj?.fields;
|
|
422
|
+
const list = Array.isArray(raw)
|
|
423
|
+
? raw.map((f) => ({ name: f?.name, label: f?.label, type: f?.type }))
|
|
424
|
+
: raw && typeof raw === 'object'
|
|
425
|
+
? Object.entries(raw).map(([name, f]) => ({ name, label: f?.label, type: f?.type }))
|
|
426
|
+
: [];
|
|
427
|
+
setObjectFields(list.filter((f) => !!f.name));
|
|
428
|
+
const rawActions = obj?.actions;
|
|
429
|
+
const acts = Array.isArray(rawActions)
|
|
430
|
+
? rawActions.map((a) => ({ name: a?.name, label: a?.label, locations: a?.locations })).filter((a) => !!a.name)
|
|
431
|
+
: [];
|
|
432
|
+
if (!cancelled)
|
|
433
|
+
setObjectActions(acts);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
if (!cancelled) {
|
|
437
|
+
setObjectFields([]);
|
|
438
|
+
setObjectActions([]);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
if (!cancelled)
|
|
443
|
+
setObjectFieldsLoading(false);
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
return () => { cancelled = true; };
|
|
447
|
+
}, [client, sourceObjectName]);
|
|
448
|
+
// View catalog of the source object — fuels the `view-ref` picker for
|
|
449
|
+
// `interfaceConfig.sourceView` so the author chooses an existing view
|
|
450
|
+
// instead of typing (and mistyping) a name. Views are standalone metadata
|
|
451
|
+
// keyed to their object via `objectName`/`object`; the LIST endpoint returns
|
|
452
|
+
// name + label, which is all the picker needs.
|
|
453
|
+
const [objectViews, setObjectViews] = React.useState([]);
|
|
454
|
+
const [objectViewsLoading, setObjectViewsLoading] = React.useState(false);
|
|
455
|
+
React.useEffect(() => {
|
|
456
|
+
let cancelled = false;
|
|
457
|
+
if (!sourceObjectName) {
|
|
458
|
+
setObjectViews([]);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
setObjectViewsLoading(true);
|
|
462
|
+
(async () => {
|
|
463
|
+
try {
|
|
464
|
+
const all = (await client.list('view'));
|
|
465
|
+
if (cancelled)
|
|
466
|
+
return;
|
|
467
|
+
const forObject = (all || []).filter((v) => {
|
|
468
|
+
const obj = v?.objectName ?? v?.object ?? v?.object_name;
|
|
469
|
+
return obj === sourceObjectName;
|
|
470
|
+
});
|
|
471
|
+
const seen = new Set();
|
|
472
|
+
const list = forObject
|
|
473
|
+
.map((v) => ({ name: v?.name, label: v?.label || undefined }))
|
|
474
|
+
.filter((v) => !!v.name && !seen.has(v.name) && seen.add(v.name));
|
|
475
|
+
setObjectViews(list);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
if (!cancelled)
|
|
479
|
+
setObjectViews([]);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
if (!cancelled)
|
|
483
|
+
setObjectViewsLoading(false);
|
|
484
|
+
}
|
|
485
|
+
})();
|
|
486
|
+
return () => { cancelled = true; };
|
|
487
|
+
}, [client, sourceObjectName]);
|
|
488
|
+
const widgetContext = React.useMemo(() => ({ objectNames, objectsLoading, objectFields, objectFieldsLoading, objectViews, objectViewsLoading, objectActions }), [objectNames, objectsLoading, objectFields, objectFieldsLoading, objectViews, objectViewsLoading, objectActions]);
|
|
102
489
|
// Load layered view + initial draft.
|
|
103
490
|
React.useEffect(() => {
|
|
104
491
|
if (createMode) {
|
|
@@ -108,21 +495,70 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
108
495
|
let cancelled = false;
|
|
109
496
|
setLoading(true);
|
|
110
497
|
setError(null);
|
|
498
|
+
setLoadFailed(false);
|
|
111
499
|
(async () => {
|
|
112
500
|
try {
|
|
113
|
-
const
|
|
501
|
+
const scope = ownerPackageId ? { packageId: ownerPackageId } : {};
|
|
502
|
+
const [lay, draftResp] = await Promise.all([
|
|
503
|
+
client.layered(type, name, scope),
|
|
504
|
+
// Draft reads are best-effort — a 404/error must not block
|
|
505
|
+
// the page; readers without overlay-write permission still
|
|
506
|
+
// see the published item.
|
|
507
|
+
client.getDraft(type, name, scope).catch(() => null),
|
|
508
|
+
]);
|
|
114
509
|
if (cancelled)
|
|
115
510
|
return;
|
|
116
511
|
setLayered(lay);
|
|
117
|
-
//
|
|
118
|
-
|
|
512
|
+
// Surface server-computed load-time validation errors as inline
|
|
513
|
+
// SchemaForm issues — operators see what's wrong with the
|
|
514
|
+
// saved metadata immediately, not just on the next Save round-trip.
|
|
515
|
+
const loadDiag = lay?._diagnostics;
|
|
516
|
+
if (loadDiag && loadDiag.valid === false && Array.isArray(loadDiag.errors)) {
|
|
517
|
+
setIssues(loadDiag.errors.map((e) => ({
|
|
518
|
+
path: e.path || '',
|
|
519
|
+
message: e.message,
|
|
520
|
+
})));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
setIssues([]);
|
|
524
|
+
}
|
|
525
|
+
// Draft envelope from the framework is `{ type, name, item }`;
|
|
526
|
+
// an empty/missing item means "no pending draft".
|
|
527
|
+
const draftReal = extractDraftBody(draftResp);
|
|
528
|
+
// Prefer the pending draft as the editing baseline — the
|
|
529
|
+
// operator is mid-flight on this item and should see their
|
|
530
|
+
// own in-progress state, not the last published version.
|
|
531
|
+
// A pending draft overlay can carry only the edited fields, so using
|
|
532
|
+
// it wholesale would drop inherited fields that were never touched —
|
|
533
|
+
// notably `type`, which section-level `visibleOn` predicates depend on
|
|
534
|
+
// (ADR-0047 hides Data Context / Layout when `data.type == 'list'`).
|
|
535
|
+
// Merge the draft over the effective baseline so those fields survive;
|
|
536
|
+
// the draft still wins for anything it does carry.
|
|
537
|
+
const baseline = (lay.effective ?? lay.code ?? {});
|
|
538
|
+
const rawInitial = draftReal
|
|
539
|
+
? { ...baseline, ...draftReal }
|
|
540
|
+
: baseline;
|
|
541
|
+
// Normalise the wire shape into the editor's draft shape (e.g.
|
|
542
|
+
// `view` unwraps an expanded ViewItem's `config` into a
|
|
543
|
+
// `{ list | form }` family key). No-op for types without a hook.
|
|
544
|
+
const initial = config.toDraft ? config.toDraft(rawInitial) : rawInitial;
|
|
119
545
|
setDraft(initial);
|
|
120
546
|
draftSnapshotRef.current = initial;
|
|
547
|
+
setHasDraft(!!draftReal);
|
|
121
548
|
setLoading(false);
|
|
122
549
|
}
|
|
123
550
|
catch (err) {
|
|
124
551
|
if (!cancelled) {
|
|
125
|
-
|
|
552
|
+
// A failed fetch is a LOAD error, not a validation error: flag it
|
|
553
|
+
// so the diagnostics banner suppresses the spurious required-field
|
|
554
|
+
// issues the empty-default form would otherwise produce, and make
|
|
555
|
+
// the top error banner explicit about what actually went wrong.
|
|
556
|
+
setLoadFailed(true);
|
|
557
|
+
setError(tFormat('engine.edit.loadFailed', locale, {
|
|
558
|
+
type,
|
|
559
|
+
name: name ?? '',
|
|
560
|
+
message: err?.message ?? String(err),
|
|
561
|
+
}));
|
|
126
562
|
setLoading(false);
|
|
127
563
|
}
|
|
128
564
|
}
|
|
@@ -130,8 +566,8 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
130
566
|
return () => {
|
|
131
567
|
cancelled = true;
|
|
132
568
|
};
|
|
133
|
-
}, [client, type, name, createMode]);
|
|
134
|
-
// Lazy-load references
|
|
569
|
+
}, [client, type, name, ownerPackageId, createMode, reloadKey, locale]);
|
|
570
|
+
// Lazy-load references the first time the References sheet opens.
|
|
135
571
|
const [refsLoading, setRefsLoading] = React.useState(false);
|
|
136
572
|
async function loadReferences() {
|
|
137
573
|
if (refs != null || refsLoading)
|
|
@@ -160,13 +596,199 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
160
596
|
// the parent payload to materialise) so we only restore metadata
|
|
161
597
|
// targets here.
|
|
162
598
|
const initialTabRef = React.useRef(null);
|
|
599
|
+
const [openSheet, setOpenSheet] = React.useState(null);
|
|
600
|
+
// ADR-0033 Phase B — `?review=1` arrival (from the chat's "Review N change(s)"
|
|
601
|
+
// affordance). The AI may have drafted this item *after* the page mounted, so
|
|
602
|
+
// we first force a fresh fetch, then — once the draft is loaded — open the
|
|
603
|
+
// generic review/diff sheet and consume the query param (so a refresh/back
|
|
604
|
+
// doesn't re-trigger it). The same-item-already-open case is covered by the
|
|
605
|
+
// reload bump (the load effect keys off `reloadKey`, not the search string).
|
|
606
|
+
const reviewParam = searchParams.get('review');
|
|
607
|
+
const reviewBumpedRef = React.useRef(false);
|
|
608
|
+
React.useEffect(() => {
|
|
609
|
+
if (reviewParam !== '1' || createMode)
|
|
610
|
+
return;
|
|
611
|
+
if (!reviewBumpedRef.current) {
|
|
612
|
+
reviewBumpedRef.current = true;
|
|
613
|
+
setReloadKey((k) => k + 1);
|
|
614
|
+
return; // wait for the reload to settle before reading hasDraft
|
|
615
|
+
}
|
|
616
|
+
if (!loading) {
|
|
617
|
+
if (hasDraft)
|
|
618
|
+
setOpenSheet('review');
|
|
619
|
+
const next = new URLSearchParams(searchParams);
|
|
620
|
+
next.delete('review');
|
|
621
|
+
setSearchParams(next, { replace: true });
|
|
622
|
+
reviewBumpedRef.current = false;
|
|
623
|
+
}
|
|
624
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
625
|
+
}, [reviewParam, createMode, loading, hasDraft]);
|
|
626
|
+
// Inspector tabs: properties form vs raw JSON source view. Source view
|
|
627
|
+
// is for power users who need to edit fields the form doesn't expose
|
|
628
|
+
// (e.g. nested arrays). Tracked locally — not persisted between
|
|
629
|
+
// navigations since most users live in the form 99% of the time.
|
|
630
|
+
const [inspectorTab, setInspectorTab] = React.useState('properties');
|
|
631
|
+
// When the References sheet opens, lazy-load the data (idempotent).
|
|
632
|
+
// Also keep the URL `?tab=` query in sync so deep-links round-trip.
|
|
633
|
+
React.useEffect(() => {
|
|
634
|
+
if (openSheet === 'references') {
|
|
635
|
+
void loadReferences();
|
|
636
|
+
}
|
|
637
|
+
if (typeof window !== 'undefined' && !embedded) {
|
|
638
|
+
const url = new URL(window.location.href);
|
|
639
|
+
if (openSheet)
|
|
640
|
+
url.searchParams.set('tab', openSheet);
|
|
641
|
+
else
|
|
642
|
+
url.searchParams.delete('tab');
|
|
643
|
+
window.history.replaceState({}, '', url.toString());
|
|
644
|
+
}
|
|
645
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
646
|
+
}, [openSheet, embedded]);
|
|
647
|
+
// Designer-style split-panel state. The inspector (right form panel)
|
|
648
|
+
// can collapse to give the preview the full canvas. The collapsed
|
|
649
|
+
// state is persisted in localStorage so the user's preference sticks
|
|
650
|
+
// across navigations.
|
|
651
|
+
const inspectorStorageKey = 'metadata-edit:inspector-collapsed';
|
|
652
|
+
const inspectorSizeStorageKey = 'metadata-edit:inspector-size';
|
|
653
|
+
const [inspectorCollapsed, setInspectorCollapsed] = React.useState(() => {
|
|
654
|
+
if (typeof window === 'undefined')
|
|
655
|
+
return false;
|
|
656
|
+
return window.localStorage.getItem(inspectorStorageKey) === '1';
|
|
657
|
+
});
|
|
658
|
+
// Remember the user's preferred inspector size so collapsing then
|
|
659
|
+
// re-expanding restores it instead of leaving a sliver. react-resizable-
|
|
660
|
+
// panels' built-in expand() returns to the size right before collapse
|
|
661
|
+
// which is often near 0, hence the explicit memory.
|
|
662
|
+
const lastInspectorSizeRef = React.useRef(38);
|
|
663
|
+
// Hydrate from localStorage on mount.
|
|
664
|
+
React.useEffect(() => {
|
|
665
|
+
if (typeof window === 'undefined')
|
|
666
|
+
return;
|
|
667
|
+
const v = Number(window.localStorage.getItem(inspectorSizeStorageKey));
|
|
668
|
+
if (Number.isFinite(v) && v >= 22 && v <= 80) {
|
|
669
|
+
lastInspectorSizeRef.current = v;
|
|
670
|
+
}
|
|
671
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
672
|
+
}, []);
|
|
673
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
674
|
+
const inspectorPanelRef = React.useRef(null);
|
|
675
|
+
const toggleInspector = React.useCallback(() => {
|
|
676
|
+
setInspectorCollapsed((prev) => {
|
|
677
|
+
const next = !prev;
|
|
678
|
+
if (typeof window !== 'undefined') {
|
|
679
|
+
window.localStorage.setItem(inspectorStorageKey, next ? '1' : '0');
|
|
680
|
+
}
|
|
681
|
+
return next;
|
|
682
|
+
});
|
|
683
|
+
}, []);
|
|
684
|
+
// Drive the imperative panel resize from a state-change effect rather
|
|
685
|
+
// than inside the setter — the latter runs before React has committed
|
|
686
|
+
// the new state and react-resizable-panels can race with its own
|
|
687
|
+
// onResize observer, producing tiny re-expanded sizes.
|
|
688
|
+
// ⚠️ resize() treats numeric values as **pixels**; pass a string to
|
|
689
|
+
// get a percentage. resize(38) → 38px (~2.7%); resize('38%') → 38%.
|
|
690
|
+
React.useEffect(() => {
|
|
691
|
+
const handle = inspectorPanelRef.current;
|
|
692
|
+
if (!handle)
|
|
693
|
+
return;
|
|
694
|
+
if (inspectorCollapsed) {
|
|
695
|
+
handle.resize?.('0%');
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
const target = lastInspectorSizeRef.current || 38;
|
|
699
|
+
handle.resize?.(`${target}%`);
|
|
700
|
+
}
|
|
701
|
+
}, [inspectorCollapsed]);
|
|
702
|
+
// Canvas-local UX state — preview-only view (hides design chrome
|
|
703
|
+
// without dropping dirty edits) and fullscreen (canvas takes over the
|
|
704
|
+
// viewport so designers can focus). Both are session-scoped.
|
|
705
|
+
const [previewOnly, setPreviewOnly] = React.useState(false);
|
|
706
|
+
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
|
707
|
+
// Lock body scroll while fullscreen so the underlying page can't peek
|
|
708
|
+
// through and the user's scroll position is preserved on exit.
|
|
709
|
+
React.useEffect(() => {
|
|
710
|
+
if (typeof document === 'undefined')
|
|
711
|
+
return;
|
|
712
|
+
if (!isFullscreen)
|
|
713
|
+
return;
|
|
714
|
+
const prev = document.body.style.overflow;
|
|
715
|
+
document.body.style.overflow = 'hidden';
|
|
716
|
+
return () => {
|
|
717
|
+
document.body.style.overflow = prev;
|
|
718
|
+
};
|
|
719
|
+
}, [isFullscreen]);
|
|
720
|
+
// Escape exits fullscreen.
|
|
721
|
+
React.useEffect(() => {
|
|
722
|
+
if (typeof window === 'undefined' || !isFullscreen)
|
|
723
|
+
return;
|
|
724
|
+
function onKey(e) {
|
|
725
|
+
if (e.key === 'Escape') {
|
|
726
|
+
e.preventDefault();
|
|
727
|
+
setIsFullscreen(false);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
window.addEventListener('keydown', onKey);
|
|
731
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
732
|
+
}, [isFullscreen]);
|
|
733
|
+
// Auto-enable design mode for designer-capable types. We do this once
|
|
734
|
+
// per (type,name) navigation so the user lands in the productive
|
|
735
|
+
// state instead of having to click "Edit". Truly read-only types
|
|
736
|
+
// (canWrite=false) keep the old behavior. The check happens inside
|
|
737
|
+
// the effect to avoid hook-order issues with the early `loading`
|
|
738
|
+
// return below.
|
|
739
|
+
const designerAutoOnRef = React.useRef(null);
|
|
740
|
+
React.useEffect(() => {
|
|
741
|
+
designerAutoOnRef.current = null;
|
|
742
|
+
}, [type, name]);
|
|
743
|
+
React.useEffect(() => {
|
|
744
|
+
if (createMode || embedded || loading)
|
|
745
|
+
return;
|
|
746
|
+
const key = `${type}/${name ?? ''}`;
|
|
747
|
+
if (designerAutoOnRef.current === key)
|
|
748
|
+
return;
|
|
749
|
+
const PC = getMetadataPreview(type);
|
|
750
|
+
if (!PC)
|
|
751
|
+
return;
|
|
752
|
+
const isArtifact = layered?.code != null;
|
|
753
|
+
const cw = isArtifact
|
|
754
|
+
? !!entry?.allowOrgOverride
|
|
755
|
+
: !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate);
|
|
756
|
+
if (!cw)
|
|
757
|
+
return;
|
|
758
|
+
designerAutoOnRef.current = key;
|
|
759
|
+
setEditing(true);
|
|
760
|
+
}, [type, name, createMode, embedded, loading, entry, layered]);
|
|
761
|
+
// Keyboard shortcut: Cmd/Ctrl+\ toggles the inspector. This is the
|
|
762
|
+
// designer convention shared by Figma, VS Code (Cmd+B), Sketch — `\`
|
|
763
|
+
// sits next to Return so it's reachable one-handed.
|
|
764
|
+
React.useEffect(() => {
|
|
765
|
+
if (typeof window === 'undefined' || embedded)
|
|
766
|
+
return;
|
|
767
|
+
function onKey(e) {
|
|
768
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
769
|
+
if (!mod || e.shiftKey || e.altKey)
|
|
770
|
+
return;
|
|
771
|
+
if (e.key !== '\\')
|
|
772
|
+
return;
|
|
773
|
+
// Ignore when typing in an editor (textarea / contenteditable).
|
|
774
|
+
const t = e.target;
|
|
775
|
+
if (t && (t.tagName === 'TEXTAREA' || t.isContentEditable))
|
|
776
|
+
return;
|
|
777
|
+
e.preventDefault();
|
|
778
|
+
toggleInspector();
|
|
779
|
+
}
|
|
780
|
+
window.addEventListener('keydown', onKey);
|
|
781
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
782
|
+
}, [embedded, toggleInspector]);
|
|
163
783
|
React.useEffect(() => {
|
|
164
784
|
if (typeof window === 'undefined' || embedded)
|
|
165
785
|
return;
|
|
166
786
|
const sp = new URLSearchParams(window.location.search);
|
|
167
787
|
const tab = sp.get('tab');
|
|
168
|
-
if (tab)
|
|
169
|
-
|
|
788
|
+
if (tab === 'layers' || tab === 'references' || tab === 'related' || tab === 'audit') {
|
|
789
|
+
setOpenSheet(tab);
|
|
790
|
+
}
|
|
791
|
+
initialTabRef.current = tab;
|
|
170
792
|
const open = sp.get('open');
|
|
171
793
|
if (open && open.includes(':')) {
|
|
172
794
|
const [t, n] = open.split(':', 2);
|
|
@@ -189,36 +811,125 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
189
811
|
}
|
|
190
812
|
window.history.replaceState({}, '', url.toString());
|
|
191
813
|
}, [relatedTarget, embedded]);
|
|
814
|
+
function labelForIssuePath(path) {
|
|
815
|
+
const key = path.split('.')[0];
|
|
816
|
+
if (!key)
|
|
817
|
+
return path;
|
|
818
|
+
const formForLabels = (createMode && config.createSchema ? undefined : entry?.form);
|
|
819
|
+
const sections = Array.isArray(formForLabels?.sections) ? formForLabels.sections : [];
|
|
820
|
+
for (const section of sections) {
|
|
821
|
+
const fields = Array.isArray(section?.fields) ? section.fields : [];
|
|
822
|
+
for (const field of fields) {
|
|
823
|
+
if (typeof field === 'string') {
|
|
824
|
+
if (field === key)
|
|
825
|
+
return field;
|
|
826
|
+
}
|
|
827
|
+
else if (field?.field === key) {
|
|
828
|
+
return String(field.label ?? key);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const props = (schema?.properties ?? {});
|
|
833
|
+
return String(props[key]?.title ?? key);
|
|
834
|
+
}
|
|
192
835
|
async function doSave(force) {
|
|
193
836
|
setSaving(true);
|
|
194
837
|
setError(null);
|
|
195
838
|
setIssues([]);
|
|
196
839
|
try {
|
|
197
|
-
// Ensure
|
|
840
|
+
// Ensure identity is set on create, and that any `createDefaults`
|
|
841
|
+
// / `createBuildBody` shape (e.g. `{ fields: {} }` for object,
|
|
842
|
+
// or `{ list: { data: { object } } }` for view) is present so
|
|
843
|
+
// the saved body satisfies its JSONSchema. User-supplied values
|
|
844
|
+
// always win over the defaults.
|
|
845
|
+
let builtBody = createMode
|
|
846
|
+
? (config.createBuildBody
|
|
847
|
+
? config.createBuildBody(draft)
|
|
848
|
+
: { ...(config.createDefaults ?? {}), ...draft })
|
|
849
|
+
// Edit mode: serialise the editor draft back to the wire shape
|
|
850
|
+
// (inverse of `toDraft` — e.g. `view` folds the `{ list | form }`
|
|
851
|
+
// family key back into the ViewItem `config` wrapper).
|
|
852
|
+
: (config.fromDraft ? config.fromDraft(draft) : draft);
|
|
853
|
+
// Async create-time augmentation (e.g. seed a record page's regions from
|
|
854
|
+
// the bound object's synthesized default). Best-effort — a failure leaves
|
|
855
|
+
// the un-augmented body. User/builder-supplied keys win over the seed.
|
|
856
|
+
if (createMode && config.createSeed) {
|
|
857
|
+
try {
|
|
858
|
+
const seeded = await config.createSeed(draft, { client });
|
|
859
|
+
if (seeded && typeof seeded === 'object') {
|
|
860
|
+
// Seed wins over the empty defaults (`builtBody` already folded the
|
|
861
|
+
// user's draft in, which only carries default-empty `regions`).
|
|
862
|
+
builtBody = { ...builtBody, ...seeded };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
catch { /* seed is best-effort; proceed with the un-augmented body */ }
|
|
866
|
+
}
|
|
867
|
+
const savedName = String(builtBody[identityField] ?? draft[identityField] ?? name);
|
|
198
868
|
const itemToSave = createMode
|
|
199
|
-
? { ...
|
|
200
|
-
:
|
|
201
|
-
const savedName = String(itemToSave.name ?? name);
|
|
869
|
+
? { ...builtBody, [identityField]: savedName }
|
|
870
|
+
: builtBody;
|
|
202
871
|
if (!savedName) {
|
|
203
|
-
setError('
|
|
872
|
+
setError(t('engine.validation.nameRequired', locale));
|
|
204
873
|
setSaving(false);
|
|
205
874
|
return;
|
|
206
875
|
}
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
876
|
+
// Save lands in the draft buffer — the runtime keeps serving the
|
|
877
|
+
// last published version until the operator clicks Publish. The
|
|
878
|
+
// backend defaults to publish mode for backward-compatibility, so
|
|
879
|
+
// Studio must opt into draft explicitly.
|
|
880
|
+
// Bind to the active software package (sys_metadata.package_id) when a
|
|
881
|
+
// real package scope is carried in the URL (`?package=`). The backend
|
|
882
|
+
// stamps it on create and preserves an existing binding on update, so
|
|
883
|
+
// env-local overlays (no `?package=`) are unaffected.
|
|
884
|
+
const activePackage = (() => {
|
|
885
|
+
try {
|
|
886
|
+
const p = new URLSearchParams(window.location.search).get('package');
|
|
887
|
+
return p && p !== 'all' ? p : undefined;
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
})();
|
|
893
|
+
await client.save(type, savedName, itemToSave, {
|
|
894
|
+
force,
|
|
895
|
+
mode: 'draft',
|
|
896
|
+
...(activePackage ? { packageId: activePackage } : {}),
|
|
897
|
+
});
|
|
898
|
+
// Refresh layered + draft state after save — scope to the same package
|
|
899
|
+
// as the initial load (ADR-0048) so a same-name collision re-reads this
|
|
900
|
+
// package's own row, not another's.
|
|
901
|
+
const refreshScope = ownerPackageId ? { packageId: ownerPackageId } : {};
|
|
902
|
+
const [lay, draftResp] = await Promise.all([
|
|
903
|
+
client.layered(type, savedName, refreshScope),
|
|
904
|
+
client.getDraft(type, savedName, refreshScope).catch(() => null),
|
|
905
|
+
]);
|
|
210
906
|
setLayered(lay);
|
|
211
|
-
const
|
|
907
|
+
const draftReal = extractDraftBody(draftResp);
|
|
908
|
+
setHasDraft(!!draftReal);
|
|
909
|
+
// Merge the draft over the effective baseline (see the load effect):
|
|
910
|
+
// a partial draft overlay must not drop inherited fields like `type`.
|
|
911
|
+
const freshBaseline = (lay.effective ?? itemToSave);
|
|
912
|
+
const rawFresh = draftReal
|
|
913
|
+
? { ...freshBaseline, ...draftReal }
|
|
914
|
+
: freshBaseline;
|
|
915
|
+
// Re-normalise the refreshed wire shape so the editor keeps showing
|
|
916
|
+
// the canonical draft shape after a save (e.g. the backend re-expands
|
|
917
|
+
// a view into the ViewItem `config` wrapper).
|
|
918
|
+
const fresh = config.toDraft ? config.toDraft(rawFresh) : rawFresh;
|
|
212
919
|
setDraft(fresh);
|
|
213
920
|
draftSnapshotRef.current = fresh;
|
|
921
|
+
setLastSavedAt(new Date());
|
|
922
|
+
lastAutoSaveSnapshotRef.current = JSON.stringify(fresh);
|
|
214
923
|
setDestructiveIssues(null);
|
|
215
924
|
setPendingItem(null);
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
925
|
+
// Stay in design mode after save for designer-capable types so the
|
|
926
|
+
// user keeps their inspector context. Non-designer types fall back
|
|
927
|
+
// to the previous "exit edit on save" UX.
|
|
928
|
+
const stayInEditing = !createMode && !!getMetadataPreview(type);
|
|
929
|
+
if (!createMode && !stayInEditing)
|
|
219
930
|
setEditing(false);
|
|
220
931
|
if (createMode) {
|
|
221
|
-
navigate(`../${encodeURIComponent(savedName)}
|
|
932
|
+
navigate(`../${encodeURIComponent(savedName)}`, { relative: 'path' });
|
|
222
933
|
}
|
|
223
934
|
}
|
|
224
935
|
catch (err) {
|
|
@@ -233,7 +944,7 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
233
944
|
const i = err?.body?.issues ?? [];
|
|
234
945
|
let mapped = (Array.isArray(i) ? i : []).map((x) => ({
|
|
235
946
|
path: Array.isArray(x.path) ? x.path.join('.') : String(x.path ?? ''),
|
|
236
|
-
message: String(x.message ?? 'Invalid'),
|
|
947
|
+
message: translateValidationMessage(String(x.message ?? 'Invalid'), locale),
|
|
237
948
|
}));
|
|
238
949
|
// Backend's invalid_metadata sometimes returns a flat string like
|
|
239
950
|
// "<type>/<name> failed spec validation: <path>: <message>".
|
|
@@ -243,10 +954,10 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
243
954
|
if (mapped.length === 0 && raw) {
|
|
244
955
|
const m = raw.match(/failed spec validation:\s*(.+?):\s*(.+)$/);
|
|
245
956
|
if (m) {
|
|
246
|
-
mapped = [{ path: m[1].trim(), message: m[2].trim() }];
|
|
957
|
+
mapped = [{ path: m[1].trim(), message: translateValidationMessage(m[2].trim(), locale) }];
|
|
247
958
|
}
|
|
248
959
|
else {
|
|
249
|
-
mapped = [{ path: '', message: raw }];
|
|
960
|
+
mapped = [{ path: '', message: translateValidationMessage(raw, locale) }];
|
|
250
961
|
}
|
|
251
962
|
}
|
|
252
963
|
setIssues(mapped);
|
|
@@ -254,10 +965,10 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
254
965
|
setError(mapped[0].message);
|
|
255
966
|
}
|
|
256
967
|
else if (mapped.length === 1) {
|
|
257
|
-
setError(`${mapped[0].path}: ${mapped[0].message}`);
|
|
968
|
+
setError(`${labelForIssuePath(mapped[0].path)}: ${mapped[0].message}`);
|
|
258
969
|
}
|
|
259
970
|
else {
|
|
260
|
-
setError(
|
|
971
|
+
setError(tFormat('engine.validation.failed', locale, { count: mapped.length }));
|
|
261
972
|
}
|
|
262
973
|
}
|
|
263
974
|
else {
|
|
@@ -269,19 +980,96 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
269
980
|
}
|
|
270
981
|
}
|
|
271
982
|
async function doReset() {
|
|
272
|
-
|
|
983
|
+
// Two semantics:
|
|
984
|
+
// - artifact-backed item: "Reset overlay" — keep the code default.
|
|
985
|
+
// - DB-only item: "Delete" — the item disappears entirely (no
|
|
986
|
+
// artifact baseline to fall back to). Navigate back to the list
|
|
987
|
+
// since the current URL no longer refers to anything.
|
|
988
|
+
const itemIsArtifact = !createMode && layered?.code != null;
|
|
989
|
+
const confirmKey = itemIsArtifact
|
|
990
|
+
? 'engine.edit.resetConfirm'
|
|
991
|
+
: 'engine.edit.deleteConfirm';
|
|
992
|
+
if (!confirm(tFormat(confirmKey, locale, { type, name: name ?? '' }))) {
|
|
273
993
|
return;
|
|
274
994
|
}
|
|
275
995
|
setSaving(true);
|
|
276
996
|
setError(null);
|
|
277
997
|
try {
|
|
278
998
|
await client.reset(type, name);
|
|
999
|
+
if (itemIsArtifact) {
|
|
1000
|
+
const lay = await client.layered(type, name);
|
|
1001
|
+
setLayered(lay);
|
|
1002
|
+
const fresh = (lay.effective ?? lay.code ?? {});
|
|
1003
|
+
setDraft(fresh);
|
|
1004
|
+
draftSnapshotRef.current = fresh;
|
|
1005
|
+
// Designer-capable types stay in design mode; allow the auto-on
|
|
1006
|
+
// effect to re-trigger after this reset.
|
|
1007
|
+
if (getMetadataPreview(type)) {
|
|
1008
|
+
designerAutoOnRef.current = null;
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
setEditing(false);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
// No artifact baseline → return to the list view.
|
|
1016
|
+
navigate(`../`, { relative: 'path' });
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch (err) {
|
|
1020
|
+
setError(err?.message ?? String(err));
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
setSaving(false);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// Promote the pending draft to the active overlay. Mirrors `doSave`'s
|
|
1027
|
+
// refresh pattern so the editor stays in sync with the new baseline.
|
|
1028
|
+
async function doPublish() {
|
|
1029
|
+
setPublishing(true);
|
|
1030
|
+
setError(null);
|
|
1031
|
+
try {
|
|
1032
|
+
await client.publish(type, name);
|
|
1033
|
+
const [lay, draftResp] = await Promise.all([
|
|
1034
|
+
client.layered(type, name),
|
|
1035
|
+
client.getDraft(type, name).catch(() => null),
|
|
1036
|
+
]);
|
|
1037
|
+
setLayered(lay);
|
|
1038
|
+
const draftReal = extractDraftBody(draftResp);
|
|
1039
|
+
setHasDraft(!!draftReal);
|
|
1040
|
+
// Merge the draft over the effective baseline so a partial draft overlay
|
|
1041
|
+
// doesn't drop inherited fields like `type` (section visibleOn depends
|
|
1042
|
+
// on it — ADR-0047).
|
|
1043
|
+
const freshBaseline = (lay.effective ?? draft);
|
|
1044
|
+
const fresh = draftReal
|
|
1045
|
+
? { ...freshBaseline, ...draftReal }
|
|
1046
|
+
: freshBaseline;
|
|
1047
|
+
setDraft(fresh);
|
|
1048
|
+
draftSnapshotRef.current = fresh;
|
|
1049
|
+
}
|
|
1050
|
+
catch (err) {
|
|
1051
|
+
setError(err?.message ?? String(err));
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
setPublishing(false);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Discard the pending draft (`DELETE ?state=draft`). The published
|
|
1058
|
+
// overlay is untouched; the editor reverts to showing the live body.
|
|
1059
|
+
async function doDiscardDraft() {
|
|
1060
|
+
if (!confirm(tFormat('engine.edit.discardDraftConfirm', locale, { type, name: name ?? '' }))) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
setSaving(true);
|
|
1064
|
+
setError(null);
|
|
1065
|
+
try {
|
|
1066
|
+
await client.reset(type, name, { state: 'draft' });
|
|
279
1067
|
const lay = await client.layered(type, name);
|
|
280
1068
|
setLayered(lay);
|
|
281
1069
|
const fresh = (lay.effective ?? lay.code ?? {});
|
|
282
1070
|
setDraft(fresh);
|
|
283
1071
|
draftSnapshotRef.current = fresh;
|
|
284
|
-
|
|
1072
|
+
setHasDraft(false);
|
|
285
1073
|
}
|
|
286
1074
|
catch (err) {
|
|
287
1075
|
setError(err?.message ?? String(err));
|
|
@@ -290,18 +1078,206 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
290
1078
|
setSaving(false);
|
|
291
1079
|
}
|
|
292
1080
|
}
|
|
1081
|
+
// Dirty detection: cheap structural comparison via JSON. The draft is
|
|
1082
|
+
// small (a single metadata record) so this is fine on each render.
|
|
1083
|
+
// Used to surface an "unsaved" indicator next to the Save button.
|
|
1084
|
+
// Must be declared BEFORE any early returns to preserve hook order.
|
|
1085
|
+
const isDirty = React.useMemo(() => {
|
|
1086
|
+
if (createMode)
|
|
1087
|
+
return Object.keys(draft).length > 0;
|
|
1088
|
+
const snap = draftSnapshotRef.current;
|
|
1089
|
+
if (!snap)
|
|
1090
|
+
return false;
|
|
1091
|
+
try {
|
|
1092
|
+
return JSON.stringify(draft) !== JSON.stringify(snap);
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
}, [draft, createMode]);
|
|
1098
|
+
// Two-tier authorization (PR-10d.7) — hoisted above the early `loading`
|
|
1099
|
+
// return so the auto-save / keyboard / blocker effects below can read
|
|
1100
|
+
// them. Recomputed cheaply on every render.
|
|
1101
|
+
// - artifact-backed items (layered.code != null) need allowOrgOverride
|
|
1102
|
+
// - DB-only items (no artifact) need allowOrgOverride OR allowRuntimeCreate
|
|
1103
|
+
// - createMode is always writable (the server will gate on intent)
|
|
1104
|
+
const isArtifactItem = !createMode && layered?.code != null;
|
|
1105
|
+
// ADR-0010 — server-computed lock flags. undefined means "no opinion"
|
|
1106
|
+
// (older server / non-lockable item) → preserve legacy behaviour.
|
|
1107
|
+
const lockEditable = layered?.editable !== false;
|
|
1108
|
+
const lockDeletable = layered?.deletable !== false;
|
|
1109
|
+
const lockResettable = layered?.resettable !== false;
|
|
1110
|
+
const lockReason = layered?.lockReason;
|
|
1111
|
+
const isLocked = layered?.lock && layered.lock !== 'none';
|
|
1112
|
+
const canWriteByType = createMode
|
|
1113
|
+
? !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate)
|
|
1114
|
+
: isArtifactItem
|
|
1115
|
+
? !!entry?.allowOrgOverride
|
|
1116
|
+
: !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate);
|
|
1117
|
+
const canWrite = canWriteByType && (createMode || lockEditable);
|
|
1118
|
+
const readOnly = !canWrite && !createMode;
|
|
1119
|
+
// Auto-save: debounce edits and persist silently once the user pauses
|
|
1120
|
+
// for AUTOSAVE_DEBOUNCE_MS. Skipped for create mode (need an explicit
|
|
1121
|
+
// name first), read-only forms, and while a save is already in flight.
|
|
1122
|
+
// We track the last attempted snapshot so a validation failure doesn't
|
|
1123
|
+
// loop on the same payload — the user has to mutate the draft again.
|
|
1124
|
+
const AUTOSAVE_DEBOUNCE_MS = 1500;
|
|
1125
|
+
// Keep doSave fresh inside the effect without re-arming the timer on
|
|
1126
|
+
// every render.
|
|
1127
|
+
const doSaveRef = React.useRef(doSave);
|
|
1128
|
+
React.useEffect(() => {
|
|
1129
|
+
doSaveRef.current = doSave;
|
|
1130
|
+
});
|
|
1131
|
+
React.useEffect(() => {
|
|
1132
|
+
if (!autoSaveEnabled)
|
|
1133
|
+
return;
|
|
1134
|
+
if (createMode || readOnly || !editing || !isDirty || saving)
|
|
1135
|
+
return;
|
|
1136
|
+
let snap;
|
|
1137
|
+
try {
|
|
1138
|
+
snap = JSON.stringify(draft);
|
|
1139
|
+
}
|
|
1140
|
+
catch {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (snap === lastAutoSaveSnapshotRef.current)
|
|
1144
|
+
return;
|
|
1145
|
+
const handle = window.setTimeout(() => {
|
|
1146
|
+
lastAutoSaveSnapshotRef.current = snap;
|
|
1147
|
+
doSaveRef.current(false);
|
|
1148
|
+
}, AUTOSAVE_DEBOUNCE_MS);
|
|
1149
|
+
return () => window.clearTimeout(handle);
|
|
1150
|
+
}, [draft, isDirty, editing, saving, createMode, readOnly, autoSaveEnabled]);
|
|
1151
|
+
// Keyboard shortcut — ⌘S / Ctrl+S triggers save when dirty.
|
|
1152
|
+
React.useEffect(() => {
|
|
1153
|
+
const handler = (e) => {
|
|
1154
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
|
|
1155
|
+
if (!canWrite || readOnly)
|
|
1156
|
+
return;
|
|
1157
|
+
if (!editing && !createMode)
|
|
1158
|
+
return;
|
|
1159
|
+
e.preventDefault();
|
|
1160
|
+
if (!saving && (createMode || isDirty)) {
|
|
1161
|
+
doSaveRef.current(false);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
window.addEventListener('keydown', handler);
|
|
1166
|
+
return () => window.removeEventListener('keydown', handler);
|
|
1167
|
+
}, [canWrite, readOnly, editing, createMode, saving, isDirty]);
|
|
1168
|
+
// Beforeunload guard — browser-native "leave site?" prompt when the
|
|
1169
|
+
// user closes the tab / reloads with unsaved changes.
|
|
1170
|
+
React.useEffect(() => {
|
|
1171
|
+
if (!isDirty)
|
|
1172
|
+
return;
|
|
1173
|
+
const handler = (e) => {
|
|
1174
|
+
e.preventDefault();
|
|
1175
|
+
// Required for Chrome to actually show the prompt.
|
|
1176
|
+
e.returnValue = '';
|
|
1177
|
+
};
|
|
1178
|
+
window.addEventListener('beforeunload', handler);
|
|
1179
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
1180
|
+
}, [isDirty]);
|
|
1181
|
+
// In-app navigation guard — intercept anchor / link clicks before the
|
|
1182
|
+
// router consumes them. Cheaper and more compatible than useBlocker,
|
|
1183
|
+
// which requires a data router (the host app uses BrowserRouter).
|
|
1184
|
+
React.useEffect(() => {
|
|
1185
|
+
if (!isDirty)
|
|
1186
|
+
return;
|
|
1187
|
+
const handler = (e) => {
|
|
1188
|
+
if (e.defaultPrevented)
|
|
1189
|
+
return;
|
|
1190
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
1191
|
+
return;
|
|
1192
|
+
const target = e.target;
|
|
1193
|
+
const anchor = target?.closest?.('a[href]');
|
|
1194
|
+
if (!anchor)
|
|
1195
|
+
return;
|
|
1196
|
+
// Allow new-tab / download / external links — they don't replace
|
|
1197
|
+
// the current page.
|
|
1198
|
+
if (anchor.target && anchor.target !== '_self')
|
|
1199
|
+
return;
|
|
1200
|
+
if (anchor.hasAttribute('download'))
|
|
1201
|
+
return;
|
|
1202
|
+
try {
|
|
1203
|
+
const url = new URL(anchor.href, window.location.href);
|
|
1204
|
+
if (url.origin !== window.location.origin)
|
|
1205
|
+
return;
|
|
1206
|
+
if (url.pathname === window.location.pathname)
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (!confirm(t('engine.edit.unsavedLeaveConfirm', locale))) {
|
|
1213
|
+
e.preventDefault();
|
|
1214
|
+
e.stopPropagation();
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
document.addEventListener('click', handler, true);
|
|
1218
|
+
return () => document.removeEventListener('click', handler, true);
|
|
1219
|
+
}, [isDirty, locale]);
|
|
1220
|
+
// Publish "what's being edited" to the global AI chat so the agent can
|
|
1221
|
+
// act on the open item (and offer item-specific starter prompts). Kept
|
|
1222
|
+
// to a light summary — the agent can `describe_object` for full detail.
|
|
1223
|
+
// Declared above the early returns to satisfy the Rules of Hooks.
|
|
1224
|
+
const assistantEditorCtx = React.useMemo(() => {
|
|
1225
|
+
if (embedded)
|
|
1226
|
+
return null;
|
|
1227
|
+
const itemName = String(draft.name ?? name ?? '');
|
|
1228
|
+
if (!itemName)
|
|
1229
|
+
return null;
|
|
1230
|
+
const ctx = {
|
|
1231
|
+
type,
|
|
1232
|
+
name: itemName,
|
|
1233
|
+
label: typeof draft.label === 'string' ? draft.label : undefined,
|
|
1234
|
+
};
|
|
1235
|
+
if (type === 'object') {
|
|
1236
|
+
ctx.fields = readFields(draft.fields).entries.slice(0, 60).map((e) => ({
|
|
1237
|
+
name: e.name,
|
|
1238
|
+
type: typeof e.def.type === 'string' ? e.def.type : undefined,
|
|
1239
|
+
label: typeof e.def.label === 'string' ? e.def.label : undefined,
|
|
1240
|
+
required: !!e.def.required || undefined,
|
|
1241
|
+
}));
|
|
1242
|
+
}
|
|
1243
|
+
return ctx;
|
|
1244
|
+
}, [embedded, type, name, draft]);
|
|
1245
|
+
useRegisterAssistantEditor(assistantEditorCtx);
|
|
293
1246
|
if (loading) {
|
|
294
1247
|
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" }), " Loading ", type, "/", name, "\u2026"] }) }));
|
|
295
1248
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1249
|
+
// `schema`, `createFieldList`, `effectiveHiddenFields`,
|
|
1250
|
+
// `effectiveFieldOrder`, and `handleCreateAwareChange` are all
|
|
1251
|
+
// hoisted to the top of the component (next to the rest of the
|
|
1252
|
+
// create-mode harness) to avoid placing hooks *after* the loading
|
|
1253
|
+
// early-return.
|
|
1254
|
+
// Banner variant: when type ships with allowRuntimeCreate but this
|
|
1255
|
+
// specific item is locked because it comes from a code package, we
|
|
1256
|
+
// show a different message inviting the user to create their own.
|
|
1257
|
+
const showArtifactLockedBanner = readOnly && isArtifactItem && !!entry?.allowRuntimeCreate;
|
|
301
1258
|
// Preview tab — opt-in via `registerMetadataPreview()`. Hidden in
|
|
302
1259
|
// create mode (nothing to preview yet) and inside the embedded
|
|
303
1260
|
// drawer (the parent context owns the preview surface).
|
|
304
|
-
|
|
1261
|
+
//
|
|
1262
|
+
// Exception: a few types host their primary authoring surface IN the
|
|
1263
|
+
// canvas (object → field designer). For those we light the canvas up
|
|
1264
|
+
// during create too, so authors design fields immediately instead of
|
|
1265
|
+
// round-tripping through a save first. Object-level basics (name,
|
|
1266
|
+
// label, …) stay editable via the default inspector shown when no
|
|
1267
|
+
// field is selected, so naming still works before any field exists.
|
|
1268
|
+
const showPreviewInCreate = CREATE_MODE_CANVAS_TYPES.has(type);
|
|
1269
|
+
const PreviewComponent = !embedded && (!createMode || showPreviewInCreate)
|
|
1270
|
+
? getMetadataPreview(type)
|
|
1271
|
+
: undefined;
|
|
1272
|
+
// Optional scoped inspector for the selected sub-element (e.g. a
|
|
1273
|
+
// dashboard widget). Registered separately via
|
|
1274
|
+
// `registerMetadataInspector()` so a type can opt in independently
|
|
1275
|
+
// of having a Preview, and so plugins can swap implementations.
|
|
1276
|
+
const InspectorComponent = getMetadataInspector(type);
|
|
1277
|
+
// Optional "home" inspector shown when there is NO selection, replacing
|
|
1278
|
+
// the generic whole-draft SchemaForm with a curated panel (e.g. the View
|
|
1279
|
+
// type + fields manager). Falls back to SchemaForm when unregistered.
|
|
1280
|
+
const DefaultInspectorComponent = getMetadataDefaultInspector(type);
|
|
305
1281
|
// Cancel edits: revert the draft to the last saved snapshot and exit
|
|
306
1282
|
// edit mode. Safe to call even with no snapshot (no-op).
|
|
307
1283
|
function doCancelEdit() {
|
|
@@ -316,20 +1292,170 @@ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, creat
|
|
|
316
1292
|
// read-only. createMode is always editing; truly read-only types
|
|
317
1293
|
// (no allowOrgOverride) ignore the editing toggle entirely.
|
|
318
1294
|
const formReadOnly = readOnly || (!editing && !createMode);
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
1295
|
+
// Note: URL `?tab=` deep-links were repurposed to open side-panel
|
|
1296
|
+
// sheets (Layers / References / Related). Anything else is ignored —
|
|
1297
|
+
// the main work area is always the form+preview.
|
|
1298
|
+
// Action group rendered identically in either the PageShell header
|
|
1299
|
+
// (form-only types) or the canvas toolbar (types with a PreviewComponent).
|
|
1300
|
+
// Centralising it lets us merge the two top bars into one when a
|
|
1301
|
+
// designer is present, saving a full row of vertical chrome.
|
|
1302
|
+
const actionsNode = (_jsxs(_Fragment, { children: [!createMode && (_jsx(MetadataTypeActions, { entry: entry, location: "record_header", recordId: name, onAfter: () => setReloadKey((k) => k + 1) })), (!createMode || hasAnchors) && (_jsxs("div", { className: "flex items-center rounded-md border bg-background p-0.5", children: [!createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('layers'), title: t('engine.edit.layers', locale), className: "h-7 w-7 p-0 relative", children: [_jsx(Layers3, { className: "h-3.5 w-3.5" }), layered?.overlay && (() => {
|
|
1303
|
+
const n = countOverlaidFields(layered.code, layered.effective);
|
|
1304
|
+
return n > 0 ? (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-emerald-600 text-emerald-50 text-[9px] leading-[14px] text-center font-medium", title: t('engine.layers.diff', locale), children: n })) : null;
|
|
1305
|
+
})()] })), !createMode && hasDraft && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('review'), title: t('designer.draftReview.title', locale), className: "h-7 w-7 p-0 relative", "data-testid": "resource-review-trigger", children: [_jsx(GitCompareArrows, { className: "h-3.5 w-3.5" }), (() => {
|
|
1306
|
+
const n = computeDraftChangeCount(layered?.effective ?? null, draft);
|
|
1307
|
+
return n > 0 ? (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-amber-500 text-amber-50 text-[9px] leading-[14px] text-center font-medium", title: tFormat('designer.draftReview.badge', locale, { n }), children: n })) : null;
|
|
1308
|
+
})()] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('references'), title: t('engine.edit.references', locale), className: "h-7 w-7 p-0 relative", children: [_jsx(Link2, { className: "h-3.5 w-3.5" }), refs && refs.length > 0 && (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-muted text-foreground text-[9px] leading-[14px] text-center font-medium border", children: refs.length }))] })), hasAnchors && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('related'), title: t('engine.edit.related', locale), className: "h-7 w-7 p-0", children: _jsx(Boxes, { className: "h-3.5 w-3.5" }) })), !createMode && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('history'), title: t('engine.edit.history', locale), className: "h-7 w-7 p-0", children: _jsx(History, { className: "h-3.5 w-3.5" }) })), !createMode && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpenSheet('audit'), title: t('engine.edit.auditTab', locale), className: "h-7 w-7 p-0", children: _jsx(ShieldCheck, { className: "h-3.5 w-3.5" }) }))] })), !createMode && canWrite && layered?.overlay && (isArtifactItem ? lockResettable : lockDeletable) && (_jsx(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, title: isArtifactItem
|
|
1309
|
+
? t('engine.edit.reset', locale)
|
|
1310
|
+
: t('engine.edit.delete', locale), className: "h-7 w-7 p-0 text-muted-foreground hover:text-destructive", children: isArtifactItem ? (_jsx(RotateCcw, { className: "h-3.5 w-3.5" })) : (_jsx(Trash2, { className: "h-3.5 w-3.5" })) })), canWrite && !createMode && !editing && !PreviewComponent && (_jsxs(Button, { size: "sm", onClick: () => setEditing(true), className: "h-7", children: [_jsx(Pencil, { className: "h-3.5 w-3.5 mr-1" }), t('engine.edit.edit', locale)] })), canWrite && (editing || createMode) && (_jsx(SaveStatusIndicator, { saving: saving, isDirty: isDirty, autoSaveEnabled: autoSaveEnabled, lastSavedAt: lastSavedAt, createMode: !!createMode, locale: locale })), canWrite && (editing || createMode) && !createMode && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setAutoSaveEnabled((v) => !v), className: "h-7 w-7 p-0 text-muted-foreground", title: autoSaveEnabled
|
|
1311
|
+
? t('engine.edit.autoSaveOn', locale)
|
|
1312
|
+
: t('engine.edit.autoSaveOff', locale), children: autoSaveEnabled ? (_jsx(Zap, { className: "h-3.5 w-3.5 text-emerald-600" })) : (_jsx(ZapOff, { className: "h-3.5 w-3.5" })) })), canWrite && !createMode && editing && !PreviewComponent && (_jsx(Button, { variant: "ghost", size: "sm", onClick: doCancelEdit, disabled: saving, className: "h-7 w-7 p-0", title: t('engine.cancel', locale), children: _jsx(X, { className: "h-3.5 w-3.5" }) })), canWrite && (editing || createMode) && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving || (!createMode && !isDirty), className: "h-7 w-7 p-0 relative", title: saving
|
|
1313
|
+
? t('engine.edit.saving', locale)
|
|
1314
|
+
: !createMode && !isDirty
|
|
1315
|
+
? t('engine.edit.noChanges', locale)
|
|
1316
|
+
: `${t('engine.edit.save', locale)} (⌘S)`, children: [saving ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(Save, { className: "h-3.5 w-3.5" })), isDirty && !saving && (_jsx("span", { "aria-hidden": true, className: "absolute -top-0.5 -right-0.5 inline-block h-2 w-2 rounded-full bg-amber-300 ring-2 ring-background" }))] })), canWrite && !createMode && hasDraft && (_jsx(Button, { variant: "ghost", size: "sm", onClick: doDiscardDraft, disabled: saving || publishing, className: "h-7 w-7 p-0 text-muted-foreground", title: t('engine.edit.discardDraft', locale), children: _jsx(Undo2, { className: "h-3.5 w-3.5" }) })), canWrite && !createMode && hasDraft && (_jsx(Button, { size: "sm", onClick: doPublish, disabled: saving || publishing || isDirty, className: "h-7 px-2 relative bg-emerald-600 hover:bg-emerald-700 text-emerald-50", title: isDirty
|
|
1317
|
+
? t('engine.edit.publishBlockedDirty', locale)
|
|
1318
|
+
: t('engine.edit.publish', locale), children: publishing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(Send, { className: "h-3.5 w-3.5 mr-1" }), _jsx("span", { className: "text-xs", children: t('engine.edit.publish', locale) })] })) }))] }));
|
|
1319
|
+
return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? t('engine.edit.createNew', locale) : undefined, actions: PreviewComponent ? null : actionsNode, children: [_jsxs("div", { className: PreviewComponent
|
|
1320
|
+
? 'flex h-full min-h-0 flex-col'
|
|
1321
|
+
: 'p-6 space-y-6 max-w-7xl', children: [(error || readOnly || hasDraft || isLocked) && (_jsxs("div", { className: PreviewComponent
|
|
1322
|
+
? 'px-6 pt-4 space-y-3'
|
|
1323
|
+
: 'space-y-3', children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), isLocked && (_jsxs("div", { className: "text-xs text-amber-900 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-2.5", children: [_jsx(Lock, { className: "h-3.5 w-3.5 mt-0.5 shrink-0 opacity-80" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "font-medium", children: [layered?.lock === 'full' && t('engine.edit.lockFull', locale), layered?.lock === 'no-overlay' && t('engine.edit.lockNoOverlay', locale), layered?.lock === 'no-delete' && t('engine.edit.lockNoDelete', locale)] }), lockReason && _jsx("div", { className: "mt-0.5 opacity-90", children: lockReason }), layered?.lockDocsUrl && (_jsxs("a", { href: layered.lockDocsUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex items-center gap-1 text-amber-800 underline hover:text-amber-900 dark:text-amber-200 dark:hover:text-amber-100", children: [locale === 'zh-CN' ? '查看文档' : 'View docs', " \u2192"] })), layered?.packageId && (_jsx("div", { className: "mt-0.5 text-amber-700 dark:text-amber-300/80", children: _jsxs("code", { className: "font-mono", children: [layered.packageId, layered.packageVersion ? `@${layered.packageVersion}` : ''] }) }))] }), showArtifactLockedBanner && (_jsx(Button, { size: "sm", variant: "outline", className: "shrink-0 h-7 bg-background/60", onClick: () => navigate(`../new`, { relative: 'path' }), children: t('engine.list.create', locale) }))] })), hasDraft && !createMode && (_jsxs("div", { className: "text-xs text-emerald-900 border border-emerald-300 bg-emerald-50 rounded p-3 dark:text-emerald-200 dark:border-emerald-700/50 dark:bg-emerald-950/30 flex items-center gap-3", children: [_jsx(Send, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1", children: t('engine.edit.draftPending', locale) }), canWrite && (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: doDiscardDraft, disabled: saving || publishing, className: "h-7", children: t('engine.edit.discardDraft', locale) }), _jsx(Button, { size: "sm", onClick: doPublish, disabled: saving || publishing || isDirty, className: "h-7 bg-emerald-600 hover:bg-emerald-700 text-emerald-50", children: publishing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (t('engine.edit.publish', locale)) })] }))] })), readOnly && !isLocked && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-3", children: [_jsx("div", { className: "flex-1", children: showArtifactLockedBanner ? (
|
|
1324
|
+
/* Type allows runtime-create but THIS item ships from
|
|
1325
|
+
a code package. Tell the user clearly and provide
|
|
1326
|
+
a CTA to author their own. */
|
|
1327
|
+
t('engine.edit.artifactLockedBanner', locale)
|
|
1328
|
+
.split(/(\{type\})/)
|
|
1329
|
+
.map((part, i) => {
|
|
1330
|
+
if (part === '{type}')
|
|
1331
|
+
return _jsx("code", { className: "font-mono", children: type }, i);
|
|
1332
|
+
return _jsx(React.Fragment, { children: part }, i);
|
|
1333
|
+
})) : (
|
|
1334
|
+
/* The platform i18n bundle ships `engine.edit.readOnlyTypeBanner`
|
|
1335
|
+
with `{flag} / {type} / {override}` placeholders so the
|
|
1336
|
+
monospace tokens are inlined inside the translated sentence
|
|
1337
|
+
in any locale. Splitting on the three tokens preserves the
|
|
1338
|
+
sentence order across translations. */
|
|
1339
|
+
t('engine.edit.readOnlyTypeBanner', locale)
|
|
1340
|
+
.split(/(\{flag\}|\{type\}|\{override\})/)
|
|
1341
|
+
.map((part, i) => {
|
|
1342
|
+
if (part === '{flag}')
|
|
1343
|
+
return _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }, i);
|
|
1344
|
+
if (part === '{type}')
|
|
1345
|
+
return _jsx("code", { className: "font-mono", children: type }, i);
|
|
1346
|
+
if (part === '{override}')
|
|
1347
|
+
return _jsx("code", { className: "font-mono", children: "allowOrgOverride" }, i);
|
|
1348
|
+
return _jsx(React.Fragment, { children: part }, i);
|
|
1349
|
+
})) }), showArtifactLockedBanner && (_jsx(Button, { size: "sm", variant: "outline", className: "shrink-0", onClick: () => navigate(`../new`, { relative: 'path' }), children: t('engine.list.create', locale) }))] }))] })), _jsx("div", { className: PreviewComponent
|
|
1350
|
+
? 'flex w-full flex-1 min-h-0 flex-col'
|
|
1351
|
+
: 'w-full', children: _jsxs("div", { className: PreviewComponent
|
|
1352
|
+
? 'mt-2 flex-1 min-h-0 flex flex-col px-6 pb-4'
|
|
1353
|
+
: 'mt-4 space-y-3', children: [!PreviewComponent && formReadOnly && !readOnly && canWrite && !createMode && (_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs text-muted-foreground border rounded p-2.5 bg-muted/30", children: [_jsx("span", { children: t('engine.edit.readOnlyBanner', locale).split(/\{edit\}/).map((part, i, arr) => (_jsxs(React.Fragment, { children: [part, i < arr.length - 1 && _jsx("strong", { children: t('engine.edit.edit', locale) })] }, i))) }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setEditing(true), children: [_jsx(Pencil, { className: "h-3.5 w-3.5 mr-1" }), t('engine.edit.edit', locale)] })] })), (() => {
|
|
1354
|
+
// Server-computed load-time validation errors on the
|
|
1355
|
+
// effective payload — surfaced here so operators can see
|
|
1356
|
+
// a structural problem without saving first. The same
|
|
1357
|
+
// errors are also threaded into SchemaForm as `issues`
|
|
1358
|
+
// and rendered inline next to each broken field.
|
|
1359
|
+
const diag = layered?._diagnostics;
|
|
1360
|
+
// When client-side Zod validation is available for this
|
|
1361
|
+
// type, drive the error portion of the banner from the
|
|
1362
|
+
// live `issues` state instead of the stale load-time
|
|
1363
|
+
// diagnostics, so it stays in sync with every keystroke.
|
|
1364
|
+
// Warnings remain server-sourced (Zod doesn't model them).
|
|
1365
|
+
const liveErrors = hasClientValidator(type)
|
|
1366
|
+
? issues.map((i) => ({
|
|
1367
|
+
path: i.path,
|
|
1368
|
+
message: translateValidationMessage(i.message, locale),
|
|
1369
|
+
}))
|
|
1370
|
+
: (diag?.errors ?? []).map((i) => ({
|
|
1371
|
+
...i,
|
|
1372
|
+
message: translateValidationMessage(i.message, locale),
|
|
1373
|
+
}));
|
|
1374
|
+
const liveValid = hasClientValidator(type)
|
|
1375
|
+
? liveErrors.length === 0
|
|
1376
|
+
: diag?.valid !== false;
|
|
1377
|
+
// Gate the whole diagnostics block on a successful load with
|
|
1378
|
+
// a diagnostics source. A failed load shows the explicit
|
|
1379
|
+
// "failed to load" banner above instead; the empty-default
|
|
1380
|
+
// form's required-field issues here would be noise, not a
|
|
1381
|
+
// verdict on the item (see shouldRenderDiagnostics).
|
|
1382
|
+
if (createMode && !createDirty) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
if (!shouldRenderDiagnostics({
|
|
1386
|
+
loadFailed,
|
|
1387
|
+
hasDiag: !!diag,
|
|
1388
|
+
hasClientValidator: hasClientValidator(type),
|
|
1389
|
+
})) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
const errs = liveErrors;
|
|
1393
|
+
const warns = diag?.warnings ?? [];
|
|
1394
|
+
const hasErrs = !liveValid && errs.length > 0;
|
|
1395
|
+
const hasWarns = warns.length > 0;
|
|
1396
|
+
if (!hasErrs && !hasWarns)
|
|
1397
|
+
return null;
|
|
1398
|
+
const renderBlock = (kind, items) => {
|
|
1399
|
+
const head = items.slice(0, 3);
|
|
1400
|
+
const rest = Math.max(0, items.length - head.length);
|
|
1401
|
+
const cls = kind === 'error'
|
|
1402
|
+
? 'border-destructive/40 bg-destructive/[0.06] text-destructive'
|
|
1403
|
+
: 'border-amber-500/40 bg-amber-500/[0.08] text-amber-800 dark:text-amber-200';
|
|
1404
|
+
const titleKey = kind === 'error'
|
|
1405
|
+
? 'engine.edit.diagnostics.title'
|
|
1406
|
+
: 'engine.edit.diagnostics.warnTitle';
|
|
1407
|
+
return (_jsxs("div", { className: `flex items-start gap-2 text-xs border rounded p-2.5 ${cls}`, children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium", children: tFormat(titleKey, locale, { count: items.length }) }), _jsxs("ul", { className: "mt-1 space-y-0.5 font-mono text-[11px]", children: [head.map((e, i) => (_jsxs("li", { className: "truncate", children: [_jsx("span", { className: "opacity-70", children: e.path ? labelForIssuePath(e.path) : '(root)' }), ": ", e.message] }, i))), rest > 0 && (_jsx("li", { className: "opacity-70", children: tFormat('engine.edit.diagnostics.more', locale, { count: rest }) }))] })] })] }, kind));
|
|
1408
|
+
};
|
|
1409
|
+
return (_jsxs("div", { className: "space-y-2", children: [hasErrs && renderBlock('error', errs), hasWarns && renderBlock('warning', warns)] }));
|
|
1410
|
+
})(), PreviewComponent ? (_jsx("div", { className: isFullscreen
|
|
1411
|
+
? 'fixed inset-0 z-50 bg-background flex flex-col p-3'
|
|
1412
|
+
: 'relative flex-1 min-h-0 flex', children: _jsxs(PanelGroup, { direction: "horizontal", className: "flex-1 min-h-0 rounded-md border bg-background overflow-hidden", id: `metadata-edit-${type}`, children: [_jsx(ResizablePanel, { defaultSize: 62, minSize: 30, children: _jsxs("div", { className: "relative h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b bg-background/95 backdrop-blur px-3 py-2 sticky top-0 z-10", children: [_jsx("div", { className: "flex items-center gap-1", children: canWrite && (_jsxs("div", { role: "tablist", "aria-label": t('engine.edit.designer', locale), className: "inline-flex items-center rounded-md border bg-muted/40 p-0.5", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": !previewOnly, onClick: () => setPreviewOnly(false), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
|
|
1413
|
+
(!previewOnly
|
|
1414
|
+
? 'bg-background shadow-sm text-foreground'
|
|
1415
|
+
: 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.designMode', locale), children: [_jsx(MousePointer2, { className: "h-3.5 w-3.5" }), t('engine.edit.designMode', locale)] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": previewOnly, onClick: () => setPreviewOnly(true), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
|
|
1416
|
+
(previewOnly
|
|
1417
|
+
? 'bg-background shadow-sm text-foreground'
|
|
1418
|
+
: 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.previewMode', locale), children: [_jsx(Eye, { className: "h-3.5 w-3.5" }), t('engine.edit.previewMode', locale)] })] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [actionsNode, _jsx("span", { className: "mx-1 h-5 w-px bg-border", "aria-hidden": true }), PreviewComponent && (_jsx(Button, { variant: "ghost", size: "sm", onClick: toggleInspector, className: "h-7 w-7 p-0", title: (inspectorCollapsed
|
|
1419
|
+
? t('engine.edit.showInspector', locale)
|
|
1420
|
+
: t('engine.edit.hideInspector', locale)) + ' (⌘\\)', children: inspectorCollapsed ? (_jsx(PanelRightOpen, { className: "h-3.5 w-3.5" })) : (_jsx(PanelRightClose, { className: "h-3.5 w-3.5" })) })), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setIsFullscreen((v) => !v), className: "h-7 w-7 p-0", title: isFullscreen
|
|
1421
|
+
? t('engine.edit.exitFullscreen', locale)
|
|
1422
|
+
: t('engine.edit.fullscreen', locale), children: isFullscreen ? (_jsx(Minimize2, { className: "h-3.5 w-3.5" })) : (_jsx(Maximize2, { className: "h-3.5 w-3.5" })) })] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4 bg-[radial-gradient(circle_at_1px_1px,theme(colors.border)_1px,transparent_0)] [background-size:16px_16px] bg-muted/30", children: _jsx(PreviewComponent, { type: type, name: name, draft: draft, baseline: !createMode
|
|
1423
|
+
? (layered?.effective ?? undefined)
|
|
1424
|
+
: undefined, editing: editing && !previewOnly, selection: previewOnly ? null : selection, onSelectionChange: setSelection, locale: locale, onPatch: (patch) => handleDraftChange((d) => ({ ...d, ...patch })) }) })] }) }), _jsx(ResizableHandle, { withHandle: true, className: inspectorCollapsed
|
|
1425
|
+
? 'hidden'
|
|
1426
|
+
: 'w-1.5 bg-border/40 hover:bg-primary/40 active:bg-primary/60 transition-colors' }), _jsx(ResizablePanel, { panelRef: inspectorPanelRef, defaultSize: lastInspectorSizeRef.current, minSize: 22, collapsible: true, collapsedSize: 0, onResize: (size) => {
|
|
1427
|
+
const pct = size.asPercentage;
|
|
1428
|
+
const collapsed = pct <= 0.5;
|
|
1429
|
+
if (!collapsed) {
|
|
1430
|
+
lastInspectorSizeRef.current = pct;
|
|
1431
|
+
if (typeof window !== 'undefined') {
|
|
1432
|
+
window.localStorage.setItem(inspectorSizeStorageKey, String(Math.round(pct)));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
setInspectorCollapsed((prev) => {
|
|
1436
|
+
if (prev === collapsed)
|
|
1437
|
+
return prev;
|
|
1438
|
+
if (typeof window !== 'undefined') {
|
|
1439
|
+
window.localStorage.setItem(inspectorStorageKey, collapsed ? '1' : '0');
|
|
1440
|
+
}
|
|
1441
|
+
return collapsed;
|
|
1442
|
+
});
|
|
1443
|
+
}, children: _jsxs("div", { className: "h-full overflow-auto", children: [_jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b bg-background/95 backdrop-blur px-3 py-2", children: [_jsxs("div", { role: "tablist", className: "inline-flex items-center rounded-md border bg-muted/40 p-0.5", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": inspectorTab === 'properties', onClick: () => setInspectorTab('properties'), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
|
|
1444
|
+
(inspectorTab === 'properties'
|
|
1445
|
+
? 'bg-background shadow-sm text-foreground'
|
|
1446
|
+
: 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.inspector.properties', locale), children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), t('engine.edit.inspector.properties', locale)] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": inspectorTab === 'source', onClick: () => setInspectorTab('source'), className: 'inline-flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ' +
|
|
1447
|
+
(inspectorTab === 'source'
|
|
1448
|
+
? 'bg-background shadow-sm text-foreground'
|
|
1449
|
+
: 'text-muted-foreground hover:text-foreground'), title: t('engine.edit.inspector.source', locale), children: [_jsx(FileCode2, { className: "h-3.5 w-3.5" }), t('engine.edit.inspector.source', locale)] })] }), isDirty && (_jsxs(Badge, { variant: "outline", className: "text-[10px] border-amber-400/60 text-amber-600 dark:text-amber-300", children: [_jsx("span", { className: "mr-1 inline-block h-1.5 w-1.5 rounded-full bg-amber-400" }), t('engine.edit.unsaved', locale)] }))] }), _jsx("div", { className: "p-4", children: inspectorTab === 'source' ? (_jsx(JsonSourceEditor, { value: draft, onChange: handleDraftChange, readOnly: formReadOnly, issues: displayIssues.map((i) => ({
|
|
1450
|
+
path: i.path ?? '',
|
|
1451
|
+
message: i.message,
|
|
1452
|
+
})) })) : selection && InspectorComponent ? (_jsx(InspectorComponent, { type: type, name: name, draft: draft, selection: selection, onPatch: (patch) => handleDraftChange((d) => ({
|
|
1453
|
+
...d,
|
|
1454
|
+
...patch,
|
|
1455
|
+
})), onClearSelection: () => setSelection(null), onSelectionChange: setSelection, readOnly: formReadOnly, locale: locale })) : !selection && DefaultInspectorComponent ? (_jsx(DefaultInspectorComponent, { type: type, name: name, draft: draft, onPatch: (patch) => handleDraftChange((d) => ({
|
|
1456
|
+
...d,
|
|
1457
|
+
...patch,
|
|
1458
|
+
})), onSelectionChange: setSelection, readOnly: formReadOnly, locale: locale, serverSchema: entry?.schema })) : (_jsx(SchemaForm, { schema: schema, form: createMode && config.createSchema ? undefined : entry?.form, value: draft, onChange: handleCreateAwareChange, issues: displayIssues, hiddenFields: effectiveHiddenFields, fieldOrder: effectiveFieldOrder, readOnly: formReadOnly, createMode: createMode, widgetContext: widgetContext })) })] }) })] }) })) : (_jsx(SchemaForm, { schema: schema, form: createMode && config.createSchema ? undefined : entry?.form, value: draft, onChange: handleCreateAwareChange, issues: displayIssues, hiddenFields: effectiveHiddenFields, fieldOrder: effectiveFieldOrder, readOnly: formReadOnly, createMode: createMode, widgetContext: widgetContext }))] }) })] }), _jsx(Sheet, { open: openSheet === 'layers', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.layers', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(LayeredDiff, { layered: layered, locale: locale }) })] }) }), _jsx(Sheet, { open: openSheet === 'review', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('designer.draftReview.title', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name, " \u00B7 ", t('designer.canvas.reviewVsPublished', locale)] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(DraftReviewPanel, { published: layered?.effective ?? null, draft: draft, locale: locale }) })] }) }), _jsx(Sheet, { open: openSheet === 'references', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsxs(SheetTitle, { className: "text-base", children: [t('engine.edit.references', locale), refs && (_jsx(Badge, { variant: "outline", className: "ml-2 text-[10px]", children: refs.length }))] }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })] }) }), hasAnchors && (_jsx(Sheet, { open: openSheet === 'related', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[860px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.related', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) })] }) })), !createMode && (_jsx(Sheet, { open: openSheet === 'history', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[720px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.history', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4", children: _jsx(HistoryPanel, { type: type, name: name, client: client, onRollback: () => setReloadKey((k) => k + 1), rollbackLabel: t('engine.edit.rollback', locale), rollbackConfirm: (version) => t('engine.edit.rollbackConfirm', locale).replace('{version}', String(version)) }) })] }) })), !createMode && (_jsx(Sheet, { open: openSheet === 'audit', onOpenChange: (o) => !o && setOpenSheet(null), children: _jsxs(SheetContent, { side: "right", className: "w-[92vw] sm:max-w-[860px] p-0 flex flex-col gap-0", children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b", children: [_jsx(SheetTitle, { className: "text-base", children: t('engine.edit.auditTab', locale) }), _jsxs(SheetDescription, { className: "text-xs", children: [type, " / ", name] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-hidden p-3", children: _jsx(AuditPanel, { type: type, name: name, client: client, locale: locale }) })] }) })), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
|
|
333
1459
|
if (!open) {
|
|
334
1460
|
setDestructiveIssues(null);
|
|
335
1461
|
setPendingItem(null);
|
|
@@ -345,3 +1471,40 @@ function ReferencesPanel({ refs, loading, }) {
|
|
|
345
1471
|
}
|
|
346
1472
|
return (_jsx("div", { className: "border rounded-lg overflow-hidden", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-xs uppercase tracking-wider text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left", children: "From type" }), _jsx("th", { className: "px-3 py-2 text-left", children: "From name" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Path" })] }) }), _jsx("tbody", { className: "divide-y", children: refs.map((r, i) => (_jsxs("tr", { className: "hover:bg-accent/50", children: [_jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: "text-[10px] font-mono", children: r.fromType }) }), _jsx("td", { className: "px-3 py-2 font-mono text-xs", children: r.fromName }), _jsx("td", { className: "px-3 py-2 font-mono text-xs text-muted-foreground", children: r.path })] }, i))) })] }) }));
|
|
347
1473
|
}
|
|
1474
|
+
/**
|
|
1475
|
+
* SaveStatusIndicator — small inline label next to the Save icon that
|
|
1476
|
+
* communicates auto-save state so the icon-only button is not a black
|
|
1477
|
+
* box. Five states:
|
|
1478
|
+
* - saving → "Saving…" with spinner
|
|
1479
|
+
* - dirty + on → "Auto-saving in 1.5s" (subtle, amber)
|
|
1480
|
+
* - dirty + off → "Unsaved" (amber)
|
|
1481
|
+
* - clean + ts → "Saved 14:32" (muted)
|
|
1482
|
+
* - createMode → hidden until first save
|
|
1483
|
+
*/
|
|
1484
|
+
function SaveStatusIndicator({ saving, isDirty, autoSaveEnabled, lastSavedAt, createMode, locale, }) {
|
|
1485
|
+
// Re-render every 30s so "Saved 14:32" stays accurate without
|
|
1486
|
+
// requiring the caller to manage a ticker.
|
|
1487
|
+
const [, force] = React.useReducer((n) => n + 1, 0);
|
|
1488
|
+
React.useEffect(() => {
|
|
1489
|
+
if (!lastSavedAt)
|
|
1490
|
+
return;
|
|
1491
|
+
const id = window.setInterval(force, 30000);
|
|
1492
|
+
return () => window.clearInterval(id);
|
|
1493
|
+
}, [lastSavedAt]);
|
|
1494
|
+
if (saving) {
|
|
1495
|
+
return (_jsxs("span", { className: "text-xs text-muted-foreground hidden md:inline-flex items-center gap-1", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), t('engine.edit.saving', locale)] }));
|
|
1496
|
+
}
|
|
1497
|
+
if (isDirty) {
|
|
1498
|
+
if (createMode)
|
|
1499
|
+
return null;
|
|
1500
|
+
return (_jsxs("span", { className: "text-xs text-amber-600 dark:text-amber-300 hidden md:inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-1.5 w-1.5 rounded-full bg-amber-400" }), autoSaveEnabled
|
|
1501
|
+
? t('engine.edit.autoSavingShortly', locale)
|
|
1502
|
+
: t('engine.edit.unsaved', locale)] }));
|
|
1503
|
+
}
|
|
1504
|
+
if (lastSavedAt) {
|
|
1505
|
+
const hh = String(lastSavedAt.getHours()).padStart(2, '0');
|
|
1506
|
+
const mm = String(lastSavedAt.getMinutes()).padStart(2, '0');
|
|
1507
|
+
return (_jsx("span", { className: "text-xs text-muted-foreground hidden md:inline-flex items-center gap-1", children: tFormat('engine.edit.savedAt', locale, { time: `${hh}:${mm}` }) }));
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|