@rapidmx/web-client 0.9.0 → 0.11.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 (443) hide show
  1. package/README.md +373 -88
  2. package/apps/admin/_layout.tsx +8 -2
  3. package/apps/admin/index.tsx +7 -1
  4. package/apps/admin/ingest-queue/index.tsx +2 -1
  5. package/apps/admin/mailboxes/[uid].tsx +23 -10
  6. package/apps/admin/quarantine/index.tsx +2 -1
  7. package/apps/admin/signing-certificates/index.tsx +343 -0
  8. package/apps/escrow/_layout.tsx +8 -2
  9. package/apps/shared/appearance/AppearanceForm.tsx +392 -0
  10. package/apps/shared/appearance/AppearanceHead.tsx +49 -0
  11. package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
  12. package/apps/shared/appearance/ColorField.tsx +107 -0
  13. package/apps/shared/appearance/appearanceCache.ts +106 -0
  14. package/apps/shared/appearance/appearanceContext.ts +77 -0
  15. package/apps/shared/appearance/bootScript.ts +35 -0
  16. package/apps/shared/appearance/color.ts +169 -0
  17. package/apps/shared/appearance/photo.ts +88 -0
  18. package/apps/shared/appearance/resolvedTheme.ts +67 -0
  19. package/apps/shared/appearance/theme.ts +355 -0
  20. package/apps/shared/auth/accountUrl.ts +9 -0
  21. package/apps/shared/auth/adminAccess.ts +99 -0
  22. package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
  23. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
  25. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
  27. package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
  29. package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
  30. package/apps/shared/components/contacts/ContactForm.tsx +11 -5
  31. package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
  32. package/apps/shared/components/contacts/ContactsToolbar.tsx +98 -115
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +25 -8
  34. package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
  35. package/apps/shared/components/layout/AppShell.tsx +477 -288
  36. package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
  37. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
  38. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
  39. package/apps/shared/components/layout/RailIcon.tsx +100 -0
  40. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
  41. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
  42. package/apps/shared/components/layout/UserMenu.tsx +372 -185
  43. package/apps/shared/components/mail/ConversationList.tsx +271 -262
  44. package/apps/shared/components/mail/ConversationThreadPane.tsx +147 -94
  45. package/apps/shared/components/mail/LazyReadingPane.tsx +113 -0
  46. package/apps/shared/components/mail/MailSelectionBar.tsx +227 -222
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +1537 -1387
  48. package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
  49. package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
  50. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -160
  51. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
  52. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1644 -1736
  53. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
  54. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
  55. package/apps/shared/components/mail/compose/composePerf.ts +46 -0
  56. package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
  58. package/apps/shared/components/mail/layout/MailShell.tsx +490 -472
  59. package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
  60. package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
  61. package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
  62. package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
  63. package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
  64. package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
  65. package/apps/shared/components/mail/reading/color.ts +245 -0
  66. package/apps/shared/components/mail/reading/frameControl.ts +142 -0
  67. package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
  68. package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
  69. package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
  70. package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
  71. package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
  72. package/apps/shared/components/mail/unreadStyle.tsx +75 -0
  73. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
  74. package/apps/shared/components/settings/layout/SettingsShell.tsx +253 -240
  75. package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
  76. package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
  77. package/apps/shared/components/tasks/layout/TasksShell.tsx +24 -8
  78. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
  79. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
  80. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
  81. package/apps/shared/keyboard/dispatch.ts +124 -0
  82. package/apps/shared/keyboard/format.ts +89 -0
  83. package/apps/shared/keyboard/keymap.ts +114 -0
  84. package/apps/shared/keyboard/match.ts +44 -0
  85. package/apps/shared/keyboard/parse.ts +136 -0
  86. package/apps/shared/keyboard/platform.ts +34 -0
  87. package/apps/shared/keyboard/registry.ts +65 -0
  88. package/apps/shared/keyboard/targets.ts +79 -0
  89. package/apps/shared/keyboard/useShortcut.ts +50 -0
  90. package/apps/shared/keyboard/useShortcutProps.ts +17 -0
  91. package/apps/shared/mail/folderCounts.ts +308 -0
  92. package/apps/shared/mail/folderTree.ts +143 -0
  93. package/apps/shared/mail/listSnapshots.ts +87 -0
  94. package/apps/shared/mail/messageReadState.ts +85 -0
  95. package/apps/shared/mail/newMailNotifications.ts +183 -0
  96. package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
  97. package/apps/shared/mail/outbox/composeBridge.ts +69 -0
  98. package/apps/shared/mail/outbox/outboxState.ts +88 -0
  99. package/apps/shared/mail/outbox/pendingSends.ts +146 -0
  100. package/apps/shared/mail/outbox/sendDecision.ts +157 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +427 -0
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
  103. package/apps/shared/mail/outbox/sendState.ts +35 -0
  104. package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
  105. package/apps/shared/mail/useMailConnection.ts +203 -0
  106. package/apps/shared/mail/useMailLiveUpdates.ts +277 -224
  107. package/apps/shared/mail/useMarkMessageRead.ts +47 -0
  108. package/apps/shared/mail/useNewMailNotifications.ts +178 -0
  109. package/apps/shared/mail/useUnreadTitle.ts +42 -0
  110. package/apps/shared/navigation/AppRouter.tsx +300 -0
  111. package/apps/shared/navigation/appHrefs.ts +23 -0
  112. package/apps/shared/navigation/frameContext.tsx +35 -0
  113. package/apps/shared/navigation/idle.ts +45 -0
  114. package/apps/shared/navigation/routerContext.tsx +83 -0
  115. package/apps/shared/navigation/routes.ts +72 -0
  116. package/apps/shared/notifications/NotificationCenter.tsx +215 -0
  117. package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
  118. package/apps/shared/notifications/apiErrors.ts +98 -0
  119. package/apps/shared/notifications/headerOffset.ts +35 -0
  120. package/apps/shared/notifications/pushStatus.ts +56 -0
  121. package/apps/shared/notifications/store.ts +549 -0
  122. package/apps/shared/notifications/systemErrors.ts +63 -0
  123. package/apps/shared/notifications/useNotifications.ts +41 -0
  124. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
  125. package/apps/shared/signing/enrollmentStorage.ts +33 -0
  126. package/apps/shared/signing/enrollmentTracker.ts +385 -0
  127. package/apps/shared/signing/enrollmentView.ts +251 -0
  128. package/apps/shared/signing/signingInfo.ts +58 -0
  129. package/apps/shared/signing/useNow.ts +19 -0
  130. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
  131. package/apps/shared/styles/app.css +108 -10
  132. package/apps/www/_layout.tsx +8 -2
  133. package/apps/www/_routedPage.tsx +24 -0
  134. package/apps/www/_routes.ts +36 -0
  135. package/apps/www/calendar/index.tsx +64 -33
  136. package/apps/www/contacts/[uid].tsx +109 -107
  137. package/apps/www/contacts/index.tsx +62 -42
  138. package/apps/www/index.tsx +2291 -1917
  139. package/apps/www/messages/[uid].tsx +108 -101
  140. package/apps/www/settings/appearance/index.tsx +21 -0
  141. package/apps/www/settings/auto-reply/index.tsx +7 -8
  142. package/apps/www/settings/encryption/index.tsx +1287 -1249
  143. package/apps/www/settings/filters/[uid].tsx +7 -2
  144. package/apps/www/settings/filters/index.tsx +102 -99
  145. package/apps/www/settings/filters/new/index.tsx +140 -133
  146. package/apps/www/settings/labels/index.tsx +204 -201
  147. package/apps/www/settings/privacy/index.tsx +7 -9
  148. package/apps/www/settings/read-receipts/index.tsx +7 -8
  149. package/apps/www/settings/sharing/index.tsx +256 -274
  150. package/apps/www/settings/signatures/[uid].tsx +172 -167
  151. package/apps/www/settings/signatures/index.tsx +88 -85
  152. package/apps/www/settings/signatures/new/index.tsx +135 -129
  153. package/apps/www/tasks/index.tsx +29 -18
  154. package/dist/apps/admin/_layout.d.ts +5 -1
  155. package/dist/apps/admin/_layout.js +3 -2
  156. package/dist/apps/admin/index.js +3 -2
  157. package/dist/apps/admin/ingest-queue/index.js +2 -1
  158. package/dist/apps/admin/mailboxes/[uid].js +9 -6
  159. package/dist/apps/admin/quarantine/index.js +2 -1
  160. package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
  161. package/dist/apps/admin/signing-certificates/index.js +161 -0
  162. package/dist/apps/escrow/_layout.d.ts +5 -1
  163. package/dist/apps/escrow/_layout.js +3 -2
  164. package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
  165. package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
  166. package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
  167. package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
  168. package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
  169. package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
  170. package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
  171. package/dist/apps/shared/appearance/ColorField.js +40 -0
  172. package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
  173. package/dist/apps/shared/appearance/appearanceCache.js +80 -0
  174. package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
  175. package/dist/apps/shared/appearance/appearanceContext.js +29 -0
  176. package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
  177. package/dist/apps/shared/appearance/bootScript.js +33 -0
  178. package/dist/apps/shared/appearance/color.d.ts +65 -0
  179. package/dist/apps/shared/appearance/color.js +132 -0
  180. package/dist/apps/shared/appearance/photo.d.ts +17 -0
  181. package/dist/apps/shared/appearance/photo.js +79 -0
  182. package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
  183. package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
  184. package/dist/apps/shared/appearance/theme.d.ts +111 -0
  185. package/dist/apps/shared/appearance/theme.js +251 -0
  186. package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
  187. package/dist/apps/shared/auth/accountUrl.js +8 -0
  188. package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
  189. package/dist/apps/shared/auth/adminAccess.js +89 -0
  190. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  191. package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
  192. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
  193. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
  194. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
  195. package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
  196. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
  197. package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
  198. package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
  199. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
  201. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
  202. package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
  203. package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
  204. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +6 -2
  205. package/dist/apps/shared/components/contacts/ContactsToolbar.js +28 -10
  206. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
  207. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +19 -6
  208. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
  209. package/dist/apps/shared/components/layout/AppShell.d.ts +34 -4
  210. package/dist/apps/shared/components/layout/AppShell.js +122 -17
  211. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
  212. package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
  213. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
  215. package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
  216. package/dist/apps/shared/components/layout/RailIcon.js +85 -0
  217. package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
  218. package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
  219. package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
  220. package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
  221. package/dist/apps/shared/components/layout/UserMenu.d.ts +25 -3
  222. package/dist/apps/shared/components/layout/UserMenu.js +88 -7
  223. package/dist/apps/shared/components/mail/ConversationList.js +5 -11
  224. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +6 -2
  225. package/dist/apps/shared/components/mail/ConversationThreadPane.js +87 -40
  226. package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
  227. package/dist/apps/shared/components/mail/LazyReadingPane.js +83 -0
  228. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -4
  229. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  230. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +25 -8
  231. package/dist/apps/shared/components/mail/MessageDetailPane.js +167 -73
  232. package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
  233. package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
  234. package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
  235. package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
  236. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +38 -0
  237. package/dist/apps/shared/components/mail/compose/ComposeContext.js +97 -6
  238. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
  239. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +3 -2
  240. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +316 -339
  241. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
  242. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
  243. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
  244. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
  245. package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
  246. package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
  247. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
  248. package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
  249. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
  250. package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
  251. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +14 -4
  252. package/dist/apps/shared/components/mail/layout/MailShell.js +90 -104
  253. package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
  254. package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
  255. package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
  256. package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
  257. package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
  258. package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
  259. package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
  260. package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
  261. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
  262. package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
  263. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
  264. package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
  265. package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
  266. package/dist/apps/shared/components/mail/reading/color.js +201 -0
  267. package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
  268. package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
  269. package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
  270. package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
  271. package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
  272. package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
  273. package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
  274. package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
  275. package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
  276. package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
  277. package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
  278. package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
  279. package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
  280. package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
  281. package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
  282. package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
  283. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
  284. package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -5
  285. package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
  286. package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
  287. package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
  288. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
  289. package/dist/apps/shared/components/tasks/layout/TasksShell.js +18 -6
  290. package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
  291. package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
  292. package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
  293. package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
  294. package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
  295. package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
  296. package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
  297. package/dist/apps/shared/keyboard/dispatch.js +109 -0
  298. package/dist/apps/shared/keyboard/format.d.ts +12 -0
  299. package/dist/apps/shared/keyboard/format.js +74 -0
  300. package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
  301. package/dist/apps/shared/keyboard/keymap.js +84 -0
  302. package/dist/apps/shared/keyboard/match.d.ts +20 -0
  303. package/dist/apps/shared/keyboard/match.js +31 -0
  304. package/dist/apps/shared/keyboard/parse.d.ts +31 -0
  305. package/dist/apps/shared/keyboard/parse.js +107 -0
  306. package/dist/apps/shared/keyboard/platform.d.ts +15 -0
  307. package/dist/apps/shared/keyboard/platform.js +22 -0
  308. package/dist/apps/shared/keyboard/registry.d.ts +39 -0
  309. package/dist/apps/shared/keyboard/registry.js +33 -0
  310. package/dist/apps/shared/keyboard/targets.d.ts +14 -0
  311. package/dist/apps/shared/keyboard/targets.js +67 -0
  312. package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
  313. package/dist/apps/shared/keyboard/useShortcut.js +35 -0
  314. package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
  315. package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
  316. package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
  317. package/dist/apps/shared/mail/folderCounts.js +218 -0
  318. package/dist/apps/shared/mail/folderTree.d.ts +55 -0
  319. package/dist/apps/shared/mail/folderTree.js +116 -0
  320. package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
  321. package/dist/apps/shared/mail/listSnapshots.js +43 -0
  322. package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
  323. package/dist/apps/shared/mail/messageReadState.js +63 -0
  324. package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
  325. package/dist/apps/shared/mail/newMailNotifications.js +138 -0
  326. package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
  327. package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
  328. package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
  329. package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
  330. package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
  331. package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
  332. package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
  333. package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
  334. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
  335. package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
  336. package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
  337. package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
  338. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
  339. package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
  340. package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
  341. package/dist/apps/shared/mail/outbox/sendState.js +8 -0
  342. package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
  343. package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
  344. package/dist/apps/shared/mail/useMailConnection.d.ts +56 -0
  345. package/dist/apps/shared/mail/useMailConnection.js +138 -0
  346. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +29 -9
  347. package/dist/apps/shared/mail/useMailLiveUpdates.js +60 -32
  348. package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
  349. package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
  350. package/dist/apps/shared/mail/useNewMailNotifications.d.ts +35 -0
  351. package/dist/apps/shared/mail/useNewMailNotifications.js +130 -0
  352. package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
  353. package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
  354. package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
  355. package/dist/apps/shared/navigation/AppRouter.js +242 -0
  356. package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
  357. package/dist/apps/shared/navigation/appHrefs.js +20 -0
  358. package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
  359. package/dist/apps/shared/navigation/frameContext.js +24 -0
  360. package/dist/apps/shared/navigation/idle.d.ts +14 -0
  361. package/dist/apps/shared/navigation/idle.js +43 -0
  362. package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
  363. package/dist/apps/shared/navigation/routerContext.js +56 -0
  364. package/dist/apps/shared/navigation/routes.d.ts +32 -0
  365. package/dist/apps/shared/navigation/routes.js +37 -0
  366. package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
  367. package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
  368. package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
  369. package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
  370. package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
  371. package/dist/apps/shared/notifications/apiErrors.js +86 -0
  372. package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
  373. package/dist/apps/shared/notifications/headerOffset.js +33 -0
  374. package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
  375. package/dist/apps/shared/notifications/pushStatus.js +54 -0
  376. package/dist/apps/shared/notifications/store.d.ts +136 -0
  377. package/dist/apps/shared/notifications/store.js +416 -0
  378. package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
  379. package/dist/apps/shared/notifications/systemErrors.js +58 -0
  380. package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
  381. package/dist/apps/shared/notifications/useNotifications.js +18 -0
  382. package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
  383. package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
  384. package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
  385. package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
  386. package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
  387. package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
  388. package/dist/apps/shared/signing/enrollmentView.js +214 -0
  389. package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
  390. package/dist/apps/shared/signing/signingInfo.js +48 -0
  391. package/dist/apps/shared/signing/useNow.d.ts +2 -0
  392. package/dist/apps/shared/signing/useNow.js +18 -0
  393. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
  394. package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
  395. package/dist/apps/shared/styles/app.css +108 -10
  396. package/dist/apps/www/_layout.d.ts +5 -1
  397. package/dist/apps/www/_layout.js +3 -2
  398. package/dist/apps/www/_routedPage.d.ts +12 -0
  399. package/dist/apps/www/_routedPage.js +19 -0
  400. package/dist/apps/www/_routes.d.ts +11 -0
  401. package/dist/apps/www/_routes.js +30 -0
  402. package/dist/apps/www/calendar/index.d.ts +2 -2
  403. package/dist/apps/www/calendar/index.js +41 -11
  404. package/dist/apps/www/contacts/[uid].d.ts +3 -9
  405. package/dist/apps/www/contacts/[uid].js +11 -9
  406. package/dist/apps/www/contacts/index.d.ts +2 -2
  407. package/dist/apps/www/contacts/index.js +30 -24
  408. package/dist/apps/www/index.d.ts +2 -2
  409. package/dist/apps/www/index.js +332 -48
  410. package/dist/apps/www/messages/[uid].d.ts +3 -7
  411. package/dist/apps/www/messages/[uid].js +10 -5
  412. package/dist/apps/www/settings/appearance/index.d.ts +4 -0
  413. package/dist/apps/www/settings/appearance/index.js +13 -0
  414. package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
  415. package/dist/apps/www/settings/auto-reply/index.js +6 -7
  416. package/dist/apps/www/settings/encryption/index.d.ts +2 -2
  417. package/dist/apps/www/settings/encryption/index.js +106 -96
  418. package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
  419. package/dist/apps/www/settings/filters/[uid].js +6 -2
  420. package/dist/apps/www/settings/filters/index.d.ts +2 -2
  421. package/dist/apps/www/settings/filters/index.js +3 -1
  422. package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
  423. package/dist/apps/www/settings/filters/new/index.js +9 -3
  424. package/dist/apps/www/settings/labels/index.d.ts +2 -2
  425. package/dist/apps/www/settings/labels/index.js +3 -1
  426. package/dist/apps/www/settings/privacy/index.d.ts +2 -2
  427. package/dist/apps/www/settings/privacy/index.js +8 -9
  428. package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
  429. package/dist/apps/www/settings/read-receipts/index.js +6 -7
  430. package/dist/apps/www/settings/sharing/index.d.ts +2 -2
  431. package/dist/apps/www/settings/sharing/index.js +22 -34
  432. package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
  433. package/dist/apps/www/settings/signatures/[uid].js +6 -2
  434. package/dist/apps/www/settings/signatures/index.d.ts +2 -2
  435. package/dist/apps/www/settings/signatures/index.js +3 -1
  436. package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
  437. package/dist/apps/www/settings/signatures/new/index.js +9 -4
  438. package/dist/apps/www/tasks/index.d.ts +2 -2
  439. package/dist/apps/www/tasks/index.js +22 -16
  440. package/package.json +2 -2
  441. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
  442. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
  443. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
