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
@@ -2,5 +2,5 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Button, Group, Modal, Paper, Stack, Text, TextInput, Textarea } from '@mantine/core';
4
4
  export const GroupForm = ({ isOpen, editingGroup, formData, onFormChange, onSave, onClose, }) => {
5
- return (_jsx(Modal, { opened: isOpen, onClose: onClose, title: editingGroup ? 'Edit Group' : 'Create Group', children: _jsxs(Stack, { gap: "md", children: [editingGroup && (_jsxs("div", { children: [_jsx(Text, { size: "sm", fw: 500, mb: 4, children: "ID (system-generated)" }), _jsx(Paper, { p: "xs", withBorder: true, bg: "gray.0", children: _jsx(Text, { size: "sm", ff: "monospace", c: "dimmed", children: editingGroup.id }) })] })), _jsx(TextInput, { label: "Group Name", placeholder: "e.g., Content Editors", value: formData.name, onChange: (e) => onFormChange({ name: e.target.value }), required: true }), _jsx(Textarea, { label: "Description", placeholder: "Optional description", value: formData.description, onChange: (e) => onFormChange({ description: e.target.value }), rows: 3 }), _jsxs(Group, { justify: "flex-end", gap: "sm", children: [_jsx(Button, { variant: "subtle", onClick: onClose, children: "Cancel" }), _jsx(Button, { onClick: onSave, children: editingGroup ? 'Save' : 'Create' })] })] }) }));
5
+ return (_jsx(Modal, { opened: isOpen, onClose: onClose, title: editingGroup ? 'Edit group' : 'Create group', children: _jsxs(Stack, { gap: "md", children: [editingGroup && (_jsxs("div", { children: [_jsx(Text, { size: "sm", fw: 500, mb: 4, children: "ID (system-generated)" }), _jsx(Paper, { p: "xs", withBorder: true, bg: "var(--canopy-surface-subtle)", children: _jsx(Text, { size: "sm", ff: "monospace", c: "dimmed", children: editingGroup.id }) })] })), _jsx(TextInput, { label: "Group name", placeholder: "e.g. Content editors", value: formData.name, onChange: (e) => onFormChange({ name: e.target.value }), required: true }), _jsx(Textarea, { label: "Description", placeholder: "Optional description", value: formData.description, onChange: (e) => onFormChange({ description: e.target.value }), rows: 3 }), _jsxs(Group, { justify: "flex-end", gap: "sm", children: [_jsx(Button, { size: "sm", variant: "subtle", onClick: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", onClick: onSave, children: editingGroup ? 'Apply' : 'Add' })] })] }) }));
6
6
  };
@@ -2,13 +2,14 @@ import React from 'react';
2
2
  import type { InternalGroup, UserSearchResult, CanopyGroupId, CanopyUserId } from './types.js';
3
3
  export interface InternalGroupsTabProps {
4
4
  groups: InternalGroup[];
5
+ unsavedGroupIds: ReadonlySet<CanopyGroupId>;
5
6
  canEdit: boolean;
6
7
  onCreateGroup: () => void;
7
8
  onEditGroup: (group: InternalGroup) => void;
8
9
  onDeleteGroup: (groupId: CanopyGroupId) => void;
9
10
  onAddMember: (groupId: CanopyGroupId, userId: CanopyUserId) => void;
10
11
  onRemoveMember: (groupId: CanopyGroupId, userId: CanopyUserId) => void;
11
- onGetUserMetadata?: (userId: string) => Promise<UserSearchResult | null>;
12
+ onGetUserMetadata: (userId: string) => Promise<UserSearchResult | null>;
12
13
  activeSearchGroupId: string | null;
13
14
  searchQuery: string;
14
15
  searchResults: UserSearchResult[];
@@ -2,14 +2,14 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Button, Group, Paper, ScrollArea, Stack, Text } from '@mantine/core';
4
4
  import { IconPlus, IconUsers } from '@tabler/icons-react';
5
- import { GroupCard } from './GroupCard.js';
6
- export const InternalGroupsTab = ({ groups, canEdit, onCreateGroup, onEditGroup, onDeleteGroup, onAddMember, onRemoveMember, onGetUserMetadata, activeSearchGroupId, searchQuery, searchResults, isSearching, searchError, onSearchQueryChange, onShowSearch, onHideSearch, canSearch, }) => {
5
+ import { GroupCard, memberCountLabel } from './GroupCard.js';
6
+ export const InternalGroupsTab = ({ groups, unsavedGroupIds, canEdit, onCreateGroup, onEditGroup, onDeleteGroup, onAddMember, onRemoveMember, onGetUserMetadata, activeSearchGroupId, searchQuery, searchResults, isSearching, searchError, onSearchQueryChange, onShowSearch, onHideSearch, canSearch, }) => {
7
7
  if (!canEdit) {
8
8
  return (_jsx("div", { style: {
9
9
  flex: 1,
10
10
  overflow: 'auto',
11
11
  paddingTop: 'var(--mantine-spacing-md)',
12
- }, children: groups.length === 0 ? (_jsxs(Paper, { withBorder: true, p: "md", style: { textAlign: 'center' }, children: [_jsx(IconUsers, { size: 32, color: "gray", style: { margin: '0 auto', marginBottom: 8 } }), _jsx(Text, { size: "sm", c: "dimmed", children: "No internal groups" })] })) : (_jsx(Stack, { gap: "xs", pb: "md", children: groups.map((group) => (_jsx(Paper, { withBorder: true, p: "sm", children: _jsxs(Group, { justify: "space-between", children: [_jsxs("div", { children: [_jsx(Text, { size: "sm", fw: 500, children: group.name }), group.description && (_jsx(Text, { size: "xs", c: "dimmed", children: group.description }))] }), _jsxs(Text, { size: "sm", variant: "light", children: [group.members?.length || 0, " members"] })] }) }, group.id))) })) }));
12
+ }, children: groups.length === 0 ? (_jsxs(Paper, { withBorder: true, p: "md", style: { textAlign: 'center' }, children: [_jsx(IconUsers, { size: 32, color: "gray", style: { margin: '0 auto', marginBottom: 8 } }), _jsx(Text, { size: "sm", c: "dimmed", children: "No internal groups" })] })) : (_jsx(Stack, { gap: "xs", pb: "md", children: groups.map((group) => (_jsx(Paper, { withBorder: true, p: "sm", children: _jsxs(Group, { justify: "space-between", children: [_jsxs("div", { children: [_jsx(Text, { size: "sm", fw: 500, children: group.name }), group.description && (_jsx(Text, { size: "xs", c: "dimmed", children: group.description }))] }), _jsx(Text, { size: "sm", variant: "light", children: memberCountLabel(group.members?.length ?? 0) })] }) }, group.id))) })) }));
13
13
  }
14
- return (_jsx(ScrollArea, { style: { height: '100%' }, pt: "md", children: _jsxs(Stack, { gap: "sm", pb: "md", children: [_jsxs(Group, { justify: "space-between", mb: "xs", children: [_jsx(Text, { size: "sm", fw: 500, c: "dimmed", children: "Manage your internal groups" }), _jsx(Button, { size: "xs", variant: "light", leftSection: _jsx(IconPlus, { size: 14 }), onClick: onCreateGroup, children: "Create Group" })] }), groups.length === 0 ? (_jsxs(Paper, { withBorder: true, p: "md", style: { textAlign: 'center' }, children: [_jsx(IconUsers, { size: 32, color: "gray", style: { margin: '0 auto', marginBottom: 8 } }), _jsx(Text, { size: "sm", c: "dimmed", children: "No internal groups yet. Create one to get started." })] })) : (groups.map((group) => (_jsx(GroupCard, { group: group, onEdit: onEditGroup, onDelete: onDeleteGroup, onAddMember: onAddMember, onRemoveMember: onRemoveMember, onGetUserMetadata: onGetUserMetadata, showUserSearch: activeSearchGroupId === group.id, searchQuery: searchQuery, searchResults: searchResults, isSearching: isSearching, searchError: searchError, onSearchQueryChange: onSearchQueryChange, onShowSearch: () => onShowSearch(group.id), onHideSearch: onHideSearch, canSearch: canSearch }, group.id))))] }) }));
14
+ return (_jsx(ScrollArea, { style: { height: '100%' }, pt: "md", children: _jsxs(Stack, { gap: "sm", pb: "md", children: [_jsxs(Group, { justify: "space-between", mb: "xs", children: [_jsx(Text, { size: "sm", fw: 500, c: "dimmed", children: "Manage your internal groups" }), _jsx(Button, { variant: "light", leftSection: _jsx(IconPlus, { size: 14 }), onClick: onCreateGroup, children: "Create group" })] }), groups.length === 0 ? (_jsxs(Paper, { withBorder: true, p: "md", style: { textAlign: 'center' }, children: [_jsx(IconUsers, { size: 32, color: "gray", style: { margin: '0 auto', marginBottom: 8 } }), _jsx(Text, { size: "sm", c: "dimmed", children: "No internal groups yet. Create one to get started." })] })) : (groups.map((group) => (_jsx(GroupCard, { group: group, isUnsaved: unsavedGroupIds.has(group.id), onEdit: onEditGroup, onDelete: onDeleteGroup, onAddMember: onAddMember, onRemoveMember: onRemoveMember, onGetUserMetadata: onGetUserMetadata, showUserSearch: activeSearchGroupId === group.id, searchQuery: searchQuery, searchResults: searchResults, isSearching: isSearching, searchError: searchError, onSearchQueryChange: onSearchQueryChange, onShowSearch: () => onShowSearch(group.id), onHideSearch: onHideSearch, canSearch: canSearch }, group.id))))] }) }));
15
15
  };
@@ -5,7 +5,8 @@ export interface MemberListProps {
5
5
  members: CanopyUserId[];
6
6
  onRemoveMember: (groupId: CanopyGroupId, userId: CanopyUserId) => void;
7
7
  onAddMember: (groupId: CanopyGroupId, userId: CanopyUserId) => void;
8
- onGetUserMetadata?: (userId: string) => Promise<UserSearchResult | null>;
8
+ /** Names each member; an id never shows as the label, since an unresolved one reads "Name unavailable". */
9
+ onGetUserMetadata: (userId: string) => Promise<UserSearchResult | null>;
9
10
  showUserSearch: boolean;
10
11
  searchQuery: string;
11
12
  searchResults: UserSearchResult[];
@@ -1,12 +1,23 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { ActionIcon, Badge, Button, Group, Loader, Paper, Stack, Text, TextInput, } from '@mantine/core';
4
- import { IconSearch, IconX } from '@tabler/icons-react';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef } from 'react';
4
+ import { Button, Group, Loader, Paper, Stack, Text, TextInput } from '@mantine/core';
5
+ import { IconSearch } from '@tabler/icons-react';
5
6
  import { UserBadge } from '../components/UserBadge.js';
7
+ import { useFocusAfterRemove } from '../utils/focus-return.js';
6
8
  export const MemberList = ({ groupId, members, onRemoveMember, onAddMember, onGetUserMetadata, showUserSearch, searchQuery, searchResults, isSearching, searchError, onSearchQueryChange, onShowSearch, onHideSearch, canSearch, }) => {
9
+ const { listRef, focusAfterRemove } = useFocusAfterRemove();
10
+ const addMemberRef = useRef(null);
11
+ const searchInputRef = useRef(null);
12
+ const headingRef = useRef(null);
13
+ // With no member left, focus goes to whichever of these the section shows and can focus.
14
+ const emptyListFocus = showUserSearch ? searchInputRef : canSearch ? addMemberRef : headingRef;
7
15
  const handleAddMember = (userId) => {
8
16
  onAddMember(groupId, userId);
9
17
  onHideSearch();
10
18
  };
11
- return (_jsxs("div", { children: [_jsx(Text, { size: "xs", fw: 500, mb: 4, children: "Members" }), _jsx(Group, { gap: "xs", mb: "xs", children: members.length > 0 ? (members.map((userId) => onGetUserMetadata ? (_jsx(UserBadge, { userId: userId, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", badgeVariant: "filled", color: "blue", onRemove: () => onRemoveMember(groupId, userId), showEmailTooltip: true }, userId)) : (_jsx(Badge, { variant: "filled", color: "blue", pr: 3, rightSection: _jsx(ActionIcon, { size: "xs", color: "blue", radius: "xl", variant: "transparent", onClick: () => onRemoveMember(groupId, userId), children: _jsx(IconX, { size: 10, style: { color: 'white' } }) }), children: userId }, userId)))) : (_jsx(Text, { size: "xs", c: "dimmed", children: "No members" })) }), showUserSearch ? (_jsx(Paper, { withBorder: true, p: "sm", mt: "xs", children: _jsxs(Stack, { gap: "xs", children: [_jsx(TextInput, { placeholder: "Search users by name or email...", value: searchQuery, onChange: (e) => onSearchQueryChange(e.target.value), "aria-label": "Search users by name or email", size: "xs" }), isSearching && _jsx(Loader, { size: "xs" }), searchError && (_jsx(Text, { size: "xs", c: "red", children: searchError })), searchResults.length > 0 && (_jsx(Stack, { gap: 4, children: searchResults.map((user) => (_jsx(Paper, { p: "xs", withBorder: true, style: { cursor: 'pointer' }, onClick: () => handleAddMember(user.id), children: onGetUserMetadata ? (_jsx(UserBadge, { userId: user.id, getUserMetadata: onGetUserMetadata, variant: "full", size: "sm", cachedUser: user, showEmailTooltip: false })) : (_jsxs(_Fragment, { children: [_jsx(Text, { size: "sm", fw: 500, children: user.name }), _jsx(Text, { size: "xs", c: "dimmed", children: user.email })] })) }, user.id))) })), searchQuery.trim() && !isSearching && searchResults.length === 0 && (_jsx(Text, { size: "xs", c: "dimmed", children: "No users found" })), _jsx(Button, { size: "xs", variant: "subtle", onClick: onHideSearch, children: "Cancel" })] }) })) : (_jsx(Button, { size: "xs", variant: "subtle", leftSection: _jsx(IconSearch, { size: 14 }), onClick: onShowSearch, disabled: !canSearch, children: "Add Member" }))] }));
19
+ return (_jsxs("div", { children: [_jsx(Text, { size: "xs", fw: 500, mb: 4, ref: headingRef, tabIndex: -1, children: "Members" }), _jsx(Group, { gap: "xs", mb: "xs", ref: listRef, children: members.length > 0 ? (members.map((userId, index) => (_jsx(UserBadge, { userId: userId, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", badgeVariant: "light", color: "blue", onRemove: () => {
20
+ onRemoveMember(groupId, userId);
21
+ focusAfterRemove(index, emptyListFocus);
22
+ }, showEmailTooltip: true, revealIdOnFallback: true }, userId)))) : (_jsx(Text, { size: "xs", c: "dimmed", children: "No members" })) }), showUserSearch ? (_jsx(Paper, { withBorder: true, p: "sm", mt: "xs", children: _jsxs(Stack, { gap: "xs", children: [_jsx(TextInput, { ref: searchInputRef, placeholder: "Search users by name or email...", value: searchQuery, onChange: (e) => onSearchQueryChange(e.target.value), "aria-label": "Search users by name or email", size: "xs" }), isSearching && _jsx(Loader, { size: "xs" }), searchError && (_jsx(Text, { size: "xs", c: "var(--canopy-danger-text)", children: searchError })), searchResults.length > 0 && (_jsx(Stack, { gap: 4, children: searchResults.map((user) => (_jsx(Paper, { p: "xs", withBorder: true, style: { cursor: 'pointer' }, onClick: () => handleAddMember(user.id), children: _jsx(UserBadge, { userId: user.id, getUserMetadata: onGetUserMetadata, variant: "full", size: "sm", cachedUser: user, showEmailTooltip: false }) }, user.id))) })), searchQuery.trim() && !isSearching && searchResults.length === 0 && (_jsx(Text, { size: "xs", c: "dimmed", children: "No users found" })), _jsx(Button, { variant: "subtle", onClick: onHideSearch, children: "Cancel" })] }) })) : (_jsx(Button, { ref: addMemberRef, variant: "subtle", leftSection: _jsx(IconSearch, { size: 14 }), onClick: onShowSearch, disabled: !canSearch, children: "Add member" }))] }));
12
23
  };
@@ -1,13 +1,18 @@
1
1
  import type { InternalGroup, CanopyGroupId, CanopyUserId } from '../types.js';
2
+ export declare const isTempGroupId: (id: CanopyGroupId) => boolean;
2
3
  export interface UseGroupStateOptions {
3
4
  initialGroups: InternalGroup[];
4
5
  onSave?: (groups: InternalGroup[]) => Promise<void>;
5
6
  }
6
7
  export interface UseGroupStateResult {
7
8
  groups: InternalGroup[];
9
+ /** Listed groups that are new or differ from the last loaded or saved state. */
10
+ unsavedGroupIds: ReadonlySet<CanopyGroupId>;
8
11
  isDirty: boolean;
9
12
  isSaving: boolean;
10
13
  error: string | null;
14
+ /** The error is a version conflict (`SettingsChangedError`), shown yellow. */
15
+ errorIsConflict: boolean;
11
16
  setError: (error: string | null) => void;
12
17
  createGroup: (name: string, description: string) => void;
13
18
  updateGroup: (groupId: CanopyGroupId, name: string, description: string) => void;
@@ -1,15 +1,33 @@
1
- import { useState, useEffect, useCallback } from 'react';
1
+ import { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { SettingsChangedError } from '../../settings-conflict.js';
2
3
  const TEMP_GROUP_ID_PREFIX = 'temp-';
3
- const isTempGroupId = (id) => id.startsWith(TEMP_GROUP_ID_PREFIX);
4
+ export const isTempGroupId = (id) => id.startsWith(TEMP_GROUP_ID_PREFIX);
5
+ const sameMembers = (a = [], b = []) => a.length === b.length && [...a].sort().join('\n') === [...b].sort().join('\n');
6
+ const sameGroup = (a, b) => a.name === b.name &&
7
+ (a.description ?? '') === (b.description ?? '') &&
8
+ sameMembers(a.members, b.members);
4
9
  export function useGroupState({ initialGroups, onSave, }) {
5
10
  const [groups, setGroups] = useState(initialGroups);
6
11
  const [isDirty, setIsDirty] = useState(false);
7
12
  const [isSaving, setIsSaving] = useState(false);
8
- const [error, setError] = useState(null);
13
+ const [failure, setFailure] = useState(null);
14
+ const setError = useCallback((message) => setFailure(message === null ? null : { message, conflict: false }), []);
9
15
  useEffect(() => {
10
16
  setGroups(initialGroups);
11
17
  setIsDirty(false);
12
18
  }, [initialGroups]);
19
+ // Only while dirty: after a save whose reload failed, initialGroups is stale.
20
+ const unsavedGroupIds = useMemo(() => {
21
+ if (!isDirty)
22
+ return new Set();
23
+ const initialById = new Map(initialGroups.map((g) => [g.id, g]));
24
+ return new Set(groups
25
+ .filter((g) => {
26
+ const initial = initialById.get(g.id);
27
+ return !initial || !sameGroup(g, initial);
28
+ })
29
+ .map((g) => g.id));
30
+ }, [groups, initialGroups, isDirty]);
13
31
  const createGroup = useCallback((name, description) => {
14
32
  const newGroup = {
15
33
  id: `${TEMP_GROUP_ID_PREFIX}${crypto.randomUUID()}`,
@@ -66,22 +84,27 @@ export function useGroupState({ initialGroups, onSave, }) {
66
84
  setIsDirty(false);
67
85
  }
68
86
  catch (err) {
69
- setError(err instanceof Error ? err.message : 'Failed to save groups');
87
+ setFailure({
88
+ message: err instanceof Error ? err.message : 'Failed to save groups',
89
+ conflict: err instanceof SettingsChangedError,
90
+ });
70
91
  }
71
92
  finally {
72
93
  setIsSaving(false);
73
94
  }
74
- }, [onSave, groups]);
95
+ }, [onSave, groups, setError]);
75
96
  const discard = useCallback(() => {
76
97
  setGroups(initialGroups);
77
98
  setIsDirty(false);
78
99
  setError(null);
79
- }, [initialGroups]);
100
+ }, [initialGroups, setError]);
80
101
  return {
81
102
  groups,
103
+ unsavedGroupIds,
82
104
  isDirty,
83
105
  isSaving,
84
- error,
106
+ error: failure?.message ?? null,
107
+ errorIsConflict: failure?.conflict ?? false,
85
108
  setError,
86
109
  createGroup,
87
110
  updateGroup,
@@ -1,22 +1,29 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback } from 'react';
4
- import { Alert, Button, Group, Loader, Stack, Tabs, Text } from '@mantine/core';
4
+ import { Alert, Group, Loader, Stack, Tabs, Text } from '@mantine/core';
5
5
  import { IconAlertCircle } from '@tabler/icons-react';
6
- import { useGroupState } from './hooks/useGroupState.js';
6
+ import { isTempGroupId, useGroupState } from './hooks/useGroupState.js';
7
7
  import { useUserSearch } from './hooks/useUserSearch.js';
8
8
  import { useExternalGroupSearch } from './hooks/useExternalGroupSearch.js';
9
9
  import { InternalGroupsTab } from './InternalGroupsTab.js';
10
10
  import { ExternalGroupsTab } from './ExternalGroupsTab.js';
11
11
  import { GroupForm } from './GroupForm.js';
12
- export const GroupManager = ({ internalGroups: initialInternalGroups, loading = false, canEdit, onSave, onSearchUsers, onGetUserMetadata, onSearchExternalGroups, onClose: _, }) => {
13
- const { groups, isDirty, isSaving, error, setError, createGroup, updateGroup, deleteGroup, addMember, removeMember, save, discard, } = useGroupState({
12
+ import { openConfirm } from '../utils/confirm-modal.js';
13
+ import { StagedChangesDrawer, useOpeningKey } from '../components/StagedChangesDrawer.js';
14
+ export const GroupManager = (props) => {
15
+ const { key, onExited } = useOpeningKey();
16
+ return _jsx(GroupManagerDrawer, { ...props, onExited: onExited }, key);
17
+ };
18
+ const GroupManagerDrawer = ({ opened, onClose, onExited, returnFocusTo, internalGroups: initialInternalGroups, loading = false, canEdit, onSave, onSearchUsers, onGetUserMetadata, onSearchExternalGroups, }) => {
19
+ const { groups, unsavedGroupIds, isDirty, isSaving, error, errorIsConflict, setError, createGroup, updateGroup, deleteGroup, addMember, removeMember, save, discard, } = useGroupState({
14
20
  initialGroups: initialInternalGroups,
15
21
  onSave,
16
22
  });
17
23
  const userSearch = useUserSearch({ onSearchUsers });
18
24
  const externalGroupSearch = useExternalGroupSearch({ onSearchExternalGroups });
19
25
  const [isModalOpen, setIsModalOpen] = useState(false);
26
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
20
27
  const [editingGroup, setEditingGroup] = useState(null);
21
28
  const [formData, setFormData] = useState({
22
29
  name: '',
@@ -27,6 +34,28 @@ export const GroupManager = ({ internalGroups: initialInternalGroups, loading =
27
34
  setFormData({ name: '', description: '' });
28
35
  setIsModalOpen(true);
29
36
  }, []);
37
+ // Save groups applies a delete with no further look; a group not yet saved has nothing to lose.
38
+ const confirmDeleteGroup = useCallback((groupId) => {
39
+ if (isTempGroupId(groupId)) {
40
+ deleteGroup(groupId);
41
+ return;
42
+ }
43
+ const group = groups.find((g) => g.id === groupId);
44
+ const memberCount = group?.members?.length ?? 0;
45
+ const consequence = memberCount === 0
46
+ ? 'It is deleted when you save.'
47
+ : `Its ${memberCount} ${memberCount === 1 ? 'member loses' : 'members lose'} the access it grants when you save.`;
48
+ setDeleteConfirmOpen(true);
49
+ openConfirm({
50
+ title: 'Delete group',
51
+ children: _jsx(Text, { size: "sm", children: `Delete "${group?.name ?? groupId}"? ${consequence}` }),
52
+ labels: { confirm: 'Delete group', cancel: 'Cancel' },
53
+ confirmProps: { color: 'red' },
54
+ onConfirm: () => deleteGroup(groupId),
55
+ // Deferred: Mantine runs onClose, for every exit, inside its reducer during render.
56
+ onClose: () => queueMicrotask(() => setDeleteConfirmOpen(false)),
57
+ });
58
+ }, [groups, deleteGroup]);
30
59
  const handleEditGroup = useCallback((group) => {
31
60
  setEditingGroup(group);
32
61
  setFormData({ name: group.name, description: group.description || '' });
@@ -53,7 +82,7 @@ export const GroupManager = ({ internalGroups: initialInternalGroups, loading =
53
82
  addMember(groupId, userId);
54
83
  userSearch.hideSearch();
55
84
  }, [addMember, userSearch]);
56
- return (_jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, children: [!canEdit && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "yellow", mb: "sm", title: "Read-only", children: "You need admin access to manage groups." })), error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", mb: "sm", title: "Error", withCloseButton: true, onClose: () => setError(null), children: error })), loading ? (_jsxs(Group, { justify: "center", py: "xl", children: [_jsx(Loader, { size: "md" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Loading groups..." })] })) : (_jsxs(Tabs, { defaultValue: "internal", style: { flex: 1, display: 'flex', flexDirection: 'column' }, children: [_jsxs(Tabs.List, { children: [_jsx(Tabs.Tab, { value: "internal", children: "Internal Groups" }), _jsx(Tabs.Tab, { value: "external", children: "External Groups" })] }), _jsx(Tabs.Panel, { value: "internal", style: { flex: 1, overflow: 'auto' }, children: _jsx(InternalGroupsTab, { groups: groups, canEdit: canEdit, onCreateGroup: handleCreateGroup, onEditGroup: handleEditGroup, onDeleteGroup: deleteGroup, onAddMember: handleAddMember, onRemoveMember: removeMember, onGetUserMetadata: onGetUserMetadata, activeSearchGroupId: userSearch.activeGroupId, searchQuery: userSearch.searchQuery, searchResults: userSearch.searchResults, isSearching: userSearch.isSearching, searchError: userSearch.searchError, onSearchQueryChange: userSearch.setSearchQuery, onShowSearch: userSearch.showSearch, onHideSearch: userSearch.hideSearch, canSearch: !!onSearchUsers }) }), _jsx(Tabs.Panel, { value: "external", style: { flex: 1, overflow: 'auto' }, children: _jsx(ExternalGroupsTab, { canEdit: canEdit, searchQuery: externalGroupSearch.searchQuery, searchResults: externalGroupSearch.searchResults, isSearching: externalGroupSearch.isSearching, searchError: externalGroupSearch.searchError, onSearchQueryChange: externalGroupSearch.setSearchQuery, canSearch: !!onSearchExternalGroups }) })] })), canEdit && isDirty && (_jsxs(Group, { justify: "flex-end", py: "sm", gap: "sm", style: { borderTop: '1px solid var(--mantine-color-gray-3)' }, children: [_jsx(Button, { variant: "subtle", color: "neutral", onClick: discard, disabled: isSaving, children: "Discard Changes" }), _jsx(Button, { onClick: save, loading: isSaving, disabled: isSaving, children: "Save Groups" })] })), _jsx(GroupForm, { isOpen: isModalOpen, editingGroup: editingGroup, formData: formData, onFormChange: handleFormChange, onSave: handleSaveModal, onClose: () => setIsModalOpen(false) })] }));
85
+ return (_jsx(StagedChangesDrawer, { opened: opened, onClose: onClose, title: "Groups", description: "Manage groups and organizations", size: 600, isDirty: isDirty, isSaving: isSaving, canEdit: canEdit, saveLabel: "Save groups", onSave: save, onDiscard: discard, childModalOpen: isModalOpen || deleteConfirmOpen, onExited: onExited, returnFocusTo: returnFocusTo, children: _jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, children: [!canEdit && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "yellow", mb: "sm", title: "Read-only", children: "You need admin access to manage groups." })), error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: errorIsConflict ? 'yellow' : 'red', mb: "sm", title: errorIsConflict ? 'Not saved' : 'Error', withCloseButton: true, onClose: () => setError(null), children: error })), loading ? (_jsxs(Group, { justify: "center", py: "xl", children: [_jsx(Loader, { size: "md" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Loading groups..." })] })) : (_jsxs(Tabs, { defaultValue: "internal", style: { flex: 1, display: 'flex', flexDirection: 'column' }, children: [_jsxs(Tabs.List, { children: [_jsx(Tabs.Tab, { value: "internal", children: "Internal groups" }), _jsx(Tabs.Tab, { value: "external", children: "External groups" })] }), _jsx(Tabs.Panel, { value: "internal", style: { flex: 1, overflow: 'auto' }, children: _jsx(InternalGroupsTab, { groups: groups, unsavedGroupIds: unsavedGroupIds, canEdit: canEdit, onCreateGroup: handleCreateGroup, onEditGroup: handleEditGroup, onDeleteGroup: confirmDeleteGroup, onAddMember: handleAddMember, onRemoveMember: removeMember, onGetUserMetadata: onGetUserMetadata, activeSearchGroupId: userSearch.activeGroupId, searchQuery: userSearch.searchQuery, searchResults: userSearch.searchResults, isSearching: userSearch.isSearching, searchError: userSearch.searchError, onSearchQueryChange: userSearch.setSearchQuery, onShowSearch: userSearch.showSearch, onHideSearch: userSearch.hideSearch, canSearch: !!onSearchUsers }) }), _jsx(Tabs.Panel, { value: "external", style: { flex: 1, overflow: 'auto' }, children: _jsx(ExternalGroupsTab, { canEdit: canEdit, searchQuery: externalGroupSearch.searchQuery, searchResults: externalGroupSearch.searchResults, isSearching: externalGroupSearch.isSearching, searchError: externalGroupSearch.searchError, onSearchQueryChange: externalGroupSearch.setSearchQuery, canSearch: !!onSearchExternalGroups }) })] })), _jsx(GroupForm, { isOpen: isModalOpen, editingGroup: editingGroup, formData: formData, onFormChange: handleFormChange, onSave: handleSaveModal, onClose: () => setIsModalOpen(false) })] }) }));
57
86
  };
58
87
  export { useGroupState } from './hooks/useGroupState.js';
59
88
  export { useUserSearch } from './hooks/useUserSearch.js';
@@ -5,14 +5,17 @@ import type { ExternalGroup } from '../../api/groups.js';
5
5
  export type { UserSearchResult, InternalGroup, ExternalGroup };
6
6
  export type { CanopyGroupId, CanopyUserId };
7
7
  export interface GroupManagerProps {
8
+ opened: boolean;
9
+ /** Closes the drawer; while there are unsaved changes it runs only after the user confirms discarding them. */
10
+ onClose: () => void;
11
+ returnFocusTo?: () => HTMLElement | null;
8
12
  internalGroups: InternalGroup[];
9
13
  loading?: boolean;
10
14
  canEdit: boolean;
11
15
  onSave?: (groups: InternalGroup[]) => Promise<void>;
12
16
  onSearchUsers?: (query: string, limit?: number) => Promise<UserSearchResult[]>;
13
- onGetUserMetadata?: (userId: string) => Promise<UserSearchResult | null>;
17
+ onGetUserMetadata: (userId: string) => Promise<UserSearchResult | null>;
14
18
  onSearchExternalGroups?: (query: string) => Promise<ExternalGroup[]>;
15
- onClose?: () => void;
16
19
  }
17
20
  export interface GroupFormData {
18
21
  name: string;
@@ -0,0 +1,20 @@
1
+ import type { BranchListItem, CreateBranchBody } from '../../api/branch.js';
2
+ import { type CanopyApiClient } from '../../api/client.js';
3
+ /** @internal Exported for tests. Past the 60 s a Lambda behind CloudFront gets, so its 504 lands first. */
4
+ export declare const CREATE_BRANCH_DEADLINE_MS = 90000;
5
+ /** @internal Exported for tests. */
6
+ export declare const CREATE_TIMED_OUT_MESSAGE = "Creating the branch didn't finish (the server timed out). It's safe to try again.";
7
+ export type CreateBranchOutcome = {
8
+ kind: 'created';
9
+ branch: BranchListItem | undefined;
10
+ } | {
11
+ kind: 'failed';
12
+ message: string;
13
+ };
14
+ /**
15
+ * Create a branch. Past the deadline, after an error page from in front of the API, or after a
16
+ * busy 503 the branch may exist anyway, so the branch list decides; retrying is safe because a
17
+ * create publishes the whole branch or nothing. `userId` is the current user, the only creator
18
+ * whose listed branch can be this create's result.
19
+ */
20
+ export declare function requestBranchCreate(apiClient: Pick<CanopyApiClient, 'branches'>, body: CreateBranchBody, userId: string | undefined, deadlineMs?: number): Promise<CreateBranchOutcome>;
@@ -0,0 +1,54 @@
1
+ import { isCreatorsRecentBranch } from '../../api/branch-create-window.js';
2
+ import { isApiTimeoutError, isNonApiResponse } from '../../api/client.js';
3
+ import { sanitizeBranchName } from '../../paths/branch-name.js';
4
+ /** @internal Exported for tests. Past the 60 s a Lambda behind CloudFront gets, so its 504 lands first. */
5
+ export const CREATE_BRANCH_DEADLINE_MS = 90_000;
6
+ /** @internal Exported for tests. */
7
+ export const CREATE_TIMED_OUT_MESSAGE = "Creating the branch didn't finish (the server timed out). It's safe to try again.";
8
+ /**
9
+ * Create a branch. Past the deadline, after an error page from in front of the API, or after a
10
+ * busy 503 the branch may exist anyway, so the branch list decides; retrying is safe because a
11
+ * create publishes the whole branch or nothing. `userId` is the current user, the only creator
12
+ * whose listed branch can be this create's result.
13
+ */
14
+ export async function requestBranchCreate(apiClient, body, userId, deadlineMs = CREATE_BRANCH_DEADLINE_MS) {
15
+ let result;
16
+ try {
17
+ result = await apiClient.branches.create(body, { timeoutMs: deadlineMs });
18
+ }
19
+ catch (err) {
20
+ if (isApiTimeoutError(err)) {
21
+ return settle(apiClient, body.branch, userId, CREATE_TIMED_OUT_MESSAGE);
22
+ }
23
+ throw err;
24
+ }
25
+ if (result.ok)
26
+ return { kind: 'created', branch: result.data?.branch };
27
+ if (isNonApiResponse(result))
28
+ return settle(apiClient, body.branch, userId, CREATE_TIMED_OUT_MESSAGE);
29
+ const message = result.error || 'Failed to create branch';
30
+ return result.status === 503
31
+ ? settle(apiClient, body.branch, userId, message)
32
+ : { kind: 'failed', message };
33
+ }
34
+ /** A same-named branch the server would not answer this create with is a name conflict. */
35
+ async function settle(apiClient, requested, userId, message) {
36
+ const listed = await findListedBranch(apiClient, requested);
37
+ if (!listed)
38
+ return { kind: 'failed', message };
39
+ if (isCreatorsRecentBranch(listed, userId))
40
+ return { kind: 'created', branch: listed };
41
+ return { kind: 'failed', message: `A branch named "${listed.name}" already exists` };
42
+ }
43
+ async function findListedBranch(apiClient, requested) {
44
+ const name = sanitizeBranchName(requested);
45
+ try {
46
+ const listing = await apiClient.branches.list();
47
+ if (!listing.ok)
48
+ return undefined;
49
+ return listing.data?.branches.find((branch) => sanitizeBranchName(branch.name) === name);
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
@@ -0,0 +1,68 @@
1
+ import type { LogicalPath } from '../../paths/types.js';
2
+ /**
3
+ * Why one entry of a Save all was not saved. `unanswered` and `conflict` lose nothing yet, so
4
+ * they warn (yellow); the rest are errors (red).
5
+ */
6
+ export type SaveAllFailureKind = 'unanswered' | 'conflict' | 'invalid' | 'blocked' | 'error';
7
+ export interface SaveAllFailure {
8
+ contentId: string;
9
+ label: string;
10
+ path: LogicalPath;
11
+ kind: SaveAllFailureKind;
12
+ /** A full sentence for the result's list, after the entry's name. */
13
+ reason: string;
14
+ /** "Save as new entry", for an entry deleted since its edits started. */
15
+ recreate?: () => void;
16
+ /** Refused for now only, so the result's "Try again" can pass it. */
17
+ retryable?: true;
18
+ }
19
+ export interface SaveAllResult {
20
+ branch: string;
21
+ /** The entries Save all set out to save. */
22
+ total: number;
23
+ saved: Array<{
24
+ contentId: string;
25
+ label: string;
26
+ path: LogicalPath;
27
+ }>;
28
+ failed: SaveAllFailure[];
29
+ }
30
+ export interface SaveAllProgress {
31
+ /** Entries settled so far, saved or not. */
32
+ done: number;
33
+ total: number;
34
+ }
35
+ /** One result toast at a time: a new run replaces it, and leaving its branch takes a partly failed one down. */
36
+ export declare const SAVE_ALL_RESULT_TOAST_ID = "canopy-save-all-result";
37
+ export declare const SAVE_ALL_REASONS: {
38
+ readonly unanswered: "Saving it may have gone through even though the server didn't answer. Open it and choose \"Refresh from server\" to check before saving again.";
39
+ readonly earlierUnanswered: "An earlier save of it may have gone through even though the server didn't answer. Open it and choose \"Refresh from server\" to check before saving again.";
40
+ readonly staleBase: "It changed on the server since your edits started, so saving would overwrite someone else's changes. Open it and choose \"Refresh from server\" to see the latest version (your unsaved edits will be lost).";
41
+ readonly unknownBase: "Its unsaved changes come from an older version of the editor. Open it and save it on its own to confirm the overwrite.";
42
+ readonly unavailable: "Its entry type isn't available in this version of the site, so it can't be saved.";
43
+ readonly noEditAccess: "You don't have edit access to it.";
44
+ readonly forbidden: "You don't have permission to save it. Ask a CanopyCMS admin for access.";
45
+ readonly loadFailed: "It couldn't be loaded from the server, so it wasn't saved. Try Save all again.";
46
+ readonly renamed: "It was renamed since your edits started, so it wasn't saved. Your changes moved with it: save again.";
47
+ readonly deleted: "It was deleted since your edits started, so it wasn't saved. Choose \"Save as new entry\" to keep your changes.";
48
+ readonly goneUnchecked: "It was deleted or renamed since your edits started, so it wasn't saved, and the editor couldn't check which. Choose \"Refresh branch\" in the branch menu: if it was renamed, your changes moved with it.";
49
+ readonly failed: "Save failed. Try again.";
50
+ };
51
+ export declare const invalidReason: (count: number) => string;
52
+ /**
53
+ * Classifies a refused or unanswered save the way `handleSave` does, minus its toasts: Save all
54
+ * reports every entry in one result. `ENTRY_GONE`, a may-have-landed 409 and an unanswered write
55
+ * need the caller's follow-up, so they come back as `gone`, `landed?` and `unanswered`.
56
+ */
57
+ export declare function classifySaveAllError(err: unknown, retryOfUnanswered: boolean): {
58
+ kind: 'gone';
59
+ } | {
60
+ kind: 'landed?';
61
+ } | {
62
+ kind: 'unanswered';
63
+ } | {
64
+ kind: Exclude<SaveAllFailureKind, 'unanswered'>;
65
+ reason: string;
66
+ fieldErrors?: boolean;
67
+ retryable?: true;
68
+ };
@@ -0,0 +1,61 @@
1
+ import { ENTRY_CHANGED_MESSAGE } from '../../api/entries-constants.js';
2
+ import { SCHEMA_UNAVAILABLE_CODE } from '../unavailable-entry-type.js';
3
+ import { isUnanswered } from '../unanswered.js';
4
+ import { EDITOR_ACTIONS } from '../copy.js';
5
+ import { SaveApiError } from './useEntryManager.js';
6
+ /** One result toast at a time: a new run replaces it, and leaving its branch takes a partly failed one down. */
7
+ export const SAVE_ALL_RESULT_TOAST_ID = 'canopy-save-all-result';
8
+ const ENTRY_GONE_CODE = 'ENTRY_GONE';
9
+ const BRANCH_SYNCING_CODE = 'BRANCH_SYNCING';
10
+ export const SAVE_ALL_REASONS = {
11
+ unanswered: `Saving it may have gone through even though the server didn't answer. Open it and choose "${EDITOR_ACTIONS.refreshFromServer}" to check before saving again.`,
12
+ earlierUnanswered: `An earlier save of it may have gone through even though the server didn't answer. Open it and choose "${EDITOR_ACTIONS.refreshFromServer}" to check before saving again.`,
13
+ staleBase: `It changed on the server since your edits started, so saving would overwrite someone else's changes. Open it and choose "${EDITOR_ACTIONS.refreshFromServer}" to see the latest version (your unsaved edits will be lost).`,
14
+ unknownBase: 'Its unsaved changes come from an older version of the editor. Open it and save it on its own to confirm the overwrite.',
15
+ unavailable: "Its entry type isn't available in this version of the site, so it can't be saved.",
16
+ noEditAccess: "You don't have edit access to it.",
17
+ forbidden: "You don't have permission to save it. Ask a CanopyCMS admin for access.",
18
+ loadFailed: "It couldn't be loaded from the server, so it wasn't saved. Try Save all again.",
19
+ renamed: "It was renamed since your edits started, so it wasn't saved. Your changes moved with it: save again.",
20
+ deleted: 'It was deleted since your edits started, so it wasn\'t saved. Choose "Save as new entry" to keep your changes.',
21
+ goneUnchecked: 'It was deleted or renamed since your edits started, so it wasn\'t saved, and the editor couldn\'t check which. Choose "Refresh branch" in the branch menu: if it was renamed, your changes moved with it.',
22
+ failed: 'Save failed. Try again.',
23
+ };
24
+ export const invalidReason = (count) => `${count} ${count === 1 ? 'field needs' : 'fields need'} fixing. Open it to see which.`;
25
+ /**
26
+ * Classifies a refused or unanswered save the way `handleSave` does, minus its toasts: Save all
27
+ * reports every entry in one result. `ENTRY_GONE`, a may-have-landed 409 and an unanswered write
28
+ * need the caller's follow-up, so they come back as `gone`, `landed?` and `unanswered`.
29
+ */
30
+ export function classifySaveAllError(err, retryOfUnanswered) {
31
+ if (isUnanswered(err))
32
+ return { kind: 'unanswered' };
33
+ if (!(err instanceof SaveApiError))
34
+ return { kind: 'error', reason: SAVE_ALL_REASONS.failed };
35
+ if (err.status === 409) {
36
+ if (err.code === ENTRY_GONE_CODE)
37
+ return { kind: 'gone' };
38
+ // The retry of a save that got no answer, refused as changed, most likely bounced off itself.
39
+ if (err.code === 'WRITE_OUTCOME_UNKNOWN' ||
40
+ (retryOfUnanswered && err.serverMessage === ENTRY_CHANGED_MESSAGE)) {
41
+ return { kind: 'landed?' };
42
+ }
43
+ if (err.code === BRANCH_SYNCING_CODE) {
44
+ return { kind: 'conflict', reason: err.message, retryable: true };
45
+ }
46
+ // The version check's own wording names the single-entry recovery, so Save all says it its way.
47
+ const changed = !err.serverMessage || err.serverMessage === ENTRY_CHANGED_MESSAGE;
48
+ return { kind: 'conflict', reason: changed ? SAVE_ALL_REASONS.staleBase : err.serverMessage };
49
+ }
50
+ if (err.code === SCHEMA_UNAVAILABLE_CODE)
51
+ return { kind: 'blocked', reason: err.message };
52
+ if (err.status === 422) {
53
+ return { kind: 'invalid', reason: err.message, fieldErrors: !!err.fieldErrors?.length };
54
+ }
55
+ // A path rule's refusal says only "Forbidden"; the guards' own refusals explain themselves.
56
+ if (err.status === 403) {
57
+ const bare = !err.serverMessage || err.serverMessage === 'Forbidden';
58
+ return { kind: 'blocked', reason: bare ? SAVE_ALL_REASONS.forbidden : err.serverMessage };
59
+ }
60
+ return { kind: 'error', reason: SAVE_ALL_REASONS.failed };
61
+ }
@@ -1,21 +1,31 @@
1
1
  import type { BranchListItem } from '../../api/branch.js';
2
+ import type { UnsavedSummary } from './useDraftManager.js';
2
3
  export interface UseBranchActionsOptions {
3
4
  branchName: string;
4
5
  setBranchName: (name: string) => void;
5
- isAnyDirty: () => boolean;
6
+ /** From useDraftManager's `resolveUnsaved`: what a branch-level action would leave behind. */
7
+ getUnsaved: () => Promise<UnsavedSummary>;
6
8
  onReloadBranches: () => Promise<void>;
7
9
  /** Receives the branch the server just created, so it can be shown before any listing includes it. */
8
10
  onBranchCreated: (branch: BranchListItem) => void;
9
11
  onBranchSwitch?: (branch: string) => void;
12
+ /** The current user; a create the server never answered adopts only a listed branch they made. */
13
+ userId?: string;
10
14
  }
11
15
  export interface UseBranchActionsReturn {
12
16
  handleBranchChange: (branch: string | null) => Promise<void>;
17
+ /** True when nothing is unsaved or the user accepted the warning. Run before any in-flight UI. */
18
+ confirmCreate: () => Promise<boolean>;
13
19
  handleCreateBranch: (branch: {
14
20
  name: string;
15
21
  title?: string;
16
22
  description?: string;
17
23
  }) => Promise<boolean>;
24
+ /** True while the unsaved-changes confirm is on screen. */
25
+ confirmOpen: boolean;
18
26
  }
27
+ /** 'Unsaved changes in: "A", "B".' */
28
+ export declare const describeUnsaved: ({ labels }: UnsavedSummary) => string;
19
29
  /**
20
30
  * Custom hook for branch navigation actions with dirty check support.
21
31
  */