canopycms 0.0.68-int.98 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2131 -0
- package/README.md +47 -63
- package/dist/ai/generate.d.ts +5 -0
- package/dist/ai/generate.js +244 -115
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/json-to-markdown.d.ts +21 -1
- package/dist/ai/json-to-markdown.js +120 -57
- package/dist/ai/references.d.ts +72 -0
- package/dist/ai/references.js +223 -0
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/ai/to-plain-text.d.ts +17 -8
- package/dist/ai/to-plain-text.js +18 -15
- package/dist/ai/types.d.ts +84 -3
- package/dist/api/admin-branch-health.d.ts +24 -1
- package/dist/api/admin-branch-health.js +52 -20
- package/dist/api/admin.d.ts +23 -5
- package/dist/api/admin.js +36 -1
- package/dist/api/assets.js +88 -78
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +16 -7
- package/dist/api/branch-status.d.ts +16 -6
- package/dist/api/branch-status.js +57 -12
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +8 -3
- package/dist/api/branch.d.ts +25 -12
- package/dist/api/branch.js +164 -103
- package/dist/api/client.d.ts +78 -58
- package/dist/api/client.js +150 -117
- package/dist/api/comments.d.ts +12 -3
- package/dist/api/comments.js +118 -9
- package/dist/api/content.d.ts +9 -9
- package/dist/api/content.js +131 -22
- package/dist/api/entries-constants.d.ts +14 -3
- package/dist/api/entries-constants.js +14 -3
- package/dist/api/entries.d.ts +40 -2
- package/dist/api/entries.js +102 -6
- package/dist/api/github-sync.d.ts +19 -0
- package/dist/api/github-sync.js +50 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/groups.js +2 -1
- package/dist/api/guards.js +23 -3
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -0
- package/dist/api/permissions.d.ts +21 -2
- package/dist/api/permissions.js +50 -3
- package/dist/api/request-deadline.d.ts +59 -0
- package/dist/api/request-deadline.js +73 -0
- package/dist/api/resolve-references.js +8 -7
- package/dist/api/route-builder.d.ts +14 -0
- package/dist/api/route-builder.js +14 -1
- package/dist/api/schema.d.ts +11 -9
- package/dist/api/schema.js +2 -0
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +18 -0
- package/dist/api/users-constants.d.ts +6 -0
- package/dist/api/users-constants.js +6 -0
- package/dist/assets/asset-url.d.ts +28 -16
- package/dist/assets/asset-url.js +51 -25
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +2 -0
- package/dist/auth/caching-auth-plugin.js +12 -2
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +18 -8
- package/dist/auth/person-name.d.ts +5 -0
- package/dist/auth/person-name.js +8 -0
- package/dist/auth/plugin.d.ts +10 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/user-metadata-lookup.d.ts +21 -0
- package/dist/auth/user-metadata-lookup.js +116 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +12 -13
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/helpers.d.ts +8 -0
- package/dist/authorization/helpers.js +8 -0
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.d.ts +35 -8
- package/dist/branch-health.js +54 -18
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +45 -6
- package/dist/branch-metadata.js +142 -50
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-schema-cache.d.ts +29 -12
- package/dist/branch-schema-cache.js +64 -21
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +239 -100
- package/dist/build/asset-refs.d.ts +94 -0
- package/dist/build/asset-refs.js +267 -0
- package/dist/build/generate-ai-content.js +5 -0
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +8543 -3193
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +4244 -1072
- package/dist/cli/init.js +217 -126
- package/dist/cli/project-detect.d.ts +2 -0
- package/dist/cli/project-detect.js +2 -0
- package/dist/cli/template-files/Dockerfile.cms.template +13 -5
- package/dist/cli/template-files/cms-stack.ts.template +16 -13
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/comment-store.d.ts +25 -0
- package/dist/comment-store.js +43 -8
- package/dist/config/flatten.js +2 -0
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +12 -29
- package/dist/config/schemas/config.js +9 -12
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +58 -4
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +2 -20
- package/dist/config/schemas/url.js +2 -25
- package/dist/config/types.d.ts +116 -22
- package/dist/config/types.js +3 -9
- package/dist/config/validation.d.ts +25 -4
- package/dist/config/validation.js +116 -3
- package/dist/content-id-index.js +2 -2
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.d.ts +10 -1
- package/dist/content-listing.js +22 -5
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +16 -11
- package/dist/content-store.d.ts +75 -12
- package/dist/content-store.js +249 -86
- package/dist/content-tree.js +2 -2
- package/dist/context.js +5 -2
- package/dist/dangling-reference-log.d.ts +17 -0
- package/dist/dangling-reference-log.js +29 -0
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +23 -7
- package/dist/editor/BranchManager.js +119 -54
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.d.ts +14 -0
- package/dist/editor/CommentsPanel.js +68 -43
- package/dist/editor/Editor.d.ts +11 -5
- package/dist/editor/Editor.js +891 -218
- package/dist/editor/EditorAuthGate.d.ts +7 -1
- package/dist/editor/EditorAuthGate.js +41 -9
- package/dist/editor/EntryNavigator.d.ts +22 -0
- package/dist/editor/EntryNavigator.js +353 -105
- package/dist/editor/FormHeader.d.ts +25 -0
- package/dist/editor/FormHeader.js +82 -0
- package/dist/editor/FormRenderer.d.ts +27 -3
- package/dist/editor/FormRenderer.js +171 -76
- package/dist/editor/PreviewFrame.d.ts +40 -0
- package/dist/editor/PreviewFrame.js +215 -0
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +163 -33
- package/dist/editor/admin/useSystemHealth.d.ts +18 -0
- package/dist/editor/admin/useSystemHealth.js +70 -1
- package/dist/editor/branch-status.d.ts +17 -0
- package/dist/editor/branch-status.js +29 -0
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/client-reference-resolver.d.ts +43 -16
- package/dist/editor/client-reference-resolver.js +155 -64
- package/dist/editor/comments/BranchComments.d.ts +1 -1
- package/dist/editor/comments/BranchComments.js +2 -2
- package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
- package/dist/editor/comments/CloseRefusedNotice.js +7 -0
- package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
- package/dist/editor/comments/CommentThreadsPopover.js +274 -0
- package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
- package/dist/editor/comments/CommentWriteAlert.js +13 -0
- package/dist/editor/comments/EntryComments.d.ts +10 -4
- package/dist/editor/comments/EntryComments.js +14 -17
- package/dist/editor/comments/FieldWrapper.d.ts +18 -4
- package/dist/editor/comments/FieldWrapper.js +62 -28
- package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
- package/dist/editor/comments/InlineCommentThread.js +11 -47
- package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
- package/dist/editor/comments/ThreadCarousel.js +18 -21
- package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
- package/dist/editor/comments/ThreadReplyControls.js +92 -0
- package/dist/editor/comments/field-comment-context.d.ts +14 -0
- package/dist/editor/comments/field-comment-context.js +14 -0
- package/dist/editor/comments/form-comments.d.ts +48 -0
- package/dist/editor/comments/form-comments.js +178 -0
- package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
- package/dist/editor/comments/item-anchor-copy.js +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
- package/dist/editor/comments/popover-placement.d.ts +18 -0
- package/dist/editor/comments/popover-placement.js +58 -0
- package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
- package/dist/editor/comments/use-tracked-writes.js +53 -0
- package/dist/editor/components/BranchAccessList.d.ts +15 -0
- package/dist/editor/components/BranchAccessList.js +52 -0
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/BranchesDrawer.d.ts +11 -0
- package/dist/editor/components/BranchesDrawer.js +5 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
- package/dist/editor/components/DestructiveMenuItem.js +8 -0
- package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
- package/dist/editor/components/EditorCrashScreen.js +22 -0
- package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
- package/dist/editor/components/EditorErrorBoundary.js +40 -0
- package/dist/editor/components/EditorHeader.d.ts +56 -106
- package/dist/editor/components/EditorHeader.js +130 -131
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/EntryCreateModal.d.ts +12 -7
- package/dist/editor/components/EntryCreateModal.js +51 -57
- package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
- package/dist/editor/components/NoEditPermissionNotice.js +5 -4
- package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
- package/dist/editor/components/QuickOpenPalette.js +66 -0
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
- package/dist/editor/components/ReasonedMenuItem.js +15 -0
- package/dist/editor/components/ReferencedByList.d.ts +11 -0
- package/dist/editor/components/ReferencedByList.js +28 -0
- package/dist/editor/components/RelativeTime.d.ts +10 -0
- package/dist/editor/components/RelativeTime.js +10 -0
- package/dist/editor/components/RenameEntryModal.d.ts +9 -1
- package/dist/editor/components/RenameEntryModal.js +19 -31
- package/dist/editor/components/RetryToastMessage.d.ts +9 -0
- package/dist/editor/components/RetryToastMessage.js +6 -0
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
- package/dist/editor/components/StagedChangesDrawer.js +92 -0
- package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
- package/dist/editor/components/UnavailableEntryNotice.js +9 -0
- package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
- package/dist/editor/components/UnavailableTypeMessage.js +9 -0
- package/dist/editor/components/UndoToastMessage.d.ts +18 -0
- package/dist/editor/components/UndoToastMessage.js +10 -0
- package/dist/editor/components/UserBadge.d.ts +2 -5
- package/dist/editor/components/UserBadge.js +62 -33
- package/dist/editor/components/ValidationSummary.d.ts +14 -0
- package/dist/editor/components/ValidationSummary.js +24 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -2
- package/dist/editor/context/ApiClientContext.js +19 -8
- package/dist/editor/context/AssetContext.d.ts +13 -35
- package/dist/editor/context/AssetContext.js +12 -31
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/context/SWRProvider.d.ts +3 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +2 -1
- package/dist/editor/context/index.js +2 -1
- package/dist/editor/contrast.d.ts +19 -0
- package/dist/editor/contrast.js +41 -0
- package/dist/editor/copy.d.ts +18 -0
- package/dist/editor/copy.js +18 -0
- package/dist/editor/editor-config.js +16 -0
- package/dist/editor/editor-utils.d.ts +34 -8
- package/dist/editor/editor-utils.js +42 -20
- package/dist/editor/entry-search.d.ts +20 -0
- package/dist/editor/entry-search.js +34 -0
- package/dist/editor/field-breadcrumb.d.ts +12 -0
- package/dist/editor/field-breadcrumb.js +85 -0
- package/dist/editor/field-props.d.ts +86 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/field-target.d.ts +30 -0
- package/dist/editor/field-target.js +77 -0
- package/dist/editor/fields/BlockField.d.ts +12 -0
- package/dist/editor/fields/BlockField.js +89 -73
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +6 -2
- package/dist/editor/fields/DateTimeField.d.ts +4 -29
- package/dist/editor/fields/DateTimeField.js +79 -41
- package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
- package/dist/editor/fields/ExpandAllButton.js +4 -0
- package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
- package/dist/editor/fields/FieldCrashFallback.js +30 -0
- package/dist/editor/fields/FieldLabel.d.ts +37 -0
- package/dist/editor/fields/FieldLabel.js +19 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +29 -14
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/ItemList.d.ts +42 -0
- package/dist/editor/fields/ItemList.js +130 -0
- package/dist/editor/fields/MarkdownField.d.ts +32 -0
- package/dist/editor/fields/MarkdownField.js +223 -46
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +8 -5
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +19 -16
- package/dist/editor/fields/ObjectField.d.ts +2 -7
- package/dist/editor/fields/ObjectField.js +15 -5
- package/dist/editor/fields/ObjectListField.d.ts +29 -0
- package/dist/editor/fields/ObjectListField.js +57 -0
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +34 -9
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +11 -2
- package/dist/editor/fields/StringListField.d.ts +4 -1
- package/dist/editor/fields/StringListField.js +22 -8
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +6 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +8 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/form-expansion.d.ts +9 -0
- package/dist/editor/fields/form-expansion.js +2 -0
- package/dist/editor/fields/item-expansion.d.ts +15 -0
- package/dist/editor/fields/item-expansion.js +60 -0
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/fields/list-item-title.d.ts +20 -0
- package/dist/editor/fields/list-item-title.js +85 -0
- package/dist/editor/fields/markdown-export-options.d.ts +2 -0
- package/dist/editor/fields/markdown-export-options.js +8 -0
- package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
- package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +157 -69
- package/dist/editor/fields/rich-text-failures.d.ts +5 -0
- package/dist/editor/fields/rich-text-failures.js +26 -0
- package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
- package/dist/editor/fields/toolbar-wrap.js +35 -0
- package/dist/editor/form-outline.d.ts +13 -0
- package/dist/editor/form-outline.js +70 -0
- package/dist/editor/group-manager/GroupCard.d.ts +4 -1
- package/dist/editor/group-manager/GroupCard.js +12 -3
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
- package/dist/editor/group-manager/MemberList.d.ts +2 -1
- package/dist/editor/group-manager/MemberList.js +15 -4
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
- package/dist/editor/group-manager/index.js +34 -5
- package/dist/editor/group-manager/types.d.ts +5 -2
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +54 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useBranchActions.d.ts +11 -1
- package/dist/editor/hooks/useBranchActions.js +42 -19
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +321 -125
- package/dist/editor/hooks/useBranchesData.d.ts +8 -4
- package/dist/editor/hooks/useBranchesData.js +9 -5
- package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
- package/dist/editor/hooks/useCommentSystem.js +79 -55
- package/dist/editor/hooks/useCommentsData.d.ts +11 -7
- package/dist/editor/hooks/useCommentsData.js +20 -19
- package/dist/editor/hooks/useDraftManager.d.ts +77 -3
- package/dist/editor/hooks/useDraftManager.js +887 -94
- package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
- package/dist/editor/hooks/useEditorLayout.js +98 -20
- package/dist/editor/hooks/useEntriesData.d.ts +2 -1
- package/dist/editor/hooks/useEntriesData.js +18 -4
- package/dist/editor/hooks/useEntryManager.d.ts +41 -2
- package/dist/editor/hooks/useEntryManager.js +227 -41
- package/dist/editor/hooks/useGroupManager.js +13 -16
- package/dist/editor/hooks/usePermissionManager.js +14 -6
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
- package/dist/editor/hooks/useReferenceResolution.js +34 -177
- package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
- package/dist/editor/hooks/useSchemaManager.js +97 -39
- package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
- package/dist/editor/hooks/useUserMetadata.js +76 -47
- package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
- package/dist/editor/hooks/user-metadata-batcher.js +33 -0
- package/dist/editor/media/AssetCard.d.ts +6 -7
- package/dist/editor/media/AssetCard.js +24 -31
- package/dist/editor/media/AssetListRow.d.ts +16 -0
- package/dist/editor/media/AssetListRow.js +41 -0
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibrary.d.ts +4 -3
- package/dist/editor/media/MediaLibrary.js +11 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
- package/dist/editor/media/MediaLibraryBody.js +195 -80
- package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
- package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +8 -0
- package/dist/editor/media/editor-image-src.js +10 -0
- package/dist/editor/media/format-bytes.d.ts +2 -0
- package/dist/editor/media/format-bytes.js +23 -0
- package/dist/editor/media/media-library-prefs.d.ts +23 -0
- package/dist/editor/media/media-library-prefs.js +49 -0
- package/dist/editor/media/media-library-view.d.ts +34 -0
- package/dist/editor/media/media-library-view.js +40 -0
- package/dist/editor/media/useAssetLibrary.d.ts +30 -0
- package/dist/editor/media/useAssetLibrary.js +126 -0
- package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
- package/dist/editor/media/useStableAssetOrder.js +37 -0
- package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
- package/dist/editor/media/useThumbnailFailure.js +16 -0
- package/dist/editor/permission-manager/GroupSelector.js +2 -2
- package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
- package/dist/editor/permission-manager/PermissionEditor.js +15 -4
- package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
- package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.js +3 -3
- package/dist/editor/permission-manager/index.js +12 -3
- package/dist/editor/permission-manager/types.d.ts +5 -2
- package/dist/editor/platform.d.ts +3 -0
- package/dist/editor/platform.js +9 -0
- package/dist/editor/preview-asset-base.d.ts +9 -0
- package/dist/editor/preview-asset-base.js +17 -0
- package/dist/editor/preview-bridge.d.ts +51 -27
- package/dist/editor/preview-bridge.js +103 -144
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/raw-asset-base.d.ts +5 -0
- package/dist/editor/raw-asset-base.js +14 -0
- package/dist/editor/relative-time.d.ts +4 -4
- package/dist/editor/relative-time.js +16 -8
- package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
- package/dist/editor/schema-editor/CollectionEditor.js +361 -62
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
- package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
- package/dist/editor/schema-editor/collection-draft.js +40 -0
- package/dist/editor/schema-editor/index.d.ts +1 -1
- package/dist/editor/schema-label.d.ts +10 -0
- package/dist/editor/schema-label.js +17 -0
- package/dist/editor/settings-conflict.d.ts +5 -0
- package/dist/editor/settings-conflict.js +8 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/slug.d.ts +16 -0
- package/dist/editor/slug.js +43 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.js +314 -13
- package/dist/editor/unanswered.d.ts +17 -0
- package/dist/editor/unanswered.js +26 -0
- package/dist/editor/unavailable-entry-type.d.ts +20 -0
- package/dist/editor/unavailable-entry-type.js +28 -0
- package/dist/editor/undo-removal.d.ts +57 -0
- package/dist/editor/undo-removal.js +163 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/editor/utils/editor-errors.d.ts +19 -0
- package/dist/editor/utils/editor-errors.js +32 -0
- package/dist/editor/utils/focus-return.d.ts +56 -0
- package/dist/editor/utils/focus-return.js +106 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/entry-schema-registry.d.ts +3 -3
- package/dist/entry-schema-registry.js +7 -4
- package/dist/entry-schema.d.ts +49 -3
- package/dist/entry-schema.js +18 -0
- package/dist/git-manager.d.ts +172 -10
- package/dist/git-manager.js +449 -107
- package/dist/github-service.d.ts +39 -2
- package/dist/github-service.js +101 -7
- package/dist/http/handler.js +74 -9
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +9 -4
- package/dist/http/worker-not-ready.js +59 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/item-keys.d.ts +46 -0
- package/dist/item-keys.js +235 -0
- package/dist/operating-mode/editor-mode-check.d.ts +15 -0
- package/dist/operating-mode/editor-mode-check.js +24 -0
- package/dist/operating-mode/mode-env.d.ts +13 -14
- package/dist/operating-mode/mode-env.js +13 -14
- package/dist/operating-mode/types.d.ts +2 -2
- package/dist/paths/branch.d.ts +0 -1
- package/dist/paths/branch.js +1 -8
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.js +1 -0
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/meta-loader.d.ts +18 -8
- package/dist/schema/meta-loader.js +42 -9
- package/dist/schema/registry-fingerprint.d.ts +14 -0
- package/dist/schema/registry-fingerprint.js +63 -0
- package/dist/schema/resolver.d.ts +7 -4
- package/dist/schema/resolver.js +7 -6
- package/dist/schema/schema-store.d.ts +7 -10
- package/dist/schema/schema-store.js +13 -11
- package/dist/schema/schema-unavailable-error.d.ts +11 -0
- package/dist/schema/schema-unavailable-error.js +15 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema-registry-record.d.ts +35 -0
- package/dist/schema-registry-record.js +134 -0
- package/dist/server.d.ts +79 -9
- package/dist/server.js +62 -8
- package/dist/services.d.ts +33 -5
- package/dist/services.js +155 -37
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +159 -45
- package/dist/stamp-on-publish.d.ts +36 -0
- package/dist/stamp-on-publish.js +71 -0
- package/dist/static/index.d.ts +58 -5
- package/dist/static/index.js +133 -3
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +22 -0
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/task-queue/cms-task-queue.js +1 -1
- package/dist/task-queue/index.d.ts +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/task-queue.d.ts +6 -0
- package/dist/task-queue/task-queue.js +29 -0
- package/dist/task-queue/worker-status.d.ts +35 -9
- package/dist/task-queue/worker-status.js +84 -17
- package/dist/types.d.ts +90 -9
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +8 -1
- package/dist/url-exclusivity-fixtures.js +11 -1
- package/dist/user.js +2 -1
- package/dist/utils/atomic-write.js +2 -1
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-serialize.d.ts +15 -7
- package/dist/utils/content-serialize.js +353 -51
- package/dist/utils/content-write-lock.d.ts +6 -1
- package/dist/utils/content-write-lock.js +21 -6
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/error.js +13 -2
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/utils/fs.d.ts +12 -4
- package/dist/utils/fs.js +18 -5
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +63 -8
- package/dist/utils/json-source-splice.d.ts +14 -0
- package/dist/utils/json-source-splice.js +120 -0
- package/dist/utils/markdown-body-splice.d.ts +39 -0
- package/dist/utils/markdown-body-splice.js +421 -0
- package/dist/utils/occ-json-write.js +18 -12
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +32 -1
- package/dist/utils/provisioning-lock.js +78 -32
- package/dist/utils/sanitize-href.d.ts +9 -23
- package/dist/utils/sanitize-href.js +14 -28
- package/dist/utils/title-field.d.ts +10 -0
- package/dist/utils/title-field.js +47 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/validation/deletion-checker.d.ts +21 -29
- package/dist/validation/deletion-checker.js +41 -117
- package/dist/validation/entry-validator.d.ts +6 -10
- package/dist/validation/entry-validator.js +19 -26
- package/dist/validation/field-traversal.d.ts +22 -0
- package/dist/validation/field-traversal.js +39 -8
- package/dist/validation/markdown-safety.d.ts +68 -0
- package/dist/validation/markdown-safety.js +596 -0
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/validation/reference-validator.d.ts +2 -0
- package/dist/validation/reference-validator.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +236 -142
- package/dist/worker/cms-worker.js +772 -285
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +34 -17
- package/dist/worker/git-sync.js +394 -131
- package/dist/worker/github-auth.d.ts +9 -9
- package/dist/worker/github-auth.js +13 -15
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +590 -0
- package/dist/worker/github-gateway-wire.js +488 -0
- package/dist/worker/github-gateway.d.ts +238 -0
- package/dist/worker/github-gateway.js +726 -0
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +364 -0
- package/dist/worker/github-mirror.js +1030 -0
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/history-rewrite.d.ts +4 -5
- package/dist/worker/history-rewrite.js +12 -17
- package/dist/worker/provisioned-workspace.d.ts +5 -5
- package/dist/worker/provisioned-workspace.js +5 -5
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +58 -54
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +12 -0
- package/dist/worker/schema-gate.d.ts +49 -0
- package/dist/worker/schema-gate.js +226 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +96 -0
- package/dist/worker/shared-repo-git.js +459 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +136 -0
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +5 -3
- package/dist/worker/task-runner.js +287 -186
- package/dist/worker/worker-context.d.ts +36 -37
- package/package.json +35 -7
- package/dist/editor/EditorPanes.d.ts +0 -10
- package/dist/editor/EditorPanes.js +0 -98
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -67
- package/dist/editor/components/EditorSidebar.js +0 -22
package/dist/editor/Editor.js
CHANGED
|
@@ -1,39 +1,76 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useState, useRef } from 'react';
|
|
4
|
-
import { ActionIcon, Alert, Box,
|
|
5
|
-
import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, } from '@tabler/icons-react';
|
|
4
|
+
import { ActionIcon, Alert, Box, Button, Menu, Paper, Skeleton, Stack, Text, Tooltip, } from '@mantine/core';
|
|
5
|
+
import { IconChevronDown, IconChevronUp, IconDots, IconFolderPlus, IconPlus, IconSearch, } from '@tabler/icons-react';
|
|
6
|
+
import { useMediaQuery } from '@mantine/hooks';
|
|
6
7
|
import { notifications } from '@mantine/notifications';
|
|
8
|
+
const noop = () => { };
|
|
7
9
|
import { EntryNavigator } from './EntryNavigator.js';
|
|
8
10
|
import { FormRenderer } from './FormRenderer.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import { PreviewPane } from './PreviewPane.js';
|
|
12
|
+
import { clientOperatingStrategy } from '../operating-mode/client.js';
|
|
13
|
+
import { EditorShell } from './shell/EditorShell.js';
|
|
14
|
+
import { PaneLayout } from './shell/PaneLayout.js';
|
|
15
|
+
import { SidePanelHost } from './shell/SidePanelHost.js';
|
|
11
16
|
import { CanopyCMSProvider } from './theme.js';
|
|
12
17
|
import { BranchManager } from './BranchManager.js';
|
|
18
|
+
import { BranchesDrawer } from './components/BranchesDrawer.js';
|
|
13
19
|
import { CommentsPanel } from './CommentsPanel.js';
|
|
14
20
|
import { GroupManager } from './GroupManager.js';
|
|
15
21
|
import { PermissionManager } from './PermissionManager.js';
|
|
16
22
|
import { SystemHealthPanel } from './admin/SystemHealthPanel.js';
|
|
17
23
|
// Import directly from helpers to avoid server-only code in authorization barrel
|
|
18
|
-
import { isAdmin } from '../authorization/helpers.js';
|
|
19
|
-
import {
|
|
24
|
+
import { isAdmin, isPrivileged, isReviewer } from '../authorization/helpers.js';
|
|
25
|
+
import { fieldBreadcrumb } from './field-breadcrumb.js';
|
|
26
|
+
import { buildPreviewRoute, buildPreviewSrc, buildCollectionLabels, entryStatusLabel, entryTypeLabel, buildBreadcrumbSegments, } from './editor-utils.js';
|
|
20
27
|
import { useEditorLayout, useDraftManager, useEntryManager, useGroupManager, usePermissionManager, useCommentSystem, useBranchManager, useUserContext, useSchemaManager, } from './hooks/index.js';
|
|
21
28
|
import { useBranchActions } from './hooks/useBranchActions.js';
|
|
29
|
+
import { CONTENT_PANEL_WIDTH_MAX, CONTENT_PANEL_WIDTH_MIN, editorLayoutStorageKey, } from './hooks/useEditorLayout.js';
|
|
30
|
+
import { EntrySchemaUnavailableError } from './hooks/useEntryManager.js';
|
|
22
31
|
import { useEntryLinkResolution } from './hooks/useEntryLinkResolution.js';
|
|
23
|
-
import {
|
|
32
|
+
import { useReferenceResolution } from './hooks/useReferenceResolution.js';
|
|
33
|
+
import { usePreviewMarks } from './hooks/usePreviewMarks.js';
|
|
34
|
+
import { BranchLockBanner, EditorHeader, EditorRail } from './components/index.js';
|
|
35
|
+
import { showSaveAllResult } from './components/SaveAllResult.js';
|
|
24
36
|
import { RenameEntryModal } from './components/RenameEntryModal.js';
|
|
37
|
+
import { QuickOpenPalette } from './components/QuickOpenPalette.js';
|
|
38
|
+
import { rememberOpener, returnTarget } from './utils/focus-return.js';
|
|
39
|
+
import { modKeyLabel } from './platform.js';
|
|
40
|
+
import { EDITOR_ACTIONS } from './copy.js';
|
|
25
41
|
import { EntryCreateModal } from './components/EntryCreateModal.js';
|
|
26
42
|
import { ConfirmDeleteModal } from './components/ConfirmDeleteModal.js';
|
|
43
|
+
import { ReferencedByList, referencedDeleteMessage } from './components/ReferencedByList.js';
|
|
27
44
|
import { NoEditPermissionNotice } from './components/NoEditPermissionNotice.js';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
45
|
+
import { ReadOnlyDraftNotice } from './components/ReadOnlyDraftNotice.js';
|
|
46
|
+
import { UnavailableEntryNotice } from './components/UnavailableEntryNotice.js';
|
|
47
|
+
import { unavailableSchemaRefs } from './unavailable-entry-type.js';
|
|
48
|
+
import { EditorCrashBoundary } from './components/EditorCrashScreen.js';
|
|
49
|
+
import { CollectionEditor, } from './schema-editor/index.js';
|
|
50
|
+
import { collectionDraftLanded } from './schema-editor/collection-draft.js';
|
|
51
|
+
import { hideCollectionOutcomeUnknown } from './hooks/useSchemaManager.js';
|
|
52
|
+
import { AssetContextProvider, SiteMdxAllowContext, authenticatedAssetBase, useApiClient, } from './context/index.js';
|
|
30
53
|
import { EntryLinkContext } from './fields/entry-link/index.js';
|
|
31
54
|
import { MediaLibrary } from './media/MediaLibrary.js';
|
|
55
|
+
import { removalAnchors, restoreRemoval, openThreadsWithin, restoredFieldPath, } from './undo-removal.js';
|
|
56
|
+
import { formatCanopyPath } from './canopy-path.js';
|
|
57
|
+
import { itemKeysOnPath, resolveThreadPath, withItemKeys } from '../item-keys.js';
|
|
58
|
+
import { ENTRY_NOT_LOADED_COMMENT_MESSAGE, NEW_ITEM_COMMENT_MESSAGE, } from './comments/item-anchor-copy.js';
|
|
59
|
+
import { findFieldTarget, focusFieldTarget } from './field-target.js';
|
|
60
|
+
import { UndoToastMessage } from './components/UndoToastMessage.js';
|
|
61
|
+
import { getNotificationDuration } from './utils/env.js';
|
|
62
|
+
import { MayHaveLandedError } from './unanswered.js';
|
|
63
|
+
// Stable fallbacks, so reference resolution's memo and effect don't re-run on every render
|
|
64
|
+
// while no entry is open.
|
|
65
|
+
const EMPTY_SCHEMA = [];
|
|
66
|
+
const CONTENT_PANEL_ID = 'canopy-content-panel';
|
|
67
|
+
const EMPTY_VALUE = {};
|
|
32
68
|
/**
|
|
33
69
|
* High-level editor wrapper that wires entry navigation, form rendering,
|
|
34
70
|
* saving/loading, and preview rendering using entry definitions.
|
|
35
71
|
*/
|
|
36
|
-
export const Editor = (
|
|
72
|
+
export const Editor = (props) => (_jsx(EditorCrashBoundary, { themeOptions: props.themeOptions, children: _jsx(EditorContent, { ...props }) }));
|
|
73
|
+
const EditorContent = ({ entries, title, siteTitle = title, branchName = '', collections, contentRoot, entryLinkUrl, initialSelectedId, initialValues, renderPreview, onCreateEntry, themeOptions, operatingMode, previewBaseByCollection, previewPrefix, mdxAllow, currentUser = 'current-user', canResolveComments = true, basePath, AccountComponent, onAccountClick, onLogoutClick, customRenderers, }) => {
|
|
37
74
|
const [branchesLoading, setBranchesLoading] = useState(false);
|
|
38
75
|
const [entriesLoading, setEntriesLoading] = useState(false);
|
|
39
76
|
const [commentsLoading] = useState(false);
|
|
@@ -47,6 +84,29 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
47
84
|
const [editingCollection, setEditingCollection] = useState(null);
|
|
48
85
|
const [collectionEditorParentPath, setCollectionEditorParentPath] = useState(undefined);
|
|
49
86
|
const [collectionEditorError, setCollectionEditorError] = useState(null);
|
|
87
|
+
// What the last unanswered collection create or details save sent, until its dialog's opener
|
|
88
|
+
// sees it land on a fresh read, a matching save succeeds, or the user discards it.
|
|
89
|
+
const collectionDraftRef = useRef(null);
|
|
90
|
+
// The draft the open dialog was filled from, if any: its save replaces that draft.
|
|
91
|
+
const refilledDraftRef = useRef(null);
|
|
92
|
+
// What the dialog fills in, decided by its opener against a fresh tree.
|
|
93
|
+
const [restoredCollectionInput, setRestoredCollectionInput] = useState(null);
|
|
94
|
+
// Each open or close of the collection dialog moves it on, so a slow open answers only if current.
|
|
95
|
+
const collectionOpeningRef = useRef(0);
|
|
96
|
+
const [quickOpenOpen, setQuickOpenOpen] = useState(false);
|
|
97
|
+
const [quickOpenFromOverlay, setQuickOpenFromOverlay] = useState(false);
|
|
98
|
+
const navigatorToggleRef = useRef(null);
|
|
99
|
+
const mediaButtonRef = useRef(null);
|
|
100
|
+
const branchToggleRef = useRef(null);
|
|
101
|
+
const settingsToggleRef = useRef(null);
|
|
102
|
+
// Null when the branches drawer opened from a menu item; see `rememberOpener`.
|
|
103
|
+
const branchOpenerRef = useRef(null);
|
|
104
|
+
// Null when the panel opened from a menu item, which unmounts on click, or with focus on the page.
|
|
105
|
+
const navigatorOpenerRef = useRef(null);
|
|
106
|
+
const quickOpenOpenRef = useRef(false);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
quickOpenOpenRef.current = quickOpenOpen;
|
|
109
|
+
}, [quickOpenOpen]);
|
|
50
110
|
const [renameModalOpen, setRenameModalOpen] = useState(false);
|
|
51
111
|
const [renamingEntry, setRenamingEntry] = useState(null);
|
|
52
112
|
const [renameModalError, setRenameModalError] = useState(null);
|
|
@@ -55,27 +115,53 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
55
115
|
const [deletingCollectionPath, setDeletingCollectionPath] = useState(null);
|
|
56
116
|
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false);
|
|
57
117
|
const [deletingEntryPath, setDeletingEntryPath] = useState(null);
|
|
118
|
+
// Named at open: a delete that lands takes the entry out of the list while the dialog fades.
|
|
119
|
+
const [deletingEntryLabel, setDeletingEntryLabel] = useState();
|
|
120
|
+
// Set once the server refuses a delete because other entries reference the entry.
|
|
121
|
+
const [deletingEntryReferencedBy, setDeletingEntryReferencedBy] = useState(null);
|
|
58
122
|
const [deleteInProgress, setDeleteInProgress] = useState(false);
|
|
59
|
-
//
|
|
60
|
-
const [
|
|
61
|
-
const
|
|
123
|
+
// A rename or delete asked for from the top bar's entry menu, whose item is gone once it acts.
|
|
124
|
+
const [entryChangeFromMenu, setEntryChangeFromMenu] = useState(false);
|
|
125
|
+
const allContentCrumbRef = useRef(null);
|
|
62
126
|
// Draft compile/render error reported by the preview page (null = renders cleanly)
|
|
63
127
|
const [previewError, setPreviewError] = useState(null);
|
|
64
128
|
const apiClient = useApiClient();
|
|
65
129
|
const { userContext } = useUserContext();
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
130
|
+
// Admin surfaces (permissions, groups, system health, collection management and entry order,
|
|
131
|
+
// whose API routes all take the `admin` guard) render only for admins: each menu item, action
|
|
132
|
+
// and drawer mount is gated here. Until whoami answers, the user counts as no admin.
|
|
133
|
+
const userIsAdmin = isAdmin(userContext?.groups);
|
|
134
|
+
// Name lookups take the `privileged` guard (admins and reviewers); no lookup is sent that would
|
|
135
|
+
// only be refused. Comment surfaces name their people for everyone (getCommentUserMetadata).
|
|
136
|
+
const canResolveUserNames = isPrivileged(userContext?.groups);
|
|
137
|
+
// Reviewers and admins resolve any thread; everyone else only the threads they started.
|
|
138
|
+
const canResolveAnyComment = canResolveComments && isReviewer(userContext?.groups);
|
|
139
|
+
// The signed-in user, so "started by me" matches the thread authors the server records.
|
|
140
|
+
const commentUserId = userContext?.userId ?? currentUser;
|
|
141
|
+
// Below 64em (1024px) the editor starts stacked and the content panel never docks.
|
|
142
|
+
const wideWindow = useMediaQuery('(min-width: 64em)', true, { getInitialValueInEffect: false }) !== false;
|
|
143
|
+
const { loaded: layoutLoaded, layout, setLayout, highlightEnabled, setHighlightEnabled, sideSplitPercent, stackedSplitPercent, setSplitPercent, previewWidth, setPreviewWidth, contentPanelOpen, setContentPanelOpen, contentPanelWidth, setContentPanelWidth, } = useEditorLayout({
|
|
144
|
+
storageKey: editorLayoutStorageKey(basePath),
|
|
145
|
+
defaultLayout: wideWindow ? 'side' : 'stacked',
|
|
146
|
+
});
|
|
147
|
+
// In the stacked layout the content panel lays over the form only, so the preview stays in view.
|
|
148
|
+
const contentPanelDocked = layout === 'side' && wideWindow;
|
|
71
149
|
// Comments state (shared between useCommentSystem and useBranchManager)
|
|
72
150
|
const [commentsForBranchSummaries, setCommentsForBranchSummaries] = useState([]);
|
|
73
151
|
// 1. Branch manager (provides branchNameState, no dependencies)
|
|
152
|
+
const resolveUnsavedRef = useRef(async () => ({
|
|
153
|
+
count: 0,
|
|
154
|
+
labels: [],
|
|
155
|
+
}));
|
|
156
|
+
const applySubmitStampsRef = useRef(() => { });
|
|
74
157
|
const { branchName: branchNameState, setBranchName, branchSummaries, currentBranch, addCreatedBranch, handleSubmit, handleWithdraw, handleRequestChanges, handleDelete, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
75
158
|
initialBranch: branchName,
|
|
76
159
|
operatingMode,
|
|
77
160
|
setBusy: setBranchesLoading,
|
|
78
161
|
comments: commentsForBranchSummaries,
|
|
162
|
+
// The draft manager comes later, as it depends on this hook's branch.
|
|
163
|
+
getUnsaved: () => resolveUnsavedRef.current(),
|
|
164
|
+
onStamped: (stamped) => applySubmitStampsRef.current(stamped),
|
|
79
165
|
});
|
|
80
166
|
// Content mutations (add/delete/rename/reorder entries) are locked whenever the
|
|
81
167
|
// server says writes are blocked -- the protected base branch, or a workflow
|
|
@@ -90,7 +176,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
90
176
|
// loading state during that window anyway.
|
|
91
177
|
const branchContentLocked = currentBranch?.writeBlocked ?? true;
|
|
92
178
|
// 2. Entry manager (depends on branchNameState, owns selectedPath)
|
|
93
|
-
const { selectedPath, setSelectedPath, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, navigatorOpen, setNavigatorOpen, refreshEntries, handleCreateEntry, renameEntry, loadEntry, saveEntry, getEntryVersion, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
|
|
179
|
+
const { selectedPath, setSelectedPath, closeEntry, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, entriesLoadFailed, navigatorOpen, setNavigatorOpen, refreshEntries, refreshContent, handleCreateEntry, renameEntry, loadEntry, readEntryValue, saveEntry, getEntryVersion, adoptEntryVersion, followMovedEntry, recreateEntry, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
|
|
94
180
|
initialEntries: entries,
|
|
95
181
|
initialSelectedId,
|
|
96
182
|
branchName: branchNameState,
|
|
@@ -104,6 +190,28 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
104
190
|
}),
|
|
105
191
|
setBusy: setEntriesLoading,
|
|
106
192
|
});
|
|
193
|
+
// The content overlay and a maximized preview hide the form; `navigatorOpen` is only the
|
|
194
|
+
// overlay's. Anything that focuses something in the form reveals it first. Opening an entry
|
|
195
|
+
// closes the overlay only, so the preview stays maximized across entries (see
|
|
196
|
+
// `previewShownMaximized` for the loads in between).
|
|
197
|
+
const [previewMaximized, setPreviewMaximized] = useState(false);
|
|
198
|
+
const revealForm = () => {
|
|
199
|
+
setNavigatorOpen(false);
|
|
200
|
+
setPreviewMaximized(false);
|
|
201
|
+
};
|
|
202
|
+
const openEntry = (path) => {
|
|
203
|
+
setSelectedPath(path);
|
|
204
|
+
setNavigatorOpen(false);
|
|
205
|
+
};
|
|
206
|
+
// An open overlay docks open, as the URL's ask for no entry does at load; `navigatorOpen` is only
|
|
207
|
+
// the overlay's, so a docked panel never resurfaces as one. Waits for the stored layout, whose
|
|
208
|
+
// arrival could otherwise dock an overlay the stacked layout keeps.
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (layoutLoaded && contentPanelDocked && navigatorOpen) {
|
|
211
|
+
setContentPanelOpen(true);
|
|
212
|
+
setNavigatorOpen(false);
|
|
213
|
+
}
|
|
214
|
+
}, [layoutLoaded, contentPanelDocked, navigatorOpen, setContentPanelOpen, setNavigatorOpen]);
|
|
107
215
|
// Keeps the entry-type list referentially stable while the create modal shows
|
|
108
216
|
// the same collection: a new array on every render would churn Mantine's
|
|
109
217
|
// Select `data` identity and give EntryCreateModal's form-seeding effect a
|
|
@@ -114,52 +222,72 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
114
222
|
format: et.format,
|
|
115
223
|
default: et.default,
|
|
116
224
|
maxItems: et.maxItems,
|
|
225
|
+
unavailable: et.unavailable,
|
|
226
|
+
titleField: et.titleField,
|
|
117
227
|
})) ?? [], [createModalCollection]);
|
|
228
|
+
// The route a slug's page is served at, shown under the slug: `buildPreviewSrc`'s route before
|
|
229
|
+
// its prefix, base path, trailing slash and `?branch=`. Undefined when the entry has no page.
|
|
230
|
+
const previewPathFor = (entry) => buildPreviewRoute(entry, { previewBaseByCollection, contentRoot });
|
|
118
231
|
// Use collections from API (falls back to props if not loaded yet)
|
|
119
232
|
const activeCollections = collectionsFromApi.length > 0 ? collectionsFromApi : collections;
|
|
120
|
-
//
|
|
121
|
-
// preview never lingers for entries whose FormRenderer doesn't mount
|
|
122
|
-
// (still loading, canEdit === false, or empty schema)
|
|
233
|
+
// A preview error belongs to the entry that reported it.
|
|
123
234
|
useEffect(() => {
|
|
124
235
|
setPreviewError(null);
|
|
125
|
-
setPreviewData({});
|
|
126
|
-
setPreviewLoadingState({});
|
|
127
236
|
}, [currentEntry?.contentId]);
|
|
128
237
|
// 3. Draft manager (depends on branchNameState, selectedPath from useEntryManager)
|
|
129
|
-
const { setDrafts, loadedValues, setLoadedValues, effectiveValue, modifiedCount, editedFiles, handleSave, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty,
|
|
238
|
+
const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, singleSaving, lastSaved, handleSaveAll, saveAllProgress, isSaveAllRunning, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
|
|
130
239
|
branchName: branchNameState,
|
|
131
240
|
selectedPath,
|
|
132
241
|
currentEntry,
|
|
133
242
|
entries: entriesState,
|
|
134
243
|
initialValues,
|
|
135
244
|
loadEntry,
|
|
245
|
+
readEntryValue,
|
|
136
246
|
saveEntry,
|
|
137
247
|
getEntryVersion,
|
|
248
|
+
adoptEntryVersion,
|
|
249
|
+
followMovedEntry,
|
|
250
|
+
recreateEntry,
|
|
138
251
|
setBusy: setEntriesLoading,
|
|
252
|
+
// The errors are in the form.
|
|
253
|
+
onSaveBlocked: () => setPreviewMaximized(false),
|
|
139
254
|
onSaved: () => {
|
|
140
255
|
void refreshEntries(branchNameState).catch((err) => console.error('Failed to refresh entries after save', err));
|
|
141
256
|
},
|
|
257
|
+
// An Undo built on a discarded draft would put back an item the server's copy already has.
|
|
258
|
+
onDraftsReset: (contentId) => dismissUndoToasts(contentId),
|
|
142
259
|
});
|
|
143
|
-
|
|
144
|
-
|
|
260
|
+
resolveUnsavedRef.current = resolveUnsaved;
|
|
261
|
+
applySubmitStampsRef.current = applySubmitStamps;
|
|
262
|
+
// 4. Branch actions (depends on resolveUnsaved, setBranchName)
|
|
263
|
+
const { handleBranchChange, confirmCreate: confirmCreateBranch, handleCreateBranch, confirmOpen: branchConfirmOpen, } = useBranchActions({
|
|
145
264
|
branchName: branchNameState,
|
|
146
265
|
setBranchName,
|
|
147
|
-
|
|
266
|
+
getUnsaved: resolveUnsaved,
|
|
148
267
|
onReloadBranches: () => loadBranches(),
|
|
149
268
|
onBranchCreated: addCreatedBranch,
|
|
269
|
+
userId: userContext?.userId,
|
|
150
270
|
});
|
|
151
271
|
// 5. Comment system (depends on branchNameState)
|
|
152
|
-
const { comments, focusedFieldPath, highlightThreadId, commentsPanelOpen, setCommentsPanelOpen, handleAddComment, handleResolveThread, handleJumpToField, handleJumpToEntry, handleJumpToBranch, } = useCommentSystem({
|
|
272
|
+
const { comments, getCommentUser, focusedFieldPath, highlightThreadId, commentsPanelOpen, setCommentsPanelOpen, handleAddComment, handleResolveThread, handleJumpToField, handleJumpToEntry, handleJumpToBranch, } = useCommentSystem({
|
|
153
273
|
branchName: branchNameState,
|
|
154
274
|
selectedPath,
|
|
155
275
|
currentEntry,
|
|
156
|
-
currentUser,
|
|
157
|
-
canResolveComments,
|
|
276
|
+
currentUser: commentUserId,
|
|
277
|
+
canResolveComments: canResolveAnyComment,
|
|
158
278
|
setSelectedPath,
|
|
159
|
-
setBranchManagerOpen
|
|
279
|
+
setBranchManagerOpen: (open) => {
|
|
280
|
+
if (open)
|
|
281
|
+
branchOpenerRef.current = rememberOpener();
|
|
282
|
+
setBranchManagerOpen(open);
|
|
283
|
+
},
|
|
160
284
|
onCommentsChange: setCommentsForBranchSummaries,
|
|
285
|
+
onRevealForm: revealForm,
|
|
161
286
|
});
|
|
162
287
|
const { groupsData, groupsLoading, handleSaveGroups, handleSearchUsers, handleGetUserMetadata, handleSearchExternalGroups, } = useGroupManager({ isOpen: groupManagerOpen });
|
|
288
|
+
// The comments and branches responses name their people, without email; a privileged lookup
|
|
289
|
+
// adds the email.
|
|
290
|
+
const getCommentUserMetadata = canResolveUserNames ? handleGetUserMetadata : getCommentUser;
|
|
163
291
|
const { permissionsData, permissionsLoading, handleSavePermissions, handleListGroups } = usePermissionManager({
|
|
164
292
|
isOpen: permissionManagerOpen,
|
|
165
293
|
});
|
|
@@ -168,8 +296,224 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
168
296
|
branchName: branchNameState,
|
|
169
297
|
onSchemaChange: () => refreshEntries(branchNameState),
|
|
170
298
|
});
|
|
299
|
+
// Read-only content shows the saved value, never a draft; a restored draft is kept, unseen,
|
|
300
|
+
// until it is discarded or the branch unlocks.
|
|
301
|
+
const contentReadOnly = branchContentLocked || currentEntry?.canEdit === false;
|
|
302
|
+
const displayValue = contentReadOnly ? loadedValue : effectiveValue;
|
|
303
|
+
// Read when an edit lands, which an upload or other async field work can do renders later.
|
|
304
|
+
const contentReadOnlyRef = useRef(contentReadOnly);
|
|
305
|
+
contentReadOnlyRef.current = contentReadOnly;
|
|
306
|
+
// Undo for a form Remove: a toast whose Undo puts the item back into the entry's draft as it
|
|
307
|
+
// is then, through a functional update, so edits made since are kept. It acts only on the
|
|
308
|
+
// branch and entry it was raised for, and its toast closes when either changes.
|
|
309
|
+
const loadedValuesRef = useRef(loadedValues);
|
|
310
|
+
loadedValuesRef.current = loadedValues;
|
|
311
|
+
const draftsRef = useRef(drafts);
|
|
312
|
+
draftsRef.current = drafts;
|
|
313
|
+
// Open Undo toasts, by id, with the entry each belongs to.
|
|
314
|
+
const undoToastsRef = useRef(new Map());
|
|
315
|
+
const nextUndoToastRef = useRef(0);
|
|
316
|
+
const dismissUndoToasts = (contentId) => {
|
|
317
|
+
undoToastsRef.current.forEach((entry, id) => {
|
|
318
|
+
if (contentId !== undefined && entry !== contentId)
|
|
319
|
+
return;
|
|
320
|
+
notifications.hide(id);
|
|
321
|
+
undoToastsRef.current.delete(id);
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
const handleFieldRemoved = (removal) => {
|
|
325
|
+
const contentId = currentEntry?.contentId;
|
|
326
|
+
if (!contentId || contentReadOnlyRef.current)
|
|
327
|
+
return;
|
|
328
|
+
const branch = branchNameState;
|
|
329
|
+
const fields = schema;
|
|
330
|
+
// Taken from the value this render shows, which the removal has not reached yet.
|
|
331
|
+
const anchored = {
|
|
332
|
+
...removal,
|
|
333
|
+
anchors: displayValue ? removalAnchors(fields, displayValue, removal) : undefined,
|
|
334
|
+
};
|
|
335
|
+
const doing = removal.action === 'clear' ? 'clearing' : 'removing';
|
|
336
|
+
// Open threads keyed inside what was removed leave their fields for the entry's thread list.
|
|
337
|
+
const movedThreads = currentEntry
|
|
338
|
+
? openThreadsWithin(displayComments, currentEntry.path, removal.kind === 'list-item' ? [...removal.listPath, removal.index] : removal.path)
|
|
339
|
+
: 0;
|
|
340
|
+
nextUndoToastRef.current += 1;
|
|
341
|
+
const id = `canopy-undo-${nextUndoToastRef.current}`;
|
|
342
|
+
let used = false;
|
|
343
|
+
const undo = () => {
|
|
344
|
+
// A closing toast stays clickable while it animates out: act once, and only while open.
|
|
345
|
+
if (used)
|
|
346
|
+
return;
|
|
347
|
+
used = true;
|
|
348
|
+
const open = undoToastsRef.current.has(id);
|
|
349
|
+
notifications.hide(id);
|
|
350
|
+
undoToastsRef.current.delete(id);
|
|
351
|
+
if (!open) {
|
|
352
|
+
notifications.show({
|
|
353
|
+
message: `Too late to undo ${doing} ${removal.label}.`,
|
|
354
|
+
color: 'yellow',
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (currentBranchRef.current !== branch ||
|
|
359
|
+
currentContentIdRef.current !== contentId ||
|
|
360
|
+
contentReadOnlyRef.current) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const now = draftsRef.current[contentId] ?? loadedValuesRef.current[contentId];
|
|
364
|
+
if (!now || !restoreRemoval(fields, now, anchored)) {
|
|
365
|
+
notifications.show({
|
|
366
|
+
message: `Couldn't undo ${doing} ${removal.label}: that part of the entry has changed. ${removal.action === 'clear' ? 'Fill it in again' : 'Add it again'} if you still need it.`,
|
|
367
|
+
color: 'yellow',
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const restoredPath = formatCanopyPath(restoredFieldPath(fields, now, removal));
|
|
372
|
+
setDrafts((prev) => {
|
|
373
|
+
const base = prev[contentId] ?? loadedValuesRef.current[contentId];
|
|
374
|
+
const restored = base ? restoreRemoval(fields, base, anchored) : undefined;
|
|
375
|
+
return restored ? { ...prev, [contentId]: restored } : prev;
|
|
376
|
+
});
|
|
377
|
+
// The toast's Undo button unmounts with it, so focus goes to the restored row once rendered.
|
|
378
|
+
revealForm();
|
|
379
|
+
requestAnimationFrame(() => {
|
|
380
|
+
const target = findFieldTarget(restoredPath);
|
|
381
|
+
if (target)
|
|
382
|
+
focusFieldTarget(target.element);
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
undoToastsRef.current.set(id, contentId);
|
|
386
|
+
const toast = {
|
|
387
|
+
id,
|
|
388
|
+
message: (_jsx(UndoToastMessage, { label: removal.label, action: removal.action, note: movedThreads > 0
|
|
389
|
+
? `Its ${movedThreads === 1 ? 'comment thread' : `${movedThreads} comment threads`} moved to "Comment on this entry".`
|
|
390
|
+
: undefined, onUndo: undo, onHold: () => notifications.update({ ...toast, autoClose: false }), onRelease: () => notifications.update(toast) })),
|
|
391
|
+
autoClose: getNotificationDuration(8000),
|
|
392
|
+
withCloseButton: true,
|
|
393
|
+
onClose: () => undoToastsRef.current.delete(id),
|
|
394
|
+
};
|
|
395
|
+
notifications.show(toast);
|
|
396
|
+
};
|
|
397
|
+
useEffect(() => () => dismissUndoToasts(), [currentEntry?.contentId, branchNameState]);
|
|
398
|
+
// Only once the branch has answered, so the fail-closed lock while it loads raises no notice.
|
|
399
|
+
const hiddenDraftNotice = contentReadOnly && currentBranch !== undefined && !!currentEntry && isSelectedDirty();
|
|
171
400
|
const collectionLabels = useMemo(() => buildCollectionLabels(activeCollections), [activeCollections]);
|
|
172
|
-
const schema = currentEntry?.schema ??
|
|
401
|
+
const schema = currentEntry?.schema ?? EMPTY_SCHEMA;
|
|
402
|
+
// Field threads follow their items' keys, placed in their entry's value where it is at hand.
|
|
403
|
+
const entryValue = (entry) => {
|
|
404
|
+
if (entry.path === currentEntry?.path)
|
|
405
|
+
return displayValue;
|
|
406
|
+
// As `displayValue` would show it: read-only content shows its saved value, not a draft.
|
|
407
|
+
const readOnly = branchContentLocked || entry.canEdit === false;
|
|
408
|
+
return (readOnly ? undefined : drafts[entry.contentId]) ?? loadedValues[entry.contentId];
|
|
409
|
+
};
|
|
410
|
+
const displayComments = useMemo(() => {
|
|
411
|
+
const byPath = new Map(entriesState.map((entry) => [entry.path, entry]));
|
|
412
|
+
return comments.map((thread) => {
|
|
413
|
+
const entry = thread.entryPath ? byPath.get(thread.entryPath) : undefined;
|
|
414
|
+
const value = entry ? entryValue(entry) : undefined;
|
|
415
|
+
if (thread.type !== 'field' || !thread.canopyPath || !thread.itemKeys?.length)
|
|
416
|
+
return thread;
|
|
417
|
+
if (!entry || !value)
|
|
418
|
+
return thread;
|
|
419
|
+
const path = resolveThreadPath(entry.schema, value, thread.canopyPath, thread.itemKeys);
|
|
420
|
+
if (path === null)
|
|
421
|
+
return { ...thread, onRemovedItem: true };
|
|
422
|
+
return path === thread.canopyPath ? thread : { ...thread, canopyPath: path };
|
|
423
|
+
});
|
|
424
|
+
// `entryValue` reads the open entry, its shown value, and the drafts and loads.
|
|
425
|
+
}, [
|
|
426
|
+
comments,
|
|
427
|
+
entriesState,
|
|
428
|
+
currentEntry,
|
|
429
|
+
displayValue,
|
|
430
|
+
drafts,
|
|
431
|
+
loadedValues,
|
|
432
|
+
branchContentLocked,
|
|
433
|
+
]);
|
|
434
|
+
// A jump into another entry waits for its value to place the thread; the stored path may be stale.
|
|
435
|
+
const [pendingFieldJump, setPendingFieldJump] = useState(null);
|
|
436
|
+
const jumpToThreadField = (entryPath, canopyPath, threadId) => {
|
|
437
|
+
if (entryPath === currentEntry?.path) {
|
|
438
|
+
handleJumpToField(entryPath, canopyPath, threadId);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
setPendingFieldJump({ entryPath, canopyPath, threadId });
|
|
442
|
+
setSelectedPath(entryPath);
|
|
443
|
+
};
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
if (!pendingFieldJump)
|
|
446
|
+
return;
|
|
447
|
+
if (selectedPath !== pendingFieldJump.entryPath) {
|
|
448
|
+
setPendingFieldJump(null);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (currentEntry?.path !== pendingFieldJump.entryPath || !displayValue)
|
|
452
|
+
return;
|
|
453
|
+
const { entryPath, canopyPath, threadId } = pendingFieldJump;
|
|
454
|
+
setPendingFieldJump(null);
|
|
455
|
+
const placed = displayComments.find((t) => t.id === threadId);
|
|
456
|
+
// A thread whose item is gone lives on its entry, not at the index it was made at.
|
|
457
|
+
if (placed?.onRemovedItem)
|
|
458
|
+
handleJumpToEntry(entryPath, threadId);
|
|
459
|
+
else
|
|
460
|
+
handleJumpToField(entryPath, placed?.canopyPath ?? canopyPath, threadId);
|
|
461
|
+
}, [pendingFieldJump, selectedPath, currentEntry, displayValue, displayComments]);
|
|
462
|
+
// A thread on an item only the draft has is refused: a discard would leave it on nothing.
|
|
463
|
+
const handleAddDisplayedComment = async (text, type, entryPath, canopyPath, threadId) => {
|
|
464
|
+
if (type !== 'field' ||
|
|
465
|
+
threadId ||
|
|
466
|
+
!canopyPath ||
|
|
467
|
+
!currentEntry ||
|
|
468
|
+
entryPath !== currentEntry.path ||
|
|
469
|
+
!displayValue) {
|
|
470
|
+
return handleAddComment(text, type, entryPath, canopyPath, threadId);
|
|
471
|
+
}
|
|
472
|
+
const itemKeys = itemKeysOnPath(currentEntry.schema, displayValue, canopyPath);
|
|
473
|
+
if (!itemKeys?.length)
|
|
474
|
+
return handleAddComment(text, type, entryPath, canopyPath);
|
|
475
|
+
// The field's control says so before anyone types (`commentBlockedReason`); this is the backstop.
|
|
476
|
+
if (!loadedValue)
|
|
477
|
+
throw new Error(ENTRY_NOT_LOADED_COMMENT_MESSAGE);
|
|
478
|
+
if (onUnsavedItem(itemKeys, canopyPath))
|
|
479
|
+
throw new Error(NEW_ITEM_COMMENT_MESSAGE);
|
|
480
|
+
return handleAddComment(text, type, entryPath, canopyPath, undefined, itemKeys);
|
|
481
|
+
};
|
|
482
|
+
const onUnsavedItem = (itemKeys, canopyPath) => !currentEntry ||
|
|
483
|
+
!loadedValue ||
|
|
484
|
+
resolveThreadPath(currentEntry.schema, loadedValue, canopyPath, itemKeys) === null;
|
|
485
|
+
// Says nothing until the saved value is known, which is what tells a new item from a saved one.
|
|
486
|
+
const commentBlockedReason = (canopyPath) => {
|
|
487
|
+
if (!currentEntry || !displayValue || !loadedValue)
|
|
488
|
+
return undefined;
|
|
489
|
+
const itemKeys = itemKeysOnPath(currentEntry.schema, displayValue, canopyPath);
|
|
490
|
+
return itemKeys?.length && onUnsavedItem(itemKeys, canopyPath)
|
|
491
|
+
? NEW_ITEM_COMMENT_MESSAGE
|
|
492
|
+
: undefined;
|
|
493
|
+
};
|
|
494
|
+
const commentEntryLabel = (entryPath) => entriesState.find((e) => e.path === entryPath)?.label;
|
|
495
|
+
// Only the open entry's value is at hand; for another entry the breadcrumb falls back to its
|
|
496
|
+
// schema's field labels.
|
|
497
|
+
// Items are named only where a value placed the thread; otherwise its indexes may be stale.
|
|
498
|
+
const commentFieldLabel = (thread) => {
|
|
499
|
+
const entry = entriesState.find((e) => e.path === thread.entryPath);
|
|
500
|
+
if (!entry || !thread.canopyPath)
|
|
501
|
+
return undefined;
|
|
502
|
+
const value = entryValue(entry);
|
|
503
|
+
// While a removal is only in the draft, the saved value still holds the item to name.
|
|
504
|
+
const saved = thread.onRemovedItem ? loadedValues[entry.contentId] : undefined;
|
|
505
|
+
const savedPath = saved
|
|
506
|
+
? resolveThreadPath(entry.schema, saved, thread.canopyPath, thread.itemKeys)
|
|
507
|
+
: null;
|
|
508
|
+
if (saved && savedPath !== null) {
|
|
509
|
+
return fieldBreadcrumb(entry.schema, saved, savedPath).join(' › ');
|
|
510
|
+
}
|
|
511
|
+
const placed = !!value && !thread.onRemovedItem;
|
|
512
|
+
const items = placed || !thread.itemKeys?.length;
|
|
513
|
+
return fieldBreadcrumb(entry.schema, placed ? value : undefined, thread.canopyPath, {
|
|
514
|
+
items,
|
|
515
|
+
}).join(' › ');
|
|
516
|
+
};
|
|
173
517
|
// Gated on `loadedValues[contentId]` (not `drafts[contentId]`) so a
|
|
174
518
|
// restored-from-localStorage draft never skips the load: skipping left
|
|
175
519
|
// `loadedValues[contentId]` permanently undefined, which made dirty-
|
|
@@ -186,6 +530,44 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
186
530
|
// that same id. Qualifying the key lets both loads coexist; the branch check
|
|
187
531
|
// at settle time decides which one is allowed to write.
|
|
188
532
|
const loadingEntryIdsRef = useRef(new Set());
|
|
533
|
+
// The entry (keyed like `loadingEntryIdsRef`) whose last read failed, and Retry's bump that
|
|
534
|
+
// runs the load effect below again.
|
|
535
|
+
const [loadFailedKey, setLoadFailedKey] = useState(null);
|
|
536
|
+
const [loadAttempt, setLoadAttempt] = useState(0);
|
|
537
|
+
// Reset during render, not in an effect, so returning to a failed entry never paints its Alert
|
|
538
|
+
// for the frame before its load starts again.
|
|
539
|
+
const selectionKey = `${branchNameState}:${currentEntry?.contentId ?? ''}`;
|
|
540
|
+
const [loadFailedSelection, setLoadFailedSelection] = useState(selectionKey);
|
|
541
|
+
if (loadFailedSelection !== selectionKey) {
|
|
542
|
+
setLoadFailedSelection(selectionKey);
|
|
543
|
+
setLoadFailedKey(null);
|
|
544
|
+
}
|
|
545
|
+
// Entries (keyed like `loadingEntryIdsRef`) whose read the API refused as SCHEMA_UNAVAILABLE.
|
|
546
|
+
// The ref stops the load effect retrying them; the state re-renders the notice.
|
|
547
|
+
const schemaUnavailableRef = useRef(new Set());
|
|
548
|
+
const [schemaUnavailableKeys, setSchemaUnavailableKeys] = useState(new Set());
|
|
549
|
+
// A refetch of entries or schema is the signal the server's schema may have changed, so every
|
|
550
|
+
// refusal is forgotten and the next open of each entry reads it once more. Declared before the
|
|
551
|
+
// load effect so a refetch's retry sees the cleared set in the same commit.
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
if (schemaUnavailableRef.current.size === 0)
|
|
554
|
+
return;
|
|
555
|
+
schemaUnavailableRef.current = new Set();
|
|
556
|
+
setSchemaUnavailableKeys(new Set());
|
|
557
|
+
}, [entriesState, collectionsFromApi]);
|
|
558
|
+
// A successful read (File > Reload) of a refused entry ends the refusal.
|
|
559
|
+
useEffect(() => {
|
|
560
|
+
if (schemaUnavailableRef.current.size === 0)
|
|
561
|
+
return;
|
|
562
|
+
const stale = [...schemaUnavailableRef.current].filter((key) => {
|
|
563
|
+
const [keyBranch, ...id] = key.split(':');
|
|
564
|
+
return keyBranch === branchNameState && loadedValues[id.join(':')] !== undefined;
|
|
565
|
+
});
|
|
566
|
+
if (stale.length === 0)
|
|
567
|
+
return;
|
|
568
|
+
stale.forEach((key) => schemaUnavailableRef.current.delete(key));
|
|
569
|
+
setSchemaUnavailableKeys(new Set(schemaUnavailableRef.current));
|
|
570
|
+
}, [loadedValues, branchNameState]);
|
|
189
571
|
// The contentId this effect is CURRENTLY targeting, kept in sync every
|
|
190
572
|
// render (not in an effect -- it must be current by the time an in-flight
|
|
191
573
|
// load's `finally`/`catch` runs, which can be after several re-renders).
|
|
@@ -213,6 +595,9 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
213
595
|
const load = async () => {
|
|
214
596
|
if (!currentEntry || !contentId || loadedValues[contentId] !== undefined)
|
|
215
597
|
return;
|
|
598
|
+
// An unavailable type has no schema to read it against; the server would refuse the read.
|
|
599
|
+
if (currentEntry.unavailable || schemaUnavailableRef.current.has(inFlightKey))
|
|
600
|
+
return;
|
|
216
601
|
// Guard against overlapping renders re-firing the same in-flight load
|
|
217
602
|
// (e.g. `currentEntry` getting a new object reference from an entries
|
|
218
603
|
// refresh while the fetch below is still pending).
|
|
@@ -220,6 +605,7 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
220
605
|
return;
|
|
221
606
|
loadingEntryIdsRef.current.add(inFlightKey);
|
|
222
607
|
setEntriesLoading(true);
|
|
608
|
+
setLoadFailedKey((failed) => (failed === inFlightKey ? null : failed));
|
|
223
609
|
try {
|
|
224
610
|
const loaded = await loadEntry(currentEntry);
|
|
225
611
|
// A branch switch during the await makes this response another
|
|
@@ -255,93 +641,108 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
255
641
|
}
|
|
256
642
|
};
|
|
257
643
|
load().catch((err) => {
|
|
644
|
+
if (err instanceof EntrySchemaUnavailableError) {
|
|
645
|
+
schemaUnavailableRef.current.add(inFlightKey);
|
|
646
|
+
setSchemaUnavailableKeys((prev) => new Set(prev).add(inFlightKey));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
258
649
|
console.error(err);
|
|
259
650
|
// Same staleness guard as above: don't tell the user an entry failed
|
|
260
651
|
// to load when they've already navigated to a different one (or to a
|
|
261
652
|
// different branch).
|
|
262
653
|
if (currentBranchRef.current === requestBranch && currentContentIdRef.current === contentId) {
|
|
263
654
|
notifications.show({ message: 'Failed to load entry', color: 'red' });
|
|
655
|
+
setLoadFailedKey(inFlightKey);
|
|
264
656
|
}
|
|
265
657
|
});
|
|
266
658
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- stable setters, run only on entry/path/branch/loadedValues change
|
|
267
|
-
}, [currentEntry, loadedValues, selectedPath, branchNameState]);
|
|
659
|
+
}, [currentEntry, loadedValues, selectedPath, branchNameState, loadAttempt]);
|
|
660
|
+
// The collection dialog's entry types, with schema and usage, as the branch has them; null when
|
|
661
|
+
// the API refuses the read or the collection is gone, and a rejection when the read times out.
|
|
662
|
+
const readEntryTypes = async (collectionPath) => {
|
|
663
|
+
const result = await apiClient.schema.getCollection({
|
|
664
|
+
branch: branchNameState,
|
|
665
|
+
collectionPath,
|
|
666
|
+
});
|
|
667
|
+
const withUsage = result.ok ? result.data?.entryTypesWithUsage : undefined;
|
|
668
|
+
return withUsage
|
|
669
|
+
? withUsage.map((et) => ({
|
|
670
|
+
name: et.name,
|
|
671
|
+
label: et.label,
|
|
672
|
+
format: et.format,
|
|
673
|
+
schema: et.schemaRef,
|
|
674
|
+
default: et.default,
|
|
675
|
+
maxItems: et.maxItems,
|
|
676
|
+
usageCount: et.usageCount,
|
|
677
|
+
}))
|
|
678
|
+
: null;
|
|
679
|
+
};
|
|
268
680
|
const handleOpenCollectionEditor = async (collection, parentPath) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}));
|
|
294
|
-
const existingCollection = {
|
|
295
|
-
name: collection.name,
|
|
296
|
-
label: collection.label,
|
|
297
|
-
logicalPath: collection.path,
|
|
298
|
-
entries,
|
|
299
|
-
};
|
|
300
|
-
setEditingCollection(existingCollection);
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
const existingCollection = {
|
|
304
|
-
name: collection.name,
|
|
305
|
-
label: collection.label,
|
|
306
|
-
logicalPath: collection.path,
|
|
307
|
-
entries: (collection.entryTypes ?? []).map((et) => ({
|
|
308
|
-
name: et.name,
|
|
309
|
-
label: et.label,
|
|
310
|
-
format: et.format,
|
|
311
|
-
schema: '',
|
|
312
|
-
default: et.default,
|
|
313
|
-
maxItems: et.maxItems,
|
|
314
|
-
})),
|
|
315
|
-
};
|
|
316
|
-
setEditingCollection(existingCollection);
|
|
317
|
-
}
|
|
681
|
+
const opening = ++collectionOpeningRef.current;
|
|
682
|
+
const branch = branchNameState;
|
|
683
|
+
const stillCurrent = () => opening === collectionOpeningRef.current && currentBranchRef.current === branch;
|
|
684
|
+
// A draft is checked on a fresh tree first: refilling a create that landed would duplicate it.
|
|
685
|
+
const draft = collectionDraftRef.current;
|
|
686
|
+
// A details save that changed the slug and landed is reopened at its new path, never refilled.
|
|
687
|
+
const atNewPath = draft?.kind === 'details' &&
|
|
688
|
+
!!draft.updates.slug &&
|
|
689
|
+
collection?.path ===
|
|
690
|
+
`${draft.collectionPath.slice(0, draft.collectionPath.lastIndexOf('/') + 1)}${draft.updates.slug}`;
|
|
691
|
+
const refills = draft?.branch === branch &&
|
|
692
|
+
(draft.kind === 'create'
|
|
693
|
+
? !collection && draft.input.parentPath === parentPath
|
|
694
|
+
: collection?.path === draft.collectionPath || atNewPath);
|
|
695
|
+
let restore = null;
|
|
696
|
+
if (draft && refills) {
|
|
697
|
+
const fresh = await refreshContent(branch).catch((err) => {
|
|
698
|
+
console.error(err);
|
|
699
|
+
return null;
|
|
700
|
+
});
|
|
701
|
+
if (!stillCurrent())
|
|
702
|
+
return;
|
|
703
|
+
if (fresh && collectionDraftLanded(fresh.collections, draft)) {
|
|
704
|
+
settleCollectionDraft(draft);
|
|
318
705
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
entries: (collection.entryTypes ?? []).map((et) => ({
|
|
325
|
-
name: et.name,
|
|
326
|
-
label: et.label,
|
|
327
|
-
format: et.format,
|
|
328
|
-
schema: '',
|
|
329
|
-
default: et.default,
|
|
330
|
-
maxItems: et.maxItems,
|
|
331
|
-
})),
|
|
332
|
-
};
|
|
333
|
-
setEditingCollection(existingCollection);
|
|
706
|
+
else if (collectionDraftRef.current === draft &&
|
|
707
|
+
!atNewPath &&
|
|
708
|
+
(fresh || draft.kind === 'details')) {
|
|
709
|
+
// A create no read has ruled out stays unfilled; a details save resends the same change.
|
|
710
|
+
restore = draft.kind === 'create' ? draft.input : draft.updates;
|
|
334
711
|
}
|
|
712
|
+
}
|
|
713
|
+
if (collection) {
|
|
714
|
+
// The tree's entry types, the fallback, carry no schema or usage.
|
|
715
|
+
const read = await readEntryTypes(collection.path).catch(() => null);
|
|
716
|
+
if (!stillCurrent())
|
|
717
|
+
return;
|
|
718
|
+
const entries = read ??
|
|
719
|
+
(collection.entryTypes ?? []).map((et) => ({
|
|
720
|
+
name: et.name,
|
|
721
|
+
label: et.label,
|
|
722
|
+
format: et.format,
|
|
723
|
+
schema: '',
|
|
724
|
+
default: et.default,
|
|
725
|
+
maxItems: et.maxItems,
|
|
726
|
+
}));
|
|
727
|
+
setEditingCollection({
|
|
728
|
+
name: collection.name,
|
|
729
|
+
label: collection.label,
|
|
730
|
+
logicalPath: collection.path,
|
|
731
|
+
entries,
|
|
732
|
+
});
|
|
335
733
|
setCollectionEditorParentPath(undefined);
|
|
336
734
|
}
|
|
337
735
|
else {
|
|
338
736
|
setEditingCollection(null);
|
|
339
737
|
setCollectionEditorParentPath(parentPath);
|
|
340
738
|
}
|
|
739
|
+
refilledDraftRef.current = restore ? draft : null;
|
|
740
|
+
setRestoredCollectionInput(restore);
|
|
341
741
|
setCollectionEditorError(null);
|
|
342
742
|
setCollectionEditorOpen(true);
|
|
343
743
|
};
|
|
344
744
|
const handleCloseCollectionEditor = () => {
|
|
745
|
+
collectionOpeningRef.current++;
|
|
345
746
|
setCollectionEditorOpen(false);
|
|
346
747
|
setEditingCollection(null);
|
|
347
748
|
setCollectionEditorParentPath(undefined);
|
|
@@ -352,9 +753,23 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
352
753
|
// createCollection/updateCollection are contractually non-throwing (they
|
|
353
754
|
// return result objects — see useSchemaManager), so branch on `result.ok`
|
|
354
755
|
// instead of try/catch.
|
|
756
|
+
const draft = collectionDraftRef.current;
|
|
355
757
|
if (isNew) {
|
|
356
|
-
const
|
|
357
|
-
|
|
758
|
+
const input = data;
|
|
759
|
+
const result = await createCollection(input);
|
|
760
|
+
// A retried create that had landed makes a second collection, so an unanswered one closes
|
|
761
|
+
// the dialog too, leaving the hook's toast to say to check the "Content" panel first.
|
|
762
|
+
if (result.ok || result.unanswered) {
|
|
763
|
+
if (!result.ok) {
|
|
764
|
+
collectionDraftRef.current = { branch: branchNameState, kind: 'create', input };
|
|
765
|
+
}
|
|
766
|
+
else if (collectionDraftRef.current === draft &&
|
|
767
|
+
draft?.kind === 'create' &&
|
|
768
|
+
draft.branch === branchNameState &&
|
|
769
|
+
draft.input.parentPath === input.parentPath &&
|
|
770
|
+
(draft === refilledDraftRef.current || draft.input.name === input.name)) {
|
|
771
|
+
collectionDraftRef.current = null;
|
|
772
|
+
}
|
|
358
773
|
handleCloseCollectionEditor();
|
|
359
774
|
}
|
|
360
775
|
else {
|
|
@@ -362,8 +777,29 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
362
777
|
}
|
|
363
778
|
}
|
|
364
779
|
else if (editingCollection) {
|
|
365
|
-
const
|
|
366
|
-
|
|
780
|
+
const updates = data;
|
|
781
|
+
const collectionPath = editingCollection.logicalPath;
|
|
782
|
+
const result = await updateCollection(collectionPath, updates);
|
|
783
|
+
// An unanswered save may have renamed the collection, leaving `editingCollection`'s path
|
|
784
|
+
// stale, so the dialog closes; the hook's toast says to reopen it and check.
|
|
785
|
+
if (result.ok || result.unanswered) {
|
|
786
|
+
// An update with no defined value sent nothing worth filling in again.
|
|
787
|
+
const sent = Object.values(updates).some((value) => value !== undefined);
|
|
788
|
+
if (!result.ok && sent) {
|
|
789
|
+
collectionDraftRef.current = {
|
|
790
|
+
branch: branchNameState,
|
|
791
|
+
kind: 'details',
|
|
792
|
+
collectionPath,
|
|
793
|
+
updates,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
else if (result.ok &&
|
|
797
|
+
collectionDraftRef.current === draft &&
|
|
798
|
+
draft?.kind === 'details' &&
|
|
799
|
+
draft.branch === branchNameState &&
|
|
800
|
+
draft.collectionPath === collectionPath) {
|
|
801
|
+
collectionDraftRef.current = null;
|
|
802
|
+
}
|
|
367
803
|
handleCloseCollectionEditor();
|
|
368
804
|
}
|
|
369
805
|
else {
|
|
@@ -371,6 +807,18 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
371
807
|
}
|
|
372
808
|
}
|
|
373
809
|
};
|
|
810
|
+
/** The draft's write landed: drop it, take its warning down and say so, once. */
|
|
811
|
+
const settleCollectionDraft = (draft) => {
|
|
812
|
+
if (collectionDraftRef.current !== draft)
|
|
813
|
+
return;
|
|
814
|
+
collectionDraftRef.current = null;
|
|
815
|
+
setRestoredCollectionInput(null);
|
|
816
|
+
hideCollectionOutcomeUnknown();
|
|
817
|
+
notifications.show({
|
|
818
|
+
message: draft.kind === 'create' ? `Collection "${draft.input.name}" created` : 'Collection updated',
|
|
819
|
+
color: 'green',
|
|
820
|
+
});
|
|
821
|
+
};
|
|
374
822
|
const handleDeleteCollection = (collectionPath) => {
|
|
375
823
|
setDeletingCollectionPath(collectionPath);
|
|
376
824
|
setDeleteCollectionModalOpen(true);
|
|
@@ -388,50 +836,62 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
388
836
|
setDeleteInProgress(false);
|
|
389
837
|
}
|
|
390
838
|
};
|
|
391
|
-
const handleDeleteEntry = (entryPath) => {
|
|
839
|
+
const handleDeleteEntry = (entryPath, fromMenu = false) => {
|
|
840
|
+
setEntryChangeFromMenu(fromMenu);
|
|
392
841
|
setDeletingEntryPath(entryPath);
|
|
842
|
+
setDeletingEntryLabel(entriesState.find((e) => e.path === entryPath)?.label);
|
|
843
|
+
setDeletingEntryReferencedBy(null);
|
|
393
844
|
setDeleteEntryModalOpen(true);
|
|
394
845
|
};
|
|
846
|
+
// What the dialog names stays until the next delete, so its exit shows no change of text.
|
|
847
|
+
const closeDeleteEntryModal = () => setDeleteEntryModalOpen(false);
|
|
395
848
|
const confirmDeleteEntry = async () => {
|
|
396
|
-
|
|
849
|
+
// Its button stays clickable while the closed dialog fades; that click deletes nothing.
|
|
850
|
+
if (!deleteEntryModalOpen || !deletingEntryPath)
|
|
397
851
|
return;
|
|
398
852
|
setDeleteInProgress(true);
|
|
399
853
|
try {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
854
|
+
// The user has seen the referencing entries only once the server has named them.
|
|
855
|
+
const result = await deleteEntry(deletingEntryPath, {
|
|
856
|
+
confirmReferenced: deletingEntryReferencedBy !== null,
|
|
857
|
+
});
|
|
858
|
+
if (!result.ok && 'referencedBy' in result) {
|
|
859
|
+
setDeletingEntryReferencedBy(result.referencedBy);
|
|
860
|
+
return;
|
|
403
861
|
}
|
|
404
|
-
|
|
405
|
-
|
|
862
|
+
if (result.ok && selectedPath === deletingEntryPath)
|
|
863
|
+
closeEntry();
|
|
864
|
+
closeDeleteEntryModal();
|
|
406
865
|
}
|
|
407
866
|
finally {
|
|
408
867
|
setDeleteInProgress(false);
|
|
409
868
|
}
|
|
410
869
|
};
|
|
411
|
-
const handleRenameEntry = (entryPath) => {
|
|
412
|
-
// Find the entry being renamed
|
|
870
|
+
const handleRenameEntry = (entryPath, fromMenu = false) => {
|
|
413
871
|
const entry = entriesState.find((e) => e.path === entryPath);
|
|
414
872
|
if (!entry) {
|
|
415
873
|
notifications.show({ message: 'Entry not found', color: 'red' });
|
|
416
874
|
return;
|
|
417
875
|
}
|
|
876
|
+
setEntryChangeFromMenu(fromMenu);
|
|
418
877
|
setRenamingEntry(entry);
|
|
419
878
|
setRenameModalError(null);
|
|
420
879
|
setRenameModalOpen(true);
|
|
421
880
|
};
|
|
422
881
|
const handleRenameSubmit = async (newSlug) => {
|
|
423
|
-
if (!renamingEntry)
|
|
882
|
+
if (!renameModalOpen || !renamingEntry)
|
|
424
883
|
return;
|
|
425
884
|
setRenameModalSaving(true);
|
|
426
885
|
setRenameModalError(null);
|
|
427
886
|
try {
|
|
428
887
|
await renameEntry(renamingEntry.path, newSlug);
|
|
429
888
|
setRenameModalOpen(false);
|
|
430
|
-
setRenamingEntry(null);
|
|
431
889
|
}
|
|
432
890
|
catch (err) {
|
|
433
|
-
|
|
434
|
-
|
|
891
|
+
setRenameModalError({
|
|
892
|
+
message: err instanceof Error ? err.message : 'Rename failed',
|
|
893
|
+
mayHaveLanded: err instanceof MayHaveLandedError,
|
|
894
|
+
});
|
|
435
895
|
}
|
|
436
896
|
finally {
|
|
437
897
|
setRenameModalSaving(false);
|
|
@@ -494,6 +954,10 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
494
954
|
[newOrder[currentIndex], newOrder[newIndex]] = [newOrder[newIndex], newOrder[currentIndex]];
|
|
495
955
|
await updateOrder(collectionPath, newOrder);
|
|
496
956
|
};
|
|
957
|
+
// The entry's type has no schema here, or the API refused its read as unavailable. Its
|
|
958
|
+
// form is replaced by a notice, so nothing about it can be saved.
|
|
959
|
+
const isEntryUnavailable = (entry) => !!entry.unavailable || schemaUnavailableKeys.has(`${branchNameState}:${entry.contentId}`);
|
|
960
|
+
const currentEntryUnavailable = !!currentEntry && isEntryUnavailable(currentEntry);
|
|
497
961
|
// Determine if we should hide the root collection (but keep its context for ordering)
|
|
498
962
|
const hiddenRootPath = useMemo(() => {
|
|
499
963
|
if (activeCollections?.length === 1 && activeCollections[0].path === contentRoot) {
|
|
@@ -512,15 +976,25 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
512
976
|
list.push({
|
|
513
977
|
path: entry.path,
|
|
514
978
|
label: entry.label,
|
|
515
|
-
|
|
979
|
+
slug: entry.slug,
|
|
980
|
+
status: entryStatusLabel(activeCollections, entry),
|
|
516
981
|
contentId: entry.contentId,
|
|
982
|
+
readOnly: entry.canEdit === false || undefined,
|
|
517
983
|
conflictNotice: !!(entry.contentId && currentBranch?.conflictFiles?.includes(entry.contentId)),
|
|
984
|
+
unavailable: isEntryUnavailable(entry) || undefined,
|
|
518
985
|
});
|
|
519
986
|
grouped.set(entry.collectionPath, list);
|
|
520
987
|
});
|
|
521
988
|
const build = (node) => {
|
|
522
989
|
const entries = grouped.get(node.path) ?? [];
|
|
523
990
|
const children = node.children?.map((child) => build(child)) ?? [];
|
|
991
|
+
const unavailableRefs = unavailableSchemaRefs(node.entryTypes);
|
|
992
|
+
// No entry can be created where every entry type is unavailable.
|
|
993
|
+
const allUnavailable = !!node.entryTypes?.length && node.entryTypes.every((et) => et.unavailable);
|
|
994
|
+
// A collection that holds only collections has no entry type to add (`entryTypes` is
|
|
995
|
+
// undefined when its list is empty).
|
|
996
|
+
const addable = node.type !== 'entry' && !allUnavailable && (node.entryTypes?.length ?? 0) > 0;
|
|
997
|
+
const canAddEntry = addable && node.canAddEntry !== false;
|
|
524
998
|
return {
|
|
525
999
|
path: node.path,
|
|
526
1000
|
label: node.label ?? node.name,
|
|
@@ -530,14 +1004,20 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
530
1004
|
entries: entries.length > 0 ? entries : undefined,
|
|
531
1005
|
children: children.length > 0 ? children : undefined,
|
|
532
1006
|
conflictNotice: !!(node.contentId && currentBranch?.conflictFiles?.includes(node.contentId)),
|
|
533
|
-
|
|
1007
|
+
unavailableSchemaRefs: unavailableRefs.length > 0 ? unavailableRefs : undefined,
|
|
1008
|
+
addRefused: (addable && node.canAddEntry === false) || undefined,
|
|
1009
|
+
onAdd: canAddEntry
|
|
534
1010
|
? () => (onCreateEntry ? onCreateEntry(node.path) : handleCreateEntry(node.path))
|
|
535
1011
|
: undefined,
|
|
536
|
-
onEdit: node.type === 'collection'
|
|
537
|
-
|
|
1012
|
+
onEdit: userIsAdmin && node.type === 'collection'
|
|
1013
|
+
? () => handleOpenCollectionEditor(node)
|
|
1014
|
+
: undefined,
|
|
1015
|
+
onAddSubCollection: userIsAdmin && node.type === 'collection'
|
|
538
1016
|
? () => handleOpenCollectionEditor(null, node.path)
|
|
539
1017
|
: undefined,
|
|
540
|
-
onDelete: node.type === 'collection'
|
|
1018
|
+
onDelete: userIsAdmin && node.type === 'collection'
|
|
1019
|
+
? () => handleDeleteCollection(node.path)
|
|
1020
|
+
: undefined,
|
|
541
1021
|
};
|
|
542
1022
|
};
|
|
543
1023
|
return activeCollections.map((node) => build(node));
|
|
@@ -549,8 +1029,87 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
549
1029
|
handleCreateEntry,
|
|
550
1030
|
contentRoot,
|
|
551
1031
|
currentBranch,
|
|
1032
|
+
schemaUnavailableKeys,
|
|
1033
|
+
userIsAdmin,
|
|
552
1034
|
]);
|
|
553
|
-
|
|
1035
|
+
const quickOpenShortcut = modKeyLabel() === '⌘' ? '⌘P' : 'Ctrl+P';
|
|
1036
|
+
const quickOpenKeys = modKeyLabel() === '⌘' ? 'Meta+P' : 'Control+P';
|
|
1037
|
+
// Docked, the content panel's open state is the stored preference; as an overlay it is
|
|
1038
|
+
// `navigatorOpen`, which starts open when the URL asks for no entry.
|
|
1039
|
+
const contentPanelShown = contentPanelDocked ? contentPanelOpen : navigatorOpen;
|
|
1040
|
+
const setContentPanelShown = (open) => contentPanelDocked ? setContentPanelOpen(open) : setNavigatorOpen(open);
|
|
1041
|
+
// The overlay lives in the form pane, under a maximized preview.
|
|
1042
|
+
const contentOverlayShown = !contentPanelDocked && contentPanelShown;
|
|
1043
|
+
useEffect(() => {
|
|
1044
|
+
if (contentOverlayShown)
|
|
1045
|
+
setPreviewMaximized(false);
|
|
1046
|
+
}, [contentOverlayShown]);
|
|
1047
|
+
// Bumped on every ask for the panel, so one already open still takes focus.
|
|
1048
|
+
const [contentPanelFocusRequest, setContentPanelFocusRequest] = useState(0);
|
|
1049
|
+
const openNavigator = () => {
|
|
1050
|
+
navigatorOpenerRef.current = rememberOpener();
|
|
1051
|
+
// In the same commit as the overlay mounts, so its focus request meets no inert form panel.
|
|
1052
|
+
if (!contentPanelDocked)
|
|
1053
|
+
setPreviewMaximized(false);
|
|
1054
|
+
setContentPanelShown(true);
|
|
1055
|
+
setContentPanelFocusRequest((request) => request + 1);
|
|
1056
|
+
};
|
|
1057
|
+
// A collection crumb opens the panel as "All content" does, then its collection's row takes focus.
|
|
1058
|
+
const [collectionReveal, setCollectionReveal] = useState();
|
|
1059
|
+
// Only ever rises: the navigator serves each seq once, and the request is cleared once served.
|
|
1060
|
+
const revealSeqRef = useRef(0);
|
|
1061
|
+
const revealCollection = (path) => {
|
|
1062
|
+
openNavigator();
|
|
1063
|
+
revealSeqRef.current += 1;
|
|
1064
|
+
setCollectionReveal({ path, seq: revealSeqRef.current });
|
|
1065
|
+
};
|
|
1066
|
+
// An ask the panel closed on before serving it must not fire when the panel next opens.
|
|
1067
|
+
useEffect(() => {
|
|
1068
|
+
if (!contentPanelShown)
|
|
1069
|
+
setCollectionReveal(undefined);
|
|
1070
|
+
}, [contentPanelShown]);
|
|
1071
|
+
const runSaveAll = () => {
|
|
1072
|
+
void handleSaveAll()
|
|
1073
|
+
.then((result) => {
|
|
1074
|
+
if (!result)
|
|
1075
|
+
return;
|
|
1076
|
+
showSaveAllResult(result, {
|
|
1077
|
+
// Focus goes to the entry's menu, where its reason's "Refresh from server" is.
|
|
1078
|
+
onOpenEntry: (path) => {
|
|
1079
|
+
openEntry(path);
|
|
1080
|
+
requestAnimationFrame(() => navigatorToggleRef.current?.focus());
|
|
1081
|
+
},
|
|
1082
|
+
onRetry: runSaveAll,
|
|
1083
|
+
});
|
|
1084
|
+
})
|
|
1085
|
+
.catch(console.error);
|
|
1086
|
+
};
|
|
1087
|
+
// The entry menu's trigger stands in for its item; once the entry is deleted, "All content".
|
|
1088
|
+
const entryChangeReturnTarget = entryChangeFromMenu
|
|
1089
|
+
? () => navigatorToggleRef.current ?? allContentCrumbRef.current
|
|
1090
|
+
: undefined;
|
|
1091
|
+
const navigatorReturnTarget = () => returnTarget(navigatorOpenerRef.current, () => navigatorToggleRef.current);
|
|
1092
|
+
const branchReturnTarget = () => returnTarget(branchOpenerRef.current, () => branchToggleRef.current);
|
|
1093
|
+
const settingsReturnTarget = () => settingsToggleRef.current;
|
|
1094
|
+
// The palette replaces the overlay panel; a docked one stays. Opened from inside the overlay,
|
|
1095
|
+
// whose tree then unmounts, it returns focus to the panel's opener.
|
|
1096
|
+
const openQuickOpen = () => {
|
|
1097
|
+
if (!quickOpenOpen) {
|
|
1098
|
+
const panel = document.getElementById(CONTENT_PANEL_ID);
|
|
1099
|
+
setQuickOpenFromOverlay(!contentPanelDocked && navigatorOpen && !!panel?.contains(document.activeElement));
|
|
1100
|
+
}
|
|
1101
|
+
if (!contentPanelDocked)
|
|
1102
|
+
setNavigatorOpen(false);
|
|
1103
|
+
setQuickOpenOpen(true);
|
|
1104
|
+
};
|
|
1105
|
+
const dirtyPaths = useMemo(() => new Set(editedFiles.map((file) => file.path)), [editedFiles]);
|
|
1106
|
+
// Slugs of the entries beside the one being renamed, for the duplicate check.
|
|
1107
|
+
const renameExistingSlugs = useMemo(() => new Set(entriesState
|
|
1108
|
+
.filter((e) => e.collectionPath === renamingEntry?.collectionPath &&
|
|
1109
|
+
e.contentId !== renamingEntry?.contentId &&
|
|
1110
|
+
e.slug)
|
|
1111
|
+
.map((e) => e.slug)), [entriesState, renamingEntry]);
|
|
1112
|
+
// Tree expansion state - persists across panel close/open
|
|
554
1113
|
const treeExpandedStateRef = useRef({});
|
|
555
1114
|
const treeControllerRef = useRef(null);
|
|
556
1115
|
const handleTreeControllerReady = (controller) => {
|
|
@@ -572,12 +1131,15 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
572
1131
|
contentRoot,
|
|
573
1132
|
entryLinkUrl,
|
|
574
1133
|
});
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1134
|
+
// Resolved here, outside the form and its per-field crash boundaries, and during render, so
|
|
1135
|
+
// the preview never receives a bare reference id: a reference is its target or `null`.
|
|
1136
|
+
const { resolvedValue: previewValue, loadingState: previewLoadingState } = useReferenceResolution({
|
|
1137
|
+
value: displayValue ?? EMPTY_VALUE,
|
|
1138
|
+
fields: schema,
|
|
1139
|
+
branch: branchNameState,
|
|
1140
|
+
entryKey: currentEntry?.contentId,
|
|
1141
|
+
});
|
|
1142
|
+
const previewFrameData = useMemo(() => (displayValue ? resolveEntryLinks(previewValue) : displayValue), [previewValue, displayValue, resolveEntryLinks]);
|
|
581
1143
|
// Entry link context for the InsertEntryLink toolbar button
|
|
582
1144
|
const entryLinkContextValue = useMemo(() => ({
|
|
583
1145
|
entries: entriesState.map((e) => ({
|
|
@@ -594,13 +1156,38 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
594
1156
|
justifyContent: 'center',
|
|
595
1157
|
height: '100%',
|
|
596
1158
|
}, children: _jsx(Text, { size: "sm", c: "dimmed", children: children }) }));
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1159
|
+
const { onMarks: onPreviewMarks, clearMarks: clearPreviewMarks, marksNothing: previewMarksNothing, inexactMarks: previewInexactMarks, } = usePreviewMarks({
|
|
1160
|
+
src: currentEntry?.previewSrc,
|
|
1161
|
+
highlightEnabled,
|
|
1162
|
+
fields: schema,
|
|
1163
|
+
data: displayValue,
|
|
1164
|
+
});
|
|
1165
|
+
// Only the default preview has the toolbar that maximizes and restores it. With no value to
|
|
1166
|
+
// preview (an entry loading, or failed to load, with no draft) the form shows instead, and the
|
|
1167
|
+
// choice reapplies once a value arrives.
|
|
1168
|
+
const previewCanMaximize = !renderPreview && Boolean(currentEntry?.previewSrc) && !currentEntryUnavailable;
|
|
1169
|
+
const previewShownMaximized = previewMaximized && previewCanMaximize && Boolean(previewFrameData);
|
|
1170
|
+
useEffect(() => {
|
|
1171
|
+
if (!previewCanMaximize)
|
|
1172
|
+
setPreviewMaximized(false);
|
|
1173
|
+
}, [previewCanMaximize]);
|
|
1174
|
+
const [paneResizing, setPaneResizing] = useState(false);
|
|
1175
|
+
const previewErrorField = previewError?.fieldPath
|
|
1176
|
+
? fieldBreadcrumb(schema, displayValue, previewError.fieldPath).join(' › ')
|
|
1177
|
+
: '';
|
|
1178
|
+
// An unavailable entry previews nothing: the site cannot read it, and a stored draft would
|
|
1179
|
+
// otherwise mount the frame at a page that answers not-found.
|
|
1180
|
+
const defaultPreview = currentEntry?.previewSrc && previewFrameData && !currentEntryUnavailable ? (_jsx(PreviewPane, { src: currentEntry.previewSrc, data: previewFrameData, isLoading: previewLoadingState, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError, onMarks: onPreviewMarks, assetBase: authenticatedAssetBase(basePath), mount: { basePath, previewPrefix }, previewWidth: previewWidth, onPreviewWidthChange: setPreviewWidth, onHighlightToggle: () => {
|
|
1181
|
+
setHighlightEnabled(!highlightEnabled);
|
|
1182
|
+
clearPreviewMarks();
|
|
1183
|
+
}, marksNothing: previewMarksNothing, inexactMarks: previewInexactMarks, showMarkDiagnostics: userIsAdmin, layout: layout, maximized: previewShownMaximized, onMaximizedChange: (maximized) => {
|
|
1184
|
+
// A docked panel stays beside a maximized preview; an overlay one would sit inert under it.
|
|
1185
|
+
if (maximized && !contentPanelDocked)
|
|
1186
|
+
setNavigatorOpen(false);
|
|
1187
|
+
setPreviewMaximized(maximized);
|
|
1188
|
+
}, resizing: paneResizing, notice: previewError && (_jsx(Alert, { color: "red", title: "Preview error", withCloseButton: true, onClose: () => setPreviewError(null), radius: 0, children: previewErrorField
|
|
1189
|
+
? `${previewErrorField}: ${previewError.message}`
|
|
1190
|
+
: previewError.message })) })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "var(--mantine-color-body)", style: {
|
|
604
1191
|
display: 'flex',
|
|
605
1192
|
alignItems: 'center',
|
|
606
1193
|
justifyContent: 'center',
|
|
@@ -611,89 +1198,175 @@ export const Editor = ({ entries, title, subtitle, siteTitle = title, siteSubtit
|
|
|
611
1198
|
: currentEntry && !currentEntry.previewSrc
|
|
612
1199
|
? 'No preview for this entry.'
|
|
613
1200
|
: 'Select an item to start editing.' }) }));
|
|
614
|
-
const sidebarWidth = 64;
|
|
615
|
-
const footerHeight = 40;
|
|
616
|
-
const headerTitle = currentEntry?.label ?? currentEntry?.slug ?? title;
|
|
617
1201
|
const breadcrumbSegments = useMemo(() => buildBreadcrumbSegments(currentEntry, collectionLabels), [collectionLabels, currentEntry]);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1202
|
+
// The browser tab names the open entry and the site.
|
|
1203
|
+
const tabTitle = currentEntry
|
|
1204
|
+
? `${currentEntry.label ?? currentEntry.slug} · ${siteTitle}`
|
|
1205
|
+
: siteTitle;
|
|
1206
|
+
// The host page's title comes back on unmount only if the tab still shows the editor's: a route
|
|
1207
|
+
// navigated to may have set its own by then.
|
|
1208
|
+
const writtenTitleRef = useRef(null);
|
|
1209
|
+
useEffect(() => {
|
|
1210
|
+
const hostTitle = document.title;
|
|
1211
|
+
return () => {
|
|
1212
|
+
if (document.title === writtenTitleRef.current)
|
|
1213
|
+
document.title = hostTitle;
|
|
1214
|
+
};
|
|
1215
|
+
}, []);
|
|
1216
|
+
useEffect(() => {
|
|
1217
|
+
document.title = tabTitle;
|
|
1218
|
+
writtenTitleRef.current = tabTitle;
|
|
1219
|
+
}, [tabTitle]);
|
|
1220
|
+
const openBranchManager = () => {
|
|
1221
|
+
branchOpenerRef.current = rememberOpener();
|
|
1222
|
+
setBranchManagerOpen(true);
|
|
1223
|
+
// Always retry, not only when branchless. Opening the manager is
|
|
1224
|
+
// the one in-app retry for a failed branches fetch, and the fetch
|
|
1225
|
+
// is terminal on its own: SWRProvider sets shouldRetryOnError and
|
|
1226
|
+
// revalidateOnFocus false, and useBranchesData's refreshInterval
|
|
1227
|
+
// is 0 without data. Reloading is idempotent.
|
|
1228
|
+
loadBranches().catch(console.error);
|
|
1229
|
+
};
|
|
1230
|
+
const contentPanel = (_jsx(SidePanelHost, { id: CONTENT_PANEL_ID, title: "Content", mode: contentPanelDocked ? 'docked' : 'overlay', opened: contentPanelShown, width: contentPanelWidth, minWidth: CONTENT_PANEL_WIDTH_MIN, maxWidth: CONTENT_PANEL_WIDTH_MAX, onWidthChange: setContentPanelWidth, onClose: () => setContentPanelShown(false), focusRequest: contentPanelFocusRequest,
|
|
1231
|
+
// Quick open takes focus as it replaces the overlay.
|
|
1232
|
+
returnFocusTo: () => (quickOpenOpen ? null : navigatorReturnTarget()), "data-testid": "content-panel", actions: _jsxs(_Fragment, { children: [navCollections &&
|
|
1233
|
+
navCollections.length > 0 &&
|
|
1234
|
+
!branchContentLocked &&
|
|
1235
|
+
(navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", children: [_jsx(Tooltip, { label: "Content actions", withinPortal: true, children: _jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray", "aria-label": "Content actions", children: _jsx(IconDots, { size: 16 }) }) }) }), _jsxs(Menu.Dropdown, { children: [navCollections[0].onAdd && (_jsx(Menu.Item, { leftSection: _jsx(IconPlus, { size: 14 }), onClick: () => navCollections[0].onAdd?.(), children: "Add entry\u2026" })), navCollections[0].onAddSubCollection && (_jsxs(_Fragment, { children: [navCollections[0].onAdd && _jsx(Menu.Divider, {}), _jsx(Menu.Label, { children: "Admin" }), _jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: () => navCollections[0].onAddSubCollection?.(), children: "Add collection\u2026" })] }))] })] })), _jsx(Tooltip, { label: `${EDITOR_ACTIONS.openEntry} (${quickOpenShortcut})`, withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: openQuickOpen, "aria-label": EDITOR_ACTIONS.openEntry, "aria-keyshortcuts": quickOpenKeys, "data-testid": "quick-open-button", children: _jsx(IconSearch, { size: 16 }) }) }), _jsx(Tooltip, { label: "Collapse all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleCollapseAll, "aria-label": "Collapse all folders", children: _jsx(IconChevronUp, { size: 16 }) }) }), _jsx(Tooltip, { label: "Expand all folders", withinPortal: true, children: _jsx(ActionIcon, { variant: "subtle", color: "gray", onClick: handleExpandAll, "aria-label": "Expand all folders", children: _jsx(IconChevronDown, { size: 16 }) }) })] }), children: _jsx(Box, { px: "md", children: _jsx(EntryNavigator, { collections: navCollections, items: navCollections
|
|
1236
|
+
? undefined
|
|
1237
|
+
: entriesState.map((e) => ({
|
|
1238
|
+
path: e.path,
|
|
1239
|
+
label: e.label,
|
|
1240
|
+
slug: e.slug,
|
|
1241
|
+
status: entryStatusLabel(activeCollections, e),
|
|
1242
|
+
readOnly: e.canEdit === false || undefined,
|
|
1243
|
+
})), selectedPath: selectedPath, onSelect: openEntry, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: userIsAdmin ? handleReorderEntry : undefined, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked, dirtyPaths: dirtyPaths, revealCollection: collectionReveal, onRevealed: () => setCollectionReveal(undefined) }) }) }));
|
|
1244
|
+
return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { basePath: basePath, children: _jsx(EditorShell, { topBar: _jsx(EditorHeader, { currentEntry: currentEntry, branchName: branchNameState, operatingMode: operatingMode, branchStatus: currentBranch?.status, busy: busy, breadcrumbSegments: breadcrumbSegments, editedFiles: editedFiles, modifiedCount: modifiedCount,
|
|
1245
|
+
// A draft kept for an unavailable entry stays in storage but cannot be saved.
|
|
1246
|
+
hasUnsavedChanges: !currentEntryUnavailable && !contentReadOnly && isSelectedDirty(), entryHasDraft: isSelectedDirty(), entryReadOnly: currentEntry?.canEdit === false, draftStorageFailed: draftStorageFailed, userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
|
|
1247
|
+
// Fail-closed for the same reason branchContentLocked above is:
|
|
1248
|
+
// isProtected/writeBlocked gate real actions in EditorHeader (hiding
|
|
1249
|
+
// Submit, disabling Save), so a missing wire value must read as
|
|
1250
|
+
// "protected"/"blocked", not "unprotected"/"unblocked".
|
|
1251
|
+
// branchWriteBlocked reuses branchContentLocked itself rather than
|
|
1252
|
+
// re-deriving `?? true` a second time here, so there is exactly one
|
|
1253
|
+
// fail-closed computation of "are writes blocked" in this component.
|
|
1254
|
+
branchIsProtected: currentBranch?.isProtected ?? true, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked,
|
|
1255
|
+
// A failed fetch leaves the list empty too; the banner says that one.
|
|
1256
|
+
branchLoading: !currentBranch && (branchesLoading || branchSummaries.length === 0), onNavigatorOpen: openNavigator, onCollectionOpen: revealCollection, navigatorToggleRef: navigatorToggleRef, branchToggleRef: branchToggleRef, onFileReload: () => handleReload(() => navigatorToggleRef.current), onFileDiscardDraft: () => handleDiscardFileDraft(() => navigatorToggleRef.current), onEntryRename: () => currentEntry && handleRenameEntry(currentEntry.path, true), onEntryDelete: () => currentEntry && handleDeleteEntry(currentEntry.path, true), allContentRef: allContentCrumbRef, entryUnavailable: currentEntryUnavailable, onEntrySelect: openEntry, onBranchReloadData: () => {
|
|
1257
|
+
void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
|
|
1258
|
+
}, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager, onSave: currentEntryUnavailable ? noop : handleSave, onSaveAll: runSaveAll, saveAllProgress: saveAllProgress, saving: singleSaving?.branch === branchNameState &&
|
|
1259
|
+
singleSaving.contentId === currentEntry?.contentId, savedSignal: lastSaved?.seq, savedHere: lastSaved?.branch === branchNameState &&
|
|
1260
|
+
lastSaved.contentId === currentEntry?.contentId,
|
|
1261
|
+
// Save all finds each draft's entry in the list, so with none listed it would save nothing.
|
|
1262
|
+
entryListFailed: entriesLoadFailed && entriesState.length === 0, onSubmit: () => {
|
|
1263
|
+
if (branchNameState && !isSaveAllRunning()) {
|
|
1264
|
+
handleSubmit(branchNameState).catch(console.error);
|
|
1265
|
+
}
|
|
1266
|
+
}, onWithdraw: () => {
|
|
1267
|
+
if (branchNameState && !isSaveAllRunning()) {
|
|
1268
|
+
handleWithdraw(branchNameState).catch(console.error);
|
|
1269
|
+
}
|
|
1270
|
+
} }), banner: _jsx(BranchLockBanner, { branchName: branchNameState, branchStatus: currentBranch?.status, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, onBranchManagerOpen: openBranchManager }), rail: _jsx(EditorRail, { siteTitle: siteTitle, onHomeOpen: () => {
|
|
1271
|
+
const first = entriesState[0];
|
|
1272
|
+
if (first)
|
|
1273
|
+
openEntry(first.path);
|
|
1274
|
+
}, contentPanelOpen: contentPanelShown, contentPanelId: CONTENT_PANEL_ID, onContentToggle: () => contentPanelShown ? setContentPanelShown(false) : openNavigator(), onQuickOpen: openQuickOpen, quickOpenShortcut: quickOpenShortcut, quickOpenKeys: quickOpenKeys, branchesOpen: branchManagerOpen, onBranchesOpen: openBranchManager, onCommentsOpen: clientOperatingStrategy(operatingMode ?? 'prod').supportsComments() &&
|
|
1275
|
+
branchNameState
|
|
1276
|
+
? () => setCommentsPanelOpen(true)
|
|
1277
|
+
: undefined, commentsOpen: commentsPanelOpen, unresolvedCommentCount: comments.filter((t) => !t.resolved).length, mediaLibraryOpen: mediaLibraryOpen, onMediaLibraryOpen: () => setMediaLibraryOpen(true), mediaButtonRef: mediaButtonRef, layout: layout, onLayoutChange: setLayout, layoutBlocked: previewShownMaximized, onPermissionManagerOpen: userIsAdmin ? () => setPermissionManagerOpen(true) : undefined, onGroupManagerOpen: userIsAdmin ? () => setGroupManagerOpen(true) : undefined, onSystemHealthOpen: userIsAdmin ? () => setSystemHealthOpen(true) : undefined, settingsToggleRef: settingsToggleRef, AccountComponent: AccountComponent, onAccountClick: onAccountClick, onLogoutClick: onLogoutClick }), panel: contentPanelDocked ? contentPanel : undefined, main: _jsx(Box, { h: "100%", children: _jsx(PaneLayout, { formOverlay: contentPanelDocked ? undefined : contentPanel, formInert: contentOverlayShown, previewMaximized: previewShownMaximized,
|
|
1278
|
+
// Sizing the pane, by pointer or key, gives the page the pane's width.
|
|
1279
|
+
onSeparatorDrag: (dragging) => {
|
|
1280
|
+
setPaneResizing(dragging);
|
|
1281
|
+
if (dragging)
|
|
1282
|
+
setPreviewWidth('fit');
|
|
1283
|
+
}, arrangement: layout, sideSplitPercent: sideSplitPercent, stackedSplitPercent: stackedSplitPercent, onSplitPercentChange: (arrangement, percent) => {
|
|
1284
|
+
setSplitPercent(arrangement, percent);
|
|
1285
|
+
setPreviewWidth('fit');
|
|
1286
|
+
}, preview: renderPreview && currentEntry && !currentEntryUnavailable
|
|
1287
|
+
? renderPreview(currentEntry, displayValue)
|
|
1288
|
+
: defaultPreview, form: !currentEntry ? (_jsx(CenteredMessage, { children: !branchNameState && busy
|
|
1289
|
+
? 'Setting up your branch workspace…'
|
|
1290
|
+
: entriesInitializing
|
|
1291
|
+
? 'Loading content…'
|
|
1292
|
+
: 'Select an item to start editing.' })) : currentEntryUnavailable ? (_jsx(UnavailableEntryNotice, { schemaRefs: currentEntry.unavailable ? [currentEntry.unavailable.schemaRef] : [] })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryLabel: currentEntry.label })) : schema.length > 0 && displayValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(SiteMdxAllowContext.Provider, { value: mdxAllow, children: _jsx(FormRenderer, { notice: hiddenDraftNotice && (_jsx(ReadOnlyDraftNotice, { permanentlyReadOnly: (currentBranch?.isProtected ?? true) ||
|
|
1293
|
+
currentBranch?.status === 'archived',
|
|
1294
|
+
// A hidden draft's discard changes nothing on screen, so it says so.
|
|
1295
|
+
onDiscard: () => handleDiscardFileDraft(() => navigatorToggleRef.current, {
|
|
1296
|
+
announce: true,
|
|
1297
|
+
}) })), fields: schema, value: displayValue, readOnly: contentReadOnly, onRemoved: handleFieldRemoved, commentBlockedReason: commentBlockedReason, removedItemFieldLabel: commentFieldLabel, onGetUserMetadata: getCommentUserMetadata, header: {
|
|
1298
|
+
title: currentEntry.label,
|
|
1299
|
+
typeLabel: entryTypeLabel(activeCollections, currentEntry),
|
|
1300
|
+
},
|
|
1301
|
+
// The one writer of drafts. Read-only content gets none, whatever a
|
|
1302
|
+
// field emits (MDXEditor's mount-time normalisation included).
|
|
1303
|
+
onChange: (next) => {
|
|
1304
|
+
const contentId = currentEntry?.contentId;
|
|
1305
|
+
if (contentId && !contentReadOnlyRef.current) {
|
|
1306
|
+
// Every list item keeps a key, whatever field made it.
|
|
1307
|
+
const keyed = withItemKeys(schema, next, 'random');
|
|
1308
|
+
setDrafts((prev) => ({ ...prev, [contentId]: keyed }));
|
|
1309
|
+
}
|
|
1310
|
+
}, customRenderers: customRenderers, branch: branchNameState, comments: displayComments, currentEntryPath: selectedPath, currentUserId: commentUserId, canResolve: canResolveAnyComment, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, format: currentEntry.format, conflictNotice: !!(currentEntry?.contentId &&
|
|
1311
|
+
currentBranch?.conflictFiles?.includes(currentEntry.contentId)) }) }) })) : schema.length === 0 ? (_jsx(CenteredMessage, { children: "No fields to edit." })) : loadFailedKey !== `${branchNameState}:${currentEntry.contentId}` ? (_jsx(Stack, { gap: "lg", p: "md", "data-testid": "entry-loading-skeleton", children: [0, 1, 2].map((row) => (_jsxs(Stack, { gap: 6, children: [_jsx(Skeleton, { h: 12, w: 120 }), _jsx(Skeleton, { h: 36 })] }, row))) })) : (_jsx(Alert, { color: "red", variant: "light", m: "md", title: "This entry couldn't be loaded.", "data-testid": "entry-load-failed", children: _jsx(Button, { variant: "default", onClick: () => {
|
|
1312
|
+
setLoadFailedKey(null);
|
|
1313
|
+
setLoadAttempt((attempt) => attempt + 1);
|
|
1314
|
+
}, children: "Retry" }) })) }) }), overlays: _jsxs(_Fragment, { children: [_jsx(QuickOpenPalette, { opened: quickOpenOpen, onOpen: openQuickOpen, onClose: () => setQuickOpenOpen(false), returnFocusTo: quickOpenFromOverlay ? navigatorReturnTarget : undefined, entries: entriesState,
|
|
1315
|
+
// The hidden root's entries sit at the top of the tree, so they get no heading here either.
|
|
1316
|
+
collectionLabel: (path) => path === hiddenRootPath ? '' : collectionLabels.get(path), onSelect: setSelectedPath }), _jsx(BranchesDrawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), confirmOpen: branchConfirmOpen, returnFocusTo: branchReturnTarget, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, currentBranchName: currentBranch?.name ?? branchNameState, currentWorkflowBlockedReason: saveAllProgress ? 'Wait for Save all to finish.' : undefined, onSelect: async (name) => {
|
|
1317
|
+
try {
|
|
1318
|
+
await handleBranchChange(name);
|
|
1319
|
+
setBranchManagerOpen(false);
|
|
1320
|
+
}
|
|
1321
|
+
catch (err) {
|
|
1322
|
+
console.error('Branch change failed or was cancelled:', err);
|
|
1323
|
+
// Don't close branch manager if there was an error or user cancelled
|
|
1324
|
+
}
|
|
1325
|
+
}, onBeforeCreate: confirmCreateBranch, onCreate: handleCreateBranch,
|
|
1326
|
+
// A Save all mid-run on the open branch would be submitted partway through.
|
|
1327
|
+
onSubmit: (name) => {
|
|
1328
|
+
if (name === branchNameState && isSaveAllRunning())
|
|
1329
|
+
return;
|
|
1330
|
+
handleSubmit(name).catch((err) => console.error(err));
|
|
1331
|
+
}, onWithdraw: (name) => {
|
|
1332
|
+
if (name === branchNameState && isSaveAllRunning())
|
|
1333
|
+
return;
|
|
1334
|
+
handleWithdraw(name).catch((err) => console.error(err));
|
|
1335
|
+
}, onRequestChanges: (name) => {
|
|
1336
|
+
handleRequestChanges(name).catch((err) => console.error(err));
|
|
1337
|
+
}, onDelete: (name) => {
|
|
1338
|
+
handleDelete(name).catch((err) => console.error(err));
|
|
1339
|
+
}, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: commentUserId, canResolve: canResolveAnyComment, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: canResolveUserNames ? handleGetUserMetadata : undefined, onGetOwnerMetadata: getCommentUserMetadata, onGetCommentUserMetadata: getCommentUserMetadata }) }), branchNameState && (_jsx(CommentsPanel, { opened: commentsPanelOpen, branchName: branchNameState, comments: displayComments, canResolve: canResolveAnyComment, currentUserId: commentUserId, entryLabel: commentEntryLabel, fieldLabel: commentFieldLabel, onAddComment: handleAddDisplayedComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: jumpToThreadField, onGetUserMetadata: getCommentUserMetadata, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch }, branchNameState)), userIsAdmin && (_jsx(GroupManager, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), returnFocusTo: settingsReturnTarget, internalGroups: groupsData, loading: groupsLoading, canEdit: userIsAdmin, onSave: handleSaveGroups, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onSearchExternalGroups: handleSearchExternalGroups })), userIsAdmin && (_jsx(PermissionManager, { opened: permissionManagerOpen, onClose: () => setPermissionManagerOpen(false), returnFocusTo: settingsReturnTarget, collections: activeCollections, contentRoot: contentRoot, permissions: permissionsData, loading: permissionsLoading, canEdit: userIsAdmin, onSave: handleSavePermissions, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onListGroups: handleListGroups })), userIsAdmin && (_jsx(SystemHealthPanel, { opened: systemHealthOpen, onClose: () => setSystemHealthOpen(false), returnFocusTo: settingsReturnTarget })), _jsx(MediaLibrary, { opened: mediaLibraryOpen, onClose: () => setMediaLibraryOpen(false), mode: "manage", returnFocusTo: () => mediaButtonRef.current }), userIsAdmin && (_jsx(CollectionEditor, { isOpen: collectionEditorOpen, editingCollection: editingCollection, parentPath: collectionEditorParentPath, availableSchemas: availableSchemas, onSave: handleCollectionSave, onAddEntryType: editingCollection
|
|
1340
|
+
? (path, entryType) => addEntryType(path, entryType)
|
|
1341
|
+
: undefined, onUpdateEntryType: editingCollection
|
|
1342
|
+
? (path, name, updates) => updateEntryType(path, name, updates)
|
|
1343
|
+
: undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onReloadEntryTypes: editingCollection ? readEntryTypes : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError, restored: restoredCollectionInput, onDiscardRestored: () => {
|
|
1344
|
+
collectionDraftRef.current = null;
|
|
1345
|
+
setRestoredCollectionInput(null);
|
|
1346
|
+
} })), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit,
|
|
1347
|
+
// `renamingEntry` stays until the next rename, so the dialog plays its exit and
|
|
1348
|
+
// returns focus.
|
|
1349
|
+
onClose: () => {
|
|
1350
|
+
setRenameModalOpen(false);
|
|
1351
|
+
setRenameModalError(null);
|
|
1352
|
+
}, isSaving: renameModalSaving, returnFocusTo: entryChangeReturnTarget, error: renameModalError?.mayHaveLanded ? null : renameModalError?.message, warning: renameModalError?.mayHaveLanded ? renameModalError.message : null, existingSlugs: renameExistingSlugs, previewPath: (slug) => previewPathFor({
|
|
1353
|
+
collectionPath: renamingEntry.collectionPath ?? '',
|
|
1354
|
+
collectionName: renamingEntry.collectionName,
|
|
1355
|
+
slug,
|
|
1356
|
+
entryType: renamingEntry.entryType,
|
|
1357
|
+
}) })), createModalCollection && (_jsx(EntryCreateModal, { isOpen: createModalOpen, collectionLabel: createModalCollection.label || createModalCollection.name, entryTypes: createModalEntryTypes, onCreate: handleCreateModalSubmit, onClose: closeCreateModal, isCreating: createModalCreating, error: createModalError, existingSlugs: createModalExistingSlugs, previewPath: (slug, entryTypeName) => previewPathFor({
|
|
1358
|
+
collectionPath: createModalCollection.path,
|
|
1359
|
+
collectionName: createModalCollection.name,
|
|
1360
|
+
slug,
|
|
1361
|
+
entryType: entryTypeName,
|
|
1362
|
+
}) })), _jsx(ConfirmDeleteModal, { isOpen: deleteCollectionModalOpen, title: "Delete Collection", message: "Are you sure you want to delete this collection? This cannot be undone.", confirmLabel: "Delete Collection", onConfirm: confirmDeleteCollection, onClose: () => {
|
|
1363
|
+
setDeleteCollectionModalOpen(false);
|
|
1364
|
+
setDeletingCollectionPath(null);
|
|
1365
|
+
}, loading: deleteInProgress }), _jsx(ConfirmDeleteModal, { isOpen: deleteEntryModalOpen, title: "Delete entry", message: deletingEntryReferencedBy
|
|
1366
|
+
? referencedDeleteMessage(deletingEntryReferencedBy)
|
|
1367
|
+
: `Delete ${deletingEntryLabel ? `"${deletingEntryLabel}"` : 'this entry'}? This cannot be undone.`, confirmLabel: deletingEntryReferencedBy ? 'Delete anyway' : 'Delete entry', onConfirm: confirmDeleteEntry, onClose: closeDeleteEntryModal, loading: deleteInProgress, returnFocusTo: entryChangeReturnTarget, children: deletingEntryReferencedBy && (_jsx(ReferencedByList, { referencedBy: deletingEntryReferencedBy, onOpenEntry: (entryPath) => {
|
|
1368
|
+
closeDeleteEntryModal();
|
|
1369
|
+
openEntry(entryPath);
|
|
1370
|
+
} })) })] }) }) }) }));
|
|
698
1371
|
};
|
|
699
1372
|
export default Editor;
|