@@ -1,59 +1,52 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { getMailboxAcl, grantMailboxAccess, revokeMailboxAccess } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
11
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
12
- const DEFAULT_DELEGATE_ACTIONS = ["read", "list", "count", "exists"];
12
+ import PrincipalPicker from "../../sharing/PrincipalPicker.js";
13
+ const ROLE_LABELS = { viewer: "Read only", manager: "Full access" };
14
+ function roleLabel(member) {
15
+ if (member.role === "viewer")
16
+ return ROLE_LABELS.viewer;
17
+ if (member.role === "manager")
18
+ return ROLE_LABELS.manager;
19
+ return (member.actions ?? []).join(", ");
20
+ }
13
21
  /**
14
- * Lets an admin (or, eventually, the mailbox owner) view and edit a mailbox's delegate access — the
15
- * mechanism behind Exchange-style shared mailboxes. `records` on a mailbox's own ACL doubles as both the
16
- * owner's grant (if any) and every delegate's — see `BaseMailboxRoute`'s doc comment in `@rapidmx/restapi`.
22
+ * A mailbox's members - the mechanism behind Exchange-style shared mailboxes - through the Sharing endpoints
23
+ * (`/mail/mailboxes/:id/access`), the audited way an administrator reaches a mailbox they hold no grant on. An
24
+ * administrator can review any mailbox's members and revoke any of them; they can grant access only on a shared
25
+ * (ownerless) mailbox - themselves included, which is how it appears in their own mail client. A personal mailbox is
26
+ * shared by its owner (or by an administrator impersonating them).
17
27
  */
