@agent-native/core 0.80.4 → 0.80.6
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +28 -2
- package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +1 -1
- package/corpus/core/docs/content/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/template-design.mdx +1 -1
- package/corpus/core/package.json +3 -7
- package/corpus/core/src/action.ts +21 -0
- package/corpus/core/src/agent/durable-background.ts +32 -11
- package/corpus/core/src/agent/production-agent.ts +11 -1
- package/corpus/core/src/agent/run-loop-with-resume.ts +3 -1
- package/corpus/core/src/brand-kit/fig/index.ts +17 -37
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/cli/index.ts +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +187 -51
- package/corpus/core/src/cli/skills.ts +18 -3
- package/corpus/core/src/client/AssistantChat.tsx +11 -1
- package/corpus/core/src/client/agent-chat.ts +12 -1
- package/corpus/core/src/client/composer/PromptComposer.tsx +100 -41
- package/corpus/core/src/client/org/OrgSwitcher.tsx +1 -1
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -27
- package/corpus/core/src/collab/presence.ts +1 -1
- package/corpus/core/src/mcp/connect-route.ts +34 -5
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +20 -0
- package/corpus/core/src/sharing/registry.ts +17 -0
- package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +8 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +117 -33
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -0
- package/corpus/templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-pages-now-sit-closer-under-the-header-with-tighter.md +6 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +3 -0
- package/corpus/templates/assets/actions/generate-image.ts +3 -0
- package/corpus/templates/assets/changelog/2026-06-29-longer-agent-generation-window.md +6 -0
- package/corpus/templates/assets/netlify.toml +3 -3
- package/corpus/templates/assets/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/clips/app/components/capture-install-options.tsx +7 -8
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +1 -1
- package/corpus/templates/content/AGENTS.md +57 -44
- package/corpus/templates/content/README.md +16 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
- package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
- package/corpus/templates/content/actions/_database-utils.ts +17 -14
- package/corpus/templates/content/actions/_federation-join.ts +14 -2
- package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
- package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
- package/corpus/templates/content/actions/create-document.ts +8 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
- package/corpus/templates/content/actions/get-content-database.ts +10 -2
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
- package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
- package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
- package/corpus/templates/content/app/blocks/BuilderDocsBlocks.tsx +10 -0
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -0
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +185 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +19 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
- package/corpus/templates/content/app/components/editor/extensions/ContentReferenceNode.tsx +82 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +76 -0
- package/corpus/templates/content/app/i18n-data.ts +924 -0
- package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-reusable-mdx-references-now-preview-linked-local-documents-i.md +6 -0
- package/corpus/templates/content/shared/api.ts +91 -1
- package/corpus/templates/content/shared/builder-docs-blocks.ts +8 -0
- package/corpus/templates/content/shared/builder-mdx.ts +141 -4
- package/corpus/templates/content/shared/nfm.ts +62 -0
- package/corpus/templates/content/vite.config.ts +14 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +10 -11
- package/corpus/templates/design/AGENTS.md +5 -0
- package/corpus/templates/design/_gitignore +7 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +22 -12
- package/corpus/templates/design/actions/create-design-system.ts +15 -4
- package/corpus/templates/design/actions/create-file.ts +20 -1
- package/corpus/templates/design/actions/delete-design-system.ts +2 -2
- package/corpus/templates/design/actions/duplicate-design.ts +35 -9
- package/corpus/templates/design/actions/generate-design.ts +146 -69
- package/corpus/templates/design/actions/generate-screens.ts +51 -19
- package/corpus/templates/design/actions/get-design-snapshot.ts +5 -1
- package/corpus/templates/design/actions/get-design.ts +2 -0
- package/corpus/templates/design/actions/import-design-project.ts +28 -9
- package/corpus/templates/design/actions/import-document.ts +22 -8
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +6 -153
- package/corpus/templates/design/actions/navigate.ts +105 -54
- package/corpus/templates/design/actions/set-default-design-system.ts +17 -3
- package/corpus/templates/design/actions/show-design-questions.ts +2 -0
- package/corpus/templates/design/actions/update-design.ts +2 -1
- package/corpus/templates/design/actions/view-screen.ts +12 -6
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +4 -4
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +159 -41
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +8 -4
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +37 -42
- package/corpus/templates/design/app/components/design/EditPanel.tsx +336 -183
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +153 -34
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +135 -33
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +6 -0
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +4 -10
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +84 -43
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +4 -4
- package/corpus/templates/design/app/components/design/inspector/{FigmaColorPicker.tsx → DesignColorPicker.tsx} +89 -62
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +14 -26
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +40 -12
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +13 -13
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -9
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
- package/corpus/templates/design/app/components/design/inspector/{figma-icons.tsx → design-icons.tsx} +2 -2
- package/corpus/templates/design/app/components/design/inspector/index.ts +12 -12
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +5 -2
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +3 -3
- package/corpus/templates/design/app/components/layout/Layout.tsx +5 -2
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +167 -91
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +16 -3
- package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +15 -0
- package/corpus/templates/design/app/global.css +4 -0
- package/corpus/templates/design/app/hooks/use-design-system.ts +4 -1
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +38 -3
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +9 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
- package/corpus/templates/design/app/i18n-data.ts +527 -0
- package/corpus/templates/design/app/lib/design-systems.ts +6 -4
- package/corpus/templates/design/app/lib/design-templates.ts +15 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +641 -158
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +42 -34
- package/corpus/templates/design/app/pages/VisualEdit.tsx +100 -0
- package/corpus/templates/design/app/root.tsx +42 -9
- package/corpus/templates/design/app/routes/visual-edit.tsx +7 -0
- package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +1 -1
- package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +1 -1
- package/corpus/templates/design/changelog/2026-06-28-visual-code-design-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-generation-prompts-now-show-connect-ai-before-sending.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +1 -1
- package/corpus/templates/design/changelog/2026-06-29-fixed-design-editor-exports-and-gradient-edits-so-multi-scre.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-png-export-handles-modern-css-color-functions-more-reliably.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-png-export-now-ignores-duplicate-clicks-while-a-download-is-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-public-visual-edit-and-shared-design-links-now-invite-visito.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-visual-edit-actions-now-reject-missing-design-targets-before.md +6 -0
- package/corpus/templates/design/e2e/README.md +71 -0
- package/corpus/templates/design/e2e/global-setup.ts +157 -0
- package/corpus/templates/design/e2e/helpers.ts +148 -0
- package/corpus/templates/design/netlify.toml +3 -4
- package/corpus/templates/design/package.json +5 -1
- package/corpus/templates/design/playwright.config.ts +51 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +121 -0
- package/corpus/templates/design/server/handlers/uploads.ts +89 -38
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +181 -0
- package/corpus/templates/design/server/lib/coding-handoff.ts +33 -4
- package/corpus/templates/design/server/lib/design-export.ts +35 -8
- package/corpus/templates/design/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/design/server/plugins/auth.ts +10 -1
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +1 -0
- package/corpus/templates/design/shared/api.ts +2 -0
- package/corpus/templates/design/shared/apply-edits.ts +12 -8
- package/corpus/templates/design/shared/canvas-math.ts +19 -1
- package/corpus/templates/design/shared/code-layer.ts +87 -28
- package/corpus/templates/design/shared/generation-session.ts +1 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +4 -3
- package/corpus/templates/design/shared/shader-presets.ts +10 -0
- package/corpus/templates/design/shared/source-mode.ts +9 -0
- package/corpus/templates/design/vitest.config.ts +2 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +1 -1
- package/corpus/templates/plan/AGENTS.md +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/validate-local-plan-source.ts +2 -10
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -0
- package/corpus/templates/plan/docs/commenting-ux-plan.md +2 -2
- package/corpus/templates/plan/server/plan-content.ts +1 -1
- package/corpus/templates/plan/server/ui-plan-html.ts +2 -2
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +6 -153
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +53 -38
- package/corpus/templates/slides/app/components/design-system/{fig-import-response.ts → builder-index-response.ts} +9 -6
- package/corpus/templates/slides/app/components/visual-editor/MultiSelectChip.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-06-29-design-system-setup-now-indexes-uploaded-design-files-throug.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
- package/corpus/templates/slides/netlify.toml +3 -6
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +116 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +179 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +1 -0
- package/dist/action.d.ts +15 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +6 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +9 -7
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +27 -11
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts +7 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +4 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +2 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +2 -2
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/brand-kit/fig/index.d.ts +4 -4
- package/dist/brand-kit/fig/index.d.ts.map +1 -1
- package/dist/brand-kit/fig/index.js +14 -27
- package/dist/brand-kit/fig/index.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +15 -7
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +198 -45
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +15 -2
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +35 -1
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/org/OrgSwitcher.js +1 -1
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +1 -1
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +2 -15
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/collab/presence.d.ts +1 -1
- package/dist/collab/presence.js +1 -1
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/mcp/connect-route.d.ts.map +1 -1
- package/dist/mcp/connect-route.js +34 -5
- package/dist/mcp/connect-route.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +6 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +12 -3
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js +11 -0
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ar-SA/template-design.mdx +1 -1
- package/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
- package/docs/content/locales/de-DE/template-design.mdx +1 -1
- package/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/es-ES/template-design.mdx +1 -1
- package/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/fr-FR/template-design.mdx +1 -1
- package/docs/content/locales/hi-IN/template-design.mdx +1 -1
- package/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ja-JP/template-design.mdx +1 -1
- package/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ko-KR/template-design.mdx +1 -1
- package/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/pt-BR/template-design.mdx +1 -1
- package/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/zh-CN/template-design.mdx +1 -1
- package/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/zh-TW/template-design.mdx +1 -1
- package/docs/content/real-time-collaboration.mdx +2 -2
- package/docs/content/template-design.mdx +1 -1
- package/package.json +3 -7
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/brand-kit/fig/decode.ts +0 -410
- package/corpus/core/src/brand-kit/fig/extract-design-system.ts +0 -947
- package/corpus/core/src/brand-kit/fig/fig-to-html.ts +0 -1906
- package/corpus/templates/design/actions/import-figma.ts +0 -55
- package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +0 -6
- package/corpus/templates/design/server/handlers/import-figma-system.ts +0 -86
- package/corpus/templates/design/server/routes/api/import-figma-system.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +0 -101
- package/corpus/templates/slides/server/routes/api/import-figma-system.post.ts +0 -1
- package/dist/brand-kit/fig/decode.d.ts +0 -33
- package/dist/brand-kit/fig/decode.d.ts.map +0 -1
- package/dist/brand-kit/fig/decode.js +0 -358
- package/dist/brand-kit/fig/decode.js.map +0 -1
- package/dist/brand-kit/fig/extract-design-system.d.ts +0 -44
- package/dist/brand-kit/fig/extract-design-system.d.ts.map +0 -1
- package/dist/brand-kit/fig/extract-design-system.js +0 -752
- package/dist/brand-kit/fig/extract-design-system.js.map +0 -1
- package/dist/brand-kit/fig/fig-to-html.d.ts +0 -246
- package/dist/brand-kit/fig/fig-to-html.d.ts.map +0 -1
- package/dist/brand-kit/fig/fig-to-html.js +0 -1506
- package/dist/brand-kit/fig/fig-to-html.js.map +0 -1
|
@@ -9,12 +9,15 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
BUILDER_CMS_SAFE_WRITE_MODEL,
|
|
11
11
|
type BuilderCmsModelSummary,
|
|
12
|
+
type BuilderCmsWriteEffect,
|
|
12
13
|
type ContentDatabaseItem,
|
|
13
14
|
type ContentDatabaseResponse,
|
|
14
15
|
type ContentDatabaseSource,
|
|
15
16
|
type ContentDatabaseSourceChangeSet,
|
|
16
17
|
type ContentDatabaseSourceJoinRequest,
|
|
17
18
|
type ContentDatabaseSourceReviewPayload,
|
|
19
|
+
type ContentDatabaseSourceWriteMode,
|
|
20
|
+
type ExecuteBuilderSourceBatchResponse,
|
|
18
21
|
type SourceJoinSuggestion,
|
|
19
22
|
type ContentDatabaseView,
|
|
20
23
|
type ContentDatabaseViewConfig,
|
|
@@ -134,13 +137,15 @@ import {
|
|
|
134
137
|
import { Spinner } from "@/components/ui/spinner";
|
|
135
138
|
import {
|
|
136
139
|
useAddDatabaseItem,
|
|
140
|
+
useAddContentDatabaseSourceFieldProperty,
|
|
137
141
|
useAttachContentDatabaseSource,
|
|
138
142
|
useBuilderCmsModels,
|
|
143
|
+
useChangeContentDatabaseSourceRole,
|
|
139
144
|
useContentDatabase,
|
|
140
145
|
useContentDatabases,
|
|
141
146
|
useDisconnectContentDatabaseSource,
|
|
142
147
|
useDuplicateDatabaseItem,
|
|
143
|
-
|
|
148
|
+
useExecuteBuilderSourceBatch,
|
|
144
149
|
useMoveDatabaseItem,
|
|
145
150
|
usePrepareBuilderSourceReview,
|
|
146
151
|
useRefreshContentDatabaseSource,
|
|
@@ -159,7 +164,9 @@ import {
|
|
|
159
164
|
} from "@/hooks/use-documents";
|
|
160
165
|
import { cn } from "@/lib/utils";
|
|
161
166
|
|
|
167
|
+
import { resolveBuilderCmsWriteEffect } from "../../../actions/_builder-cms-write-adapter.js";
|
|
162
168
|
import { BuilderSourceReviewDialog } from "./database-sources/BuilderSourceReviewDialog";
|
|
169
|
+
import { type BuilderReviewPublicationTransitions } from "./database-sources/BuilderSourceReviewDialog";
|
|
163
170
|
import { DocumentBlockFields } from "./DocumentBlockFields";
|
|
164
171
|
import {
|
|
165
172
|
AddProperty,
|
|
@@ -188,12 +195,18 @@ import {
|
|
|
188
195
|
} from "./previewDocumentSaveRegistry";
|
|
189
196
|
import { VisualEditor } from "./VisualEditor";
|
|
190
197
|
|
|
198
|
+
type BuilderSourceWriteSettingsInput = {
|
|
199
|
+
writeMode: ContentDatabaseSourceWriteMode;
|
|
200
|
+
allowPublicationTransitions?: boolean;
|
|
201
|
+
};
|
|
202
|
+
|
|
191
203
|
interface DocumentDatabaseProps {
|
|
192
204
|
document: Document;
|
|
193
205
|
canEdit: boolean;
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
const CONTENT_DATABASE_PAGE_SIZE = 100;
|
|
209
|
+
const CONTENT_DATABASE_MAX_ITEM_LIMIT = 5_000;
|
|
197
210
|
|
|
198
211
|
export type SortDirection = ContentDatabaseSortDirection;
|
|
199
212
|
export type DatabaseSort = ContentDatabaseSort;
|
|
@@ -469,10 +482,11 @@ function DatabaseTable({
|
|
|
469
482
|
const database = useContentDatabase(document.id, databaseItemLimit);
|
|
470
483
|
const addItem = useAddDatabaseItem(document.id);
|
|
471
484
|
const attachSource = useAttachContentDatabaseSource(document.id);
|
|
485
|
+
const changeSourceRole = useChangeContentDatabaseSourceRole(document.id);
|
|
472
486
|
const refreshSource = useRefreshContentDatabaseSource(document.id);
|
|
473
487
|
const disconnectSource = useDisconnectContentDatabaseSource(document.id);
|
|
474
488
|
const prepareBuilderReview = usePrepareBuilderSourceReview(document.id);
|
|
475
|
-
const
|
|
489
|
+
const executeBuilderBatch = useExecuteBuilderSourceBatch(document.id);
|
|
476
490
|
const setSourceWriteMode = useSetContentDatabaseSourceWriteMode(document.id);
|
|
477
491
|
const setProperty = useSetDocumentProperty(document.id);
|
|
478
492
|
const updateView = useUpdateContentDatabaseView(document.id);
|
|
@@ -480,10 +494,43 @@ function DatabaseTable({
|
|
|
480
494
|
const properties = data?.properties ?? [];
|
|
481
495
|
const items = data?.items ?? [];
|
|
482
496
|
const totalItemCount = data?.pagination?.totalItems ?? items.length;
|
|
483
|
-
const hasMoreItems =
|
|
497
|
+
const hasMoreItems =
|
|
498
|
+
data?.pagination?.hasMore === true &&
|
|
499
|
+
databaseItemLimit < CONTENT_DATABASE_MAX_ITEM_LIMIT;
|
|
500
|
+
const isLoadingMoreItems =
|
|
501
|
+
database.isFetching && data?.pagination?.limit !== databaseItemLimit;
|
|
484
502
|
const databaseId = data?.database.id ?? null;
|
|
485
503
|
const source = data?.source ?? null;
|
|
486
504
|
const sources = data?.sources ?? (source ? [source] : []);
|
|
505
|
+
// New-row source picker (row-union): when a database has 2+ sources, the auto
|
|
506
|
+
// -created "Source" select tags which collection a row belongs to. Surface a
|
|
507
|
+
// picker on "New" so a row is created already tagged for its collection (the
|
|
508
|
+
// backend then routes its create_draft to that source). Mirrors the server's
|
|
509
|
+
// SOURCE_PROPERTY_NAME ("Source", a select).
|
|
510
|
+
const sourceTagPicker = useMemo(() => {
|
|
511
|
+
if (sources.length < 2) return null;
|
|
512
|
+
const property = properties.find(
|
|
513
|
+
(item) =>
|
|
514
|
+
item.definition.name === "Source" && item.definition.type === "select",
|
|
515
|
+
);
|
|
516
|
+
if (!property) return null;
|
|
517
|
+
// Each Source option's id IS the source id (and "local" is the Local
|
|
518
|
+
// sentinel), so a collection always resolves to a valid tag — never a
|
|
519
|
+
// missing option that would silently create an untagged row.
|
|
520
|
+
const optionIds = new Set(
|
|
521
|
+
(property.definition.options.options ?? []).map((option) => option.id),
|
|
522
|
+
);
|
|
523
|
+
const collections = sources
|
|
524
|
+
.filter(
|
|
525
|
+
(item) => item.sourceType === "builder-cms" && optionIds.has(item.id),
|
|
526
|
+
)
|
|
527
|
+
.map((item) => ({ label: item.sourceName, optionId: item.id }));
|
|
528
|
+
return {
|
|
529
|
+
propertyId: property.definition.id,
|
|
530
|
+
collections,
|
|
531
|
+
localOptionId: optionIds.has("local") ? "local" : null,
|
|
532
|
+
};
|
|
533
|
+
}, [properties, sources]);
|
|
487
534
|
const [previewDocumentId, setPreviewDocumentId] = useState<string | null>(
|
|
488
535
|
null,
|
|
489
536
|
);
|
|
@@ -499,9 +546,17 @@ function DatabaseTable({
|
|
|
499
546
|
const [builderReviewOpen, setBuilderReviewOpen] = useState(false);
|
|
500
547
|
const [builderReviewResult, setBuilderReviewResult] =
|
|
501
548
|
useState<ContentDatabaseSourceReviewPayload | null>(null);
|
|
549
|
+
const [builderBatchResult, setBuilderBatchResult] =
|
|
550
|
+
useState<ExecuteBuilderSourceBatchResponse | null>(null);
|
|
502
551
|
const [builderReviewCheckedAt, setBuilderReviewCheckedAt] = useState<
|
|
503
552
|
string | null
|
|
504
553
|
>(null);
|
|
554
|
+
// Which Builder source the review dialog / push targets. null ⇒ the primary
|
|
555
|
+
// source (single-source behavior). Set when opening a non-primary source's
|
|
556
|
+
// writable leaf so review/push/write-mode scope to that collection.
|
|
557
|
+
const [builderReviewSourceId, setBuilderReviewSourceId] = useState<
|
|
558
|
+
string | null
|
|
559
|
+
>(null);
|
|
505
560
|
const [settingsPanel, setSettingsPanel] =
|
|
506
561
|
useState<DatabaseSettingsPanel>("main");
|
|
507
562
|
const [viewConfig, setViewConfig] = useState<ContentDatabaseViewConfig>(
|
|
@@ -619,16 +674,30 @@ function DatabaseTable({
|
|
|
619
674
|
() => databaseSelectedItems(visibleItems, selectedItemIds),
|
|
620
675
|
[visibleItems, selectedItemIds],
|
|
621
676
|
);
|
|
677
|
+
// The review dialog operates on the source the user opened, which may be a
|
|
678
|
+
// non-primary row-union collection. Fall back to the primary when no
|
|
679
|
+
// explicit target is set (single-source behavior).
|
|
680
|
+
const activeReviewSource = useMemo(
|
|
681
|
+
() =>
|
|
682
|
+
builderReviewSourceId
|
|
683
|
+
? (sources.find((item) => item.id === builderReviewSourceId) ?? source)
|
|
684
|
+
: source,
|
|
685
|
+
[builderReviewSourceId, sources, source],
|
|
686
|
+
);
|
|
622
687
|
const builderReviewChangeSets = useMemo(
|
|
623
|
-
() => builderReviewableChangeSets(
|
|
624
|
-
[
|
|
688
|
+
() => builderReviewableChangeSets(activeReviewSource),
|
|
689
|
+
[activeReviewSource],
|
|
625
690
|
);
|
|
626
691
|
const builderReviewPreview = useMemo(
|
|
627
692
|
() =>
|
|
628
|
-
|
|
629
|
-
|
|
693
|
+
activeReviewSource?.sourceType === "builder-cms" &&
|
|
694
|
+
builderReviewChangeSets.length > 0
|
|
695
|
+
? buildClientBuilderReviewPayload(
|
|
696
|
+
activeReviewSource,
|
|
697
|
+
builderReviewChangeSets,
|
|
698
|
+
)
|
|
630
699
|
: null,
|
|
631
|
-
[builderReviewChangeSets,
|
|
700
|
+
[builderReviewChangeSets, activeReviewSource],
|
|
632
701
|
);
|
|
633
702
|
const activeBuilderReview = builderReviewResult ?? builderReviewPreview;
|
|
634
703
|
|
|
@@ -937,56 +1006,59 @@ function DatabaseTable({
|
|
|
937
1006
|
updateActiveView((view) => ({ ...view, hideEmptyGroups }));
|
|
938
1007
|
}
|
|
939
1008
|
|
|
940
|
-
async function handleBuilderReviewPush(
|
|
1009
|
+
async function handleBuilderReviewPush(
|
|
1010
|
+
transitions: BuilderReviewPublicationTransitions = {},
|
|
1011
|
+
) {
|
|
941
1012
|
setBuilderReviewResult(null);
|
|
1013
|
+
setBuilderBatchResult(null);
|
|
942
1014
|
setBuilderReviewCheckedAt(null);
|
|
1015
|
+
// Target the SAME source the dialog renders. activeReviewSource falls back
|
|
1016
|
+
// to the primary if the selected source was disconnected/refetched out, so
|
|
1017
|
+
// the backend never receives a stale id that the UI no longer reflects.
|
|
1018
|
+
const targetSourceId =
|
|
1019
|
+
activeReviewSource && activeReviewSource.id !== source?.id
|
|
1020
|
+
? activeReviewSource.id
|
|
1021
|
+
: undefined;
|
|
943
1022
|
try {
|
|
944
1023
|
const prepared = await prepareBuilderReview.mutateAsync({
|
|
945
1024
|
documentId: document.id,
|
|
946
|
-
|
|
1025
|
+
sourceId: targetSourceId,
|
|
947
1026
|
});
|
|
948
1027
|
let nextReview = prepared.review;
|
|
1028
|
+
let batchResult: ExecuteBuilderSourceBatchResponse | null = null;
|
|
949
1029
|
|
|
950
1030
|
if (
|
|
951
1031
|
nextReview.liveWritesEnabled &&
|
|
952
1032
|
nextReview.result.status === "validated"
|
|
953
1033
|
) {
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
);
|
|
973
|
-
if (reviewedChangeSets.length > 0) {
|
|
974
|
-
nextReview = buildClientBuilderReviewPayload(
|
|
975
|
-
executedSource,
|
|
976
|
-
reviewedChangeSets,
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
1034
|
+
const changeSetIds = nextReview.rows.map((row) => row.changeSetId);
|
|
1035
|
+
const scopedTransitions = Object.fromEntries(
|
|
1036
|
+
changeSetIds.flatMap((changeSetId) =>
|
|
1037
|
+
transitions[changeSetId]
|
|
1038
|
+
? [[changeSetId, transitions[changeSetId]]]
|
|
1039
|
+
: [],
|
|
1040
|
+
),
|
|
1041
|
+
);
|
|
1042
|
+
batchResult = await executeBuilderBatch.mutateAsync({
|
|
1043
|
+
documentId: document.id,
|
|
1044
|
+
sourceId: targetSourceId,
|
|
1045
|
+
changeSetIds,
|
|
1046
|
+
transitions:
|
|
1047
|
+
Object.keys(scopedTransitions).length > 0
|
|
1048
|
+
? scopedTransitions
|
|
1049
|
+
: undefined,
|
|
1050
|
+
});
|
|
1051
|
+
setBuilderBatchResult(batchResult);
|
|
980
1052
|
}
|
|
981
1053
|
|
|
982
1054
|
setBuilderReviewResult(nextReview);
|
|
983
1055
|
setBuilderReviewCheckedAt(new Date().toISOString());
|
|
984
1056
|
toast.success(
|
|
985
|
-
|
|
986
|
-
? "Builder
|
|
1057
|
+
builderBatchSucceeded(batchResult)
|
|
1058
|
+
? "Builder updates pushed"
|
|
987
1059
|
: "Builder update checked",
|
|
988
1060
|
{
|
|
989
|
-
description: nextReview
|
|
1061
|
+
description: builderBatchToastMessage(batchResult, nextReview),
|
|
990
1062
|
},
|
|
991
1063
|
);
|
|
992
1064
|
} catch (error) {
|
|
@@ -1021,11 +1093,14 @@ function DatabaseTable({
|
|
|
1021
1093
|
const nextViewConfig = normalizeClientDatabaseViewConfig(
|
|
1022
1094
|
data.database.viewConfig,
|
|
1023
1095
|
);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1096
|
+
const nextKey = databaseViewStateKey(data.database.id, nextViewConfig);
|
|
1097
|
+
if (hydratedViewRef.current === nextKey) return;
|
|
1098
|
+
hydratedViewRef.current = nextKey;
|
|
1099
|
+
setViewConfig((current) =>
|
|
1100
|
+
databaseViewStateKey(data.database.id, current) === nextKey
|
|
1101
|
+
? current
|
|
1102
|
+
: nextViewConfig,
|
|
1027
1103
|
);
|
|
1028
|
-
setViewConfig(nextViewConfig);
|
|
1029
1104
|
}, [data?.database.id, data?.database.viewConfig]);
|
|
1030
1105
|
|
|
1031
1106
|
useEffect(() => {
|
|
@@ -1194,18 +1269,76 @@ function DatabaseTable({
|
|
|
1194
1269
|
) : null}
|
|
1195
1270
|
</Button>
|
|
1196
1271
|
{canEdit ? (
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1272
|
+
sourceTagPicker ? (
|
|
1273
|
+
<DropdownMenu>
|
|
1274
|
+
<DropdownMenuTrigger asChild>
|
|
1275
|
+
<Button
|
|
1276
|
+
type="button"
|
|
1277
|
+
size="sm"
|
|
1278
|
+
className="h-7 rounded-md bg-foreground px-2.5 text-xs font-medium text-background hover:bg-foreground/90"
|
|
1279
|
+
disabled={addItem.isPending || !databaseId}
|
|
1280
|
+
>
|
|
1281
|
+
{addItem.isPending ? (
|
|
1282
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
1283
|
+
) : null}
|
|
1284
|
+
New
|
|
1285
|
+
<IconChevronDown className="ml-1 size-3.5" />
|
|
1286
|
+
</Button>
|
|
1287
|
+
</DropdownMenuTrigger>
|
|
1288
|
+
<DropdownMenuContent align="end" className="w-52">
|
|
1289
|
+
<DropdownMenuLabel>{db("addARowTo")}</DropdownMenuLabel>
|
|
1290
|
+
{sourceTagPicker.collections.map((collection) => (
|
|
1291
|
+
<DropdownMenuItem
|
|
1292
|
+
key={collection.label}
|
|
1293
|
+
onClick={() =>
|
|
1294
|
+
void createRow(
|
|
1295
|
+
"",
|
|
1296
|
+
collection.optionId
|
|
1297
|
+
? {
|
|
1298
|
+
[sourceTagPicker.propertyId]:
|
|
1299
|
+
collection.optionId,
|
|
1300
|
+
}
|
|
1301
|
+
: {},
|
|
1302
|
+
)
|
|
1303
|
+
}
|
|
1304
|
+
>
|
|
1305
|
+
<BuilderLogoMark className="mr-2 size-3.5 shrink-0" />
|
|
1306
|
+
<span className="truncate">{collection.label}</span>
|
|
1307
|
+
</DropdownMenuItem>
|
|
1308
|
+
))}
|
|
1309
|
+
<DropdownMenuSeparator />
|
|
1310
|
+
<DropdownMenuItem
|
|
1311
|
+
onClick={() =>
|
|
1312
|
+
void createRow(
|
|
1313
|
+
"",
|
|
1314
|
+
sourceTagPicker.localOptionId
|
|
1315
|
+
? {
|
|
1316
|
+
[sourceTagPicker.propertyId]:
|
|
1317
|
+
sourceTagPicker.localOptionId,
|
|
1318
|
+
}
|
|
1319
|
+
: {},
|
|
1320
|
+
)
|
|
1321
|
+
}
|
|
1322
|
+
>
|
|
1323
|
+
<IconLayoutGrid className="mr-2 size-3.5 shrink-0" />
|
|
1324
|
+
<span className="truncate">{db("localNoCollection")}</span>
|
|
1325
|
+
</DropdownMenuItem>
|
|
1326
|
+
</DropdownMenuContent>
|
|
1327
|
+
</DropdownMenu>
|
|
1328
|
+
) : (
|
|
1329
|
+
<Button
|
|
1330
|
+
type="button"
|
|
1331
|
+
size="sm"
|
|
1332
|
+
className="h-7 rounded-md bg-foreground px-2.5 text-xs font-medium text-background hover:bg-foreground/90"
|
|
1333
|
+
disabled={addItem.isPending || !databaseId}
|
|
1334
|
+
onClick={() => void createRow()}
|
|
1335
|
+
>
|
|
1336
|
+
{addItem.isPending ? (
|
|
1337
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
1338
|
+
) : null}
|
|
1339
|
+
New
|
|
1340
|
+
</Button>
|
|
1341
|
+
)
|
|
1209
1342
|
) : null}
|
|
1210
1343
|
</div>
|
|
1211
1344
|
</div>
|
|
@@ -1431,14 +1564,18 @@ function DatabaseTable({
|
|
|
1431
1564
|
type="button"
|
|
1432
1565
|
variant="outline"
|
|
1433
1566
|
size="sm"
|
|
1434
|
-
disabled={
|
|
1567
|
+
disabled={isLoadingMoreItems}
|
|
1435
1568
|
onClick={() =>
|
|
1436
|
-
setDatabaseItemLimit(
|
|
1437
|
-
(
|
|
1569
|
+
setDatabaseItemLimit((current) =>
|
|
1570
|
+
Math.min(
|
|
1571
|
+
current + CONTENT_DATABASE_PAGE_SIZE,
|
|
1572
|
+
totalItemCount,
|
|
1573
|
+
CONTENT_DATABASE_MAX_ITEM_LIMIT,
|
|
1574
|
+
),
|
|
1438
1575
|
)
|
|
1439
1576
|
}
|
|
1440
1577
|
>
|
|
1441
|
-
{
|
|
1578
|
+
{isLoadingMoreItems
|
|
1442
1579
|
? "Loading..."
|
|
1443
1580
|
: `Load more rows (${items.length} of ${totalItemCount})`}
|
|
1444
1581
|
</Button>
|
|
@@ -1482,42 +1619,71 @@ function DatabaseTable({
|
|
|
1482
1619
|
groupIds={toolbarGroups.map((group) => group.id)}
|
|
1483
1620
|
onClose={() => setSettingsOpen(false)}
|
|
1484
1621
|
onPanelChange={setSettingsPanel}
|
|
1485
|
-
onAttachBuilderSource={(model) =>
|
|
1486
|
-
attachSource.
|
|
1622
|
+
onAttachBuilderSource={(model, relationshipMode) =>
|
|
1623
|
+
attachSource.mutateAsync({
|
|
1487
1624
|
documentId: document.id,
|
|
1488
1625
|
sourceType: "builder-cms",
|
|
1489
1626
|
sourceName: model.displayName,
|
|
1490
1627
|
sourceTable: model.name,
|
|
1628
|
+
relationshipMode,
|
|
1629
|
+
mode:
|
|
1630
|
+
relationshipMode === "items"
|
|
1631
|
+
? "add"
|
|
1632
|
+
: sources.length > 0 || source
|
|
1633
|
+
? undefined
|
|
1634
|
+
: "replace",
|
|
1491
1635
|
})
|
|
1492
1636
|
}
|
|
1493
1637
|
onFederateSource={(candidate, join) =>
|
|
1494
|
-
attachSource.
|
|
1638
|
+
attachSource.mutateAsync({
|
|
1495
1639
|
documentId: document.id,
|
|
1496
1640
|
sourceType: candidate.sourceType,
|
|
1497
1641
|
sourceName: candidate.sourceName,
|
|
1498
1642
|
sourceTable: candidate.sourceTable,
|
|
1643
|
+
relationshipMode: "details",
|
|
1644
|
+
join,
|
|
1645
|
+
})
|
|
1646
|
+
}
|
|
1647
|
+
onChangeSourceRole={(sourceId, relationshipMode, join) =>
|
|
1648
|
+
changeSourceRole.mutateAsync({
|
|
1649
|
+
documentId: document.id,
|
|
1650
|
+
sourceId,
|
|
1651
|
+
relationshipMode,
|
|
1499
1652
|
join,
|
|
1500
1653
|
})
|
|
1501
1654
|
}
|
|
1502
1655
|
onDisconnectSecondary={(sourceId) =>
|
|
1503
|
-
disconnectSource.mutate(
|
|
1656
|
+
disconnectSource.mutate(
|
|
1657
|
+
{ documentId: document.id, sourceId },
|
|
1658
|
+
{
|
|
1659
|
+
onSuccess: () => {
|
|
1660
|
+
setBuilderReviewSourceId((current) =>
|
|
1661
|
+
current === sourceId ? null : current,
|
|
1662
|
+
);
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
)
|
|
1504
1666
|
}
|
|
1505
|
-
onRefreshSource={() =>
|
|
1667
|
+
onRefreshSource={(sourceId) =>
|
|
1506
1668
|
refreshSource.mutate({
|
|
1507
1669
|
documentId: document.id,
|
|
1670
|
+
sourceId,
|
|
1508
1671
|
})
|
|
1509
1672
|
}
|
|
1510
|
-
onDisconnectSource={() =>
|
|
1673
|
+
onDisconnectSource={(sourceId) =>
|
|
1511
1674
|
disconnectSource.mutate(
|
|
1512
1675
|
{
|
|
1513
1676
|
documentId: document.id,
|
|
1677
|
+
sourceId,
|
|
1514
1678
|
},
|
|
1515
1679
|
{
|
|
1516
1680
|
onSuccess: () => {
|
|
1517
1681
|
setSettingsPanel("source");
|
|
1518
1682
|
setBuilderReviewOpen(false);
|
|
1519
1683
|
setBuilderReviewResult(null);
|
|
1684
|
+
setBuilderBatchResult(null);
|
|
1520
1685
|
setBuilderReviewCheckedAt(null);
|
|
1686
|
+
setBuilderReviewSourceId(null);
|
|
1521
1687
|
toast.success(db("sourceDisconnected"), {
|
|
1522
1688
|
description: db(
|
|
1523
1689
|
"databaseRowsAndLocalPropertiesWereKeptIntact",
|
|
@@ -1533,30 +1699,34 @@ function DatabaseTable({
|
|
|
1533
1699
|
},
|
|
1534
1700
|
)
|
|
1535
1701
|
}
|
|
1536
|
-
onReviewBuilderUpdate={() => {
|
|
1702
|
+
onReviewBuilderUpdate={(sourceId) => {
|
|
1537
1703
|
setBuilderReviewResult(null);
|
|
1704
|
+
setBuilderBatchResult(null);
|
|
1538
1705
|
setBuilderReviewCheckedAt(null);
|
|
1706
|
+
setBuilderReviewSourceId(sourceId ?? null);
|
|
1539
1707
|
setBuilderReviewOpen(true);
|
|
1540
1708
|
}}
|
|
1541
|
-
onSetBuilderLiveWrites={(
|
|
1709
|
+
onSetBuilderLiveWrites={(settings, sourceId) =>
|
|
1542
1710
|
setSourceWriteMode.mutate(
|
|
1543
1711
|
{
|
|
1544
1712
|
documentId: document.id,
|
|
1545
|
-
|
|
1546
|
-
|
|
1713
|
+
sourceId,
|
|
1714
|
+
writeMode: settings.writeMode,
|
|
1715
|
+
allowPublicationTransitions:
|
|
1716
|
+
settings.writeMode === "publish_updates" &&
|
|
1717
|
+
settings.allowPublicationTransitions === true,
|
|
1547
1718
|
},
|
|
1548
1719
|
{
|
|
1549
1720
|
onSuccess: () => {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
);
|
|
1721
|
+
const enabled = settings.writeMode !== "read_only";
|
|
1722
|
+
toast.success(db("builderWriteModeUpdated"), {
|
|
1723
|
+
description:
|
|
1724
|
+
settings.writeMode === "publish_updates"
|
|
1725
|
+
? "Approved updates can write through to Builder while preserving publication state."
|
|
1726
|
+
: enabled
|
|
1727
|
+
? "Approved updates will stage Builder autosave revisions."
|
|
1728
|
+
: "Builder writes are disabled for this source.",
|
|
1729
|
+
});
|
|
1560
1730
|
},
|
|
1561
1731
|
onError: (error) => {
|
|
1562
1732
|
toast.error(db("builderWriteModeWasNotChanged"), {
|
|
@@ -1569,10 +1739,11 @@ function DatabaseTable({
|
|
|
1569
1739
|
}
|
|
1570
1740
|
sourceActionPending={
|
|
1571
1741
|
attachSource.isPending ||
|
|
1742
|
+
changeSourceRole.isPending ||
|
|
1572
1743
|
refreshSource.isPending ||
|
|
1573
1744
|
disconnectSource.isPending ||
|
|
1574
1745
|
prepareBuilderReview.isPending ||
|
|
1575
|
-
|
|
1746
|
+
executeBuilderBatch.isPending ||
|
|
1576
1747
|
setSourceWriteMode.isPending
|
|
1577
1748
|
}
|
|
1578
1749
|
onViewTypeChange={(type) =>
|
|
@@ -1594,14 +1765,15 @@ function DatabaseTable({
|
|
|
1594
1765
|
<BuilderSourceReviewDialog
|
|
1595
1766
|
open={builderReviewOpen}
|
|
1596
1767
|
review={activeBuilderReview}
|
|
1597
|
-
source={
|
|
1768
|
+
source={activeReviewSource}
|
|
1598
1769
|
canEdit={canEdit}
|
|
1599
1770
|
pending={
|
|
1600
|
-
prepareBuilderReview.isPending ||
|
|
1771
|
+
prepareBuilderReview.isPending || executeBuilderBatch.isPending
|
|
1601
1772
|
}
|
|
1773
|
+
batchResult={builderBatchResult}
|
|
1602
1774
|
checkedAt={builderReviewCheckedAt}
|
|
1603
1775
|
onClose={() => setBuilderReviewOpen(false)}
|
|
1604
|
-
onValidate={() => void handleBuilderReviewPush()}
|
|
1776
|
+
onValidate={(transitions) => void handleBuilderReviewPush(transitions)}
|
|
1605
1777
|
/>
|
|
1606
1778
|
|
|
1607
1779
|
{!database.isLoading ? (
|
|
@@ -3109,6 +3281,7 @@ function DatabaseTableView({
|
|
|
3109
3281
|
property={property}
|
|
3110
3282
|
documentId={databaseDocumentId}
|
|
3111
3283
|
source={source}
|
|
3284
|
+
sources={sources}
|
|
3112
3285
|
canEdit={canEdit}
|
|
3113
3286
|
isDragging={draggedPropertyId === property.definition.id}
|
|
3114
3287
|
dropSide={
|
|
@@ -3396,6 +3569,7 @@ type PendingSourceCandidate = {
|
|
|
3396
3569
|
sourceName: string;
|
|
3397
3570
|
sourceTable: string;
|
|
3398
3571
|
displayName: string;
|
|
3572
|
+
existingSourceId?: string;
|
|
3399
3573
|
};
|
|
3400
3574
|
|
|
3401
3575
|
type SourceNavStep =
|
|
@@ -3404,16 +3578,18 @@ type SourceNavStep =
|
|
|
3404
3578
|
| { kind: "model"; model: BuilderCmsModelSummary }
|
|
3405
3579
|
| { kind: "addSource" }
|
|
3406
3580
|
| { kind: "secondarySource"; sourceId: string; sourceName: string }
|
|
3407
|
-
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
3581
|
+
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
3582
|
+
| { kind: "fieldPicker"; sourceId: string; sourceName: string };
|
|
3408
3583
|
|
|
3409
|
-
function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
3584
|
+
function sourceNavTitle(stack: SourceNavStep[], db: DatabaseT): string {
|
|
3410
3585
|
const top = stack[stack.length - 1];
|
|
3411
|
-
if (!top) return "
|
|
3586
|
+
if (!top) return db("sources");
|
|
3412
3587
|
if (top.kind === "provider") return "Builder";
|
|
3413
3588
|
if (top.kind === "space") return top.spaceName;
|
|
3414
|
-
if (top.kind === "addSource") return "
|
|
3589
|
+
if (top.kind === "addSource") return db("addASource");
|
|
3415
3590
|
if (top.kind === "secondarySource") return top.sourceName;
|
|
3416
|
-
if (top.kind === "keyConfirm") return "
|
|
3591
|
+
if (top.kind === "keyConfirm") return db("matchExistingItemsToDetails");
|
|
3592
|
+
if (top.kind === "fieldPicker") return db("chooseFields");
|
|
3417
3593
|
return top.model.displayName;
|
|
3418
3594
|
}
|
|
3419
3595
|
|
|
@@ -3473,6 +3649,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3473
3649
|
onPanelChange,
|
|
3474
3650
|
onAttachBuilderSource,
|
|
3475
3651
|
onFederateSource,
|
|
3652
|
+
onChangeSourceRole,
|
|
3476
3653
|
onDisconnectSecondary,
|
|
3477
3654
|
onRefreshSource,
|
|
3478
3655
|
onDisconnectSource,
|
|
@@ -3501,16 +3678,27 @@ function DatabaseSettingsPanelSheet({
|
|
|
3501
3678
|
groupIds: string[];
|
|
3502
3679
|
onClose: () => void;
|
|
3503
3680
|
onPanelChange: (panel: DatabaseSettingsPanel) => void;
|
|
3504
|
-
onAttachBuilderSource: (
|
|
3681
|
+
onAttachBuilderSource: (
|
|
3682
|
+
model: BuilderCmsModelSummary,
|
|
3683
|
+
relationshipMode?: "items" | "details",
|
|
3684
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3505
3685
|
onFederateSource: (
|
|
3506
3686
|
candidate: PendingSourceCandidate,
|
|
3507
3687
|
join: ContentDatabaseSourceJoinRequest,
|
|
3508
|
-
) =>
|
|
3688
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3689
|
+
onChangeSourceRole: (
|
|
3690
|
+
sourceId: string,
|
|
3691
|
+
relationshipMode: "items" | "details",
|
|
3692
|
+
join?: ContentDatabaseSourceJoinRequest,
|
|
3693
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3509
3694
|
onDisconnectSecondary: (sourceId: string) => void;
|
|
3510
|
-
onRefreshSource: () => void;
|
|
3511
|
-
onDisconnectSource: () => void;
|
|
3512
|
-
onReviewBuilderUpdate: () => void;
|
|
3513
|
-
onSetBuilderLiveWrites: (
|
|
3695
|
+
onRefreshSource: (sourceId?: string) => void;
|
|
3696
|
+
onDisconnectSource: (sourceId?: string) => void;
|
|
3697
|
+
onReviewBuilderUpdate: (sourceId?: string) => void;
|
|
3698
|
+
onSetBuilderLiveWrites: (
|
|
3699
|
+
settings: BuilderSourceWriteSettingsInput,
|
|
3700
|
+
sourceId?: string,
|
|
3701
|
+
) => void;
|
|
3514
3702
|
sourceActionPending: boolean;
|
|
3515
3703
|
onViewTypeChange: (type: ContentDatabaseViewType) => void;
|
|
3516
3704
|
onWrapCellsChange: (wrapCells: boolean) => void;
|
|
@@ -3538,7 +3726,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3538
3726
|
panel === "main"
|
|
3539
3727
|
? "Database settings"
|
|
3540
3728
|
: panel === "source"
|
|
3541
|
-
? sourceNavTitle(sourceNavStack)
|
|
3729
|
+
? sourceNavTitle(sourceNavStack, db)
|
|
3542
3730
|
: databaseSettingsPanelTitle(panel);
|
|
3543
3731
|
|
|
3544
3732
|
const handleBack = () => {
|
|
@@ -3583,6 +3771,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3583
3771
|
<DatabaseSettingsMainPanel
|
|
3584
3772
|
activeView={activeView}
|
|
3585
3773
|
source={source}
|
|
3774
|
+
sources={sources}
|
|
3586
3775
|
sourceCount={sources.length || (source ? 1 : 0)}
|
|
3587
3776
|
propertyCount={properties.length}
|
|
3588
3777
|
hiddenCount={hiddenCount}
|
|
@@ -3597,11 +3786,10 @@ function DatabaseSettingsPanelSheet({
|
|
|
3597
3786
|
canEdit={canEdit}
|
|
3598
3787
|
nav={sourceNavStack}
|
|
3599
3788
|
onNavPush={(step) => setSourceNavStack((stack) => [...stack, step])}
|
|
3789
|
+
onNavReplace={setSourceNavStack}
|
|
3600
3790
|
onAttachBuilderSource={onAttachBuilderSource}
|
|
3601
|
-
onFederateSource={
|
|
3602
|
-
|
|
3603
|
-
setSourceNavStack([]);
|
|
3604
|
-
}}
|
|
3791
|
+
onFederateSource={onFederateSource}
|
|
3792
|
+
onChangeSourceRole={onChangeSourceRole}
|
|
3605
3793
|
onDisconnectSecondary={(sourceId) => {
|
|
3606
3794
|
onDisconnectSecondary(sourceId);
|
|
3607
3795
|
setSourceNavStack([]);
|
|
@@ -3657,6 +3845,7 @@ function databaseSettingsPanelTitle(panel: DatabaseSettingsPanel) {
|
|
|
3657
3845
|
function DatabaseSettingsMainPanel({
|
|
3658
3846
|
activeView,
|
|
3659
3847
|
source,
|
|
3848
|
+
sources,
|
|
3660
3849
|
sourceCount,
|
|
3661
3850
|
propertyCount,
|
|
3662
3851
|
hiddenCount,
|
|
@@ -3664,6 +3853,7 @@ function DatabaseSettingsMainPanel({
|
|
|
3664
3853
|
}: {
|
|
3665
3854
|
activeView: ContentDatabaseView;
|
|
3666
3855
|
source: ContentDatabaseSource | null;
|
|
3856
|
+
sources: ContentDatabaseSource[];
|
|
3667
3857
|
sourceCount: number;
|
|
3668
3858
|
propertyCount: number;
|
|
3669
3859
|
hiddenCount: number;
|
|
@@ -3671,7 +3861,14 @@ function DatabaseSettingsMainPanel({
|
|
|
3671
3861
|
}) {
|
|
3672
3862
|
const db = useDatabaseT();
|
|
3673
3863
|
const groupLabel = activeView.groupByPropertyId ? "On" : "";
|
|
3674
|
-
|
|
3864
|
+
// Sum reviewable changes across EVERY source (row-union) so the collapsed
|
|
3865
|
+
// Sources row badges pending pushes even when they're only on a secondary.
|
|
3866
|
+
const sourceBadgeCount = (
|
|
3867
|
+
sources.length > 0 ? sources : source ? [source] : []
|
|
3868
|
+
).reduce(
|
|
3869
|
+
(total, item) => total + builderReviewableChangeSets(item).length,
|
|
3870
|
+
0,
|
|
3871
|
+
);
|
|
3675
3872
|
return (
|
|
3676
3873
|
<div className="grid gap-3">
|
|
3677
3874
|
<div className="flex h-9 items-center gap-2 rounded-md border border-border bg-background px-2">
|
|
@@ -3758,28 +3955,94 @@ export function builderReviewExecutableRows(
|
|
|
3758
3955
|
);
|
|
3759
3956
|
}
|
|
3760
3957
|
|
|
3958
|
+
function builderBatchSucceeded(
|
|
3959
|
+
result: ExecuteBuilderSourceBatchResponse | null,
|
|
3960
|
+
) {
|
|
3961
|
+
return (
|
|
3962
|
+
!!result &&
|
|
3963
|
+
result.summary.total > 0 &&
|
|
3964
|
+
result.summary.blocked === 0 &&
|
|
3965
|
+
result.summary.failed === 0
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
function builderBatchToastMessage(
|
|
3970
|
+
result: ExecuteBuilderSourceBatchResponse | null,
|
|
3971
|
+
review: ContentDatabaseSourceReviewPayload,
|
|
3972
|
+
) {
|
|
3973
|
+
if (!result) return review.result.message;
|
|
3974
|
+
const { succeeded, blocked, failed } = result.summary;
|
|
3975
|
+
return `${succeeded} succeeded, ${blocked} blocked, ${failed} failed.`;
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3761
3978
|
export function builderSourceLiveWriteControlState(
|
|
3762
3979
|
source: ContentDatabaseSource | null,
|
|
3763
3980
|
) {
|
|
3764
3981
|
const isBuilderSource = source?.sourceType === "builder-cms";
|
|
3765
3982
|
const safeTarget =
|
|
3766
3983
|
isBuilderSource && source?.sourceTable === BUILDER_CMS_SAFE_WRITE_MODEL;
|
|
3767
|
-
const
|
|
3984
|
+
const legacyAllowedWriteModes = source?.metadata.allowedWriteModes ?? [];
|
|
3985
|
+
const writeMode =
|
|
3986
|
+
source?.metadata.writeMode ??
|
|
3987
|
+
(source?.capabilities.liveWritesEnabled === true
|
|
3988
|
+
? legacyAllowedWriteModes.some((mode) => mode !== "autosave")
|
|
3989
|
+
? "publish_updates"
|
|
3990
|
+
: "stage_only"
|
|
3991
|
+
: "read_only");
|
|
3992
|
+
const enabled = writeMode !== "read_only";
|
|
3768
3993
|
return {
|
|
3769
3994
|
safeTarget,
|
|
3770
3995
|
enabled,
|
|
3996
|
+
writeMode,
|
|
3997
|
+
allowPublicationTransitions:
|
|
3998
|
+
writeMode === "publish_updates" &&
|
|
3999
|
+
source?.metadata.allowPublicationTransitions === true,
|
|
3771
4000
|
showAction: safeTarget,
|
|
3772
4001
|
actionLabel: enabled ? "Disable" : "Enable",
|
|
3773
4002
|
description: enabled
|
|
3774
|
-
?
|
|
4003
|
+
? writeMode === "publish_updates"
|
|
4004
|
+
? "Approved updates can write through to Builder while preserving publication state."
|
|
4005
|
+
: "Enabled for autosave writes to the Agent Native test collection."
|
|
3775
4006
|
: safeTarget
|
|
3776
|
-
? "Off by default.
|
|
4007
|
+
? "Off by default. Choose a write tier only when this source should send guarded Builder writes."
|
|
3777
4008
|
: isBuilderSource
|
|
3778
4009
|
? "Unavailable here; live writes are locked to the Agent Native test collection."
|
|
3779
4010
|
: "Live writes are not available for this source.",
|
|
3780
4011
|
};
|
|
3781
4012
|
}
|
|
3782
4013
|
|
|
4014
|
+
const BUILDER_WRITE_MODE_OPTIONS: Array<{
|
|
4015
|
+
mode: ContentDatabaseSourceWriteMode;
|
|
4016
|
+
labelKey: string;
|
|
4017
|
+
descriptionKey: string;
|
|
4018
|
+
}> = [
|
|
4019
|
+
{
|
|
4020
|
+
mode: "read_only",
|
|
4021
|
+
labelKey: "readOnly",
|
|
4022
|
+
descriptionKey: "noBuilderWrites",
|
|
4023
|
+
},
|
|
4024
|
+
{
|
|
4025
|
+
mode: "stage_only",
|
|
4026
|
+
labelKey: "stageOnly",
|
|
4027
|
+
descriptionKey: "savesDraftsNeverPublishes",
|
|
4028
|
+
},
|
|
4029
|
+
{
|
|
4030
|
+
mode: "publish_updates",
|
|
4031
|
+
labelKey: "publishUpdates",
|
|
4032
|
+
descriptionKey: "writesUpdatesToLiveEntries",
|
|
4033
|
+
},
|
|
4034
|
+
];
|
|
4035
|
+
|
|
4036
|
+
function builderWriteModeSummary(
|
|
4037
|
+
mode: ContentDatabaseSourceWriteMode,
|
|
4038
|
+
db: DatabaseT,
|
|
4039
|
+
) {
|
|
4040
|
+
const option = BUILDER_WRITE_MODE_OPTIONS.find(
|
|
4041
|
+
(candidate) => candidate.mode === mode,
|
|
4042
|
+
);
|
|
4043
|
+
return db(option?.descriptionKey ?? "noBuilderWrites");
|
|
4044
|
+
}
|
|
4045
|
+
|
|
3783
4046
|
export function buildClientBuilderReviewPayload(
|
|
3784
4047
|
source: ContentDatabaseSource,
|
|
3785
4048
|
changeSets: ContentDatabaseSourceChangeSet[],
|
|
@@ -3819,6 +4082,7 @@ export function buildClientBuilderReviewPayload(
|
|
|
3819
4082
|
riskLevel: changeSet.riskLevel,
|
|
3820
4083
|
riskReasons: changeSet.riskReasons,
|
|
3821
4084
|
conflictState: changeSet.conflictState,
|
|
4085
|
+
effect: resolveBuilderCmsWriteEffect({ source, changeSet }),
|
|
3822
4086
|
execution: latestExecution,
|
|
3823
4087
|
};
|
|
3824
4088
|
});
|
|
@@ -3900,8 +4164,10 @@ function DatabaseSettingsSourcePanel({
|
|
|
3900
4164
|
canEdit,
|
|
3901
4165
|
nav,
|
|
3902
4166
|
onNavPush,
|
|
4167
|
+
onNavReplace,
|
|
3903
4168
|
onAttachBuilderSource,
|
|
3904
4169
|
onFederateSource,
|
|
4170
|
+
onChangeSourceRole,
|
|
3905
4171
|
onDisconnectSecondary,
|
|
3906
4172
|
onRefreshSource,
|
|
3907
4173
|
onDisconnectSource,
|
|
@@ -3916,33 +4182,31 @@ function DatabaseSettingsSourcePanel({
|
|
|
3916
4182
|
canEdit: boolean;
|
|
3917
4183
|
nav: SourceNavStep[];
|
|
3918
4184
|
onNavPush: (step: SourceNavStep) => void;
|
|
3919
|
-
|
|
4185
|
+
onNavReplace: (stack: SourceNavStep[]) => void;
|
|
4186
|
+
onAttachBuilderSource: (
|
|
4187
|
+
model: BuilderCmsModelSummary,
|
|
4188
|
+
relationshipMode?: "items" | "details",
|
|
4189
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3920
4190
|
onFederateSource: (
|
|
3921
4191
|
candidate: PendingSourceCandidate,
|
|
3922
4192
|
join: ContentDatabaseSourceJoinRequest,
|
|
3923
|
-
) =>
|
|
4193
|
+
) => Promise<ContentDatabaseResponse>;
|
|
4194
|
+
onChangeSourceRole: (
|
|
4195
|
+
sourceId: string,
|
|
4196
|
+
relationshipMode: "items" | "details",
|
|
4197
|
+
join?: ContentDatabaseSourceJoinRequest,
|
|
4198
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3924
4199
|
onDisconnectSecondary: (sourceId: string) => void;
|
|
3925
|
-
onRefreshSource: () => void;
|
|
3926
|
-
onDisconnectSource: () => void;
|
|
3927
|
-
onReviewBuilderUpdate: () => void;
|
|
3928
|
-
onSetBuilderLiveWrites: (
|
|
4200
|
+
onRefreshSource: (sourceId?: string) => void;
|
|
4201
|
+
onDisconnectSource: (sourceId?: string) => void;
|
|
4202
|
+
onReviewBuilderUpdate: (sourceId?: string) => void;
|
|
4203
|
+
onSetBuilderLiveWrites: (
|
|
4204
|
+
settings: BuilderSourceWriteSettingsInput,
|
|
4205
|
+
sourceId?: string,
|
|
4206
|
+
) => void;
|
|
3929
4207
|
sourceActionPending: boolean;
|
|
3930
4208
|
}) {
|
|
3931
4209
|
const db = useDatabaseT();
|
|
3932
|
-
const outboundChangeSets =
|
|
3933
|
-
source?.changeSets.filter(
|
|
3934
|
-
(changeSet) => changeSet.direction === "outbound",
|
|
3935
|
-
) ?? [];
|
|
3936
|
-
const reviewableBuilderChangeSets = outboundChangeSets.filter(
|
|
3937
|
-
(changeSet) =>
|
|
3938
|
-
changeSet.state === "pending_push" ||
|
|
3939
|
-
changeSet.state === "staged_revision" ||
|
|
3940
|
-
changeSet.state === "approved",
|
|
3941
|
-
);
|
|
3942
|
-
const conflictChangeSets =
|
|
3943
|
-
source?.changeSets.filter(
|
|
3944
|
-
(changeSet) => changeSet.conflictState === "source_changed",
|
|
3945
|
-
) ?? [];
|
|
3946
4210
|
const { isCodeMode } = useCodeMode();
|
|
3947
4211
|
const isBuilderSource = source?.sourceType === "builder-cms";
|
|
3948
4212
|
const builderStatus = useBuilderStatus();
|
|
@@ -3963,33 +4227,42 @@ function DatabaseSettingsSourcePanel({
|
|
|
3963
4227
|
void builderStatus.refetch();
|
|
3964
4228
|
},
|
|
3965
4229
|
});
|
|
3966
|
-
const builderSyncFailed =
|
|
3967
|
-
isBuilderSource &&
|
|
3968
|
-
(source?.syncState === "error" || Boolean(source?.lastError));
|
|
3969
|
-
|
|
3970
4230
|
// Auto-sync: the manual Refresh button is gone, so pull the read-only
|
|
3971
4231
|
// snapshot when the panel opens and whenever the window regains focus.
|
|
3972
4232
|
// Throttled so rapid focus changes don't hammer Builder; the refresh
|
|
3973
4233
|
// mutation is silent (no toast), so this stays quiet in the background.
|
|
3974
4234
|
const refreshSourceRef = useRef(onRefreshSource);
|
|
3975
4235
|
refreshSourceRef.current = onRefreshSource;
|
|
3976
|
-
const lastAutoSyncRef = useRef(0);
|
|
4236
|
+
const lastAutoSyncRef = useRef<{ at: number; sourceId?: string }>({ at: 0 });
|
|
3977
4237
|
const autoSyncEnabled = Boolean(source) && isBuilderSource && canEdit;
|
|
4238
|
+
const top = nav[nav.length - 1];
|
|
4239
|
+
// When a non-primary Builder source's leaf is open, auto-sync should refresh
|
|
4240
|
+
// THAT source, not the primary. At the root/list (no model leaf) we fall back
|
|
4241
|
+
// to the primary (undefined ⇒ primary in the refresh action).
|
|
4242
|
+
const autoSyncSourceId =
|
|
4243
|
+
top?.kind === "model"
|
|
4244
|
+
? sources.find(
|
|
4245
|
+
(item) =>
|
|
4246
|
+
item.sourceType === "builder-cms" &&
|
|
4247
|
+
item.sourceTable === top.model.name,
|
|
4248
|
+
)?.id
|
|
4249
|
+
: undefined;
|
|
3978
4250
|
useEffect(() => {
|
|
3979
4251
|
if (!autoSyncEnabled) return;
|
|
3980
4252
|
const maybeSync = () => {
|
|
3981
4253
|
const now = Date.now();
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
4254
|
+
const last = lastAutoSyncRef.current;
|
|
4255
|
+
// Throttle repeated syncs of the SAME source, but always sync when the
|
|
4256
|
+
// viewed source changes (so opening a secondary leaf refreshes it).
|
|
4257
|
+
if (last.sourceId === autoSyncSourceId && now - last.at < 15_000) return;
|
|
4258
|
+
lastAutoSyncRef.current = { at: now, sourceId: autoSyncSourceId };
|
|
4259
|
+
refreshSourceRef.current(autoSyncSourceId);
|
|
3985
4260
|
};
|
|
3986
4261
|
maybeSync();
|
|
3987
4262
|
const onFocus = () => maybeSync();
|
|
3988
4263
|
window.addEventListener("focus", onFocus);
|
|
3989
4264
|
return () => window.removeEventListener("focus", onFocus);
|
|
3990
|
-
}, [autoSyncEnabled]);
|
|
3991
|
-
|
|
3992
|
-
const top = nav[nav.length - 1];
|
|
4265
|
+
}, [autoSyncEnabled, autoSyncSourceId]);
|
|
3993
4266
|
|
|
3994
4267
|
// ── Sources list (root) ───────────────────────────────────────────────
|
|
3995
4268
|
if (!top) {
|
|
@@ -3997,11 +4270,11 @@ function DatabaseSettingsSourcePanel({
|
|
|
3997
4270
|
<SourcesListView
|
|
3998
4271
|
source={source}
|
|
3999
4272
|
sources={sources}
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4273
|
+
onOpenConnectedBuilder={(connected) =>
|
|
4274
|
+
onNavPush({
|
|
4275
|
+
kind: "model",
|
|
4276
|
+
model: builderModelSummaryFromSource(connected),
|
|
4277
|
+
})
|
|
4005
4278
|
}
|
|
4006
4279
|
onOpenSecondary={(secondary) =>
|
|
4007
4280
|
onNavPush({
|
|
@@ -4026,6 +4299,11 @@ function DatabaseSettingsSourcePanel({
|
|
|
4026
4299
|
.map((item) => item.sourceTable),
|
|
4027
4300
|
]}
|
|
4028
4301
|
canEdit={canEdit}
|
|
4302
|
+
builderConfigured={builderConfigured}
|
|
4303
|
+
builderSpaceLabel={builderSpaceLabel}
|
|
4304
|
+
onOpenBuilder={() =>
|
|
4305
|
+
onNavPush({ kind: "provider", providerId: "builder" })
|
|
4306
|
+
}
|
|
4029
4307
|
onPickLocalTable={(table) =>
|
|
4030
4308
|
onNavPush({
|
|
4031
4309
|
kind: "keyConfirm",
|
|
@@ -4049,6 +4327,34 @@ function DatabaseSettingsSourcePanel({
|
|
|
4049
4327
|
source={secondary}
|
|
4050
4328
|
canEdit={canEdit}
|
|
4051
4329
|
pending={sourceActionPending}
|
|
4330
|
+
onAddDetails={() =>
|
|
4331
|
+
secondary
|
|
4332
|
+
? onNavPush({
|
|
4333
|
+
kind: "keyConfirm",
|
|
4334
|
+
candidate: {
|
|
4335
|
+
sourceType: secondary.sourceType,
|
|
4336
|
+
sourceName: secondary.sourceName,
|
|
4337
|
+
sourceTable: secondary.sourceTable,
|
|
4338
|
+
displayName: secondary.sourceName,
|
|
4339
|
+
existingSourceId: secondary.id,
|
|
4340
|
+
},
|
|
4341
|
+
})
|
|
4342
|
+
: undefined
|
|
4343
|
+
}
|
|
4344
|
+
onAddItems={async () => {
|
|
4345
|
+
if (!secondary) return;
|
|
4346
|
+
await onChangeSourceRole(secondary.id, "items");
|
|
4347
|
+
onNavReplace([]);
|
|
4348
|
+
}}
|
|
4349
|
+
onChooseFields={() =>
|
|
4350
|
+
secondary
|
|
4351
|
+
? onNavPush({
|
|
4352
|
+
kind: "fieldPicker",
|
|
4353
|
+
sourceId: secondary.id,
|
|
4354
|
+
sourceName: secondary.sourceName,
|
|
4355
|
+
})
|
|
4356
|
+
: undefined
|
|
4357
|
+
}
|
|
4052
4358
|
onDisconnect={() => onDisconnectSecondary(top.sourceId)}
|
|
4053
4359
|
/>
|
|
4054
4360
|
);
|
|
@@ -4062,7 +4368,41 @@ function DatabaseSettingsSourcePanel({
|
|
|
4062
4368
|
candidate={top.candidate}
|
|
4063
4369
|
canEdit={canEdit}
|
|
4064
4370
|
pending={sourceActionPending}
|
|
4065
|
-
onCommit={(join) =>
|
|
4371
|
+
onCommit={async (join) => {
|
|
4372
|
+
const result = top.candidate.existingSourceId
|
|
4373
|
+
? await onChangeSourceRole(
|
|
4374
|
+
top.candidate.existingSourceId,
|
|
4375
|
+
"details",
|
|
4376
|
+
join,
|
|
4377
|
+
)
|
|
4378
|
+
: await onFederateSource(top.candidate, join);
|
|
4379
|
+
const detailsSource = findDetailsSource(result, top.candidate);
|
|
4380
|
+
onNavReplace(
|
|
4381
|
+
detailsSource
|
|
4382
|
+
? [
|
|
4383
|
+
{
|
|
4384
|
+
kind: "fieldPicker",
|
|
4385
|
+
sourceId: detailsSource.id,
|
|
4386
|
+
sourceName: detailsSource.sourceName,
|
|
4387
|
+
},
|
|
4388
|
+
]
|
|
4389
|
+
: [],
|
|
4390
|
+
);
|
|
4391
|
+
}}
|
|
4392
|
+
/>
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
if (top.kind === "fieldPicker") {
|
|
4397
|
+
const pickerSource =
|
|
4398
|
+
sources.find((item) => item.id === top.sourceId) ?? null;
|
|
4399
|
+
return (
|
|
4400
|
+
<SourceDetailsFieldPicker
|
|
4401
|
+
documentId={documentId}
|
|
4402
|
+
source={pickerSource}
|
|
4403
|
+
canEdit={canEdit}
|
|
4404
|
+
pending={sourceActionPending}
|
|
4405
|
+
onDone={() => onNavReplace([])}
|
|
4066
4406
|
/>
|
|
4067
4407
|
);
|
|
4068
4408
|
}
|
|
@@ -4127,9 +4467,9 @@ function DatabaseSettingsSourcePanel({
|
|
|
4127
4467
|
if (top.kind === "space") {
|
|
4128
4468
|
return (
|
|
4129
4469
|
<BuilderSpaceModelsView
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4470
|
+
attachedModelNames={sources
|
|
4471
|
+
.filter((item) => item.sourceType === "builder-cms")
|
|
4472
|
+
.map((item) => item.sourceTable)}
|
|
4133
4473
|
onOpenModel={(model) => onNavPush({ kind: "model", model })}
|
|
4134
4474
|
/>
|
|
4135
4475
|
);
|
|
@@ -4137,12 +4477,21 @@ function DatabaseSettingsSourcePanel({
|
|
|
4137
4477
|
|
|
4138
4478
|
// ── Model leaf ────────────────────────────────────────────────────────
|
|
4139
4479
|
const model = top.model;
|
|
4140
|
-
|
|
4141
|
-
|
|
4480
|
+
// Resolve the source this leaf operates on by the model being viewed, NOT by
|
|
4481
|
+
// assuming the primary — a row-union database has multiple writable Builder
|
|
4482
|
+
// sources, and opening any of them must land on its own writable leaf.
|
|
4483
|
+
// Collection names are unique per database (duplicate-attach is guarded), so
|
|
4484
|
+
// matching on sourceTable is unambiguous.
|
|
4485
|
+
const leafSource =
|
|
4486
|
+
sources.find(
|
|
4487
|
+
(item) =>
|
|
4488
|
+
item.sourceType === "builder-cms" && item.sourceTable === model.name,
|
|
4489
|
+
) ?? null;
|
|
4490
|
+
const isAttachedModel = Boolean(leafSource);
|
|
4142
4491
|
|
|
4143
4492
|
// Unattached model → the attach affordance (the model is already chosen by
|
|
4144
4493
|
// drilling in, so there's no model picker here).
|
|
4145
|
-
if (!isAttachedModel || !
|
|
4494
|
+
if (!isAttachedModel || !leafSource) {
|
|
4146
4495
|
return (
|
|
4147
4496
|
<div className="grid min-w-0 gap-3">
|
|
4148
4497
|
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
@@ -4161,148 +4510,318 @@ function DatabaseSettingsSourcePanel({
|
|
|
4161
4510
|
</span>
|
|
4162
4511
|
</div>
|
|
4163
4512
|
</div>
|
|
4164
|
-
|
|
4165
|
-
<
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
)
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4513
|
+
{sources.length > 0 || source ? (
|
|
4514
|
+
<SourceRelationshipChoice
|
|
4515
|
+
documentId={documentId}
|
|
4516
|
+
candidate={{
|
|
4517
|
+
sourceType: "builder-cms",
|
|
4518
|
+
sourceName: model.displayName,
|
|
4519
|
+
sourceTable: model.name,
|
|
4520
|
+
displayName: model.displayName,
|
|
4521
|
+
}}
|
|
4522
|
+
canEdit={canEdit}
|
|
4523
|
+
pending={sourceActionPending}
|
|
4524
|
+
onAddDetails={() =>
|
|
4525
|
+
onNavPush({
|
|
4526
|
+
kind: "keyConfirm",
|
|
4527
|
+
candidate: {
|
|
4528
|
+
sourceType: "builder-cms",
|
|
4529
|
+
sourceName: model.displayName,
|
|
4530
|
+
sourceTable: model.name,
|
|
4531
|
+
displayName: model.displayName,
|
|
4532
|
+
},
|
|
4533
|
+
})
|
|
4534
|
+
}
|
|
4535
|
+
onAddItems={async () => {
|
|
4536
|
+
await onAttachBuilderSource(model, "items");
|
|
4537
|
+
onNavReplace([]);
|
|
4538
|
+
}}
|
|
4539
|
+
/>
|
|
4540
|
+
) : (
|
|
4541
|
+
<div>
|
|
4542
|
+
<Button
|
|
4543
|
+
type="button"
|
|
4544
|
+
size="sm"
|
|
4545
|
+
disabled={!canEdit || sourceActionPending}
|
|
4546
|
+
onClick={async () => {
|
|
4547
|
+
await onAttachBuilderSource(model);
|
|
4548
|
+
onNavReplace([]);
|
|
4549
|
+
}}
|
|
4550
|
+
>
|
|
4551
|
+
{sourceActionPending ? (
|
|
4552
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
4553
|
+
) : (
|
|
4554
|
+
<IconPlugConnected className="mr-1.5 size-3.5" />
|
|
4555
|
+
)}
|
|
4556
|
+
Attach
|
|
4557
|
+
</Button>
|
|
4558
|
+
</div>
|
|
4559
|
+
)}
|
|
4179
4560
|
</div>
|
|
4180
4561
|
);
|
|
4181
4562
|
}
|
|
4182
4563
|
|
|
4183
|
-
// Attached model → the
|
|
4564
|
+
// Attached model → the writable leaf panel for THIS source (primary or a
|
|
4565
|
+
// row-union secondary). All derived state below is scoped to `leafSource`,
|
|
4566
|
+
// never the component-level primary `source`.
|
|
4567
|
+
const liveWriteControl = builderSourceLiveWriteControlState(leafSource);
|
|
4568
|
+
const builderLiveWritesDisabled = !canEdit || sourceActionPending;
|
|
4569
|
+
const builderWriteMode = liveWriteControl.writeMode;
|
|
4570
|
+
const leafOutboundChangeSets = leafSource.changeSets.filter(
|
|
4571
|
+
(changeSet) => changeSet.direction === "outbound",
|
|
4572
|
+
);
|
|
4573
|
+
const leafReviewableChangeSets = leafOutboundChangeSets.filter(
|
|
4574
|
+
(changeSet) =>
|
|
4575
|
+
changeSet.state === "pending_push" ||
|
|
4576
|
+
changeSet.state === "staged_revision" ||
|
|
4577
|
+
changeSet.state === "approved",
|
|
4578
|
+
);
|
|
4579
|
+
const leafPendingChangeSets = leafOutboundChangeSets.filter(
|
|
4580
|
+
(changeSet) => changeSet.state !== "applied",
|
|
4581
|
+
);
|
|
4582
|
+
const leafAppliedChangeSets = leafOutboundChangeSets.filter(
|
|
4583
|
+
(changeSet) => changeSet.state === "applied",
|
|
4584
|
+
);
|
|
4585
|
+
const leafSyncFailed =
|
|
4586
|
+
leafSource.syncState === "error" || Boolean(leafSource.lastError);
|
|
4587
|
+
|
|
4184
4588
|
return (
|
|
4185
4589
|
<div className="grid min-w-0 gap-4">
|
|
4186
4590
|
<>
|
|
4187
4591
|
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
4188
4592
|
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
4189
|
-
<span
|
|
4190
|
-
|
|
4593
|
+
<span
|
|
4594
|
+
className="truncate font-medium"
|
|
4595
|
+
title={leafSource.sourceName}
|
|
4596
|
+
>
|
|
4597
|
+
{leafSource.sourceName}
|
|
4191
4598
|
</span>
|
|
4192
|
-
{
|
|
4193
|
-
|
|
4194
|
-
<
|
|
4195
|
-
|
|
4196
|
-
<DatabaseText k="liveWritesOn" />
|
|
4197
|
-
</span>
|
|
4198
|
-
) : (
|
|
4199
|
-
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
4200
|
-
<IconLock className="size-3" />
|
|
4201
|
-
<DatabaseText k="readOnly" />
|
|
4202
|
-
</span>
|
|
4203
|
-
)
|
|
4204
|
-
) : (
|
|
4205
|
-
<span className="shrink-0 rounded-full border border-border px-2 py-0.5 text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
4206
|
-
{source.syncState}
|
|
4599
|
+
{!liveWriteControl.safeTarget ? (
|
|
4600
|
+
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
4601
|
+
<IconLock className="size-3" />
|
|
4602
|
+
<DatabaseText k="readOnly" />
|
|
4207
4603
|
</span>
|
|
4208
|
-
)}
|
|
4604
|
+
) : null}
|
|
4209
4605
|
</div>
|
|
4606
|
+
{!liveWriteControl.safeTarget ? (
|
|
4607
|
+
<div className="text-[11px] text-muted-foreground">
|
|
4608
|
+
{db("liveWritesTestCollectionOnly")}
|
|
4609
|
+
</div>
|
|
4610
|
+
) : null}
|
|
4210
4611
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
4211
|
-
{
|
|
4612
|
+
{leafSyncFailed ? (
|
|
4212
4613
|
<button
|
|
4213
4614
|
type="button"
|
|
4214
4615
|
className="inline-flex items-center gap-1 text-destructive hover:underline disabled:opacity-60"
|
|
4215
4616
|
disabled={!canEdit || sourceActionPending}
|
|
4216
|
-
onClick={onRefreshSource}
|
|
4617
|
+
onClick={() => onRefreshSource(leafSource.id)}
|
|
4217
4618
|
>
|
|
4218
4619
|
<IconRefresh className="size-3" />
|
|
4219
4620
|
<DatabaseText k="couldntSyncRetry" />
|
|
4220
4621
|
</button>
|
|
4221
|
-
) :
|
|
4622
|
+
) : (
|
|
4222
4623
|
[
|
|
4223
4624
|
builderConfigured ? (builderSpaceLabel ?? "Connected") : null,
|
|
4224
|
-
|
|
4625
|
+
leafSource.lastRefreshedAt
|
|
4225
4626
|
? `synced ${
|
|
4226
|
-
formatRelativeSyncTime(
|
|
4227
|
-
|
|
4627
|
+
formatRelativeSyncTime(leafSource.lastRefreshedAt) ??
|
|
4628
|
+
leafSource.freshness
|
|
4228
4629
|
}`
|
|
4229
|
-
:
|
|
4630
|
+
: leafSource.freshness,
|
|
4230
4631
|
]
|
|
4231
4632
|
.filter(Boolean)
|
|
4232
4633
|
.join(" · ")
|
|
4233
|
-
) : (
|
|
4234
|
-
`Local snapshot · ${source.freshness}`
|
|
4235
4634
|
)}
|
|
4236
4635
|
</div>
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
<div className="text-sm font-medium">
|
|
4245
|
-
{conflictChangeSets.length > 0
|
|
4246
|
-
? `${conflictChangeSets.length} change${
|
|
4247
|
-
conflictChangeSets.length === 1 ? "" : "s"
|
|
4248
|
-
} need review`
|
|
4249
|
-
: `${reviewableBuilderChangeSets.length} change${
|
|
4250
|
-
reviewableBuilderChangeSets.length === 1 ? "" : "s"
|
|
4251
|
-
} ready to push`}
|
|
4252
|
-
</div>
|
|
4253
|
-
<div className="mt-0.5 break-words text-xs text-muted-foreground">
|
|
4254
|
-
<DatabaseText k="reviewBeforeTheyReachBuilder" />
|
|
4255
|
-
</div>
|
|
4636
|
+
{liveWriteControl.safeTarget ? (
|
|
4637
|
+
<div className="grid min-w-0 gap-2 text-xs text-muted-foreground">
|
|
4638
|
+
<div className="flex items-center justify-between gap-3">
|
|
4639
|
+
<span className="font-medium text-foreground">
|
|
4640
|
+
{db("builderWriteMode")}
|
|
4641
|
+
</span>
|
|
4642
|
+
<span>{builderWriteModeSummary(builderWriteMode, db)}</span>
|
|
4256
4643
|
</div>
|
|
4257
|
-
<
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
className="shrink-0"
|
|
4261
|
-
disabled={!canEdit || sourceActionPending}
|
|
4262
|
-
onClick={onReviewBuilderUpdate}
|
|
4644
|
+
<div
|
|
4645
|
+
className="grid grid-cols-3 gap-0.5 rounded-md border border-border bg-muted/35 p-0.5"
|
|
4646
|
+
aria-label={db("builderWriteMode")}
|
|
4263
4647
|
>
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4648
|
+
{BUILDER_WRITE_MODE_OPTIONS.map((option) => {
|
|
4649
|
+
const selected = builderWriteMode === option.mode;
|
|
4650
|
+
return (
|
|
4651
|
+
<button
|
|
4652
|
+
key={option.mode}
|
|
4653
|
+
type="button"
|
|
4654
|
+
aria-pressed={selected}
|
|
4655
|
+
title={db(option.descriptionKey)}
|
|
4656
|
+
disabled={builderLiveWritesDisabled}
|
|
4657
|
+
className={cn(
|
|
4658
|
+
"min-w-0 rounded px-2 py-1.5 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60",
|
|
4659
|
+
selected
|
|
4660
|
+
? "bg-[#2383e2] text-white shadow-sm"
|
|
4661
|
+
: "text-muted-foreground hover:bg-background hover:text-foreground",
|
|
4662
|
+
)}
|
|
4663
|
+
onClick={() =>
|
|
4664
|
+
onSetBuilderLiveWrites(
|
|
4665
|
+
{
|
|
4666
|
+
writeMode: option.mode,
|
|
4667
|
+
allowPublicationTransitions:
|
|
4668
|
+
option.mode === "publish_updates"
|
|
4669
|
+
? liveWriteControl.allowPublicationTransitions
|
|
4670
|
+
: false,
|
|
4671
|
+
},
|
|
4672
|
+
leafSource.id,
|
|
4673
|
+
)
|
|
4674
|
+
}
|
|
4675
|
+
>
|
|
4676
|
+
<span className="block truncate">
|
|
4677
|
+
{db(option.labelKey)}
|
|
4678
|
+
</span>
|
|
4679
|
+
</button>
|
|
4680
|
+
);
|
|
4681
|
+
})}
|
|
4682
|
+
</div>
|
|
4683
|
+
{builderWriteMode === "publish_updates" ? (
|
|
4684
|
+
<button
|
|
4685
|
+
type="button"
|
|
4686
|
+
role="checkbox"
|
|
4687
|
+
aria-checked={liveWriteControl.allowPublicationTransitions}
|
|
4688
|
+
disabled={builderLiveWritesDisabled}
|
|
4689
|
+
className="flex min-w-0 items-start gap-2 rounded px-1 py-1 text-left transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60 disabled:hover:bg-transparent"
|
|
4690
|
+
onClick={() =>
|
|
4691
|
+
onSetBuilderLiveWrites(
|
|
4692
|
+
{
|
|
4693
|
+
writeMode: "publish_updates",
|
|
4694
|
+
allowPublicationTransitions:
|
|
4695
|
+
!liveWriteControl.allowPublicationTransitions,
|
|
4696
|
+
},
|
|
4697
|
+
leafSource.id,
|
|
4698
|
+
)
|
|
4699
|
+
}
|
|
4700
|
+
>
|
|
4701
|
+
<span
|
|
4702
|
+
className={cn(
|
|
4703
|
+
"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded border",
|
|
4704
|
+
liveWriteControl.allowPublicationTransitions
|
|
4705
|
+
? "border-[#2383e2] bg-[#2383e2] text-white"
|
|
4706
|
+
: "border-muted-foreground/40 bg-background text-transparent",
|
|
4707
|
+
)}
|
|
4708
|
+
>
|
|
4709
|
+
{liveWriteControl.allowPublicationTransitions ? (
|
|
4710
|
+
<IconCheck className="size-3" />
|
|
4711
|
+
) : null}
|
|
4712
|
+
</span>
|
|
4713
|
+
<span className="min-w-0">
|
|
4714
|
+
<span className="block text-foreground">
|
|
4715
|
+
{db("allowPublishUnpublishPerItem")}
|
|
4716
|
+
</span>
|
|
4717
|
+
<span className="block break-words">
|
|
4718
|
+
Requires explicit item intent; unpublish still needs
|
|
4719
|
+
confirmation.
|
|
4720
|
+
</span>
|
|
4721
|
+
</span>
|
|
4722
|
+
</button>
|
|
4723
|
+
) : null}
|
|
4267
4724
|
</div>
|
|
4268
|
-
|
|
4269
|
-
|
|
4725
|
+
) : null}
|
|
4726
|
+
</div>
|
|
4270
4727
|
|
|
4271
|
-
{
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
: "Local edits can be staged as a Builder save revision/autosave record. Live Builder writes are disabled."
|
|
4284
|
-
: "No local outbound push lane is active for this mock source."}
|
|
4285
|
-
</div>
|
|
4728
|
+
{leafPendingChangeSets.length > 0 ||
|
|
4729
|
+
leafAppliedChangeSets.length > 0 ||
|
|
4730
|
+
isCodeMode ? (
|
|
4731
|
+
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3 text-sm">
|
|
4732
|
+
<div className="font-medium">{db("builderChanges")}</div>
|
|
4733
|
+
<div className="text-xs text-muted-foreground">
|
|
4734
|
+
{leafSource.capabilities.liveWritesEnabled
|
|
4735
|
+
? db("reviewLocalEditsBeforeBuilder")
|
|
4736
|
+
: db("liveWritesOffStagedForReview")}
|
|
4737
|
+
</div>
|
|
4738
|
+
|
|
4739
|
+
{leafPendingChangeSets.length > 0 ? (
|
|
4286
4740
|
<div className="grid min-w-0 gap-2">
|
|
4287
|
-
{
|
|
4741
|
+
{leafPendingChangeSets.slice(0, 6).map((changeSet) => (
|
|
4288
4742
|
<SourceChangeSetReviewCard
|
|
4289
4743
|
key={changeSet.id}
|
|
4290
4744
|
changeSet={changeSet}
|
|
4291
|
-
source={
|
|
4745
|
+
source={leafSource}
|
|
4746
|
+
showDetails={isCodeMode}
|
|
4292
4747
|
/>
|
|
4293
4748
|
))}
|
|
4294
|
-
{outboundChangeSets.length === 0 ? (
|
|
4295
|
-
<div className="text-xs text-muted-foreground">
|
|
4296
|
-
{source.sourceType === "builder-cms"
|
|
4297
|
-
? "No pending local Builder changes yet. Rename a source-backed row to see a local outbound diff."
|
|
4298
|
-
: "No local outbound changes yet."}
|
|
4299
|
-
</div>
|
|
4300
|
-
) : null}
|
|
4301
4749
|
</div>
|
|
4302
|
-
|
|
4303
|
-
|
|
4750
|
+
) : isCodeMode ? (
|
|
4751
|
+
<div className="text-xs text-muted-foreground">
|
|
4752
|
+
{db("noPendingChangesEditASourceBackedRow")}
|
|
4753
|
+
</div>
|
|
4754
|
+
) : null}
|
|
4755
|
+
|
|
4756
|
+
{leafReviewableChangeSets.length > 0 ? (
|
|
4757
|
+
<div className="flex justify-end">
|
|
4758
|
+
<Button
|
|
4759
|
+
type="button"
|
|
4760
|
+
size="sm"
|
|
4761
|
+
disabled={!canEdit || sourceActionPending}
|
|
4762
|
+
onClick={() => onReviewBuilderUpdate(leafSource.id)}
|
|
4763
|
+
>
|
|
4764
|
+
<IconCheck className="mr-1.5 size-3.5" />
|
|
4765
|
+
{db("reviewChanges")}
|
|
4766
|
+
</Button>
|
|
4767
|
+
</div>
|
|
4768
|
+
) : null}
|
|
4769
|
+
|
|
4770
|
+
{leafAppliedChangeSets.length > 0 ? (
|
|
4771
|
+
<div className="mt-1 grid min-w-0 gap-1.5 border-t border-border/60 pt-2">
|
|
4772
|
+
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
4773
|
+
{db("recentlyPushed")}
|
|
4774
|
+
</div>
|
|
4775
|
+
{leafAppliedChangeSets.slice(0, 3).map((changeSet) => (
|
|
4776
|
+
<SourceChangeSetReviewCard
|
|
4777
|
+
key={changeSet.id}
|
|
4778
|
+
changeSet={changeSet}
|
|
4779
|
+
source={leafSource}
|
|
4780
|
+
showDetails={isCodeMode}
|
|
4781
|
+
/>
|
|
4782
|
+
))}
|
|
4783
|
+
{leafAppliedChangeSets.length > 3 ? (
|
|
4784
|
+
<div className="text-[11px] text-muted-foreground">
|
|
4785
|
+
+{leafAppliedChangeSets.length - 3} more
|
|
4786
|
+
</div>
|
|
4787
|
+
) : null}
|
|
4788
|
+
</div>
|
|
4789
|
+
) : null}
|
|
4790
|
+
</div>
|
|
4304
4791
|
) : null}
|
|
4305
4792
|
|
|
4793
|
+
<SourceRoleCard
|
|
4794
|
+
source={leafSource}
|
|
4795
|
+
canAddDetails={sources.some(
|
|
4796
|
+
(item) => item.id !== leafSource.id && !sourceAddsDetails(item),
|
|
4797
|
+
)}
|
|
4798
|
+
canEdit={canEdit}
|
|
4799
|
+
pending={sourceActionPending}
|
|
4800
|
+
onAddDetails={() =>
|
|
4801
|
+
onNavPush({
|
|
4802
|
+
kind: "keyConfirm",
|
|
4803
|
+
candidate: {
|
|
4804
|
+
sourceType: leafSource.sourceType,
|
|
4805
|
+
sourceName: leafSource.sourceName,
|
|
4806
|
+
sourceTable: leafSource.sourceTable,
|
|
4807
|
+
displayName: leafSource.sourceName,
|
|
4808
|
+
existingSourceId: leafSource.id,
|
|
4809
|
+
},
|
|
4810
|
+
})
|
|
4811
|
+
}
|
|
4812
|
+
onAddItems={async () => {
|
|
4813
|
+
await onChangeSourceRole(leafSource.id, "items");
|
|
4814
|
+
onNavReplace([]);
|
|
4815
|
+
}}
|
|
4816
|
+
onChooseFields={() =>
|
|
4817
|
+
onNavPush({
|
|
4818
|
+
kind: "fieldPicker",
|
|
4819
|
+
sourceId: leafSource.id,
|
|
4820
|
+
sourceName: leafSource.sourceName,
|
|
4821
|
+
})
|
|
4822
|
+
}
|
|
4823
|
+
/>
|
|
4824
|
+
|
|
4306
4825
|
<div className="rounded-lg border border-border bg-background p-3">
|
|
4307
4826
|
<div className="text-xs font-medium">
|
|
4308
4827
|
<DatabaseText k="disconnectSource" />
|
|
@@ -4316,7 +4835,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
4316
4835
|
variant="outline"
|
|
4317
4836
|
className="mt-2 h-8 text-xs text-destructive hover:text-destructive"
|
|
4318
4837
|
disabled={!canEdit || sourceActionPending}
|
|
4319
|
-
onClick={onDisconnectSource}
|
|
4838
|
+
onClick={() => onDisconnectSource(leafSource.id)}
|
|
4320
4839
|
>
|
|
4321
4840
|
{sourceActionPending ? (
|
|
4322
4841
|
<Spinner className="mr-1 size-3.5" />
|
|
@@ -4333,113 +4852,103 @@ function DatabaseSettingsSourcePanel({
|
|
|
4333
4852
|
|
|
4334
4853
|
// Root of the Sources drill-down: third-party integrations + Agent-Native apps,
|
|
4335
4854
|
// each provider a row. Builder is live; the rest are disabled "coming soon".
|
|
4855
|
+
function builderModelSummaryFromSource(
|
|
4856
|
+
source: ContentDatabaseSource,
|
|
4857
|
+
): BuilderCmsModelSummary {
|
|
4858
|
+
return {
|
|
4859
|
+
id: source.sourceTable,
|
|
4860
|
+
name: source.sourceTable,
|
|
4861
|
+
displayName: source.sourceName,
|
|
4862
|
+
kind: "data",
|
|
4863
|
+
fields: [],
|
|
4864
|
+
};
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
function reviewableCountForSource(source: ContentDatabaseSource): number {
|
|
4868
|
+
return source.changeSets.filter(
|
|
4869
|
+
(changeSet) =>
|
|
4870
|
+
changeSet.direction === "outbound" &&
|
|
4871
|
+
(changeSet.state === "pending_push" ||
|
|
4872
|
+
changeSet.state === "staged_revision" ||
|
|
4873
|
+
changeSet.state === "approved"),
|
|
4874
|
+
).length;
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4336
4877
|
function SourcesListView({
|
|
4337
4878
|
source,
|
|
4338
4879
|
sources,
|
|
4339
|
-
|
|
4340
|
-
builderSpaceLabel,
|
|
4341
|
-
reviewableCount,
|
|
4342
|
-
onOpenBuilder,
|
|
4880
|
+
onOpenConnectedBuilder,
|
|
4343
4881
|
onOpenSecondary,
|
|
4344
4882
|
onAddSource,
|
|
4345
4883
|
}: {
|
|
4346
4884
|
source: ContentDatabaseSource | null;
|
|
4347
4885
|
sources: ContentDatabaseSource[];
|
|
4348
|
-
|
|
4349
|
-
builderSpaceLabel: string | null;
|
|
4350
|
-
reviewableCount: number;
|
|
4351
|
-
onOpenBuilder: () => void;
|
|
4886
|
+
onOpenConnectedBuilder: (source: ContentDatabaseSource) => void;
|
|
4352
4887
|
onOpenSecondary: (source: ContentDatabaseSource) => void;
|
|
4353
4888
|
onAddSource: () => void;
|
|
4354
4889
|
}) {
|
|
4355
4890
|
const db = useDatabaseT();
|
|
4356
|
-
const isBuilderSource = source?.sourceType === "builder-cms";
|
|
4357
4891
|
const connectedSources =
|
|
4358
4892
|
sources.length > 0 ? sources : source ? [source] : [];
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
4368
|
-
<DatabaseText k="connectedSources" />
|
|
4369
|
-
</div>
|
|
4370
|
-
{connectedSources.map((connected, index) => (
|
|
4371
|
-
<DatabaseSettingsRow
|
|
4372
|
-
key={connected.id}
|
|
4373
|
-
icon={
|
|
4374
|
-
connected.sourceType === "builder-cms" ? (
|
|
4375
|
-
<BuilderLogoMark className="size-4" />
|
|
4376
|
-
) : (
|
|
4377
|
-
<IconLayoutGrid className="size-4" />
|
|
4378
|
-
)
|
|
4379
|
-
}
|
|
4380
|
-
label={connected.sourceName}
|
|
4381
|
-
value={
|
|
4382
|
-
connected.metadata.federation?.role === "secondary"
|
|
4383
|
-
? "Federated"
|
|
4384
|
-
: index === 0
|
|
4385
|
-
? "Primary"
|
|
4386
|
-
: undefined
|
|
4387
|
-
}
|
|
4388
|
-
onClick={
|
|
4389
|
-
connected.metadata.federation?.role === "secondary"
|
|
4390
|
-
? () => onOpenSecondary(connected)
|
|
4391
|
-
: connected.sourceType === "builder-cms"
|
|
4392
|
-
? onOpenBuilder
|
|
4393
|
-
: undefined
|
|
4394
|
-
}
|
|
4395
|
-
disabled={
|
|
4396
|
-
connected.metadata.federation?.role !== "secondary" &&
|
|
4397
|
-
connected.sourceType !== "builder-cms"
|
|
4398
|
-
}
|
|
4399
|
-
/>
|
|
4400
|
-
))}
|
|
4401
|
-
<DatabaseSettingsRow
|
|
4402
|
-
icon={<IconPlus className="size-4" />}
|
|
4403
|
-
label={db("addAnotherSource")}
|
|
4404
|
-
onClick={onAddSource}
|
|
4405
|
-
/>
|
|
4406
|
-
</div>
|
|
4407
|
-
)}
|
|
4408
|
-
<div className="grid min-w-0 gap-1.5">
|
|
4409
|
-
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
4410
|
-
<DatabaseText k="integrations" />
|
|
4893
|
+
|
|
4894
|
+
// A brand-new database has no sources — the only action is to add one.
|
|
4895
|
+
// Integrations (Builder, Notion, …) live inside the "Add a source" flow.
|
|
4896
|
+
if (connectedSources.length === 0) {
|
|
4897
|
+
return (
|
|
4898
|
+
<div className="grid min-w-0 gap-2">
|
|
4899
|
+
<div className="min-w-0 break-words px-2 text-xs text-muted-foreground">
|
|
4900
|
+
{db("connectABuilderCollectionToMapRows")}
|
|
4411
4901
|
</div>
|
|
4412
4902
|
<DatabaseSettingsRow
|
|
4413
|
-
icon={<
|
|
4414
|
-
label="
|
|
4415
|
-
|
|
4416
|
-
isBuilderSource
|
|
4417
|
-
? (builderSpaceLabel ?? "Connected")
|
|
4418
|
-
: builderConfigured
|
|
4419
|
-
? "Connected"
|
|
4420
|
-
: undefined
|
|
4421
|
-
}
|
|
4422
|
-
badgeCount={reviewableCount}
|
|
4423
|
-
onClick={onOpenBuilder}
|
|
4424
|
-
/>
|
|
4425
|
-
<DatabaseSettingsRow
|
|
4426
|
-
icon={<NotionLogoMark className="size-4" />}
|
|
4427
|
-
label="Notion"
|
|
4428
|
-
value="Coming soon"
|
|
4429
|
-
disabled
|
|
4903
|
+
icon={<IconPlus className="size-4" />}
|
|
4904
|
+
label={db("addASource")}
|
|
4905
|
+
onClick={onAddSource}
|
|
4430
4906
|
/>
|
|
4431
4907
|
</div>
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4908
|
+
);
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4911
|
+
return (
|
|
4912
|
+
<div className="grid min-w-0 gap-1.5">
|
|
4913
|
+
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
4914
|
+
{db("connectedSources")}
|
|
4915
|
+
</div>
|
|
4916
|
+
{connectedSources.map((connected, index) => (
|
|
4436
4917
|
<DatabaseSettingsRow
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4918
|
+
key={connected.id}
|
|
4919
|
+
icon={
|
|
4920
|
+
connected.sourceType === "builder-cms" ? (
|
|
4921
|
+
<BuilderLogoMark className="size-4" />
|
|
4922
|
+
) : (
|
|
4923
|
+
<IconLayoutGrid className="size-4" />
|
|
4924
|
+
)
|
|
4925
|
+
}
|
|
4926
|
+
label={connected.sourceName}
|
|
4927
|
+
value={sourceRoleLabel(db, connected, index)}
|
|
4928
|
+
badgeCount={
|
|
4929
|
+
connected.metadata.federation?.role !== "secondary" &&
|
|
4930
|
+
connected.sourceType === "builder-cms"
|
|
4931
|
+
? reviewableCountForSource(connected)
|
|
4932
|
+
: 0
|
|
4933
|
+
}
|
|
4934
|
+
onClick={
|
|
4935
|
+
connected.metadata.federation?.role === "secondary"
|
|
4936
|
+
? () => onOpenSecondary(connected)
|
|
4937
|
+
: connected.sourceType === "builder-cms"
|
|
4938
|
+
? () => onOpenConnectedBuilder(connected)
|
|
4939
|
+
: undefined
|
|
4940
|
+
}
|
|
4941
|
+
disabled={
|
|
4942
|
+
connected.metadata.federation?.role !== "secondary" &&
|
|
4943
|
+
connected.sourceType !== "builder-cms"
|
|
4944
|
+
}
|
|
4441
4945
|
/>
|
|
4442
|
-
|
|
4946
|
+
))}
|
|
4947
|
+
<DatabaseSettingsRow
|
|
4948
|
+
icon={<IconPlus className="size-4" />}
|
|
4949
|
+
label={db("addAnotherSource")}
|
|
4950
|
+
onClick={onAddSource}
|
|
4951
|
+
/>
|
|
4443
4952
|
</div>
|
|
4444
4953
|
);
|
|
4445
4954
|
}
|
|
@@ -4458,7 +4967,7 @@ function CanonicalKeyConfirmView({
|
|
|
4458
4967
|
candidate: PendingSourceCandidate;
|
|
4459
4968
|
canEdit: boolean;
|
|
4460
4969
|
pending: boolean;
|
|
4461
|
-
onCommit: (join: ContentDatabaseSourceJoinRequest) => void
|
|
4970
|
+
onCommit: (join: ContentDatabaseSourceJoinRequest) => void | Promise<void>;
|
|
4462
4971
|
}) {
|
|
4463
4972
|
const db = useDatabaseT();
|
|
4464
4973
|
const suggestionQuery = useSuggestSourceJoinKey({
|
|
@@ -4517,7 +5026,7 @@ function CanonicalKeyConfirmView({
|
|
|
4517
5026
|
return (
|
|
4518
5027
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
4519
5028
|
<Spinner className="size-3.5" />
|
|
4520
|
-
<DatabaseText k="
|
|
5029
|
+
<DatabaseText k="checkingHowTheseRecordsMatch" />
|
|
4521
5030
|
</div>
|
|
4522
5031
|
);
|
|
4523
5032
|
}
|
|
@@ -4526,7 +5035,7 @@ function CanonicalKeyConfirmView({
|
|
|
4526
5035
|
return (
|
|
4527
5036
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
4528
5037
|
{suggestionQuery.data?.message ??
|
|
4529
|
-
"Couldn
|
|
5038
|
+
"Couldn't find a match field automatically."}
|
|
4530
5039
|
</div>
|
|
4531
5040
|
);
|
|
4532
5041
|
}
|
|
@@ -4538,7 +5047,7 @@ function CanonicalKeyConfirmView({
|
|
|
4538
5047
|
{candidate.displayName}
|
|
4539
5048
|
</div>
|
|
4540
5049
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
4541
|
-
Match
|
|
5050
|
+
Match existing items to details. Suggested match:{" "}
|
|
4542
5051
|
<span className="font-medium text-foreground">
|
|
4543
5052
|
{suggestion.canonicalKey.label}
|
|
4544
5053
|
</span>
|
|
@@ -4630,7 +5139,7 @@ function CanonicalKeyConfirmView({
|
|
|
4630
5139
|
) : (
|
|
4631
5140
|
<IconPlugConnected className="mr-1.5 size-3.5" />
|
|
4632
5141
|
)}
|
|
4633
|
-
|
|
5142
|
+
Add details
|
|
4634
5143
|
</Button>
|
|
4635
5144
|
</div>
|
|
4636
5145
|
);
|
|
@@ -4641,79 +5150,448 @@ function CanonicalKeyConfirmView({
|
|
|
4641
5150
|
function AddSourceView({
|
|
4642
5151
|
excludeDatabaseIds,
|
|
4643
5152
|
canEdit,
|
|
4644
|
-
|
|
5153
|
+
builderConfigured,
|
|
5154
|
+
builderSpaceLabel,
|
|
5155
|
+
onOpenBuilder,
|
|
5156
|
+
onPickLocalTable,
|
|
5157
|
+
}: {
|
|
5158
|
+
excludeDatabaseIds: string[];
|
|
5159
|
+
canEdit: boolean;
|
|
5160
|
+
builderConfigured: boolean;
|
|
5161
|
+
builderSpaceLabel: string | null;
|
|
5162
|
+
onOpenBuilder: () => void;
|
|
5163
|
+
onPickLocalTable: (table: {
|
|
5164
|
+
databaseId: string;
|
|
5165
|
+
documentId: string;
|
|
5166
|
+
title: string;
|
|
5167
|
+
}) => void;
|
|
5168
|
+
}) {
|
|
5169
|
+
const db = useDatabaseT();
|
|
5170
|
+
const query = useContentDatabases({ enabled: true });
|
|
5171
|
+
// Exclude this database (no self-reference) and any table already federated
|
|
5172
|
+
// onto it — those live in the "Connected sources" group above.
|
|
5173
|
+
const excluded = new Set(excludeDatabaseIds);
|
|
5174
|
+
const tables = (query.data?.databases ?? []).filter(
|
|
5175
|
+
(table) => !excluded.has(table.databaseId),
|
|
5176
|
+
);
|
|
5177
|
+
return (
|
|
5178
|
+
<div className="grid min-w-0 gap-4">
|
|
5179
|
+
<div className="grid min-w-0 gap-1.5">
|
|
5180
|
+
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
5181
|
+
<DatabaseText k="localTables" />
|
|
5182
|
+
</div>
|
|
5183
|
+
{query.isLoading ? (
|
|
5184
|
+
<div className="flex items-center gap-2 px-2 text-xs text-muted-foreground">
|
|
5185
|
+
<Spinner className="size-3.5" />
|
|
5186
|
+
<DatabaseText k="loadingTables" />
|
|
5187
|
+
</div>
|
|
5188
|
+
) : tables.length === 0 ? (
|
|
5189
|
+
<div className="min-w-0 break-words px-2 text-xs text-muted-foreground">
|
|
5190
|
+
<DatabaseText k="noOtherDatabasesAvailableToAdd" />
|
|
5191
|
+
</div>
|
|
5192
|
+
) : (
|
|
5193
|
+
tables.map((table) => (
|
|
5194
|
+
<DatabaseSettingsRow
|
|
5195
|
+
key={table.databaseId}
|
|
5196
|
+
icon={<IconLayoutGrid className="size-4" />}
|
|
5197
|
+
label={table.title}
|
|
5198
|
+
onClick={canEdit ? () => onPickLocalTable(table) : undefined}
|
|
5199
|
+
disabled={!canEdit}
|
|
5200
|
+
/>
|
|
5201
|
+
))
|
|
5202
|
+
)}
|
|
5203
|
+
</div>
|
|
5204
|
+
<div className="grid min-w-0 gap-1.5">
|
|
5205
|
+
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
5206
|
+
<DatabaseText k="integrations" />
|
|
5207
|
+
</div>
|
|
5208
|
+
<DatabaseSettingsRow
|
|
5209
|
+
icon={<BuilderLogoMark className="size-4" />}
|
|
5210
|
+
label="Builder"
|
|
5211
|
+
value={
|
|
5212
|
+
builderSpaceLabel ?? (builderConfigured ? "Connected" : undefined)
|
|
5213
|
+
}
|
|
5214
|
+
onClick={canEdit ? onOpenBuilder : undefined}
|
|
5215
|
+
disabled={!canEdit}
|
|
5216
|
+
/>
|
|
5217
|
+
<DatabaseSettingsRow
|
|
5218
|
+
icon={<NotionLogoMark className="size-4" />}
|
|
5219
|
+
label="Notion"
|
|
5220
|
+
value="Coming soon"
|
|
5221
|
+
disabled
|
|
5222
|
+
/>
|
|
5223
|
+
</div>
|
|
5224
|
+
<div className="grid min-w-0 gap-1.5">
|
|
5225
|
+
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
5226
|
+
<DatabaseText k="agentNativeApps" />
|
|
5227
|
+
</div>
|
|
5228
|
+
<DatabaseSettingsRow
|
|
5229
|
+
icon={<IconTimeline className="size-4" />}
|
|
5230
|
+
label="Analytics"
|
|
5231
|
+
value="Coming soon"
|
|
5232
|
+
disabled
|
|
5233
|
+
/>
|
|
5234
|
+
</div>
|
|
5235
|
+
</div>
|
|
5236
|
+
);
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
// A connected federated (secondary) source: read-only details + remove.
|
|
5240
|
+
function SecondarySourceLeaf({
|
|
5241
|
+
source,
|
|
5242
|
+
canEdit,
|
|
5243
|
+
pending,
|
|
5244
|
+
onAddDetails,
|
|
5245
|
+
onAddItems,
|
|
5246
|
+
onChooseFields,
|
|
5247
|
+
onDisconnect,
|
|
5248
|
+
}: {
|
|
5249
|
+
source: ContentDatabaseSource | null;
|
|
5250
|
+
canEdit: boolean;
|
|
5251
|
+
pending: boolean;
|
|
5252
|
+
onAddDetails: () => void;
|
|
5253
|
+
onAddItems: () => void | Promise<void>;
|
|
5254
|
+
onChooseFields: () => void;
|
|
5255
|
+
onDisconnect: () => void;
|
|
5256
|
+
}) {
|
|
5257
|
+
const db = useDatabaseT();
|
|
5258
|
+
if (!source) {
|
|
5259
|
+
return (
|
|
5260
|
+
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
5261
|
+
<DatabaseText k="thisSourceIsNoLongerConnected" />
|
|
5262
|
+
</div>
|
|
5263
|
+
);
|
|
5264
|
+
}
|
|
5265
|
+
const federation = source.metadata.federation;
|
|
5266
|
+
const fieldCount = source.fields.length;
|
|
5267
|
+
return (
|
|
5268
|
+
<div className="grid min-w-0 gap-4">
|
|
5269
|
+
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
5270
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
5271
|
+
<span className="truncate font-medium" title={source.sourceName}>
|
|
5272
|
+
{source.sourceName}
|
|
5273
|
+
</span>
|
|
5274
|
+
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
5275
|
+
<IconLock className="size-3" />
|
|
5276
|
+
<DatabaseText k="readOnly" />
|
|
5277
|
+
</span>
|
|
5278
|
+
</div>
|
|
5279
|
+
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
5280
|
+
{`${sourceRoleLabel(db, source, 0)} · ${fieldCount} field${
|
|
5281
|
+
fieldCount === 1 ? "" : "s"
|
|
5282
|
+
}`}
|
|
5283
|
+
{federation?.canonicalKey?.label
|
|
5284
|
+
? ` · joined on ${federation.canonicalKey.label}`
|
|
5285
|
+
: ""}
|
|
5286
|
+
</div>
|
|
5287
|
+
</div>
|
|
5288
|
+
<SourceRoleCard
|
|
5289
|
+
source={source}
|
|
5290
|
+
canEdit={canEdit}
|
|
5291
|
+
pending={pending}
|
|
5292
|
+
onAddDetails={onAddDetails}
|
|
5293
|
+
onAddItems={onAddItems}
|
|
5294
|
+
onChooseFields={onChooseFields}
|
|
5295
|
+
/>
|
|
5296
|
+
{federation ? (
|
|
5297
|
+
<div className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3 text-xs">
|
|
5298
|
+
<div className="font-medium">
|
|
5299
|
+
<DatabaseText k="matchFormula" />
|
|
5300
|
+
</div>
|
|
5301
|
+
<code className="block min-w-0 break-words rounded bg-muted px-1.5 py-1 font-mono text-[11px]">
|
|
5302
|
+
{federation.normalizationFormula}
|
|
5303
|
+
</code>
|
|
5304
|
+
</div>
|
|
5305
|
+
) : null}
|
|
5306
|
+
<div className="rounded-lg border border-border bg-background p-3">
|
|
5307
|
+
<div className="text-xs font-medium">
|
|
5308
|
+
<DatabaseText k="removeThisSource" />
|
|
5309
|
+
</div>
|
|
5310
|
+
<div className="mt-0.5 break-words text-xs text-muted-foreground">
|
|
5311
|
+
Removes the federated columns’ link to this source. Your local
|
|
5312
|
+
rows and columns stay.
|
|
5313
|
+
</div>
|
|
5314
|
+
<Button
|
|
5315
|
+
type="button"
|
|
5316
|
+
size="sm"
|
|
5317
|
+
variant="outline"
|
|
5318
|
+
className="mt-2 h-8 text-xs text-destructive hover:text-destructive"
|
|
5319
|
+
disabled={!canEdit || pending}
|
|
5320
|
+
onClick={onDisconnect}
|
|
5321
|
+
>
|
|
5322
|
+
{pending ? (
|
|
5323
|
+
<Spinner className="mr-1 size-3.5" />
|
|
5324
|
+
) : (
|
|
5325
|
+
<IconX className="mr-1 size-3.5" />
|
|
5326
|
+
)}
|
|
5327
|
+
Remove
|
|
5328
|
+
</Button>
|
|
5329
|
+
</div>
|
|
5330
|
+
</div>
|
|
5331
|
+
);
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
function sourceAddsDetails(source: ContentDatabaseSource | null | undefined) {
|
|
5335
|
+
return source?.metadata.federation?.role === "secondary";
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
function sourceRoleLabel(
|
|
5339
|
+
db: DatabaseT,
|
|
5340
|
+
source: ContentDatabaseSource | null | undefined,
|
|
5341
|
+
index: number,
|
|
5342
|
+
) {
|
|
5343
|
+
if (sourceAddsDetails(source)) return db("addingDetails");
|
|
5344
|
+
return index === 0 ? db("addingItems") : db("addingItems");
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
function detailsReasonText(
|
|
5348
|
+
db: DatabaseT,
|
|
5349
|
+
suggestion: SourceJoinSuggestion | null,
|
|
5350
|
+
) {
|
|
5351
|
+
if (!suggestion) return db("recommendedWhenCollectionDescribesExistingRows");
|
|
5352
|
+
const percent = Math.round(suggestion.confidence * 100);
|
|
5353
|
+
return db("recommendedBecauseSampledRowsMatchOn", {
|
|
5354
|
+
field: suggestion.canonicalKey.label,
|
|
5355
|
+
percent,
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
function findDetailsSource(
|
|
5360
|
+
response: ContentDatabaseResponse,
|
|
5361
|
+
candidate: PendingSourceCandidate,
|
|
5362
|
+
) {
|
|
5363
|
+
return (
|
|
5364
|
+
(response.sources ?? []).find((source) => {
|
|
5365
|
+
if (!sourceAddsDetails(source)) return false;
|
|
5366
|
+
if (candidate.existingSourceId)
|
|
5367
|
+
return source.id === candidate.existingSourceId;
|
|
5368
|
+
return (
|
|
5369
|
+
source.sourceType === candidate.sourceType &&
|
|
5370
|
+
source.sourceTable === candidate.sourceTable
|
|
5371
|
+
);
|
|
5372
|
+
}) ?? null
|
|
5373
|
+
);
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5376
|
+
function SourceRelationshipChoice({
|
|
5377
|
+
documentId,
|
|
5378
|
+
candidate,
|
|
5379
|
+
canEdit,
|
|
5380
|
+
pending,
|
|
5381
|
+
onAddDetails,
|
|
5382
|
+
onAddItems,
|
|
5383
|
+
}: {
|
|
5384
|
+
documentId: string;
|
|
5385
|
+
candidate: PendingSourceCandidate;
|
|
5386
|
+
canEdit: boolean;
|
|
5387
|
+
pending: boolean;
|
|
5388
|
+
onAddDetails: () => void;
|
|
5389
|
+
onAddItems: () => void | Promise<void>;
|
|
5390
|
+
}) {
|
|
5391
|
+
const db = useDatabaseT();
|
|
5392
|
+
const suggestionQuery = useSuggestSourceJoinKey({
|
|
5393
|
+
documentId,
|
|
5394
|
+
candidateSourceType: candidate.sourceType,
|
|
5395
|
+
candidateSourceTable: candidate.sourceTable,
|
|
5396
|
+
enabled: true,
|
|
5397
|
+
});
|
|
5398
|
+
const suggestion = suggestionQuery.data?.suggestion ?? null;
|
|
5399
|
+
const detailsRecommended = Boolean(suggestion);
|
|
5400
|
+
return (
|
|
5401
|
+
<div className="grid min-w-0 gap-2">
|
|
5402
|
+
<button
|
|
5403
|
+
type="button"
|
|
5404
|
+
disabled={!canEdit || pending}
|
|
5405
|
+
className={cn(
|
|
5406
|
+
"grid min-w-0 gap-1 rounded-lg border p-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60",
|
|
5407
|
+
detailsRecommended
|
|
5408
|
+
? "border-foreground/30 bg-muted/30"
|
|
5409
|
+
: "border-border bg-background hover:bg-muted/50",
|
|
5410
|
+
)}
|
|
5411
|
+
onClick={onAddDetails}
|
|
5412
|
+
>
|
|
5413
|
+
<span className="grid min-w-0 gap-1">
|
|
5414
|
+
<span className="break-words text-sm font-medium leading-snug">
|
|
5415
|
+
<DatabaseText k="addDetailsToExistingItems" />
|
|
5416
|
+
</span>
|
|
5417
|
+
{detailsRecommended ? (
|
|
5418
|
+
<span className="w-fit rounded-full bg-foreground px-2 py-0.5 text-[10px] font-medium text-background">
|
|
5419
|
+
<DatabaseText k="recommended" />
|
|
5420
|
+
</span>
|
|
5421
|
+
) : null}
|
|
5422
|
+
</span>
|
|
5423
|
+
<span className="break-words text-xs text-muted-foreground">
|
|
5424
|
+
{suggestionQuery.isLoading
|
|
5425
|
+
? db("checkingForMatchingFields")
|
|
5426
|
+
: detailsReasonText(db, suggestion)}
|
|
5427
|
+
</span>
|
|
5428
|
+
</button>
|
|
5429
|
+
<button
|
|
5430
|
+
type="button"
|
|
5431
|
+
disabled={!canEdit || pending}
|
|
5432
|
+
className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60"
|
|
5433
|
+
onClick={onAddItems}
|
|
5434
|
+
>
|
|
5435
|
+
<span className="truncate text-sm font-medium">
|
|
5436
|
+
<DatabaseText k="addMoreItemsToThisList" />
|
|
5437
|
+
</span>
|
|
5438
|
+
<span className="break-words text-xs text-muted-foreground">
|
|
5439
|
+
<DatabaseText k="bestWhenSameKindAdditionalRows" />
|
|
5440
|
+
</span>
|
|
5441
|
+
</button>
|
|
5442
|
+
</div>
|
|
5443
|
+
);
|
|
5444
|
+
}
|
|
5445
|
+
|
|
5446
|
+
function SourceRoleCard({
|
|
5447
|
+
source,
|
|
5448
|
+
canAddDetails = true,
|
|
5449
|
+
canEdit,
|
|
5450
|
+
pending,
|
|
5451
|
+
onAddDetails,
|
|
5452
|
+
onAddItems,
|
|
5453
|
+
onChooseFields,
|
|
4645
5454
|
}: {
|
|
4646
|
-
|
|
5455
|
+
source: ContentDatabaseSource;
|
|
5456
|
+
canAddDetails?: boolean;
|
|
4647
5457
|
canEdit: boolean;
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
}) => void;
|
|
5458
|
+
pending: boolean;
|
|
5459
|
+
onAddDetails: () => void;
|
|
5460
|
+
onAddItems: () => void | Promise<void>;
|
|
5461
|
+
onChooseFields: () => void;
|
|
4653
5462
|
}) {
|
|
4654
5463
|
const db = useDatabaseT();
|
|
4655
|
-
const
|
|
4656
|
-
// Exclude this database (no self-reference) and any table already federated
|
|
4657
|
-
// onto it — those live in the "Connected sources" group above.
|
|
4658
|
-
const excluded = new Set(excludeDatabaseIds);
|
|
4659
|
-
const tables = (query.data?.databases ?? []).filter(
|
|
4660
|
-
(table) => !excluded.has(table.databaseId),
|
|
4661
|
-
);
|
|
5464
|
+
const addingDetails = sourceAddsDetails(source);
|
|
4662
5465
|
return (
|
|
4663
|
-
<div className="grid min-w-0 gap-
|
|
4664
|
-
<div className="grid min-w-0 gap-
|
|
4665
|
-
<div className="
|
|
4666
|
-
<DatabaseText k="
|
|
5466
|
+
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3">
|
|
5467
|
+
<div className="grid min-w-0 gap-0.5">
|
|
5468
|
+
<div className="text-xs font-medium">
|
|
5469
|
+
<DatabaseText k="sourceRole" />
|
|
4667
5470
|
</div>
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
5471
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5472
|
+
{addingDetails
|
|
5473
|
+
? db("addingDetailsMatchedOn", {
|
|
5474
|
+
field:
|
|
5475
|
+
source.metadata.federation?.canonicalKey?.label ??
|
|
5476
|
+
db("aField"),
|
|
5477
|
+
})
|
|
5478
|
+
: db("addingItemsAsRows")}
|
|
5479
|
+
</div>
|
|
5480
|
+
</div>
|
|
5481
|
+
<div className="flex min-w-0 flex-wrap gap-2">
|
|
5482
|
+
{addingDetails ? (
|
|
5483
|
+
<>
|
|
5484
|
+
<Button
|
|
5485
|
+
type="button"
|
|
5486
|
+
size="sm"
|
|
5487
|
+
variant="outline"
|
|
5488
|
+
className="h-8 text-xs"
|
|
5489
|
+
disabled={!canEdit || pending}
|
|
5490
|
+
onClick={onChooseFields}
|
|
5491
|
+
>
|
|
5492
|
+
<DatabaseText k="chooseFields" />
|
|
5493
|
+
</Button>
|
|
5494
|
+
<Button
|
|
5495
|
+
type="button"
|
|
5496
|
+
size="sm"
|
|
5497
|
+
variant="outline"
|
|
5498
|
+
className="h-8 text-xs"
|
|
5499
|
+
disabled={!canEdit || pending}
|
|
5500
|
+
onClick={onAddItems}
|
|
5501
|
+
>
|
|
5502
|
+
<DatabaseText k="addAsItems" />
|
|
5503
|
+
</Button>
|
|
5504
|
+
</>
|
|
5505
|
+
) : canAddDetails ? (
|
|
5506
|
+
<Button
|
|
5507
|
+
type="button"
|
|
5508
|
+
size="sm"
|
|
5509
|
+
variant="outline"
|
|
5510
|
+
className="h-8 text-xs"
|
|
5511
|
+
disabled={!canEdit || pending}
|
|
5512
|
+
onClick={onAddDetails}
|
|
5513
|
+
>
|
|
5514
|
+
<DatabaseText k="addDetailsInstead" />
|
|
5515
|
+
</Button>
|
|
4677
5516
|
) : (
|
|
4678
|
-
|
|
4679
|
-
<
|
|
4680
|
-
|
|
4681
|
-
icon={<IconLayoutGrid className="size-4" />}
|
|
4682
|
-
label={table.title}
|
|
4683
|
-
onClick={canEdit ? () => onPickLocalTable(table) : undefined}
|
|
4684
|
-
disabled={!canEdit}
|
|
4685
|
-
/>
|
|
4686
|
-
))
|
|
5517
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5518
|
+
<DatabaseText k="addAnotherItemSourceBeforeChangingToDetails" />
|
|
5519
|
+
</div>
|
|
4687
5520
|
)}
|
|
4688
5521
|
</div>
|
|
4689
|
-
<div className="grid min-w-0 gap-1.5">
|
|
4690
|
-
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
4691
|
-
<DatabaseText k="integrations" />
|
|
4692
|
-
</div>
|
|
4693
|
-
<DatabaseSettingsRow
|
|
4694
|
-
icon={<NotionLogoMark className="size-4" />}
|
|
4695
|
-
label="Notion"
|
|
4696
|
-
value="Coming soon"
|
|
4697
|
-
disabled
|
|
4698
|
-
/>
|
|
4699
|
-
</div>
|
|
4700
5522
|
</div>
|
|
4701
5523
|
);
|
|
4702
5524
|
}
|
|
4703
5525
|
|
|
4704
|
-
|
|
4705
|
-
|
|
5526
|
+
const DETAIL_FIELD_NAME_HINTS = [
|
|
5527
|
+
"name",
|
|
5528
|
+
"title",
|
|
5529
|
+
"bio",
|
|
5530
|
+
"description",
|
|
5531
|
+
"image",
|
|
5532
|
+
"photo",
|
|
5533
|
+
"avatar",
|
|
5534
|
+
"url",
|
|
5535
|
+
"handle",
|
|
5536
|
+
"email",
|
|
5537
|
+
];
|
|
5538
|
+
|
|
5539
|
+
function shouldPreselectDetailField(
|
|
5540
|
+
field: ContentDatabaseSource["fields"][number],
|
|
5541
|
+
) {
|
|
5542
|
+
const key = `${field.sourceFieldKey} ${field.sourceFieldLabel}`.toLowerCase();
|
|
5543
|
+
if (
|
|
5544
|
+
field.propertyId ||
|
|
5545
|
+
field.mappingType === "system" ||
|
|
5546
|
+
field.mappingType === "title"
|
|
5547
|
+
) {
|
|
5548
|
+
return false;
|
|
5549
|
+
}
|
|
5550
|
+
if (/(\b|\.|_)(id|created|updated|published|rev)(\b|\.|_)/i.test(key)) {
|
|
5551
|
+
return false;
|
|
5552
|
+
}
|
|
5553
|
+
return DETAIL_FIELD_NAME_HINTS.some((hint) => key.includes(hint));
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
function SourceDetailsFieldPicker({
|
|
5557
|
+
documentId,
|
|
4706
5558
|
source,
|
|
4707
5559
|
canEdit,
|
|
4708
5560
|
pending,
|
|
4709
|
-
|
|
5561
|
+
onDone,
|
|
4710
5562
|
}: {
|
|
5563
|
+
documentId: string;
|
|
4711
5564
|
source: ContentDatabaseSource | null;
|
|
4712
5565
|
canEdit: boolean;
|
|
4713
5566
|
pending: boolean;
|
|
4714
|
-
|
|
5567
|
+
onDone: () => void;
|
|
4715
5568
|
}) {
|
|
4716
5569
|
const db = useDatabaseT();
|
|
5570
|
+
const addField = useAddContentDatabaseSourceFieldProperty(documentId);
|
|
5571
|
+
const fields = useMemo(
|
|
5572
|
+
() =>
|
|
5573
|
+
(source?.fields ?? []).filter(
|
|
5574
|
+
(field) =>
|
|
5575
|
+
!field.propertyId &&
|
|
5576
|
+
field.mappingType !== "system" &&
|
|
5577
|
+
field.mappingType !== "title",
|
|
5578
|
+
),
|
|
5579
|
+
[source],
|
|
5580
|
+
);
|
|
5581
|
+
const defaultSelectedIds = useMemo(
|
|
5582
|
+
() =>
|
|
5583
|
+
fields
|
|
5584
|
+
.filter(shouldPreselectDetailField)
|
|
5585
|
+
.slice(0, 8)
|
|
5586
|
+
.map((field) => field.id),
|
|
5587
|
+
[fields],
|
|
5588
|
+
);
|
|
5589
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
5590
|
+
|
|
5591
|
+
useEffect(() => {
|
|
5592
|
+
setSelectedIds(defaultSelectedIds);
|
|
5593
|
+
}, [defaultSelectedIds]);
|
|
5594
|
+
|
|
4717
5595
|
if (!source) {
|
|
4718
5596
|
return (
|
|
4719
5597
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
@@ -4721,59 +5599,106 @@ function SecondarySourceLeaf({
|
|
|
4721
5599
|
</div>
|
|
4722
5600
|
);
|
|
4723
5601
|
}
|
|
4724
|
-
|
|
4725
|
-
const
|
|
5602
|
+
|
|
5603
|
+
const selected = new Set(selectedIds);
|
|
5604
|
+
const toggleField = (fieldId: string) => {
|
|
5605
|
+
setSelectedIds((current) =>
|
|
5606
|
+
current.includes(fieldId)
|
|
5607
|
+
? current.filter((id) => id !== fieldId)
|
|
5608
|
+
: [...current, fieldId],
|
|
5609
|
+
);
|
|
5610
|
+
};
|
|
5611
|
+
const addSelected = async () => {
|
|
5612
|
+
const sourceFieldIds = fields
|
|
5613
|
+
.filter((field) => selected.has(field.id))
|
|
5614
|
+
.map((field) => field.id);
|
|
5615
|
+
if (sourceFieldIds.length === 0) {
|
|
5616
|
+
onDone();
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5619
|
+
try {
|
|
5620
|
+
for (const sourceFieldId of sourceFieldIds) {
|
|
5621
|
+
await addField.mutateAsync({ documentId, sourceFieldId });
|
|
5622
|
+
}
|
|
5623
|
+
toast.success(db("detailFieldsAdded"), {
|
|
5624
|
+
description:
|
|
5625
|
+
sourceFieldIds.length === 1
|
|
5626
|
+
? db("addedOneFieldFromSource")
|
|
5627
|
+
: db("addedFieldsFromSource", { count: sourceFieldIds.length }),
|
|
5628
|
+
});
|
|
5629
|
+
onDone();
|
|
5630
|
+
} catch (error) {
|
|
5631
|
+
toast.error(db("fieldsWereNotAdded"), {
|
|
5632
|
+
description: error instanceof Error ? error.message : db("tryAgain"),
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
};
|
|
5636
|
+
|
|
4726
5637
|
return (
|
|
4727
|
-
<div className="grid min-w-0 gap-
|
|
4728
|
-
<div className="grid min-w-0 gap-1
|
|
4729
|
-
<div className="
|
|
4730
|
-
|
|
4731
|
-
{source.sourceName}
|
|
4732
|
-
</span>
|
|
4733
|
-
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
4734
|
-
<IconLock className="size-3" />
|
|
4735
|
-
<DatabaseText k="readOnly" />
|
|
4736
|
-
</span>
|
|
5638
|
+
<div className="grid min-w-0 gap-3">
|
|
5639
|
+
<div className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3">
|
|
5640
|
+
<div className="truncate text-sm font-medium" title={source.sourceName}>
|
|
5641
|
+
{source.sourceName}
|
|
4737
5642
|
</div>
|
|
4738
|
-
<div className="
|
|
4739
|
-
|
|
4740
|
-
{federation?.canonicalKey?.label
|
|
4741
|
-
? ` · joined on ${federation.canonicalKey.label}`
|
|
4742
|
-
: ""}
|
|
5643
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5644
|
+
<DatabaseText k="pickDetailsBecomeColumns" />
|
|
4743
5645
|
</div>
|
|
4744
5646
|
</div>
|
|
4745
|
-
{
|
|
4746
|
-
<div className="
|
|
4747
|
-
<
|
|
4748
|
-
<DatabaseText k="matchFormula" />
|
|
4749
|
-
</div>
|
|
4750
|
-
<code className="block min-w-0 break-words rounded bg-muted px-1.5 py-1 font-mono text-[11px]">
|
|
4751
|
-
{federation.normalizationFormula}
|
|
4752
|
-
</code>
|
|
4753
|
-
</div>
|
|
4754
|
-
) : null}
|
|
4755
|
-
<div className="rounded-lg border border-border bg-background p-3">
|
|
4756
|
-
<div className="text-xs font-medium">
|
|
4757
|
-
<DatabaseText k="removeThisSource" />
|
|
5647
|
+
{fields.length === 0 ? (
|
|
5648
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5649
|
+
<DatabaseText k="allAvailableDetailFieldsAlreadyVisible" />
|
|
4758
5650
|
</div>
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
5651
|
+
) : (
|
|
5652
|
+
<div className="grid min-w-0 gap-1">
|
|
5653
|
+
{fields.map((field) => (
|
|
5654
|
+
<button
|
|
5655
|
+
key={field.id}
|
|
5656
|
+
type="button"
|
|
5657
|
+
className="flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
5658
|
+
onClick={() => toggleField(field.id)}
|
|
5659
|
+
>
|
|
5660
|
+
<span
|
|
5661
|
+
className={cn(
|
|
5662
|
+
"flex size-4 shrink-0 items-center justify-center rounded border",
|
|
5663
|
+
selected.has(field.id)
|
|
5664
|
+
? "border-[#2383e2] bg-[#2383e2] text-white"
|
|
5665
|
+
: "border-muted-foreground/40 text-transparent",
|
|
5666
|
+
)}
|
|
5667
|
+
>
|
|
5668
|
+
<IconCheck className="size-3" />
|
|
5669
|
+
</span>
|
|
5670
|
+
<span className="min-w-0 flex-1 truncate">
|
|
5671
|
+
{field.sourceFieldLabel}
|
|
5672
|
+
</span>
|
|
5673
|
+
<span className="shrink-0 text-[11px] text-muted-foreground">
|
|
5674
|
+
{field.sourceFieldType}
|
|
5675
|
+
</span>
|
|
5676
|
+
</button>
|
|
5677
|
+
))}
|
|
4762
5678
|
</div>
|
|
5679
|
+
)}
|
|
5680
|
+
<div className="flex justify-end gap-2">
|
|
4763
5681
|
<Button
|
|
4764
5682
|
type="button"
|
|
4765
5683
|
size="sm"
|
|
4766
5684
|
variant="outline"
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
onClick={onDisconnect}
|
|
5685
|
+
disabled={addField.isPending}
|
|
5686
|
+
onClick={onDone}
|
|
4770
5687
|
>
|
|
4771
|
-
|
|
4772
|
-
|
|
5688
|
+
Done
|
|
5689
|
+
</Button>
|
|
5690
|
+
<Button
|
|
5691
|
+
type="button"
|
|
5692
|
+
size="sm"
|
|
5693
|
+
disabled={!canEdit || pending || addField.isPending}
|
|
5694
|
+
onClick={addSelected}
|
|
5695
|
+
>
|
|
5696
|
+
{addField.isPending ? (
|
|
5697
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
4773
5698
|
) : (
|
|
4774
|
-
<
|
|
5699
|
+
<IconPlus className="mr-1.5 size-3.5" />
|
|
4775
5700
|
)}
|
|
4776
|
-
|
|
5701
|
+
Add selected
|
|
4777
5702
|
</Button>
|
|
4778
5703
|
</div>
|
|
4779
5704
|
</div>
|
|
@@ -4783,12 +5708,13 @@ function SecondarySourceLeaf({
|
|
|
4783
5708
|
// A Builder space's data models, as drill-in rows. The attached model (if any)
|
|
4784
5709
|
// is marked; selecting a row opens that model's leaf.
|
|
4785
5710
|
function BuilderSpaceModelsView({
|
|
4786
|
-
|
|
5711
|
+
attachedModelNames,
|
|
4787
5712
|
onOpenModel,
|
|
4788
5713
|
}: {
|
|
4789
|
-
|
|
5714
|
+
attachedModelNames: string[];
|
|
4790
5715
|
onOpenModel: (model: BuilderCmsModelSummary) => void;
|
|
4791
5716
|
}) {
|
|
5717
|
+
const attachedModelNameSet = new Set(attachedModelNames);
|
|
4792
5718
|
const db = useDatabaseT();
|
|
4793
5719
|
const modelsQuery = useBuilderCmsModels(true);
|
|
4794
5720
|
const models = modelsQuery.data?.models ?? [];
|
|
@@ -4860,15 +5786,15 @@ function BuilderSpaceModelsView({
|
|
|
4860
5786
|
model.displayName.toLowerCase().includes(normalizedQuery) ||
|
|
4861
5787
|
model.name.toLowerCase().includes(normalizedQuery);
|
|
4862
5788
|
const filtered = models.filter(matchesQuery);
|
|
4863
|
-
const attachedModels = filtered.filter(
|
|
4864
|
-
(model
|
|
5789
|
+
const attachedModels = filtered.filter((model) =>
|
|
5790
|
+
attachedModelNameSet.has(model.name),
|
|
4865
5791
|
);
|
|
4866
5792
|
const otherModels = filtered.filter(
|
|
4867
|
-
(model) =>
|
|
5793
|
+
(model) => !attachedModelNameSet.has(model.name),
|
|
4868
5794
|
);
|
|
4869
5795
|
|
|
4870
5796
|
const renderRow = (model: BuilderCmsModelSummary) => {
|
|
4871
|
-
const isAttached =
|
|
5797
|
+
const isAttached = attachedModelNameSet.has(model.name);
|
|
4872
5798
|
return (
|
|
4873
5799
|
<button
|
|
4874
5800
|
key={model.id}
|
|
@@ -4933,12 +5859,38 @@ function BuilderSpaceModelsView({
|
|
|
4933
5859
|
);
|
|
4934
5860
|
}
|
|
4935
5861
|
|
|
5862
|
+
const BUILDER_DEV_EFFECT_LABEL: Record<
|
|
5863
|
+
BuilderCmsWriteEffect,
|
|
5864
|
+
{ pending: string; done: string }
|
|
5865
|
+
> = {
|
|
5866
|
+
create_draft: { pending: "Create draft", done: "Created draft" },
|
|
5867
|
+
update_in_place: { pending: "Update entry", done: "Updated entry" },
|
|
5868
|
+
autosave: { pending: "Save draft", done: "Saved draft" },
|
|
5869
|
+
publish: { pending: "Publish", done: "Published" },
|
|
5870
|
+
unpublish: { pending: "Unpublish", done: "Unpublished" },
|
|
5871
|
+
};
|
|
5872
|
+
|
|
5873
|
+
function changeSetDisplayTitle(changeSet: ContentDatabaseSourceChangeSet) {
|
|
5874
|
+
const titleChange = changeSet.fieldChanges.find(
|
|
5875
|
+
(field) => field.localFieldKey === "title",
|
|
5876
|
+
);
|
|
5877
|
+
if (
|
|
5878
|
+
typeof titleChange?.proposedValue === "string" &&
|
|
5879
|
+
titleChange.proposedValue.trim()
|
|
5880
|
+
) {
|
|
5881
|
+
return titleChange.proposedValue.trim();
|
|
5882
|
+
}
|
|
5883
|
+
return changeSet.summary;
|
|
5884
|
+
}
|
|
5885
|
+
|
|
4936
5886
|
function SourceChangeSetReviewCard({
|
|
4937
5887
|
changeSet,
|
|
4938
5888
|
source,
|
|
5889
|
+
showDetails = false,
|
|
4939
5890
|
}: {
|
|
4940
5891
|
changeSet: ContentDatabaseSourceChangeSet;
|
|
4941
5892
|
source: ContentDatabaseSource;
|
|
5893
|
+
showDetails?: boolean;
|
|
4942
5894
|
}) {
|
|
4943
5895
|
const db = useDatabaseT();
|
|
4944
5896
|
const latestReview =
|
|
@@ -4948,6 +5900,49 @@ function SourceChangeSetReviewCard({
|
|
|
4948
5900
|
const dryRunStatus = latestExecution
|
|
4949
5901
|
? builderExecutionDryRunStatus(latestExecution.payload)
|
|
4950
5902
|
: null;
|
|
5903
|
+
const effect = resolveBuilderCmsWriteEffect({ source, changeSet });
|
|
5904
|
+
const effectLabel = BUILDER_DEV_EFFECT_LABEL[effect];
|
|
5905
|
+
const title = changeSetDisplayTitle(changeSet);
|
|
5906
|
+
|
|
5907
|
+
// A change that already pushed is done — collapse it to a one-line
|
|
5908
|
+
// confirmation instead of repeating the full diff + gate ceremony.
|
|
5909
|
+
if (changeSet.state === "applied") {
|
|
5910
|
+
// Report the SUCCEEDED execution (a later blocked/stale gate row could
|
|
5911
|
+
// otherwise overwrite the pushed result). Use the effect that actually ran
|
|
5912
|
+
// (recorded in the payload) — the live resolver would now report
|
|
5913
|
+
// update_in_place because the row is matched to its new entry, mislabeling
|
|
5914
|
+
// a completed create as an update.
|
|
5915
|
+
const succeededExecution =
|
|
5916
|
+
[...changeSet.executions]
|
|
5917
|
+
.reverse()
|
|
5918
|
+
.find((execution) => execution.state === "succeeded") ??
|
|
5919
|
+
latestExecution;
|
|
5920
|
+
const appliedEffect =
|
|
5921
|
+
(succeededExecution &&
|
|
5922
|
+
builderExecutionEffect(succeededExecution.payload)) ??
|
|
5923
|
+
effect;
|
|
5924
|
+
const when =
|
|
5925
|
+
formatRelativeSyncTime(
|
|
5926
|
+
succeededExecution?.updatedAt ?? changeSet.updatedAt,
|
|
5927
|
+
) ?? formatSourceTimestamp(changeSet.updatedAt);
|
|
5928
|
+
return (
|
|
5929
|
+
<div className="flex min-w-0 items-start gap-2 rounded-md border border-border/70 px-2 py-1.5 text-xs">
|
|
5930
|
+
<IconCheck className="mt-0.5 size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
5931
|
+
<div className="min-w-0">
|
|
5932
|
+
<div className="min-w-0 break-words font-medium leading-snug">
|
|
5933
|
+
{title}
|
|
5934
|
+
</div>
|
|
5935
|
+
<div className="text-muted-foreground">
|
|
5936
|
+
{BUILDER_DEV_EFFECT_LABEL[appliedEffect].done} · pushed {when}
|
|
5937
|
+
</div>
|
|
5938
|
+
</div>
|
|
5939
|
+
</div>
|
|
5940
|
+
);
|
|
5941
|
+
}
|
|
5942
|
+
|
|
5943
|
+
const blockers = latestExecution
|
|
5944
|
+
? builderExecutionBlockers(latestExecution.payload)
|
|
5945
|
+
: [];
|
|
4951
5946
|
|
|
4952
5947
|
return (
|
|
4953
5948
|
<div className="min-w-0 rounded-md border border-border/70 px-2 py-1.5">
|
|
@@ -4956,128 +5951,93 @@ function SourceChangeSetReviewCard({
|
|
|
4956
5951
|
className="min-w-0 break-words font-medium leading-snug"
|
|
4957
5952
|
title={changeSet.summary}
|
|
4958
5953
|
>
|
|
4959
|
-
{
|
|
4960
|
-
</span>
|
|
4961
|
-
<span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
4962
|
-
{changeSet.state.replace(/_/g, " ")}
|
|
4963
|
-
</span>
|
|
4964
|
-
</div>
|
|
4965
|
-
|
|
4966
|
-
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px]">
|
|
4967
|
-
<span className={sourceRiskClass(changeSet.riskLevel)}>
|
|
4968
|
-
{changeSet.riskLevel} risk
|
|
4969
|
-
</span>
|
|
4970
|
-
<span className="rounded border border-border px-1.5 py-0.5 text-muted-foreground">
|
|
4971
|
-
{changeSet.conflictState === "source_changed"
|
|
4972
|
-
? "source changed"
|
|
4973
|
-
: "no conflict"}
|
|
4974
|
-
</span>
|
|
4975
|
-
<span className="rounded border border-border px-1.5 py-0.5 text-muted-foreground">
|
|
4976
|
-
{sourcePushModeLabel(changeSet.pushMode)}
|
|
5954
|
+
{title}
|
|
4977
5955
|
</span>
|
|
4978
|
-
<span className="rounded border border-border px-1.5 py-0.5 text-muted-foreground">
|
|
4979
|
-
{
|
|
5956
|
+
<span className="shrink-0 rounded border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
|
5957
|
+
{effectLabel.pending}
|
|
4980
5958
|
</span>
|
|
4981
5959
|
</div>
|
|
4982
5960
|
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
+{changeSet.fieldChanges.length - 3} more field changes
|
|
5005
|
-
</div>
|
|
5006
|
-
) : null}
|
|
5007
|
-
{changeSet.bodyChange ? (
|
|
5008
|
-
<div className="rounded border border-border/60 bg-muted/20 p-1.5 text-xs">
|
|
5009
|
-
<div className="font-medium">{changeSet.bodyChange.summary}</div>
|
|
5010
|
-
<div className="mt-1 text-muted-foreground">
|
|
5011
|
-
<DatabaseText k="bodyDiff" />
|
|
5012
|
-
</div>
|
|
5013
|
-
</div>
|
|
5014
|
-
) : null}
|
|
5015
|
-
</div>
|
|
5961
|
+
{changeSet.riskLevel !== "low" ||
|
|
5962
|
+
changeSet.conflictState === "source_changed" ||
|
|
5963
|
+
!changeSet.localOnly ? (
|
|
5964
|
+
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px]">
|
|
5965
|
+
{changeSet.riskLevel !== "low" ? (
|
|
5966
|
+
<span className={sourceRiskClass(changeSet.riskLevel)}>
|
|
5967
|
+
{changeSet.riskLevel} risk
|
|
5968
|
+
</span>
|
|
5969
|
+
) : null}
|
|
5970
|
+
{changeSet.conflictState === "source_changed" ? (
|
|
5971
|
+
<span className="rounded border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
|
|
5972
|
+
source changed
|
|
5973
|
+
</span>
|
|
5974
|
+
) : null}
|
|
5975
|
+
{!changeSet.localOnly ? (
|
|
5976
|
+
<span className="rounded border border-border px-1.5 py-0.5 text-muted-foreground">
|
|
5977
|
+
external write
|
|
5978
|
+
</span>
|
|
5979
|
+
) : null}
|
|
5980
|
+
</div>
|
|
5981
|
+
) : null}
|
|
5016
5982
|
|
|
5017
|
-
<div className="mt-
|
|
5018
|
-
{changeSet.
|
|
5019
|
-
{"
|
|
5020
|
-
{
|
|
5021
|
-
|
|
5022
|
-
: "live writes disabled"}
|
|
5023
|
-
{" • "}
|
|
5024
|
-
{formatSourceTimestamp(changeSet.updatedAt)}
|
|
5983
|
+
<div className="mt-1.5 text-xs text-muted-foreground">
|
|
5984
|
+
{changeSet.fieldChanges.length} field change
|
|
5985
|
+
{changeSet.fieldChanges.length === 1 ? "" : "s"}
|
|
5986
|
+
{changeSet.bodyChange ? " · body diff" : ""} · “Review changes” to see
|
|
5987
|
+
the diff
|
|
5025
5988
|
</div>
|
|
5026
5989
|
|
|
5027
|
-
{
|
|
5028
|
-
<div className="mt-2
|
|
5029
|
-
{
|
|
5030
|
-
|
|
5031
|
-
|
|
5990
|
+
{blockers.length > 0 ? (
|
|
5991
|
+
<div className="mt-2 grid gap-1 text-xs text-amber-700 dark:text-amber-300">
|
|
5992
|
+
{blockers.slice(0, 2).map((blocker) => (
|
|
5993
|
+
<div key={blocker} className="break-words">
|
|
5994
|
+
Blocked: {blocker}
|
|
5995
|
+
</div>
|
|
5996
|
+
))}
|
|
5997
|
+
</div>
|
|
5998
|
+
) : null}
|
|
5999
|
+
{latestExecution?.lastError ? (
|
|
6000
|
+
<div className="mt-2 break-words text-xs text-destructive">
|
|
6001
|
+
{latestExecution.lastError}
|
|
5032
6002
|
</div>
|
|
5033
6003
|
) : null}
|
|
5034
6004
|
|
|
5035
|
-
{
|
|
5036
|
-
<
|
|
5037
|
-
<
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
<
|
|
5042
|
-
{
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
<div className="mt-1 break-words text-muted-foreground">
|
|
5046
|
-
{latestExecution.summary}
|
|
5047
|
-
</div>
|
|
5048
|
-
{builderExecutionRequestLine(latestExecution.payload) ? (
|
|
5049
|
-
<div className="mt-1 break-words text-muted-foreground">
|
|
5050
|
-
Would call {builderExecutionRequestLine(latestExecution.payload)}
|
|
6005
|
+
{showDetails ? (
|
|
6006
|
+
<details className="mt-2 text-xs text-muted-foreground">
|
|
6007
|
+
<summary className="cursor-pointer text-[11px] hover:text-foreground">
|
|
6008
|
+
Details
|
|
6009
|
+
</summary>
|
|
6010
|
+
<div className="mt-1 grid gap-1">
|
|
6011
|
+
<div className="break-words">
|
|
6012
|
+
{changeSet.riskReasons.join(", ")}
|
|
6013
|
+
{" • "}
|
|
6014
|
+
{formatSourceTimestamp(changeSet.updatedAt)}
|
|
5051
6015
|
</div>
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
.
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
6016
|
+
{latestReview ? (
|
|
6017
|
+
<div className="break-words">
|
|
6018
|
+
{latestReview.decision} by {latestReview.reviewerEmail}
|
|
6019
|
+
{" • "}
|
|
6020
|
+
{formatSourceTimestamp(latestReview.createdAt)}
|
|
6021
|
+
</div>
|
|
6022
|
+
) : null}
|
|
6023
|
+
{latestExecution ? (
|
|
6024
|
+
<>
|
|
6025
|
+
<div className="break-words">
|
|
6026
|
+
Execution gate: {latestExecution.state.replace(/_/g, " ")}
|
|
6027
|
+
{dryRunStatus ? ` · dry run ${dryRunStatus.status}` : ""}
|
|
6028
|
+
</div>
|
|
6029
|
+
{builderExecutionRequestLine(latestExecution.payload) ? (
|
|
6030
|
+
<div className="break-words">
|
|
6031
|
+
{builderExecutionRequestLine(latestExecution.payload)}
|
|
5060
6032
|
</div>
|
|
5061
|
-
)
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
{dryRunStatus.validatedAt
|
|
5068
|
-
? ` • ${formatSourceTimestamp(dryRunStatus.validatedAt)}`
|
|
5069
|
-
: ""}
|
|
5070
|
-
</div>
|
|
5071
|
-
) : null}
|
|
5072
|
-
{latestExecution.lastError ? (
|
|
5073
|
-
<div className="mt-1 break-words text-destructive">
|
|
5074
|
-
{latestExecution.lastError}
|
|
5075
|
-
</div>
|
|
5076
|
-
) : null}
|
|
5077
|
-
<div className="mt-1 break-all text-muted-foreground">
|
|
5078
|
-
{latestExecution.idempotencyKey}
|
|
6033
|
+
) : null}
|
|
6034
|
+
<div className="break-all">
|
|
6035
|
+
{latestExecution.idempotencyKey}
|
|
6036
|
+
</div>
|
|
6037
|
+
</>
|
|
6038
|
+
) : null}
|
|
5079
6039
|
</div>
|
|
5080
|
-
</
|
|
6040
|
+
</details>
|
|
5081
6041
|
) : null}
|
|
5082
6042
|
</div>
|
|
5083
6043
|
);
|
|
@@ -5142,6 +6102,19 @@ function builderExecutionRequestLine(payload: Record<string, unknown>) {
|
|
|
5142
6102
|
return `${method} ${path}${queryText ? `?${queryText}` : ""}`;
|
|
5143
6103
|
}
|
|
5144
6104
|
|
|
6105
|
+
function builderExecutionEffect(
|
|
6106
|
+
payload: Record<string, unknown>,
|
|
6107
|
+
): BuilderCmsWriteEffect | null {
|
|
6108
|
+
const effect = payload.effect;
|
|
6109
|
+
return effect === "create_draft" ||
|
|
6110
|
+
effect === "update_in_place" ||
|
|
6111
|
+
effect === "autosave" ||
|
|
6112
|
+
effect === "publish" ||
|
|
6113
|
+
effect === "unpublish"
|
|
6114
|
+
? effect
|
|
6115
|
+
: null;
|
|
6116
|
+
}
|
|
6117
|
+
|
|
5145
6118
|
function builderExecutionBlockers(payload: Record<string, unknown>) {
|
|
5146
6119
|
const safety =
|
|
5147
6120
|
payload.safety &&
|
|
@@ -5204,15 +6177,6 @@ function sourceBuilderReadModeSummary(source: ContentDatabaseSource) {
|
|
|
5204
6177
|
return "Local fixture";
|
|
5205
6178
|
}
|
|
5206
6179
|
|
|
5207
|
-
function sourcePushModeLabel(
|
|
5208
|
-
mode: ContentDatabaseSource["metadata"]["pushMode"] | null | undefined,
|
|
5209
|
-
) {
|
|
5210
|
-
if (mode === "autosave") return "Save revision / autosave";
|
|
5211
|
-
if (mode === "draft") return "Draft";
|
|
5212
|
-
if (mode === "publish") return "Publish";
|
|
5213
|
-
return "No push";
|
|
5214
|
-
}
|
|
5215
|
-
|
|
5216
6180
|
function sourceFieldMappingForColumn(
|
|
5217
6181
|
source: ContentDatabaseSource | null,
|
|
5218
6182
|
columnKey: ColumnKey,
|
|
@@ -11370,6 +12334,7 @@ function DatabasePropertyHeader({
|
|
|
11370
12334
|
property,
|
|
11371
12335
|
documentId,
|
|
11372
12336
|
source,
|
|
12337
|
+
sources,
|
|
11373
12338
|
canEdit,
|
|
11374
12339
|
isDragging,
|
|
11375
12340
|
dropSide,
|
|
@@ -11381,6 +12346,7 @@ function DatabasePropertyHeader({
|
|
|
11381
12346
|
property: DocumentProperty;
|
|
11382
12347
|
documentId: string;
|
|
11383
12348
|
source: ContentDatabaseSource | null;
|
|
12349
|
+
sources: ContentDatabaseSource[];
|
|
11384
12350
|
canEdit: boolean;
|
|
11385
12351
|
isDragging: boolean;
|
|
11386
12352
|
dropSide: DatabaseDropSide | null;
|
|
@@ -11427,6 +12393,7 @@ function DatabasePropertyHeader({
|
|
|
11427
12393
|
property.definition.id,
|
|
11428
12394
|
)}
|
|
11429
12395
|
sourceAttached={!!source}
|
|
12396
|
+
sources={sources}
|
|
11430
12397
|
/>
|
|
11431
12398
|
) : (
|
|
11432
12399
|
<div className="flex h-7 min-w-0 flex-1 items-center gap-2 px-1">
|