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.
Files changed (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
@@ -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, Drawer, Group, Menu, Paper, Text, Title, } from '@mantine/core';
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 { PreviewFrame } from './preview-bridge.js';
10
- import { EditorPanes } from './EditorPanes.js';
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 { buildPreviewSrc, buildCollectionLabels, buildBreadcrumbSegments } from './editor-utils.js';
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 { EditorFooter, EditorHeader, EditorSidebar } from './components/index.js';
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 { CollectionEditor } from './schema-editor/index.js';
29
- import { AssetContextProvider, useApiClient } from './context/index.js';
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 = ({ entries, title, subtitle, siteTitle = title, siteSubtitle = subtitle, branchName = '', collections, contentRoot, entryLinkUrl, initialSelectedId, initialValues, renderPreview, onCreateEntry, themeOptions, operatingMode, previewBaseByCollection, previewPrefix, currentUser = 'current-user', canResolveComments = true, assetBaseUrl, basePath, AccountComponent, onAccountClick, onLogoutClick, customRenderers, }) => {
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
- // Preview data with resolved references for live preview
60
- const [previewData, setPreviewData] = useState({});
61
- const [previewLoadingState, setPreviewLoadingState] = useState({});
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
- // The System Health panel is admin-only -- gate both the sidebar
67
- // menu item and the modal mount on it, same pattern BranchManager.tsx uses
68
- // for its own admin-only actions.
69
- const showSystemHealth = isAdmin(userContext?.groups);
70
- const { layout, setLayout, highlightEnabled, setHighlightEnabled, headerRef, headerHeight } = useEditorLayout();
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
- // Reset preview state when switching entries so a stale previous entry's
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, isAnyDirty, fieldErrors, } = useDraftManager({
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
- // 4. Branch actions (depends on isAnyDirty, setBranchName)
144
- const { handleBranchChange, handleCreateBranch } = useBranchActions({
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
- isAnyDirty,
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
- if (collection) {
270
- try {
271
- const result = await apiClient.schema.getCollection({
272
- branch: branchNameState,
273
- collectionPath: collection.path,
274
- });
275
- if (result.ok && result.data && result.data.collection) {
276
- const entries = result.data.entryTypesWithUsage
277
- ? result.data.entryTypesWithUsage.map((et) => ({
278
- name: et.name,
279
- label: et.label,
280
- format: et.format,
281
- schema: et.schemaRef,
282
- default: et.default,
283
- maxItems: et.maxItems,
284
- usageCount: et.usageCount,
285
- }))
286
- : (collection.entryTypes ?? []).map((et) => ({
287
- name: et.name,
288
- label: et.label,
289
- format: et.format,
290
- schema: '', // Fallback if entryTypesWithUsage not available
291
- default: et.default,
292
- maxItems: et.maxItems,
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
- catch {
320
- const existingCollection = {
321
- name: collection.name,
322
- label: collection.label,
323
- logicalPath: collection.path,
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 result = await createCollection(data);
357
- if (result.ok) {
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 result = await updateCollection(editingCollection.logicalPath, data);
366
- if (result.ok) {
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
- if (!deletingEntryPath)
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
- const result = await deleteEntry(deletingEntryPath);
401
- if (result.ok && selectedPath === deletingEntryPath) {
402
- setSelectedPath('');
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
- setDeleteEntryModalOpen(false);
405
- setDeletingEntryPath(null);
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
- const errorMessage = err instanceof Error ? err.message : 'Rename failed';
434
- setRenameModalError(errorMessage);
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
- status: entry.status,
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
- onAdd: node.type !== 'entry'
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' ? () => handleOpenCollectionEditor(node) : undefined,
537
- onAddSubCollection: node.type === 'collection'
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' ? () => handleDeleteCollection(node.path) : undefined,
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
- // Tree expansion state - persists across drawer close/open
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
- const previewFrameData = useMemo(() => {
576
- const data = Object.keys(previewData).length > 0 ? previewData : effectiveValue;
577
- if (!data)
578
- return data;
579
- return resolveEntryLinks(data);
580
- }, [previewData, effectiveValue, resolveEntryLinks]);
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 defaultPreview = currentEntry?.previewSrc && previewFrameData ? (_jsxs(Box, { pos: "relative", w: "100%", h: "100%", children: [previewError && (_jsx(Alert, { color: "red", title: "Preview error", withCloseButton: true, onClose: () => setPreviewError(null), style: { position: 'absolute', top: 8, left: 8, right: 8, zIndex: 2 }, children: previewError.fieldPath
598
- ? `${previewError.fieldPath}: ${previewError.message}`
599
- : previewError.message })), _jsx(PreviewFrame, { src: currentEntry.previewSrc, path: currentEntry.previewSrc, data: previewFrameData, isLoading: previewLoadingState, style: {
600
- width: '100%',
601
- height: '100%',
602
- border: '1px solid var(--mantine-color-gray-3)',
603
- }, highlightEnabled: highlightEnabled, onPreviewError: setPreviewError })] })) : (_jsx(Paper, { withBorder: true, shadow: "xs", h: "100%", bg: "white", style: {
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
- return (_jsx(CanopyCMSProvider, { ...(themeOptions ?? {}), children: _jsx(AssetContextProvider, { baseUrl: assetBaseUrl, basePath: basePath, children: _jsxs(Box, { bg: "gray.0", style: { minHeight: '100vh', width: '100%' }, children: [_jsx(EditorHeader, { ref: headerRef, siteTitle: siteTitle, siteSubtitle: siteSubtitle, headerTitle: headerTitle, currentEntry: currentEntry, branchName: branchNameState, operatingMode: operatingMode, branchStatus: currentBranch?.status, busy: busy, breadcrumbSegments: breadcrumbSegments, editedFiles: editedFiles, modifiedCount: modifiedCount, unresolvedCommentCount: comments.filter((t) => !t.resolved).length, comments: comments, hasUnsavedChanges: isSelectedDirty(), userContext: userContext, branchCreatedBy: currentBranch?.createdBy, branchAccess: currentBranch?.access,
619
- // Fail-closed for the same reason branchContentLocked above is:
620
- // isProtected/writeBlocked gate real actions in EditorHeader (hiding
621
- // Submit, disabling Save), so a missing wire value must read as
622
- // "protected"/"blocked", not "unprotected"/"unblocked".
623
- // branchWriteBlocked reuses branchContentLocked itself rather than
624
- // re-deriving `?? true` a second time here, so there is exactly one
625
- // fail-closed computation of "are writes blocked" in this component.
626
- branchIsProtected: currentBranch?.isProtected ?? true, branchReadOnly: currentBranch?.readOnly, branchWriteBlocked: branchContentLocked, onNavigatorOpen: () => setNavigatorOpen(true), onFileReload: handleReload, onFileDiscardDraft: handleDiscardFileDraft, onEntrySelect: setSelectedPath, onBranchReloadData: handleReloadBranchData, onBranchDiscardDrafts: handleDiscardDrafts, onBranchManagerOpen: () => {
627
- setBranchManagerOpen(true);
628
- // Always retry, not only when branchless. Opening the manager is
629
- // the one in-app retry for a failed branches fetch, and the fetch
630
- // is terminal on its own: SWRProvider sets shouldRetryOnError and
631
- // revalidateOnFocus false, and useBranchesData's refreshInterval
632
- // is 0 without data. Reloading is idempotent.
633
- loadBranches().catch(console.error);
634
- }, onCommentsPanelOpen: () => setCommentsPanelOpen(true), onSave: handleSave, onSubmit: () => branchNameState && handleSubmit(branchNameState), onWithdraw: () => branchNameState && handleWithdraw(branchNameState) }), _jsx(Box, { style: {
635
- paddingTop: headerHeight,
636
- paddingBottom: footerHeight,
637
- paddingRight: sidebarWidth,
638
- minHeight: '100vh',
639
- width: '100%',
640
- }, children: _jsxs(Box, { style: {
641
- height: `calc(100vh - ${headerHeight + footerHeight}px)`,
642
- minHeight: 0,
643
- position: 'relative',
644
- }, children: [_jsx(Box, { style: { flex: 1, minHeight: 0, height: '100%', width: '100%' }, children: _jsx(EditorPanes, { layout: layout, onLayoutChange: (next) => setLayout(next), preview: renderPreview && currentEntry
645
- ? renderPreview(currentEntry, effectiveValue)
646
- : defaultPreview, form: !currentEntry ? (_jsx(CenteredMessage, { children: !branchNameState && busy
647
- ? 'Setting up your branch workspace…'
648
- : entriesInitializing
649
- ? 'Loading content…'
650
- : 'Select an item to start editing.' })) : currentEntry.canEdit === false ? (_jsx(NoEditPermissionNotice, { entryPath: currentEntry.path })) : schema.length > 0 && effectiveValue ? (_jsx(EntryLinkContext.Provider, { value: entryLinkContextValue, children: _jsx(FormRenderer, { fields: schema, value: effectiveValue, onChange: (next) => {
651
- const contentId = currentEntry?.contentId;
652
- if (contentId) {
653
- setDrafts((prev) => ({ ...prev, [contentId]: next }));
654
- }
655
- }, customRenderers: customRenderers, branch: branchNameState, onResolvedValueChange: setPreviewData, onLoadingStateChange: setPreviewLoadingState, comments: comments, currentEntryPath: selectedPath, currentUserId: currentUser, canResolve: canResolveComments, focusedFieldPath: focusedFieldPath, highlightThreadId: highlightThreadId, onAddComment: handleAddComment, onResolveThread: handleResolveThread, fieldErrors: fieldErrors, conflictNotice: !!(currentEntry?.contentId &&
656
- currentBranch?.conflictFiles?.includes(currentEntry.contentId)) }) })) : (_jsx(CenteredMessage, { children: "No fields to edit." })) }) }), _jsx(EditorSidebar, { layout: layout, highlightEnabled: highlightEnabled, sidebarWidth: sidebarWidth, headerHeight: headerHeight, footerHeight: footerHeight, onLayoutChange: setLayout, onHighlightToggle: () => setHighlightEnabled(!highlightEnabled), onPermissionManagerOpen: () => setPermissionManagerOpen(true), onGroupManagerOpen: () => setGroupManagerOpen(true), onMediaLibraryOpen: () => setMediaLibraryOpen(true), onSystemHealthOpen: showSystemHealth ? () => setSystemHealthOpen(true) : undefined, AccountComponent: AccountComponent, onAccountClick: onAccountClick, onLogoutClick: onLogoutClick })] }) }), _jsx(EditorFooter, {}), _jsxs(Drawer.Root, { opened: navigatorOpen, onClose: () => setNavigatorOpen(false), position: "left", size: 360, children: [_jsx(Drawer.Overlay, { blur: 2 }), _jsxs(Drawer.Content, { children: [_jsxs(Drawer.Header, { children: [_jsx(Drawer.Title, { children: "Content" }), _jsxs(Group, { gap: "xs", children: [navCollections &&
657
- navCollections.length > 0 &&
658
- !branchContentLocked &&
659
- (navCollections[0].onAdd || navCollections[0].onAddSubCollection) && (_jsxs(Menu, { shadow: "md", width: 200, withinPortal: true, position: "bottom-end", children: [_jsx(Menu.Target, { children: _jsx(ActionIcon, { variant: "subtle", color: "gray", size: "sm", "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" })), navCollections[0].onAddSubCollection && (_jsx(Menu.Item, { leftSection: _jsx(IconFolderPlus, { size: 14 }), onClick: () => navCollections[0].onAddSubCollection?.(), children: "Add Collection" }))] })] })), _jsx(ActionIcon, { variant: "subtle", color: "gray", size: "sm", onClick: handleCollapseAll, title: "Collapse all folders", "aria-label": "Collapse all folders", children: _jsx(IconChevronUp, { size: 16 }) }), _jsx(ActionIcon, { variant: "subtle", color: "gray", size: "sm", onClick: handleExpandAll, title: "Expand all folders", "aria-label": "Expand all folders", children: _jsx(IconChevronDown, { size: 16 }) }), _jsx(Drawer.CloseButton, {})] })] }), _jsx(Drawer.Body, { p: 0, children: _jsx(Box, { px: "md", children: _jsx(EntryNavigator, { collections: navCollections, items: navCollections
660
- ? undefined
661
- : entriesState.map((e) => ({
662
- path: e.path,
663
- label: e.label,
664
- status: e.status,
665
- })), selectedPath: selectedPath, onSelect: (id) => {
666
- setSelectedPath(id);
667
- setNavigatorOpen(false);
668
- }, onTreeControllerReady: handleTreeControllerReady, expandedStateRef: treeExpandedStateRef, onExpandedStateChange: handleExpandedStateChange, onDeleteEntry: handleDeleteEntry, onRenameEntry: handleRenameEntry, onReorderEntry: handleReorderEntry, hiddenRootPath: hiddenRootPath, loading: entriesInitializing, readOnly: branchContentLocked }) }) })] })] }), _jsx(Drawer, { opened: branchManagerOpen, onClose: () => setBranchManagerOpen(false), position: "right", title: _jsxs("div", { children: [_jsx(Title, { order: 4, children: "Branches" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Manage access, status, and lifecycle" })] }), padding: "md", size: 420, overlayProps: { blur: 2 }, children: _jsx(BranchManager, { branches: branchSummaries, mode: operatingMode, user: userContext, onSelect: async (name) => {
669
- try {
670
- await handleBranchChange(name);
671
- setBranchManagerOpen(false);
672
- }
673
- catch (err) {
674
- console.error('Branch change failed or was cancelled:', err);
675
- // Don't close branch manager if there was an error or user cancelled
676
- }
677
- }, onCreate: handleCreateBranch, onSubmit: (name) => {
678
- handleSubmit(name).catch((err) => console.error(err));
679
- }, onWithdraw: (name) => {
680
- handleWithdraw(name).catch((err) => console.error(err));
681
- }, onRequestChanges: (name) => {
682
- handleRequestChanges(name).catch((err) => console.error(err));
683
- }, onDelete: (name) => {
684
- handleDelete(name).catch((err) => console.error(err));
685
- }, onClose: () => setBranchManagerOpen(false), comments: comments, currentUserId: currentUser, canResolve: canResolveComments, onAddComment: handleAddComment, onResolveThread: handleResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: handleGetUserMetadata }) }), commentsPanelOpen && branchNameState && (_jsx(CommentsPanel, { branchName: branchNameState, comments: comments, canResolve: true, onAddComment: handleAddComment, onResolveThread: handleResolveThread, onClose: () => setCommentsPanelOpen(false), onJumpToField: handleJumpToField, onGetUserMetadata: handleGetUserMetadata, onJumpToEntry: handleJumpToEntry, onJumpToBranch: handleJumpToBranch })), _jsx(Drawer, { opened: groupManagerOpen, onClose: () => setGroupManagerOpen(false), position: "right", title: _jsxs("div", { children: [_jsx(Title, { order: 4, children: "Groups" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Manage groups and organizations" })] }), padding: "md", size: 600, overlayProps: { blur: 2 }, children: _jsx(GroupManager, { internalGroups: groupsData, loading: groupsLoading, canEdit: true, onSave: handleSaveGroups, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onSearchExternalGroups: handleSearchExternalGroups, onClose: () => setGroupManagerOpen(false) }) }), _jsx(Drawer, { opened: permissionManagerOpen, onClose: () => setPermissionManagerOpen(false), position: "right", title: _jsxs("div", { children: [_jsx(Title, { order: 4, children: "Permissions" }), _jsx(Text, { size: "xs", c: "dimmed", children: "Manage content access by path (read, edit, review)" })] }), padding: "md", size: 700, overlayProps: { blur: 2 }, children: _jsx(PermissionManager, { collections: activeCollections, contentRoot: contentRoot, permissions: permissionsData, loading: permissionsLoading, canEdit: true, onSave: handleSavePermissions, onSearchUsers: handleSearchUsers, onGetUserMetadata: handleGetUserMetadata, onListGroups: handleListGroups, onClose: () => setPermissionManagerOpen(false) }) }), showSystemHealth && (_jsx(SystemHealthPanel, { opened: systemHealthOpen, onClose: () => setSystemHealthOpen(false) })), _jsx(MediaLibrary, { opened: mediaLibraryOpen, onClose: () => setMediaLibraryOpen(false), mode: "manage" }), _jsx(CollectionEditor, { isOpen: collectionEditorOpen, editingCollection: editingCollection, parentPath: collectionEditorParentPath, availableSchemas: availableSchemas, onSave: handleCollectionSave, onAddEntryType: editingCollection ? (path, entryType) => addEntryType(path, entryType) : undefined, onUpdateEntryType: editingCollection
686
- ? (path, name, updates) => updateEntryType(path, name, updates)
687
- : undefined, onRemoveEntryType: editingCollection ? (path, name) => removeEntryType(path, name) : undefined, onClose: handleCloseCollectionEditor, isSaving: schemaLoading, error: collectionEditorError }), renamingEntry && (_jsx(RenameEntryModal, { isOpen: renameModalOpen, entryLabel: renamingEntry.label, currentSlug: renamingEntry.slug || '', onSave: handleRenameSubmit, onClose: () => {
688
- setRenameModalOpen(false);
689
- setRenamingEntry(null);
690
- setRenameModalError(null);
691
- }, isSaving: renameModalSaving, error: renameModalError })), createModalCollection && (_jsx(EntryCreateModal, { isOpen: createModalOpen, collectionLabel: createModalCollection.label || createModalCollection.name, entryTypes: createModalEntryTypes, onCreate: handleCreateModalSubmit, onClose: closeCreateModal, isCreating: createModalCreating, error: createModalError, existingSlugs: createModalExistingSlugs })), _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: () => {
692
- setDeleteCollectionModalOpen(false);
693
- setDeletingCollectionPath(null);
694
- }, loading: deleteInProgress }), _jsx(ConfirmDeleteModal, { isOpen: deleteEntryModalOpen, title: "Delete Entry", message: "Are you sure you want to delete this entry? This cannot be undone.", confirmLabel: "Delete Entry", onConfirm: confirmDeleteEntry, onClose: () => {
695
- setDeleteEntryModalOpen(false);
696
- setDeletingEntryPath(null);
697
- }, loading: deleteInProgress })] }) }) }));
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;