18
- export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
19
- const [acl, setAcl] = useState(null);
28
+ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserUid }) {
29
+ const [members, setMembers] = useState([]);
20
30
  const [loading, setLoading] = useState(true);
21
31
  const [error, setError] = useState(null);
22
- const [notice, setNotice] = useState(null);
23
- const [newUserUid, setNewUserUid] = useState("");
24
32
  const [saving, setSaving] = useState(false);
25
33
  const [revokeTarget, setRevokeTarget] = useState(null);
34
+ // An entry that is not a user uid, being replaced with the user it was meant for.
35
+ const [replacing, setReplacing] = useState(null);
26
36
  function reload() {
27
37
  setLoading(true);
28
38
  setError(null);
29
- getMailboxAcl(mailboxUid)
30
- .then(setAcl)
39
+ listMailboxAccess(mailboxUid)
40
+ .then(setMembers)
31
41
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load share settings."))
32
42
  .finally(() => setLoading(false));
33
43
  }
34
44
  useEffect(reload, [mailboxUid]);
35
- async function handleGrant(e) {
36
- e.preventDefault();
37
- const userUid = newUserUid.trim();
38
- if (!userUid) {
39
- return;
40
- }
45
+ async function addMe(userUid) {
41
46
  setSaving(true);
42
47
  setError(null);
43
- setNotice(null);
44
48
  try {
45
- // `grantMailboxAccess()` replaces any existing record for this uid, so granting read access to someone
46
- // who already has more (e.g. the owner) would silently downgrade them. Merge with their current actions.
47
- const current = await getMailboxAcl(mailboxUid);
48
- const existing = current.records.find((record) => record.userOrRoleId === userUid)?.actions ?? [];
49
- const missing = DEFAULT_DELEGATE_ACTIONS.filter((action) => !existing.includes(action));
50
- if (missing.length === 0) {
51
- setNotice(`${userUid} already has this access.`);
52
- setNewUserUid("");
53
- return;
54
- }
55
- await grantMailboxAccess(mailboxUid, userUid, [...existing, ...missing]);
56
- setNewUserUid("");
49
+ await setMailboxAccess(mailboxUid, userUid, "manager");
57
50
  reload();
58
51
  }
59
52
  catch (err) {
@@ -63,15 +56,33 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
63
56
  setSaving(false);
64
57
  }
65
58
  }
59
+ // Once the person is granted, the entry they replace (if any) is removed: the string it named never matched anyone.
60
+ async function handleGranted() {
61
+ const stale = replacing;
62
+ setReplacing(null);
63
+ let failure = null;
64
+ if (stale) {
65
+ try {
66
+ await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
67
+ }
68
+ catch (err) {
69
+ failure = err instanceof ApiRequestError ? err.message : "Could not remove the old entry.";
70
+ }
71
+ }
72
+ reload();
73
+ // After the reload, which clears the error it started with.
74
+ if (failure) {
75
+ setError(failure);
76
+ }
77
+ }
66
78
  // Only ever invoked from the revoke-confirmation modal below, which only renders once `revokeTarget` is set.
67
79
  async function handleRevoke() {
68
80
  const userOrRoleId = revokeTarget;
69
81
  setRevokeTarget(null);
70
82
  setSaving(true);
71
83
  setError(null);
72
- setNotice(null);
73
84
  try {
74
- await revokeMailboxAccess(mailboxUid, userOrRoleId);
85
+ await removeMailboxAccess(mailboxUid, userOrRoleId);
75
86
  reload();
76
87
  }
77
88
  catch (err) {
@@ -81,5 +92,9 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
81
92
  setSaving(false);
82
93
  }
83
94
  }
84
- return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Shared access" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Grants read access to this mailbox for another user, independent of ownership." }), error && _jsx(Alert, { children: error }), notice && !error && _jsx("div", { className: "mb-4 text-sm text-text-muted", children: notice }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [(acl?.records ?? []).length === 0 && (_jsx("li", { className: "text-sm text-text-muted", children: "No grants on this mailbox yet." })), acl?.records.map((record) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: record.userOrRoleId }), _jsx("div", { className: "text-xs text-text-muted", children: record.actions.join(", ") })] }), record.userOrRoleId === ownerUserUid ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Owner" })) : (_jsx("button", { type: "button", onClick: () => setRevokeTarget(record.userOrRoleId), disabled: saving, className: "text-sm text-danger hover:underline disabled:opacity-55", children: "Revoke" }))] }, record.userOrRoleId)))] })), _jsxs("form", { onSubmit: handleGrant, className: "flex gap-2", children: [_jsx("input", { type: "text", value: newUserUid, onChange: (e) => setNewUserUid(e.target.value), placeholder: "User uid to grant access to", className: "flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary" }), _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Grant" })] }), _jsxs(Modal, { open: revokeTarget !== null, onClose: () => setRevokeTarget(null), title: "Revoke access", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Revoke ", _jsx("strong", { className: "break-all", children: revokeTarget }), "\u2019s access to this mailbox? They lose it immediately."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRevokeTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleRevoke(), children: "Revoke" })] })] })] }));
95
+ const shared = !ownerUserUid;
96
+ const isMember = members.some((member) => member.userOrRoleId === currentUserUid);
97
+ return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Shared access" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: shared
98
+ ? "Who has access to this shared mailbox. Adding yourself is how it appears in your own mail client; every change is recorded in the audit log."
99
+ : "Who the owner has shared this mailbox with. You can review and revoke access; only the owner can grant it (impersonate them to do so)." }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [members.length === 0 && _jsx("li", { className: "text-sm text-text-muted", children: "No grants on this mailbox yet." }), members.map((member) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: member.userOrRoleId }), _jsx("div", { className: "text-xs text-text-muted", children: roleLabel(member) }), member.noEffect && (_jsxs("div", { className: "text-xs text-danger mt-0.5", children: ["Not a user - this entry has no effect", shared ? " - replace it with the user it was meant for or remove it." : " - remove it."] }))] }), _jsxs("span", { className: "flex items-center gap-3 shrink-0", children: [member.noEffect && shared && (_jsx("button", { type: "button", onClick: () => setReplacing(member), disabled: saving, className: "text-sm text-primary-dark hover:underline disabled:opacity-55", children: "Replace with a user" })), _jsx("button", { type: "button", onClick: () => setRevokeTarget(member.userOrRoleId), disabled: saving, className: "text-sm text-danger hover:underline disabled:opacity-55", children: "Revoke" })] })] }, member.userOrRoleId)))] })), shared && (_jsxs(_Fragment, { children: [currentUserUid && !isMember && !loading && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", disabled: saving, onClick: () => void addMe(currentUserUid), children: "Add me" })), _jsx(PrincipalPicker, { mailboxUid: mailboxUid, roleLabels: ROLE_LABELS, initialPrincipal: replacing?.userOrRoleId, defaultRole: replacing?.role === "manager" ? "manager" : "viewer", onGranted: handleGranted, onCancel: replacing ? () => setReplacing(null) : undefined }, replacing?.userOrRoleId ?? "new")] })), _jsxs(Modal, { open: revokeTarget !== null, onClose: () => setRevokeTarget(null), title: "Revoke access", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Revoke ", _jsx("strong", { className: "break-all", children: revokeTarget }), "\u2019s access to this mailbox? They lose it immediately."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRevokeTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleRevoke(), children: "Revoke" })] })] })] }));
85
100
  }
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ /** A header that uses both variables - the snippet the Branding page offers to copy. */
4
+ export declare const HEADER_HTML_EXAMPLE = "<div style=\"display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem\">\n <span>{APP_TITLE}</span>\n <span>{USER_MENU}</span>\n</div>";
3
5
  /** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
4
6
  export declare function isSvgFile(file: File): boolean;
5
7
  export interface BrandingFormProps {
@@ -8,8 +8,14 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { deleteBrandingIcon, deleteBrandingLogo, deleteBrandingStylesheet, updateBranding, uploadBrandingIcon, uploadBrandingLogo, uploadBrandingStylesheet, } from "@rapidmx/react-shared/branding/brandingApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
+ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
11
12
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
12
13
  const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
14
+ /** A header that uses both variables - the snippet the Branding page offers to copy. */
15
+ export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
16
+ <span>{APP_TITLE}</span>
17
+ <span>{USER_MENU}</span>
18
+ </div>`;
13
19
  /** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
14
20
  export function isSvgFile(file) {
15
21
  return file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
@@ -113,5 +119,5 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
113
119
  }), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
114
120
  await deleteBrandingStylesheet();
115
121
  return { ...branding, stylesheetUrl: undefined };
116
- }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
122
+ }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: ["A header replaces the app's own title bar and the icon at the top of the icon rail, and stays at the top of the window. Write", " ", _jsx("code", { children: "{USER_MENU}" }), " where the account menu (avatar, Settings, Sign Out) should go, and ", _jsx("code", { children: "{APP_TITLE}" }), " ", "where the name of the app on screen should show. Both work in the footer too. ", _jsx("code", { children: "{USER_MENU}" }), " is replaced once (the header's, else the footer's); without it the menu sits in a small cell at the right end of the header, so it is never lost. Put it where your styles don't hide it on a phone."] })] }), _jsxs("div", { className: "flex flex-col gap-1.5 text-sm", role: "group", "aria-label": "Header example", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-semibold", children: "Example header" }), _jsx(CopyButton, { value: HEADER_HTML_EXAMPLE, label: "Copy the example header HTML" })] }), _jsx("pre", { className: "overflow-x-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs text-text", children: HEADER_HTML_EXAMPLE })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: [_jsx("code", { children: "{APP_TITLE}" }), " works here as well, and ", _jsx("code", { children: "{USER_MENU}" }), " takes the account menu (opening upward) when the header has none."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
117
123
  }
@@ -88,7 +88,7 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
88
88
  const ownership = dnsSetup.find((c) => c.type === "ownership");
89
89
  const ownershipValue = ownership?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`;
90
90
  const ownershipName = ownership?.recordName || domain.name;
91
- return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] items-start gap-x-4 gap-y-2 text-sm", children: [_jsx("dt", { className: "text-text-muted py-2", children: "Type" }), _jsx("dd", { className: "py-2", children: "TXT" }), _jsx("dt", { className: "text-text-muted py-2", children: "Name / host" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipName, copyLabel: "Copy name for the ownership TXT record" }) }), _jsx("dt", { className: "text-text-muted py-2", children: "Value" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipValue, copyLabel: "Copy value for the ownership TXT record" }) })] }), verifyError && (_jsx("div", { className: "mt-3", children: _jsx(Alert, { children: verifyError }) })), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: verifyError ? "Try again" : "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Name / host", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border", children: [RECORD_TYPE_LABELS[check.type], _jsxs("div", { className: "text-xs text-text-muted", children: ["Type: ", check.recordKind] })] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: check.recordName ? (_jsx(CopyableValue, { value: check.recordName, copyLabel: `Copy name for the ${RECORD_TYPE_NAMES[check.type]} record` })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: check.recommendedValue ? (_jsx(DnsRecordValue, { check: check, value: check.recommendedValue })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) })] }, check.type))) })] }) }), _jsxs("p", { className: "text-xs text-text-muted mt-3", children: ["Some DNS providers want the name relative to the domain (for example ", _jsx("code", { children: "@" }), " for the domain itself) rather than the full name."] })] }))] }));
91
+ return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] items-start gap-x-4 gap-y-2 text-sm", children: [_jsx("dt", { className: "text-text-muted py-2", children: "Type" }), _jsx("dd", { className: "py-2", children: "TXT" }), _jsx("dt", { className: "text-text-muted py-2", children: "Name / host" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipName, copyLabel: "Copy name for the ownership TXT record" }) }), _jsx("dt", { className: "text-text-muted py-2", children: "Value" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipValue, copyLabel: "Copy value for the ownership TXT record" }) })] }), verifyError && (_jsx("div", { className: "mt-3", children: _jsx(Alert, { children: verifyError }) })), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: verifyError ? "Try again" : "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Name / host", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border", children: [RECORD_TYPE_LABELS[check.type], _jsxs("div", { className: "text-xs text-text-muted", children: ["Type: ", check.recordKind] })] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted min-w-[10rem]", children: check.recordName ? (_jsx(CopyableValue, { value: check.recordName, copyLabel: `Copy name for the ${RECORD_TYPE_NAMES[check.type]} record` })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: check.recommendedValue ? (_jsx(DnsRecordValue, { check: check, value: check.recommendedValue })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) })] }, check.type))) })] }) }), _jsxs("p", { className: "text-xs text-text-muted mt-3", children: ["Some DNS providers want the name relative to the domain (for example ", _jsx("code", { children: "@" }), " for the domain itself) rather than the full name."] })] }))] }));
92
92
  }
93
93
  /** The value cell of a checklist row: an MX value as its two form fields (priority and mail server), anything else whole. */
94
94
  function DnsRecordValue({ check, value }) {
@@ -3,12 +3,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, searchPlugins, updatePlugin, } from "@rapidmx/react-shared/admin/pluginsApi.js";
8
+ import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, retryPluginPurge, searchPlugins, updatePlugin, } from "@rapidmx/react-shared/admin/pluginsApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
11
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
12
+ import { notify } from "../../../notifications/store.js";
13
+ import { isElevationRequired } from "../elevation.js";
12
14
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
13
15
  /** How often status is refreshed while server copies are still applying a change. */
14
16
  const PENDING_POLL_MS = 5000;
@@ -29,6 +31,18 @@ async function attempt(action, failure) {
29
31
  return errorMessage(err, failure);
30
32
  }
31
33
  }
34
+ /** The classes of a confirm button that deletes data, the same as the app's other destructive buttons. */
35
+ const DANGER_BUTTON_CLASS = "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger";
36
+ /** Shown when the server wants the administrator to have confirmed their identity recently (`api-104`). */
37
+ const ELEVATION_MESSAGE = "Deleting data needs you to have recently confirmed your identity. Reload this page, or sign in again, then try once more.";
38
+ /** What an administrator sees an uninstalled plugin's data deletion as, without the display name of a plugin that's gone. */
39
+ const purgeName = (purge) => purge.displayName ?? purge.name;
40
+ /** Why a data deletion failed: what the server recorded, else the steps that failed. */
41
+ function purgeFailure(purge) {
42
+ return purge.error ?? (purge.steps.filter((step) => !step.ok).map((step) => step.error ?? step.step).join("; ") || "The reason wasn't recorded.");
43
+ }
44
+ /** A day, in the administrator's own format. */
45
+ const formatDate = (iso) => new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
32
46
  /** Classes for a plugin table's header and body cells. */
33
47
  const TH_CLASS = "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border";
34
48
  const TD_CLASS = "py-3 px-2.5 border-b border-border align-top";
@@ -67,6 +81,8 @@ export default function PluginsManager({ embedded = false } = {}) {
67
81
  /** How many times status has been retried while none has ever been read. */
68
82
  const [statusRetries, setStatusRetries] = useState(0);
69
83
  const statusRequest = useRef(null);
84
+ /** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
85
+ const seenPurges = useRef(new Map());
70
86
  const setBusy = (uid, busy) => setBusyUids((prev) => {
71
87
  const next = new Set(prev);
72
88
  if (busy) {
@@ -116,8 +132,12 @@ export default function PluginsManager({ embedded = false } = {}) {
116
132
  .finally(() => setLoading(false));
117
133
  }, [refreshStatus, refreshUpdates]);
118
134
  const pending = !!status && status.instances.some((instance) => instance.hash !== status.hash);
135
+ // A plugin's data deletion in the status that isn't over yet, for a plugin that's uninstalled (one added again is hidden).
136
+ const installedNames = new Set(plugins.map((plugin) => plugin.name));
137
+ const purges = (status?.purges ?? []).filter((purge) => !installedNames.has(purge.name));
138
+ const purging = purges.some((purge) => purge.state === "pending" || purge.state === "running");
119
139
  useEffect(() => {
120
- if (!pending && pollUntil === 0) {
140
+ if (!pending && !purging && pollUntil === 0) {
121
141
  return;
122
142
  }
123
143
  const timer = setInterval(() => void refreshStatus(), PENDING_POLL_MS);
@@ -127,7 +147,29 @@ export default function PluginsManager({ embedded = false } = {}) {
127
147
  clearInterval(timer);
128
148
  clearTimeout(stop);
129
149
  };
130
- }, [pending, pollUntil, refreshStatus]);
150
+ }, [pending, purging, pollUntil, refreshStatus]);
151
+ // A data deletion seen under way that has now ended is worth a notification: it happens after the servers restart,
152
+ // long after the administrator asked for it.
153
+ useEffect(() => {
154
+ for (const purge of status?.purges ?? []) {
155
+ const before = seenPurges.current.get(purge.uid);
156
+ seenPurges.current.set(purge.uid, purge.state);
157
+ if (before !== "pending" && before !== "running") {
158
+ continue;
159
+ }
160
+ if (purge.state === "done") {
161
+ notify({ kind: "success", title: `${purgeName(purge)}'s data was deleted`, dedupeKey: `plugin-purge-${purge.uid}-done` });
162
+ }
163
+ else if (purge.state === "failed") {
164
+ notify({
165
+ kind: "error",
166
+ title: `Deleting ${purgeName(purge)}'s data failed`,
167
+ message: purgeFailure(purge),
168
+ dedupeKey: `plugin-purge-${purge.uid}-failed`,
169
+ });
170
+ }
171
+ }
172
+ }, [status]);
131
173
  // With no status read at all there's nothing pending to poll for, so keep retrying - less often each time.
132
174
  const neverRead = !status && statusStale;
133
175
  useEffect(() => {
@@ -214,6 +256,10 @@ export default function PluginsManager({ embedded = false } = {}) {
214
256
  return planned(name, packageVersion, displayName, async (plan) => {
215
257
  const result = await addPlugin(name, packageVersion, expectedPlanOf(plan));
216
258
  applied(...result.dependencies, result.plugin);
259
+ // Adding a plugin cancels the deletion of its data that was waiting for the servers to stop running it.
260
+ for (const warning of result.warnings ?? []) {
261
+ notify({ kind: "warning", title: "Data deletion cancelled", message: warning });
262
+ }
217
263
  }, `Could not install ${displayName}.`);
218
264
  }
219
265
  function changeVersion(plugin, packageVersion) {
@@ -232,21 +278,37 @@ export default function PluginsManager({ embedded = false } = {}) {
232
278
  function upgrade(plugin, packageVersion) {
233
279
  return onRow(plugin, () => changeVersion(plugin, packageVersion));
234
280
  }
281
+ /** Runs the steps of a failed data deletion that failed, again. */
282
+ async function retryPurge(purge) {
283
+ setBusy(purge.uid, true);
284
+ setError(null);
285
+ setRetryEnableUid(null);
286
+ try {
287
+ await retryPluginPurge(purge.uid);
288
+ // The retry starts on a server shortly; keep reading status until it ends.
289
+ setPollUntil(Date.now() + AFTER_CHANGE_POLL_MS);
290
+ await refreshStatus();
291
+ }
292
+ catch (err) {
293
+ setError(isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, `Could not retry deleting ${purgeName(purge)}'s data.`));
294
+ }
295
+ setBusy(purge.uid, false);
296
+ }
235
297
  // Retried against the plugin as it's listed now (a newer version, or already enabled elsewhere, hides the retry).
236
298
  const retryEnable = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
237
299
  const displayNameOf = (name) => plugins.find((plugin) => plugin.name === name)?.manifest.displayName ?? name;
238
300
  const SectionHeading = embedded ? "h3" : "h2";
239
301
  const addButton = (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setAdding(true), children: "Add by name" }));
240
- return (_jsxs(_Fragment, { children: [embedded ? (_jsx("p", { className: "text-sm text-text-muted mb-5 max-w-3xl", children: "Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Plugins" }), addButton] }), _jsx("p", { className: "text-sm text-text-muted mb-6 max-w-3xl", children: "Plugins add protocols and features to every server. Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })] })), error && (_jsxs(Alert, { children: [error, retryEnable && (_jsxs(_Fragment, { children: [" ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0", onClick: () => void toggle(retryEnable), children: "Try again" })] }))] })), _jsx(RolloutBanner, { status: status }), statusStale && status && (_jsx("p", { className: "mb-4 text-xs text-text-muted", children: "Couldn't refresh server status. Showing the last status reported." })), listStale && (_jsxs("p", { className: "mb-4 text-xs text-text-muted", children: ["Couldn't re-read the plugin list, so it may be out of date.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0 !text-xs", onClick: () => void reload(), children: "Retry" })] })), _jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide", children: "Installed plugins" }), embedded && addButton] }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : plugins.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins installed." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { role: "table", className: "w-full text-sm border-collapse block lg:table", children: [_jsx("thead", { role: "rowgroup", className: "hidden lg:table-header-group", children: _jsxs("tr", { role: "row", children: [_jsx("th", { role: "columnheader", className: TH_CLASS, children: "Plugin" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Version" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Status" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { role: "rowgroup", className: "block lg:table-row-group", children: plugins.map((plugin) => {
241
- const update = updates.get(plugin.uid);
242
- const latest = update?.updateAvailable ? update.latestVersion : undefined;
243
- const busy = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid;
244
- const requires = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
245
- const requiredBy = plugins
246
- .filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
247
- .map((other) => other.manifest.displayName);
248
- return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] }), _jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`, children: [_jsx("div", { children: plugin.packageVersion }), latest && _jsxs("div", { className: "mt-1 text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1`, children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-2 pb-3`, children: _jsxs("div", { className: "flex flex-col items-start gap-1.5 lg:items-end lg:min-w-[11rem]", children: [_jsxs("div", { className: "flex flex-wrap gap-2 lg:justify-end", children: [latest && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: plugin.enabled ? "secondary" : "primary", type: "button", className: "!w-auto min-w-[5.5rem]", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" })] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 lg:justify-end", children: [_jsx(Button, { variant: "text", type: "button", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
249
- }) })] }) })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
302
+ return (_jsxs(_Fragment, { children: [embedded ? (_jsx("p", { className: "text-sm text-text-muted mb-5 max-w-3xl", children: "Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Plugins" }), addButton] }), _jsx("p", { className: "text-sm text-text-muted mb-6 max-w-3xl", children: "Plugins add protocols and features to every server. Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })] })), error && (_jsxs(Alert, { children: [error, retryEnable && (_jsxs(_Fragment, { children: [" ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0", onClick: () => void toggle(retryEnable), children: "Try again" })] }))] })), _jsx(RolloutBanner, { status: status }), statusStale && status && (_jsx("p", { className: "mb-4 text-xs text-text-muted", children: "Couldn't refresh server status. Showing the last status reported." })), listStale && (_jsxs("p", { className: "mb-4 text-xs text-text-muted", children: ["Couldn't re-read the plugin list, so it may be out of date.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0 !text-xs", onClick: () => void reload(), children: "Retry" })] })), _jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide", children: "Installed plugins" }), embedded && addButton] }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : plugins.length === 0 && purges.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins installed." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { role: "table", className: "w-full text-sm border-collapse block lg:table", children: [_jsx("thead", { role: "rowgroup", className: "hidden lg:table-header-group", children: _jsxs("tr", { role: "row", children: [_jsx("th", { role: "columnheader", className: TH_CLASS, children: "Plugin" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Version" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Status" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsxs("tbody", { role: "rowgroup", className: "block lg:table-row-group", children: [plugins.map((plugin) => {
303
+ const update = updates.get(plugin.uid);
304
+ const latest = update?.updateAvailable ? update.latestVersion : undefined;
305
+ const busy = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid;
306
+ const requires = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
307
+ const requiredBy = plugins
308
+ .filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
309
+ .map((other) => other.manifest.displayName);
310
+ return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] }), _jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`, children: [_jsx("div", { children: plugin.packageVersion }), latest && _jsxs("div", { className: "mt-1 text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1`, children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-2 pb-3`, children: _jsxs("div", { className: "flex flex-col items-start gap-1.5 lg:items-end lg:min-w-[11rem]", children: [_jsxs("div", { className: "flex flex-wrap gap-2 lg:justify-end", children: [latest && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: plugin.enabled ? "secondary" : "primary", type: "button", className: "!w-auto min-w-[5.5rem]", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" })] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 lg:justify-end", children: [_jsx(Button, { variant: "text", type: "button", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
311
+ }), purges.map((purge) => (_jsx(UninstalledPluginRow, { purge: purge, busy: busyUids.has(purge.uid), onRetry: () => void retryPurge(purge) }, purge.uid)))] })] }) })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
250
312
  // The browser only offers an upgrade for a result it matched to one of these same `plugins`.
251
313
  onUpgrade: (uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid), packageVersion) }), _jsx(AddPluginModal, { open: adding, onClose: () => setAdding(false), onAdd: async (name, packageVersion, displayName) => {
252
314
  const problem = await install(name, packageVersion, displayName);
@@ -263,10 +325,21 @@ export default function PluginsManager({ embedded = false } = {}) {
263
325
  } })), confirming && (_jsx(ConfirmDependenciesModal, { change: confirming, displayNameOf: displayNameOf, onClose: () => setConfirming(null), onDone: () => setConfirming(null) })), configuring && (_jsx(SettingsModal, { plugin: configuring, onClose: () => setConfiguring(null), onSaved: (plugin) => {
264
326
  setConfiguring(null);
265
327
  applied(plugin);
266
- } })), removing && (_jsx(RemoveModal, { plugin: removing, onClose: () => setRemoving(null), onRemoved: () => {
328
+ } })), removing && (_jsx(RemoveModal, { plugin: removing, onClose: () => setRemoving(null), onRemoved: (result) => {
267
329
  const uid = removing.uid;
330
+ const displayName = removing.manifest.displayName;
268
331
  setRemoving(null);
269
332
  setPlugins((prev) => prev.filter((plugin) => plugin.uid !== uid));
333
+ if (result.purgeScheduled) {
334
+ // Shown at once; the status polling that follows replaces it with what the servers report.
335
+ const scheduled = result.purge;
336
+ setStatus((prev) => prev && scheduled ? { ...prev, purges: [...(prev.purges ?? []).filter((purge) => purge.uid !== scheduled.uid), scheduled] } : prev);
337
+ notify({
338
+ kind: "info",
339
+ title: `${displayName} uninstalled`,
340
+ message: "Its data is deleted once every server has stopped running it. Adding the plugin again before then cancels the deletion.",
341
+ });
342
+ }
270
343
  watchRollout();
271
344
  } }))] }));
272
345
  }
@@ -367,6 +440,11 @@ function PluginServers({ plugin, status }) {
367
440
  const errors = current.flatMap((instance) => instance.errors.filter((entry) => entry.name === plugin.name).map((entry) => `${instance.instance}: ${entry.message}`));
368
441
  return (_jsxs("div", { className: "text-xs", children: [_jsxs("span", { className: loaded === status.instances.length ? "text-success font-medium" : "text-text-muted", children: ["Loaded on ", loaded, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers"] }), errors.map((message) => (_jsx("div", { className: "text-danger mt-1 break-words", children: message }, message)))] }));
369
442
  }
443
+ /** The row of a plugin that was uninstalled but whose data deletion is still listed: what became of its data. */
444
+ function UninstalledPluginRow({ purge, busy, onRetry }) {
445
+ const failedSteps = purge.steps.filter((step) => !step.ok);
446
+ return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: purgeName(purge) }), _jsx("div", { className: "text-xs text-text-muted", children: purge.name })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} hidden lg:table-cell` }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-3 basis-full lg:basis-auto`, children: _jsxs("div", { className: "flex flex-col items-start gap-1 text-xs max-w-sm", children: [_jsx("span", { className: `${BADGE_CLASS} bg-surface-alt text-text-muted`, children: "Uninstalled" }), purge.state === "pending" && (_jsxs(_Fragment, { children: [_jsx("span", { children: "Uninstalled - data will be deleted after servers restart" }), !!purge.serversRunning && (_jsxs("span", { className: "text-text-muted", children: [purge.serversRunning, " of ", purge.serversTotal, " ", purge.serversTotal === 1 ? "server" : "servers", " still running it"] }))] })), purge.state === "running" && _jsx("span", { children: "Uninstalled - deleting its data now" }), purge.state === "done" && _jsxs("span", { className: "text-success font-medium", children: ["Data deleted", purge.completedAt ? ` ${formatDate(purge.completedAt)}` : ""] }), purge.state === "failed" && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-danger break-words", children: ["Data deletion failed: ", purgeFailure(purge)] }), failedSteps.length > 0 && (_jsx("ul", { className: "list-disc pl-4 text-text-muted break-words", children: failedSteps.map((step) => (_jsxs("li", { children: [step.step, ": ", step.error ?? "failed"] }, step.step))) }))] }))] }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-1 pb-3`, children: purge.state === "failed" && (_jsx("div", { className: "flex lg:justify-end", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Retry deleting the data of ${purgeName(purge)}`, disabled: busy, loading: busy, onClick: onRetry, children: "Retry" }) })) })] }));
447
+ }
370
448
  function AddPluginModal({ open, onClose, onAdd, }) {
371
449
  const [name, setName] = useState("");
372
450
  const [lookup, setLookup] = useState(null);
@@ -575,17 +653,25 @@ function SettingField({ definition, value, onChange, }) {
575
653
  function RemoveModal({ plugin, onClose, onRemoved }) {
576
654
  const [error, setError] = useState(null);
577
655
  const [busy, setBusy] = useState(false);
578
- async function remove() {
656
+ const [purgeData, setPurgeData] = useState(false);
657
+ const [typed, setTyped] = useState("");
658
+ const helpId = useId();
659
+ const displayName = plugin.manifest.displayName;
660
+ // Deleting data is destructive and permanent, so it also takes the plugin's name typed out.
661
+ const confirmed = !purgeData || typed.trim().toLowerCase() === displayName.trim().toLowerCase();
662
+ async function remove(event) {
663
+ event.preventDefault();
579
664
  setBusy(true);
580
665
  setError(null);
581
666
  try {
582
- await removePlugin(plugin.uid);
583
- onRemoved();
667
+ onRemoved(await removePlugin(plugin.uid, { purgeData }));
584
668
  }
585
669
  catch (err) {
586
- setError(errorMessage(err, "Could not uninstall the plugin."));
670
+ setError(isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, "Could not uninstall the plugin."));
587
671
  setBusy(false);
588
672
  }
589
673
  }
590
- return (_jsxs(Modal, { open: true, onClose: onClose, title: `Uninstall ${plugin.manifest.displayName}?`, children: [error && _jsx(Alert, { children: error }), _jsx("p", { className: "text-sm mb-4", children: "The servers stop running this plugin after they restart. Data it stored stays in the database, and adding the plugin again brings it back." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void remove(), children: "Uninstall" })] })] }));
674
+ return (_jsx(Modal, { open: true, onClose: onClose, title: `Uninstall ${displayName}?`, children: _jsxs("form", { onSubmit: (event) => void remove(event), children: [error && _jsx(Alert, { children: error }), _jsx("p", { className: "text-sm mb-4", children: purgeData
675
+ ? "The servers stop running this plugin after they restart, and then all the data it stored is deleted."
676
+ : "The servers stop running this plugin after they restart. Data it stored stays in the database, and adding the plugin again brings it back." }), _jsxs("div", { className: "mb-4", children: [_jsxs("label", { className: "flex items-start gap-2 text-sm font-semibold cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "mt-0.5 h-4 w-4 shrink-0 accent-danger", checked: purgeData, disabled: busy, "aria-describedby": helpId, onChange: (e) => setPurgeData(e.target.checked) }), _jsx("span", { children: "Also delete all data this plugin stored" })] }), _jsxs("div", { id: helpId, className: "mt-2 pl-6 text-xs text-text-muted", children: [_jsx("p", { children: "What is deleted:" }), _jsxs("ul", { className: "list-disc pl-5 mt-1 space-y-0.5", children: [_jsx("li", { children: "the database collections and tables the plugin's features use, with everything in them" }), _jsx("li", { children: "the plugin's saved settings" }), _jsx("li", { children: "its downloaded package and cached pages on the servers" }), _jsx("li", { children: "whatever else the plugin cleans up itself, such as files it stored or data kept outside the database" })] }), _jsx("p", { className: "mt-2 text-danger font-semibold", children: "This can't be undone." })] })] }), purgeData && (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-xs text-text-muted mb-3", children: "Nothing is deleted while a server is still running the plugin. Once every server has restarted without it, usually within a few minutes, its data is deleted. Adding the plugin again before then cancels the deletion." }), _jsxs("label", { className: "block text-sm mb-4", children: [_jsxs("span", { children: ["Type ", _jsx("strong", { children: displayName }), " to confirm"] }), _jsx("input", { type: "text", className: `${INPUT_CLASS} mt-1.5`, value: typed, autoComplete: "off", spellCheck: false, disabled: busy, onChange: (e) => setTyped(e.target.value) })] })] })), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", className: purgeData ? DANGER_BUTTON_CLASS : "!w-auto", loading: busy, disabled: busy || !confirmed, children: purgeData ? "Uninstall and delete data" : "Uninstall" })] })] }) }));
591
677
  }
@@ -197,7 +197,8 @@ function MailboxesStep({ userUid, domain }) {
197
197
  const [error, setError] = useState(null);
198
198
  const [formKey, setFormKey] = useState(0);
199
199
  useEffect(() => {
200
- listMailboxes({ limit: 100 })
200
+ // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
201
+ listMailboxes({ limit: 100, scope: "admin" })
201
202
  .then((list) =>
202
203
  // Keep any mailbox created here while the list was loading.
203
204
  setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]))
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { useState } from "react";
7
7
  import { CALENDAR_COLOR_PALETTE, accentColorForMailbox, colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
8
8
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
9
+ import { notifyApiError } from "../../notifications/apiErrors.js";
9
10
  /**
10
11
  * Calendar's left-sidebar checklist of calendars — one row per `calendar`-type folder, a color swatch,
11
12
  * and a checkbox controlling whether that calendar's events are currently shown (the checked set is owned
@@ -19,7 +20,6 @@ export default function CalendarListSidebar({ mailboxCalendars, checkedFolderUid
19
20
  const [addingFor, setAddingFor] = useState(null);
20
21
  const [name, setName] = useState("");
21
22
  const [color, setColor] = useState(CALENDAR_COLOR_PALETTE[0]);
22
- const [error, setError] = useState(null);
23
23
  const [saving, setSaving] = useState(false);
24
24
  const multiple = mailboxCalendars.length > 1;
25
25
  function startAdding(mailboxUid) {
@@ -32,7 +32,6 @@ export default function CalendarListSidebar({ mailboxCalendars, checkedFolderUid
32
32
  if (!name.trim() || !addingFor) {
33
33
  return;
34
34
  }
35
- setError(null);
36
35
  setSaving(true);
37
36
  try {
38
37
  await onAddCalendar(addingFor, name.trim(), color);
@@ -40,16 +39,17 @@ export default function CalendarListSidebar({ mailboxCalendars, checkedFolderUid
40
39
  setColor(CALENDAR_COLOR_PALETTE[0]);
41
40
  setAddingFor(null);
42
41
  }
43
- catch {
44
- setError("Could not create this calendar.");
42
+ catch (err) {
43
+ // A pop-up; the form stays open, with what was typed, for another try.
44
+ notifyApiError(err, "Couldn't create the calendar");
45
45
  }
46
46
  finally {
47
47
  setSaving(false);
48
48
  }
49
49
  }
50
50
  const addForm = (_jsxs("form", { onSubmit: handleAdd, className: "flex flex-col gap-1.5 mt-2", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New calendar name", value: name, onChange: (e) => setName(e.target.value), className: "text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("div", { className: "flex items-center gap-1", role: "radiogroup", "aria-label": "Calendar color", children: CALENDAR_COLOR_PALETTE.map((swatch) => (_jsx("button", { type: "button", "aria-label": `Color ${swatch}`, "aria-pressed": color === swatch, onClick: () => setColor(swatch), className: ["w-4 h-4 rounded-full shrink-0", color === swatch ? "ring-2 ring-offset-1 ring-text" : ""].join(" "), style: { backgroundColor: swatch } }, swatch))) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "submit", disabled: saving, className: "text-xs font-semibold text-primary-dark", children: "Add" }), _jsx("button", { type: "button", onClick: () => setAddingFor(null), className: "text-xs text-text-muted", children: "Cancel" })] })] }));
51
- return (_jsxs("nav", { "aria-label": "My calendars", className: "p-3 border-t border-border flex flex-col gap-3", children: [error && _jsx(Alert, { children: error }), mailboxCalendars.map(({ mailbox, calendarFolders, error: mailboxError }) => {
52
- const title = multiple ? `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}` : "My calendars";
53
- return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between mb-1 gap-2", children: [_jsxs("h2", { className: "flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-text-muted min-w-0", children: [multiple && (_jsx("span", { "aria-hidden": "true", className: "w-2 h-2 rounded-full shrink-0", style: { backgroundColor: accentColorForMailbox(mailbox.uid) } })), _jsx("span", { className: "truncate", children: title })] }), _jsx("button", { type: "button", onClick: () => startAdding(mailbox.uid), "aria-label": multiple ? `Add calendar to ${mailbox.displayName}` : "Add calendar", className: "text-xs font-bold text-primary-dark hover:underline shrink-0", children: "+" })] }), mailboxError && (_jsx("div", { className: "mb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children: calendarFolders.map((folder) => (_jsxs("label", { className: "flex items-center gap-2 text-sm px-1 py-1 rounded-sm hover:bg-surface-alt cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: checkedFolderUids.has(folder.uid), onChange: () => onToggle(folder.uid), style: { accentColor: colorFor(folder) } }), _jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: colorFor(folder) } }), _jsx("span", { className: "truncate", children: folder.name })] }, folder.uid))) }), addingFor === mailbox.uid && addForm] }, mailbox.uid));
54
- })] }));
51
+ return (_jsx("nav", { "aria-label": "My calendars", className: "p-3 border-t border-border flex flex-col gap-3", children: mailboxCalendars.map(({ mailbox, calendarFolders, error: mailboxError }) => {
52
+ const title = multiple ? `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}` : "My calendars";
53
+ return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between mb-1 gap-2", children: [_jsxs("h2", { className: "flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-text-muted min-w-0", children: [multiple && (_jsx("span", { "aria-hidden": "true", className: "w-2 h-2 rounded-full shrink-0", style: { backgroundColor: accentColorForMailbox(mailbox.uid) } })), _jsx("span", { className: "truncate", children: title })] }), _jsx("button", { type: "button", onClick: () => startAdding(mailbox.uid), "aria-label": multiple ? `Add calendar to ${mailbox.displayName}` : "Add calendar", className: "text-xs font-bold text-primary-dark hover:underline shrink-0", children: "+" })] }), mailboxError && (_jsx("div", { className: "mb-1", children: _jsx(Alert, { children: mailboxError }) })), _jsx("div", { className: "flex flex-col gap-0.5", children: calendarFolders.map((folder) => (_jsxs("label", { className: "flex items-center gap-2 text-sm px-1 py-1 rounded-sm hover:bg-surface-alt cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: checkedFolderUids.has(folder.uid), onChange: () => onToggle(folder.uid), style: { accentColor: colorFor(folder) } }), _jsx("span", { "aria-hidden": "true", className: "w-2.5 h-2.5 rounded-full shrink-0", style: { backgroundColor: colorFor(folder) } }), _jsx("span", { className: "truncate", children: folder.name })] }, folder.uid))) }), addingFor === mailbox.uid && addForm] }, mailbox.uid));
54
+ }) }));
55
55
  }
@@ -37,4 +37,4 @@ export declare function useCalendarShell(): CalendarShellContextValue;
37
37
  * state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
38
38
  * views/dates must be instant (no full page reload) and this framework has no client-side router.
39
39
  */
40
- export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<CalendarShellProps>): React.JSX.Element;
40
+ export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<CalendarShellProps>): React.JSX.Element;
@@ -10,6 +10,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
12
  import AppShell from "../../layout/AppShell.js";
13
+ import { useLocationSearch } from "../../../navigation/AppRouter.js";
13
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
14
15
  const CalendarShellContext = createContext({
15
16
  calendarFolders: [],
@@ -29,16 +30,19 @@ export function useCalendarShell() {
29
30
  * state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
30
31
  * views/dates must be instant (no full page reload) and this framework has no client-side router.
31
32
  */
32
- export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
33
+ export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
33
34
  const [status, setStatus] = useState("checking");
34
35
  const [error, setError] = useState(null);
35
36
  const [mailboxes, setMailboxes] = useState([]);
36
37
  const [mailboxCalendars, setMailboxCalendars] = useState([]);
37
38
  const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
38
39
  const [folderRefreshToken, setFolderRefreshToken] = useState(0);
40
+ // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
41
+ // effect, not during render, so the server render and the hydrating render agree.
42
+ const search = useLocationSearch();
39
43
  useEffect(() => {
40
- setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
41
- }, []);
44
+ setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
45
+ }, [search]);
42
46
  useEffect(() => {
43
47
  if (!userUid) {
44
48
  return;
@@ -100,5 +104,5 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
100
104
  else if (userUid && status === "ready") {
101
105
  inner = (_jsx("div", { className: "flex-1 min-w-0 flex flex-col", children: _jsx(CalendarShellContext.Provider, { value: contextValue, children: children }) }));
102
106
  }
103
- return (_jsx(AppShell, { active: "calendar", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
107
+ return (_jsx(AppShell, { active: "calendar", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
104
108
  }