@rapidmx/web-client 0.30.0 → 0.32.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 (402) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +3 -3
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +2 -2
  33. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  34. package/apps/shared/components/admin/elevation.ts +19 -0
  35. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  36. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  37. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  38. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  40. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  41. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  42. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  43. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  44. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  45. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  46. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  47. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  48. package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
  49. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  50. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  51. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  52. package/apps/shared/components/admin/signOut.ts +3 -0
  53. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  54. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  55. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  56. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  57. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  58. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  59. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  60. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  61. package/apps/shared/components/calendar/ListView.tsx +99 -25
  62. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  63. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  64. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  65. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  66. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  67. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  68. package/apps/shared/components/calendar/timePicker.ts +5 -2
  69. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  70. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  71. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  72. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  73. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  74. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  75. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  76. package/apps/shared/components/layout/AppShell.tsx +9 -0
  77. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  78. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  79. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  80. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  81. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  82. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  83. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  84. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  85. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  86. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  87. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  88. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  89. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  90. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  91. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  92. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  93. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  94. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  95. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  96. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  97. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  99. package/apps/shared/components/tasks/dueDate.ts +29 -0
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  101. package/apps/shared/mail/decryptedMessages.ts +51 -2
  102. package/apps/shared/mail/listSnapshots.ts +2 -0
  103. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  104. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  105. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  106. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  107. package/apps/shared/notifications/apiErrors.ts +2 -0
  108. package/apps/shared/notifications/preferences.ts +1 -1
  109. package/apps/shared/notifications/store.ts +3 -1
  110. package/apps/shared/plugins/pluginNav.ts +4 -2
  111. package/apps/www/calendar/index.tsx +24 -12
  112. package/apps/www/contacts/[uid].tsx +6 -1
  113. package/apps/www/contacts/index.tsx +66 -12
  114. package/apps/www/index.tsx +91 -28
  115. package/apps/www/messages/[uid].tsx +8 -3
  116. package/apps/www/settings/auto-reply/index.tsx +13 -0
  117. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  118. package/apps/www/settings/encryption/index.tsx +66 -4
  119. package/apps/www/settings/privacy/index.tsx +4 -1
  120. package/apps/www/settings/profile/index.tsx +12 -1
  121. package/apps/www/settings/sharing/index.tsx +11 -1
  122. package/apps/www/settings/signatures/[uid].tsx +6 -4
  123. package/apps/www/tasks/index.tsx +97 -29
  124. package/dist/apps/admin/audit-log/index.js +3 -3
  125. package/dist/apps/admin/branding/index.js +3 -3
  126. package/dist/apps/admin/data-requests/index.js +17 -7
  127. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  128. package/dist/apps/admin/distribution-lists/index.js +3 -3
  129. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  130. package/dist/apps/admin/domains/[uid].js +6 -5
  131. package/dist/apps/admin/domains/index.js +3 -3
  132. package/dist/apps/admin/domains/new/index.js +3 -3
  133. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  134. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  135. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  136. package/dist/apps/admin/index.js +4 -4
  137. package/dist/apps/admin/ingest-queue/index.js +3 -3
  138. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  139. package/dist/apps/admin/quarantine/index.js +4 -4
  140. package/dist/apps/admin/retention-policy/index.js +3 -3
  141. package/dist/apps/admin/signing-certificates/index.js +3 -3
  142. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  143. package/dist/apps/admin/transport-rules/index.js +3 -3
  144. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  145. package/dist/apps/escrow/matters/[uid].js +8 -3
  146. package/dist/apps/escrow/matters/new/index.js +4 -0
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  148. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  149. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  151. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  152. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
  153. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  156. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  157. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
  158. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  159. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  160. package/dist/apps/shared/components/admin/elevation.js +16 -0
  161. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  162. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  163. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  164. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  165. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  166. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  167. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  168. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  169. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  170. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  171. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  172. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  174. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
  176. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  177. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  178. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  179. package/dist/apps/shared/components/admin/signOut.js +3 -0
  180. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  181. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  182. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  183. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  184. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  185. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  186. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  187. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  188. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  189. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  190. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  191. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  192. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  193. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  194. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  195. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  196. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  197. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  198. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  199. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  201. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  202. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  203. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  204. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  205. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  206. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  207. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  208. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  209. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  210. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  211. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  212. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  213. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  214. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  215. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  216. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  217. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  218. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  219. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  221. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  222. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  224. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  226. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  227. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  228. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  229. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  230. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  231. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  232. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  233. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  234. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  235. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  236. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  237. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  238. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  239. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  240. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  241. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  242. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  243. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  244. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  245. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  246. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  247. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  248. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  249. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  250. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  251. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  252. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  253. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  254. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  255. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  256. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  257. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  258. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  259. package/dist/apps/shared/notifications/preferences.js +1 -1
  260. package/dist/apps/shared/notifications/store.js +3 -1
  261. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  262. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  263. package/dist/apps/www/calendar/index.js +24 -12
  264. package/dist/apps/www/contacts/[uid].js +6 -2
  265. package/dist/apps/www/contacts/index.js +62 -11
  266. package/dist/apps/www/index.js +85 -30
  267. package/dist/apps/www/messages/[uid].js +8 -3
  268. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  269. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  270. package/dist/apps/www/settings/encryption/index.js +29 -5
  271. package/dist/apps/www/settings/privacy/index.js +3 -2
  272. package/dist/apps/www/settings/profile/index.js +4 -1
  273. package/dist/apps/www/settings/sharing/index.js +10 -1
  274. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  275. package/dist/apps/www/tasks/index.js +94 -32
  276. package/dist/lib/admin/auditLogApi.js +2 -2
  277. package/dist/lib/admin/distributionListsApi.js +2 -2
  278. package/dist/lib/admin/domainsApi.js +2 -2
  279. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  280. package/dist/lib/admin/escrowScopesApi.js +2 -2
  281. package/dist/lib/admin/pluginsApi.d.ts +31 -2
  282. package/dist/lib/admin/pluginsApi.js +23 -1
  283. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  284. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  285. package/dist/lib/admin/transportRulesApi.js +2 -2
  286. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  287. package/dist/lib/appearance/preferencesApi.js +3 -14
  288. package/dist/lib/auth/session.d.ts +51 -12
  289. package/dist/lib/auth/session.js +214 -38
  290. package/dist/lib/branding/brandingApi.d.ts +3 -3
  291. package/dist/lib/branding/brandingApi.js +9 -21
  292. package/dist/lib/calendar/calendarApi.js +3 -1
  293. package/dist/lib/calendar/eventSearch.js +43 -6
  294. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  295. package/dist/lib/calendar/freeBusyApi.js +13 -5
  296. package/dist/lib/calendar/recurrence.js +51 -3
  297. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  298. package/dist/lib/calendar/useDayKey.js +43 -0
  299. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  300. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  301. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  302. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  303. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  304. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  305. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  306. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  307. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  308. package/dist/lib/components/menus/MenuButton.js +219 -0
  309. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  310. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  311. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  312. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  313. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  314. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  315. package/dist/lib/contacts/contactsApi.js +16 -19
  316. package/dist/lib/contacts/gravatar.d.ts +14 -3
  317. package/dist/lib/contacts/gravatar.js +42 -5
  318. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  319. package/dist/lib/contacts/preparePhoto.js +250 -0
  320. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  321. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  322. package/dist/lib/contacts/vcard.d.ts +3 -0
  323. package/dist/lib/contacts/vcard.js +59 -7
  324. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  325. package/dist/lib/crypto/escrowKeys.js +20 -1
  326. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  327. package/dist/lib/crypto/messageSecurity.js +23 -5
  328. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  329. package/dist/lib/crypto/passwordUnlock.js +13 -2
  330. package/dist/lib/crypto/smime.js +12 -2
  331. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  332. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  333. package/dist/lib/mail/compose/quotedImages.js +35 -9
  334. package/dist/lib/mail/labelsApi.js +2 -2
  335. package/dist/lib/mail/mailApi.d.ts +8 -5
  336. package/dist/lib/mail/mailApi.js +73 -39
  337. package/dist/lib/mail/mailDetailHooks.js +11 -3
  338. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  339. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  340. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  341. package/dist/lib/mail/mailboxImportApi.js +3 -15
  342. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  343. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  344. package/dist/lib/search/queryGrammar.js +12 -4
  345. package/dist/lib/search/searchTier3.d.ts +6 -1
  346. package/dist/lib/search/searchTier3.js +54 -25
  347. package/dist/lib/tasks/tasksApi.js +3 -3
  348. package/dist/lib/util/api.d.ts +26 -4
  349. package/dist/lib/util/api.js +73 -16
  350. package/dist/lib/util/apiQuery.d.ts +11 -0
  351. package/dist/lib/util/apiQuery.js +11 -0
  352. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  353. package/dist/lib/util/settleWithLimit.js +25 -0
  354. package/lib/admin/auditLogApi.ts +2 -2
  355. package/lib/admin/distributionListsApi.ts +2 -2
  356. package/lib/admin/domainsApi.ts +2 -2
  357. package/lib/admin/escrowAuditLogApi.ts +2 -2
  358. package/lib/admin/escrowScopesApi.ts +2 -2
  359. package/lib/admin/pluginsApi.ts +45 -3
  360. package/lib/admin/retentionPolicyApi.ts +4 -0
  361. package/lib/admin/transportRulesApi.ts +2 -2
  362. package/lib/appearance/preferencesApi.ts +3 -14
  363. package/lib/auth/session.ts +239 -39
  364. package/lib/branding/brandingApi.ts +9 -21
  365. package/lib/calendar/calendarApi.ts +3 -1
  366. package/lib/calendar/eventSearch.ts +46 -6
  367. package/lib/calendar/freeBusyApi.ts +14 -6
  368. package/lib/calendar/recurrence.ts +59 -8
  369. package/lib/calendar/useDayKey.ts +45 -0
  370. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  371. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  372. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  373. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  374. package/lib/components/menus/MenuButton.tsx +408 -0
  375. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  376. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  377. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  378. package/lib/contacts/contactsApi.ts +18 -19
  379. package/lib/contacts/gravatar.ts +47 -5
  380. package/lib/contacts/preparePhoto.ts +273 -0
  381. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  382. package/lib/contacts/vcard.ts +60 -7
  383. package/lib/crypto/escrowKeys.ts +21 -1
  384. package/lib/crypto/messageSecurity.ts +33 -9
  385. package/lib/crypto/passwordUnlock.ts +14 -2
  386. package/lib/crypto/smime.ts +13 -2
  387. package/lib/mail/compose/composeQuoting.ts +3 -2
  388. package/lib/mail/compose/quotedImages.ts +39 -9
  389. package/lib/mail/labelsApi.ts +2 -2
  390. package/lib/mail/mailApi.ts +96 -39
  391. package/lib/mail/mailDetailHooks.ts +11 -3
  392. package/lib/mail/mailFilterRulesApi.ts +2 -2
  393. package/lib/mail/mailSignaturesApi.ts +2 -2
  394. package/lib/mail/mailboxImportApi.ts +9 -15
  395. package/lib/mail/messageBodySanitizer.ts +47 -1
  396. package/lib/search/queryGrammar.ts +13 -4
  397. package/lib/search/searchTier3.ts +65 -32
  398. package/lib/tasks/tasksApi.ts +3 -3
  399. package/lib/util/api.ts +99 -16
  400. package/lib/util/apiQuery.ts +13 -0
  401. package/lib/util/settleWithLimit.ts +25 -0
  402. package/package.json +1 -1
@@ -4,13 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useCallback, useEffect, useId, useRef, useState } from "react";
7
+ import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, isSecretSetting, isSecretSettingKey, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, retryPluginPurge, searchPlugins, updatePlugin, uploadPlugin, } from "../../../../../lib/admin/pluginsApi.js";
7
8
  import { ApiRequestError } from "../../../../../lib/util/api.js";
8
- import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, retryPluginPurge, searchPlugins, updatePlugin, } from "../../../../../lib/admin/pluginsApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
9
+ import Alert from "../ActionAlert.js";
10
10
  import Button from "../../../../../lib/components/buttons/Button.js";
11
11
  import Modal from "../../../../../lib/components/overlays/Modal.js";
12
12
  import { notify } from "../../../notifications/store.js";
13
- import { isElevationRequired } from "../elevation.js";
13
+ import { actionErrorMessage, isElevationMessage, isElevationRequired } from "../elevation.js";
14
14
  import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
15
15
  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";
16
16
  /** How often status is refreshed while server copies are still applying a change. */
@@ -20,7 +20,7 @@ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
20
20
  /** The longest wait between retries while no status has ever been read. */
21
21
  const MAX_STATUS_RETRY_MS = 60 * 1000;
22
22
  function errorMessage(err, fallback) {
23
- return err instanceof ApiRequestError ? err.message : fallback;
23
+ return actionErrorMessage(err, fallback);
24
24
  }
25
25
  /** Runs `action`, resolving why it failed, or `null`. */
26
26
  async function attempt(action, failure) {
@@ -44,6 +44,10 @@ function purgeFailure(purge) {
44
44
  }
45
45
  /** A day, in the administrator's own format. */
46
46
  const formatDate = (iso) => new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
47
+ /** The file names an uploaded plugin may have: what `npm pack` produces (`<name>-<version>.tgz`), or the same gzipped. */
48
+ const PLUGIN_PACK_NAME = /\.(tgz|tar\.gz|gz)$/i;
49
+ /** What the server answers (409) an upload of a plugin that is already installed, which the administrator may replace. */
50
+ const ALREADY_INSTALLED = /already installed/i;
47
51
  const BULK_WORDS = {
48
52
  upgrade: { doing: "Upgrading", done: "upgraded" },
49
53
  disable: { doing: "Disabling", done: "disabled" },
@@ -126,6 +130,8 @@ export default function PluginsManager({ embedded = false } = {}) {
126
130
  const allowPrereleaseRef = useRef(allowPrerelease);
127
131
  /** Identifies the latest update check, so a slower earlier one can't replace what it found. */
128
132
  const updatesRequest = useRef(0);
133
+ // Set while a bulk action runs, which looks for updates once at its end rather than after each plugin it changes.
134
+ const bulkRunning = useRef(false);
129
135
  /** The plugins ticked for a bulk action. */
130
136
  const [selected, setSelected] = useState(new Set());
131
137
  /** Set while a bulk action runs. */
@@ -135,6 +141,11 @@ export default function PluginsManager({ embedded = false } = {}) {
135
141
  const [confirmingBulkUninstall, setConfirmingBulkUninstall] = useState(null);
136
142
  /** How many changes (or the lookups that come before them) are waiting on the server. */
137
143
  const [working, setWorking] = useState(0);
144
+ /** Set while an uploaded plugin is being sent to the server. */
145
+ const [uploading, setUploading] = useState(false);
146
+ /** An upload that would replace an installed plugin, waiting for the administrator to confirm it. */
147
+ const [replacing, setReplacing] = useState(null);
148
+ const fileInput = useRef(null);
138
149
  const statusRequest = useRef(null);
139
150
  /** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
140
151
  const seenPurges = useRef(new Map());
@@ -279,7 +290,9 @@ export default function PluginsManager({ embedded = false } = {}) {
279
290
  return [...next].sort((a, b) => a.name.localeCompare(b.name));
280
291
  });
281
292
  watchRollout();
282
- void refreshUpdates();
293
+ if (!bulkRunning.current) {
294
+ void refreshUpdates();
295
+ }
283
296
  }
284
297
  /** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
285
298
  * shows why it failed. */
@@ -370,6 +383,55 @@ export default function PluginsManager({ embedded = false } = {}) {
370
383
  function upgrade(plugin, packageVersion) {
371
384
  return onRow(plugin, () => changeVersion(plugin, packageVersion));
372
385
  }
386
+ /** Sends `file` to the server as a plugin to install - replacing the installed plugin of that name only when `replace` is set. A plugin that is
387
+ * already installed isn't an error but a question, asked of the administrator before it is replaced. */
388
+ async function runUpload(file, replace) {
389
+ setUploading(true);
390
+ setError(null);
391
+ setRetryEnableUid(null);
392
+ try {
393
+ const uploaded = await track(uploadPlugin(file, { replace }));
394
+ applied(uploaded);
395
+ notify({
396
+ kind: "success",
397
+ title: `${uploaded.name}@${uploaded.packageVersion} uploaded`,
398
+ message: "The servers restart one at a time to load it.",
399
+ });
400
+ }
401
+ catch (err) {
402
+ if (!replace && err instanceof ApiRequestError && err.status === 409 && ALREADY_INSTALLED.test(err.message)) {
403
+ const installed = plugins.find((plugin) => err.message.includes(plugin.name));
404
+ setReplacing({
405
+ file,
406
+ question: installed
407
+ ? `Replace ${installed.name} ${installed.packageVersion} with the uploaded pack?`
408
+ : "Replace the installed plugin with the uploaded pack?",
409
+ reason: err.message,
410
+ });
411
+ }
412
+ else if (err instanceof ApiRequestError && err.status === 413) {
413
+ setError("That file is too large for the server to accept.");
414
+ }
415
+ else {
416
+ setError(errorMessage(err, "Could not upload the plugin."));
417
+ }
418
+ }
419
+ setUploading(false);
420
+ }
421
+ /** Takes the file the administrator chose, unless it can't be what `npm pack` produces. */
422
+ function onFileChosen(e) {
423
+ const file = e.target.files?.[0];
424
+ // So choosing the same file again (after fixing what was wrong) still counts as a change.
425
+ e.target.value = "";
426
+ if (!file) {
427
+ return;
428
+ }
429
+ if (!PLUGIN_PACK_NAME.test(file.name)) {
430
+ setError(`${file.name} isn't a plugin pack. Choose the .tgz file that npm pack produces.`);
431
+ return;
432
+ }
433
+ void runUpload(file, false);
434
+ }
373
435
  /** Installs an uninstalled plugin again, at its latest version - as adding it by name would. */
374
436
  function reinstall(purge) {
375
437
  const displayName = purgeName(purge);
@@ -435,6 +497,7 @@ export default function PluginsManager({ embedded = false } = {}) {
435
497
  setError(null);
436
498
  setRetryEnableUid(null);
437
499
  setBulkFailure(null);
500
+ bulkRunning.current = true;
438
501
  await track((async () => {
439
502
  for (const [index, plugin] of ordered.entries()) {
440
503
  setBulk({ kind, done: index, total: ordered.length, current: plugin.manifest.displayName });
@@ -448,7 +511,7 @@ export default function PluginsManager({ embedded = false } = {}) {
448
511
  }
449
512
  failed.add(plugin.uid);
450
513
  reasons.push(`${plugin.manifest.displayName}: ${problem}`);
451
- if (problem === ELEVATION_MESSAGE) {
514
+ if (isElevationMessage(problem)) {
452
515
  for (const untried of ordered.slice(index + 1)) {
453
516
  failed.add(untried.uid);
454
517
  reasons.push(`${untried.manifest.displayName}: Not attempted.`);
@@ -457,12 +520,16 @@ export default function PluginsManager({ embedded = false } = {}) {
457
520
  }
458
521
  }
459
522
  })());
523
+ bulkRunning.current = false;
460
524
  setBulk(null);
461
525
  setSelected(failed);
462
526
  const succeeded = ordered.length - failed.size;
463
527
  if (kind === "uninstall" && succeeded > 0) {
464
528
  watchRollout();
465
529
  }
530
+ if (kind !== "uninstall" && succeeded > 0) {
531
+ void refreshUpdates();
532
+ }
466
533
  if (succeeded > 0) {
467
534
  notify({
468
535
  kind: "success",
@@ -515,9 +582,10 @@ export default function PluginsManager({ embedded = false } = {}) {
515
582
  const retryEnable = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
516
583
  const displayNameOf = (name) => plugins.find((plugin) => plugin.name === name)?.manifest.displayName ?? name;
517
584
  const SectionHeading = embedded ? "h3" : "h2";
518
- const addButton = (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setAdding(true), children: "Add by name" }));
519
585
  const bulkBusy = bulk !== null;
520
- return (_jsxs(_Fragment, { children: [(working > 0 || pending) && _jsx(PluginProgressBar, { label: working > 0 ? "Working on plugins" : "Applying plugin changes" }), 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." })] })), _jsxs("label", { className: "flex items-start gap-2 text-sm mb-5 max-w-3xl cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "mt-0.5 h-4 w-4 shrink-0", checked: allowPrerelease, onChange: (e) => changePrerelease(e.target.checked) }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-semibold", children: "Allow pre-release versions" }), _jsx("span", { className: "text-xs text-text-muted", children: "Also offer beta and other pre-release builds, such as 1.0.0-beta.2, as updates and as versions to install. They can be unstable. This is remembered in this browser." })] })] }), bulkFailure && (_jsx(Alert, { children: _jsxs("div", { children: [bulkFailure.title, _jsx("ul", { className: "list-disc pl-5 mt-1", children: bulkFailure.reasons.map((reason) => (_jsx("li", { className: "break-words", children: reason }, reason))) })] }) })), 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." })) : (_jsxs("div", { className: "overflow-x-auto", children: [plugins.length > 0 && (_jsxs("div", { role: "toolbar", "aria-label": "Actions for several plugins", className: "flex flex-wrap items-center gap-2 py-2", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm mr-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "h-4 w-4 shrink-0", "aria-label": "Select all plugins", checked: allSelected, disabled: bulkBusy, ref: (input) => {
586
+ const addButton = (_jsxs("div", { className: "flex flex-wrap gap-2 shrink-0", children: [_jsx("input", { ref: fileInput, type: "file", className: "hidden", accept: ".tgz,.tar.gz,application/gzip,application/x-gzip", "aria-label": "Plugin pack file", tabIndex: -1, onChange: onFileChosen }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", title: "Install a plugin from the .tgz file that npm pack produces", loading: uploading, disabled: uploading || bulkBusy, onClick: () => fileInput.current?.click(), children: uploading ? "Uploading\u2026" : "Upload" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setAdding(true), children: "Add by name" })] }));
587
+ return (_jsxs(_Fragment, { children: [(working > 0 || pending) && _jsx(PluginProgressBar, { label: working > 0 ? "Working on plugins" : "Applying plugin changes" }), 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." })] })), _jsxs("label", { className: "flex items-start gap-2 text-sm mb-5 max-w-3xl cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "mt-0.5 h-4 w-4 shrink-0", checked: allowPrerelease, onChange: (e) => changePrerelease(e.target.checked) }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-semibold", children: "Allow pre-release versions" }), _jsx("span", { className: "text-xs text-text-muted", children: "Also offer beta and other pre-release builds, such as 1.0.0-beta.2, as updates and as versions to install. They can be unstable. This is remembered in this browser." })] })] }), bulkFailure && (_jsx(Alert, { children: _jsxs("div", { children: [bulkFailure.title, _jsx("ul", { className: "list-disc pl-5 mt-1", children: bulkFailure.reasons.map((reason) => (_jsx("li", { className: "break-words", children: reason }, reason))) })] }) })), error &&
588
+ (retryEnable ? (_jsxs(Alert, { children: [error, " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0", onClick: () => void toggle(retryEnable), children: "Try again" })] })) : (_jsx(Alert, { children: error }))), _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." })) : (_jsxs("div", { className: "overflow-x-auto", children: [plugins.length > 0 && (_jsxs("div", { role: "toolbar", "aria-label": "Actions for several plugins", className: "flex flex-wrap items-center gap-2 py-2", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm mr-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "h-4 w-4 shrink-0", "aria-label": "Select all plugins", checked: allSelected, disabled: bulkBusy, ref: (input) => {
521
589
  if (input) {
522
590
  input.indeterminate = selectedPlugins.length > 0 && !allSelected;
523
591
  }
@@ -525,13 +593,15 @@ export default function PluginsManager({ embedded = false } = {}) {
525
593
  versions: new Map(selectedUpgrades.map((plugin) => [plugin.uid, updates.get(plugin.uid).latestVersion])),
526
594
  }), children: ["Upgrade", selectedUpgrades.length > 0 ? ` (${selectedUpgrades.length})` : ""] }), _jsxs(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Disable selected plugins", disabled: bulkBusy || selectedEnabled.length === 0, onClick: () => void runBulk("disable", selectedEnabled), children: ["Disable", selectedEnabled.length > 0 ? ` (${selectedEnabled.length})` : ""] }), _jsxs(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Uninstall selected plugins", disabled: bulkBusy || selectedPlugins.length === 0, onClick: () => setConfirmingBulkUninstall(selectedPlugins), children: ["Uninstall", selectedPlugins.length > 0 ? ` (${selectedPlugins.length})` : ""] }), _jsx(Button, { type: "button", className: "!w-auto ml-auto", "aria-label": "Upgrade all plugins", loading: bulk?.kind === "upgrade", disabled: bulkBusy, onClick: () => void upgradeAll(), children: "Upgrade all" }), bulk && (_jsxs("span", { "aria-live": "polite", className: "basis-full text-xs text-text-muted", children: [BULK_WORDS[bulk.kind].doing, " ", bulk.current, " (", bulk.done + 1, " of ", bulk.total, ")\u2026"] }))] })), _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) => {
527
595
  const update = updates.get(plugin.uid);
528
- const latest = update?.updateAvailable ? update.latestVersion : undefined;
596
+ const uploaded = plugin.source === "upload";
597
+ // An uploaded pack has no registry version to be newer than.
598
+ const latest = !uploaded && update?.updateAvailable ? update.latestVersion : undefined;
529
599
  const busy = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid || bulkBusy;
530
600
  const requires = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
531
601
  const requiredBy = plugins
532
602
  .filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
533
603
  .map((other) => other.manifest.displayName);
534
- return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("input", { type: "checkbox", className: "mt-1 h-4 w-4 shrink-0", "aria-label": `Select ${plugin.manifest.displayName}`, checked: selected.has(plugin.uid), disabled: bulkBusy, onChange: () => toggleSelected(plugin.uid) }), _jsxs("div", { className: "min-w-0", 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));
604
+ return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("input", { type: "checkbox", className: "mt-1 h-4 w-4 shrink-0", "aria-label": `Select ${plugin.manifest.displayName}`, checked: selected.has(plugin.uid), disabled: bulkBusy, onChange: () => toggleSelected(plugin.uid) }), _jsxs("div", { className: "min-w-0", 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 })), uploaded && (_jsxs("div", { className: "text-xs text-text-muted mt-1 break-words", children: [_jsx("span", { className: `${BADGE_CLASS} bg-surface-alt text-text-muted`, children: "Uploaded" }), " ", plugin.uploadFilename, plugin.uploadedAt ? `, ${formatDate(plugin.uploadedAt)}` : ""] })), 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: uploaded ? "Install the registry version" : "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
535
605
  }), purges.map((purge) => (_jsx(UninstalledPluginRow, { purge: purge, busy: busyUids.has(purge.uid), onRetry: () => void retryPurge(purge), onInstall: () => void reinstall(purge) }, purge.uid)))] })] })] })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, prerelease: allowPrerelease, onInstall: (result) => install(result.name, result.version, result.name),
536
606
  // The browser only offers an upgrade for a result it matched to one of these same `plugins`.
537
607
  onUpgrade: (uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid), packageVersion) }), _jsx(AddPluginModal, { open: adding, prerelease: allowPrerelease, onClose: () => setAdding(false), onAdd: async (name, packageVersion, displayName) => {
@@ -546,7 +616,11 @@ export default function PluginsManager({ embedded = false } = {}) {
546
616
  setUpgrading(null);
547
617
  }
548
618
  return problem;
549
- } })), confirming && (_jsx(ConfirmDependenciesModal, { change: confirming, displayNameOf: displayNameOf, onClose: () => setConfirming(null), onDone: () => setConfirming(null) })), confirmingBulkUninstall && (_jsx(BulkUninstallModal, { plugins: confirmingBulkUninstall, onClose: () => setConfirmingBulkUninstall(null), onConfirm: (purgeData) => {
619
+ } })), replacing && (_jsxs(Modal, { open: true, onClose: () => setReplacing(null), title: "Replace plugin?", children: [_jsx("p", { className: "text-sm mb-2", children: replacing.question }), _jsx("p", { className: "text-sm text-text-muted mb-4 break-words", children: replacing.reason }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setReplacing(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
620
+ const { file } = replacing;
621
+ setReplacing(null);
622
+ void runUpload(file, true);
623
+ }, children: "Replace" })] })] })), confirming && (_jsx(ConfirmDependenciesModal, { change: confirming, displayNameOf: displayNameOf, onClose: () => setConfirming(null), onDone: () => setConfirming(null) })), confirmingBulkUninstall && (_jsx(BulkUninstallModal, { plugins: confirmingBulkUninstall, onClose: () => setConfirmingBulkUninstall(null), onConfirm: (purgeData) => {
550
624
  const targets = confirmingBulkUninstall;
551
625
  setConfirmingBulkUninstall(null);
552
626
  void runBulk("uninstall", targets, { purgeData });
@@ -769,14 +843,19 @@ function ChangeVersionModal({ plugin, prerelease, onClose, onSave, }) {
769
843
  const [selected, setSelected] = useState(plugin.packageVersion);
770
844
  const [error, setError] = useState(null);
771
845
  const [busy, setBusy] = useState(false);
846
+ // An uploaded pack isn't a registry version, so any registry version is a change from it - the newest is offered first.
847
+ const uploaded = plugin.source === "upload";
772
848
  useEffect(() => {
773
849
  lookupPluginPackage(plugin.name, undefined, { prerelease })
774
850
  .then((found) => {
775
851
  setVersions(found.package.versions);
776
852
  setLatest(found.package.latest);
853
+ if (uploaded && found.package.latest) {
854
+ setSelected(found.package.latest);
855
+ }
777
856
  })
778
857
  .catch((err) => setError(errorMessage(err, "Could not load the available versions.")));
779
- }, [plugin.name, prerelease]);
858
+ }, [plugin.name, prerelease, uploaded]);
780
859
  async function save() {
781
860
  setBusy(true);
782
861
  setError(null);
@@ -786,7 +865,7 @@ function ChangeVersionModal({ plugin, prerelease, onClose, onSave, }) {
786
865
  setBusy(false);
787
866
  }
788
867
  }
789
- return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} version`, children: [error && _jsx(Alert, { children: error }), versions === null ? (!error && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selected, onChange: (e) => setSelected(e.target.value), children: versions.map((version) => (_jsxs("option", { value: version, children: [version, version === latest ? " (latest)" : "", version === plugin.packageVersion ? " (installed)" : ""] }, version))) })] }), _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 || selected === plugin.packageVersion, onClick: () => void save(), children: "Save" })] })] }))] }));
868
+ return (_jsxs(Modal, { open: true, onClose: onClose, title: uploaded ? `Install the registry version of ${plugin.manifest.displayName}` : `${plugin.manifest.displayName} version`, children: [error && _jsx(Alert, { children: error }), versions === null ? (!error && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [uploaded && (_jsxs("p", { className: "text-sm text-text-muted", children: [plugin.manifest.displayName, " ", plugin.packageVersion, " was uploaded as ", plugin.uploadFilename, ". This replaces it with the version from the registry."] })), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selected, onChange: (e) => setSelected(e.target.value), children: versions.map((version) => (_jsxs("option", { value: version, children: [version, version === latest ? " (latest)" : "", !uploaded && version === plugin.packageVersion ? " (installed)" : ""] }, version))) })] }), _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 || (!uploaded && selected === plugin.packageVersion), onClick: () => void save(), children: uploaded ? "Install" : "Save" })] })] }))] }));
790
869
  }
791
870
  /** Lists the plugins a change also installs or enables, and makes the change once confirmed. */
792
871
  function ConfirmDependenciesModal({ change, displayNameOf, onClose, onDone, }) {
@@ -848,6 +927,10 @@ function hostSuggestion(definition, saved, configured) {
848
927
  * deployment configures; else the setting's own default - or, for a required select with none of those, its first
849
928
  * option. Kept as a string for text inputs. */
850
929
  function initialValue(definition, saved, configured) {
930
+ if (isSecretSetting(saved)) {
931
+ // Write-only: the saved secret never reaches the browser, so the field starts empty. Left empty, the placeholder is sent back and the secret stays.
932
+ return "";
933
+ }
851
934
  if (configured && isUnsaved(saved)) {
852
935
  return configuredValue(definition, configured);
853
936
  }
@@ -929,21 +1012,23 @@ function SettingsModal({ plugin, onClose, onSaved }) {
929
1012
  setBusy(false);
930
1013
  }
931
1014
  }
932
- return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} settings`, children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: save, className: "flex flex-col gap-5", children: [definitions.map((definition) => (_jsx(SettingField, { definition: definition, value: values[definition.key], configured: plugin.configured?.[definition.key], onChange: (value) => setValues((prev) => ({ ...prev, [definition.key]: value })) }, definition.key))), _jsxs("div", { className: "sticky -bottom-7 -mx-7 -mb-7 px-7 py-4 bg-surface border-t border-border flex flex-col gap-3", children: [_jsx("p", { className: "text-xs text-text-muted", children: resetting
1015
+ return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} settings`, children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: save, className: "flex flex-col gap-5", children: [definitions.map((definition) => (_jsx(SettingField, { definition: definition, value: values[definition.key], configured: plugin.configured?.[definition.key], secretSaved: !resetting && isSecretSetting(plugin.settings[definition.key]), onChange: (value) => setValues((prev) => ({ ...prev, [definition.key]: value })) }, definition.key))), _jsxs("div", { className: "sticky -bottom-7 -mx-7 -mb-7 px-7 py-4 bg-surface border-t border-border flex flex-col gap-3", children: [_jsx("p", { className: "text-xs text-text-muted", children: resetting
933
1016
  ? "Every setting is back to what this deployment's configuration or the plugin's default gives. Save to apply that."
934
1017
  : "Saving restarts the servers one at a time to apply the new settings." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mr-auto", onClick: reset, disabled: busy, children: "Reset" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: busy, disabled: busy, children: "Save" })] })] })] })] }));
935
1018
  }
936
- function SettingField({ definition, value, configured, onChange, }) {
1019
+ function SettingField({ definition, value, configured, secretSaved, onChange, }) {
937
1020
  // What applies without a saved value; one saved here replaces it, and Reset goes back to it.
938
1021
  const note = configured && (_jsxs("span", { className: "text-xs text-text-muted", children: ["This deployment's configuration provides ", configured.secret ? "a value" : "the value shown", " unless one is saved here."] }));
939
- // A secret's value never reaches the browser, so the field only says that it is set.
940
- const placeholder = configured?.secret ? "Set by this deployment" : undefined;
1022
+ // A secret's value never reaches the browser, so the field only says that it is set - and never shows what is typed, or offers it to the
1023
+ // browser's form fill.
1024
+ const secretField = definition.type === "string" && (isSecretSettingKey(definition.key) || secretSaved || !!configured?.secret);
1025
+ const placeholder = secretSaved ? "Saved - type a new value to replace it" : configured?.secret ? "Set by this deployment" : undefined;
941
1026
  const help = (_jsxs(_Fragment, { children: [definition.help && _jsx("span", { className: "text-xs text-text-muted", children: definition.help }), note] }));
942
1027
  if (definition.type === "boolean") {
943
1028
  // Laid out like the admin policy forms' checkboxes: the help sits under the label, beside the box.
944
1029
  return (_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: value === true, onChange: (e) => onChange(e.target.checked) }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-semibold", children: definition.label }), help] })] }));
945
1030
  }
946
- return (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: definition.label }), definition.type === "select" ? (_jsxs("select", { "aria-label": definition.label, className: INPUT_CLASS, value: String(value), onChange: (e) => onChange(e.target.value), children: [!definition.required && _jsx("option", { value: "", children: "Default" }), (definition.options ?? []).map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })) : (_jsx("input", { "aria-label": definition.label, className: INPUT_CLASS, type: definition.type === "number" ? "number" : "text", min: definition.min, max: definition.max, required: definition.required && !configured, placeholder: placeholder, value: String(value), onChange: (e) => onChange(e.target.value) })), help] }));
1031
+ return (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: definition.label }), definition.type === "select" ? (_jsxs("select", { "aria-label": definition.label, className: INPUT_CLASS, value: String(value), onChange: (e) => onChange(e.target.value), children: [!definition.required && _jsx("option", { value: "", children: "Default" }), (definition.options ?? []).map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })) : (_jsx("input", { "aria-label": definition.label, className: INPUT_CLASS, type: definition.type === "number" ? "number" : secretField ? "password" : "text", autoComplete: secretField ? "new-password" : undefined, min: definition.min, max: definition.max, required: definition.required && !configured && !secretSaved, placeholder: placeholder, value: String(value), onChange: (e) => onChange(e.target.value) })), help] }));
947
1032
  }
948
1033
  /** The word typed to confirm deleting the data of several plugins at once, in place of a plugin's name. */
949
1034
  const BULK_DELETE_WORD = "delete";
@@ -4,9 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
- import { MIN_AUDIT_LOG_RETENTION_DAYS, updateRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
7
+ import { actionErrorMessage } from "../elevation.js";
8
+ import { MIN_AUDIT_LOG_RETENTION_DAYS, MIN_MESSAGE_RETENTION_DAYS, updateRetentionPolicy, } from "../../../../../lib/admin/retentionPolicyApi.js";
9
+ import Alert from "../ActionAlert.js";
10
10
  import Button from "../../../../../lib/components/buttons/Button.js";
11
11
  import Modal from "../../../../../lib/components/overlays/Modal.js";
12
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";
@@ -31,13 +31,20 @@ export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, e
31
31
  const [savedMessageDays, setSavedMessageDays] = useState(policy.messageRetentionDays);
32
32
  const [savedAuditDays, setSavedAuditDays] = useState(policy.auditLogRetentionDays);
33
33
  const [pendingPatch, setPendingPatch] = useState(null);
34
+ // A period that is still what is stored is neither sent nor held to the floor: it may have been stored before the floor was set.
35
+ const messageChanged = messageRetentionDays !== (savedMessageDays?.toString() ?? "");
36
+ const auditChanged = auditLogRetentionDays !== (savedAuditDays?.toString() ?? "");
34
37
  function handleSubmit(e) {
35
38
  e.preventDefault();
36
- // A blank field is sent as `null`, which clears a configured age back to no automatic purge.
37
- const patch = {
38
- messageRetentionDays: messageRetentionDays.trim() === "" ? null : Number(messageRetentionDays),
39
- auditLogRetentionDays: auditLogRetentionDays.trim() === "" ? null : Number(auditLogRetentionDays),
40
- };
39
+ // A blank field is sent as `null`, which clears a configured age back to no automatic purge. Only a field that was changed is sent: one left as
40
+ // it is would be refused by a floor that came in after it was stored, and so keep the other from being saved.
41
+ const patch = {};
42
+ if (messageChanged) {
43
+ patch.messageRetentionDays = messageRetentionDays.trim() === "" ? null : Number(messageRetentionDays);
44
+ }
45
+ if (auditChanged) {
46
+ patch.auditLogRetentionDays = auditLogRetentionDays.trim() === "" ? null : Number(auditLogRetentionDays);
47
+ }
41
48
  if (shortensRetention(patch.messageRetentionDays, savedMessageDays) ||
42
49
  shortensRetention(patch.auditLogRetentionDays, savedAuditDays)) {
43
50
  setPendingPatch(patch);
@@ -59,13 +66,13 @@ export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, e
59
66
  setSaved(true);
60
67
  }
61
68
  catch (err) {
62
- setError(err instanceof ApiRequestError ? err.message : "Could not save the retention policy.");
69
+ setError(actionErrorMessage(err, "Could not save the retention policy."));
63
70
  }
64
71
  finally {
65
72
  setSaving(false);
66
73
  }
67
74
  }
68
- return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Retention policy" })) : (_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Retention Policy" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Automatically and permanently deletes mail/audit-log entries older than the configured age. Leave a field blank for no automatic purge. This does not apply to a mailbox that is a custodian on an active legal hold \u2014 a held message is skipped and retried on a later run once the hold is lifted." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message retention (days)" }), _jsx("input", { "aria-label": "Message retention (days)", type: "number", min: 1, className: INPUT_CLASS, value: messageRetentionDays, onChange: (e) => setMessageRetentionDays(e.target.value), placeholder: "No automatic purge" }), _jsx("span", { className: "text-xs text-text-muted", children: "Applies to any message, in any folder, regardless of age." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Audit log retention (days)" }), _jsx("input", { "aria-label": "Audit log retention (days)", type: "number", min: MIN_AUDIT_LOG_RETENTION_DAYS, className: INPUT_CLASS, value: auditLogRetentionDays, onChange: (e) => setAuditLogRetentionDays(e.target.value), placeholder: "Keep forever" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Cannot be set below ", MIN_AUDIT_LOG_RETENTION_DAYS, " days. Never applies to the tamper-evident escrow audit chain."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: pendingPatch !== null, onClose: () => setPendingPatch(null), title: "Delete older data?", children: [shortensRetention(pendingPatch?.messageRetentionDays, savedMessageDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedMessageDays == null
75
+ return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Retention policy" })) : (_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Retention Policy" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Automatically and permanently deletes mail/audit-log entries older than the configured age. Leave a field blank for no automatic purge. This does not apply to a mailbox that is a custodian on an active legal hold \u2014 a held message is skipped and retried on a later run once the hold is lifted." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message retention (days)" }), _jsx("input", { "aria-label": "Message retention (days)", type: "number", min: messageChanged ? MIN_MESSAGE_RETENTION_DAYS : undefined, className: INPUT_CLASS, value: messageRetentionDays, onChange: (e) => setMessageRetentionDays(e.target.value), placeholder: "No automatic purge" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Applies to any message, in any folder, regardless of age. Cannot be set below ", MIN_MESSAGE_RETENTION_DAYS, " days."] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Audit log retention (days)" }), _jsx("input", { "aria-label": "Audit log retention (days)", type: "number", min: auditChanged ? MIN_AUDIT_LOG_RETENTION_DAYS : undefined, className: INPUT_CLASS, value: auditLogRetentionDays, onChange: (e) => setAuditLogRetentionDays(e.target.value), placeholder: "Keep forever" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Cannot be set below ", MIN_AUDIT_LOG_RETENTION_DAYS, " days. Never applies to the tamper-evident escrow audit chain."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: pendingPatch !== null, onClose: () => setPendingPatch(null), title: "Delete older data?", children: [shortensRetention(pendingPatch?.messageRetentionDays, savedMessageDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedMessageDays == null
69
76
  ? "Mail isn't deleted automatically today."
70
77
  : `Mail is currently kept for ${savedMessageDays} days.`, " ", "With this change, every message older than ", _jsxs("strong", { children: [pendingPatch?.messageRetentionDays, " days"] }), ", in every mailbox and folder, is permanently deleted on the next purge run - and so on from then on. Mailboxes on an active legal hold are skipped."] })), shortensRetention(pendingPatch?.auditLogRetentionDays, savedAuditDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedAuditDays == null
71
78
  ? "Audit-log entries are kept forever today."
@@ -4,14 +4,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { createEscrowScope, listEscrowScopes } from "../../../../../lib/admin/escrowScopesApi.js";
9
8
  import { generateEscrowKeyPair } from "../../../../../lib/crypto/escrowKeys.js";
10
9
  import { toDatetimeLocal } from "../../../../../lib/util/dateInput.js";
11
- import Alert from "../../../../../lib/components/feedback/Alert.js";
10
+ import Alert from "../ActionAlert.js";
12
11
  import Button from "../../../../../lib/components/buttons/Button.js";
13
12
  import FormField from "../../../../../lib/components/forms/FormField.js";
14
- import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, selfAsHolderError, } from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
13
+ import Modal from "../../../../../lib/components/overlays/Modal.js";
14
+ import { actionErrorMessage } from "../elevation.js";
15
+ import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, keyValidityError, selfAsHolderError, } from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
15
16
  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";
16
17
  /** How long a download's object URL is kept, since some browsers are still reading it after the click returns. */
17
18
  export const DOWNLOAD_URL_LIFETIME_MS = 60000;
@@ -43,6 +44,8 @@ export default function EscrowSetupStep({ adminUid }) {
43
44
  const [generated, setGenerated] = useState(null);
44
45
  const [downloadedPrivateKey, setDownloadedPrivateKey] = useState(false);
45
46
  const [savedPrivateKey, setSavedPrivateKey] = useState(false);
47
+ // The private key leaves the browser as a plain file, so it is only offered after the administrator has read what that means.
48
+ const [confirmingDownload, setConfirmingDownload] = useState(false);
46
49
  const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
47
50
  const [error, setError] = useState(null);
48
51
  const [busy, setBusy] = useState(false);
@@ -57,6 +60,7 @@ export default function EscrowSetupStep({ adminUid }) {
57
60
  setGenerated(null);
58
61
  setDownloadedPrivateKey(false);
59
62
  setSavedPrivateKey(false);
63
+ setConfirmingDownload(false);
60
64
  setKeyAndHolders(emptyEscrowScopeKeyAndHoldersValue());
61
65
  }
62
66
  async function generate() {
@@ -106,6 +110,11 @@ export default function EscrowSetupStep({ adminUid }) {
106
110
  setError("The public key, its type, and its fingerprint are all required.");
107
111
  return;
108
112
  }
113
+ const validityError = generated ? null : keyValidityError(keyAndHolders);
114
+ if (validityError) {
115
+ setError(validityError);
116
+ return;
117
+ }
109
118
  if (keyAndHolders.holderUserUids.length === 0) {
110
119
  setError("At least one holder is required.");
111
120
  return;
@@ -133,15 +142,16 @@ export default function EscrowSetupStep({ adminUid }) {
133
142
  setName("Escrow");
134
143
  }
135
144
  catch (err) {
136
- setError(err instanceof ApiRequestError ? err.message : "Could not create the escrow scope.");
145
+ setError(actionErrorMessage(err, "Could not create the escrow scope."));
137
146
  }
138
147
  finally {
139
148
  setBusy(false);
140
149
  }
141
150
  }
142
151
  const scopeForm = (_jsxs("form", { onSubmit: create, className: "flex flex-col gap-5", children: [_jsx(EscrowScopeKeyAndHoldersFields, { value: keyAndHolders, onChange: setKeyAndHolders, disabled: mode === "generate" && busy, keyReadOnly: mode === "generate" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: busy, disabled: busy, className: "!w-auto", children: "Create escrow scope" }) })] }));
143
- return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
144
- downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
145
- setDownloadedPrivateKey(true);
146
- }, children: "Download private key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => downloadTextFile(`${fileSafe(name)}-certificate.pem`, generated.certificatePem), children: "Download certificate" })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: savedPrivateKey, disabled: !downloadedPrivateKey, onChange: (e) => setSavedPrivateKey(e.target.checked) }), "I\u2019ve saved the private key somewhere safe"] }), !downloadedPrivateKey && (_jsx("p", { className: "text-xs text-text-muted", children: "Download the private key before continuing." }))] }))] })), mode === "existing" && scopeForm, mode === "generate" && generated && savedPrivateKey && scopeForm] }));
152
+ return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setConfirmingDownload(true), children: "Download private key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => downloadTextFile(`${fileSafe(name)}-certificate.pem`, generated.certificatePem), children: "Download certificate" })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: savedPrivateKey, disabled: !downloadedPrivateKey, onChange: (e) => setSavedPrivateKey(e.target.checked) }), "I\u2019ve saved the private key somewhere safe"] }), !downloadedPrivateKey && (_jsx("p", { className: "text-xs text-text-muted", children: "Download the private key before continuing." })), _jsxs(Modal, { open: confirmingDownload, onClose: () => setConfirmingDownload(false), title: "Download the private key?", children: [_jsxs("p", { className: "text-sm mb-3", children: ["The file is ", _jsx("strong", { children: "not encrypted" }), ": it lands in this computer\u2019s downloads folder (and its download history), and anyone who obtains it can decrypt every mailbox covered by this escrow scope on their own - the number of required holders does not apply to a copy of the key."] }), _jsx("p", { className: "text-sm mb-5", children: "Move it straight to offline storage or a password manager your escrow holders control, and delete it from here." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDownload(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
153
+ downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
154
+ setDownloadedPrivateKey(true);
155
+ setConfirmingDownload(false);
156
+ }, children: "Download unencrypted key" })] })] })] }))] })), mode === "existing" && scopeForm, mode === "generate" && generated && savedPrivateKey && scopeForm] }));
147
157
  }
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  import { useEffect, useRef, useState } from "react";
7
7
  import { useRouter } from "@rapidrest/react/client";
8
8
  import { HiCheck } from "react-icons/hi2";
9
- import { ApiRequestError } from "../../../../../lib/util/api.js";
9
+ import { actionErrorMessage } from "../elevation.js";
10
10
  import { getRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
11
11
  import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
12
12
  import { createDomain, listDomains } from "../../../../../lib/admin/domainsApi.js";
@@ -15,7 +15,8 @@ import { getBranding } from "../../../../../lib/branding/brandingApi.js";
15
15
  import { getEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
16
16
  import { listMailboxes } from "../../../../../lib/mail/mailApi.js";
17
17
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
18
- import Alert from "../../../../../lib/components/feedback/Alert.js";
18
+ import { LIST_PAGE_SIZE, listAllPages } from "../../../mail/listAllPages.js";
19
+ import Alert from "../ActionAlert.js";
19
20
  import Button from "../../../../../lib/components/buttons/Button.js";
20
21
  import FormField from "../../../../../lib/components/forms/FormField.js";
21
22
  import Modal from "../../../../../lib/components/overlays/Modal.js";
@@ -43,7 +44,7 @@ function isStepId(value) {
43
44
  return SETUP_STEPS.some((step) => step.id === value);
44
45
  }
45
46
  function errorMessage(err, fallback) {
46
- return err instanceof ApiRequestError ? err.message : fallback;
47
+ return actionErrorMessage(err, fallback);
47
48
  }
48
49
  /**
49
50
  * The first-run setup wizard. Each step saves as the administrator works through it; the wizard itself only records
@@ -203,9 +204,10 @@ function MailboxesStep({ userUid, domain }) {
203
204
  const [error, setError] = useState(null);
204
205
  const [formKey, setFormKey] = useState(0);
205
206
  useEffect(() => {
206
- // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
207
- listMailboxes({ limit: 100, scope: "admin" }, client)
208
- .then((list) =>
207
+ // The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own. All pages of them: the
208
+ // administrator's own mailbox may be past the first, and would then be offered to be created again.
209
+ listAllPages((page) => listMailboxes({ limit: LIST_PAGE_SIZE, page, scope: "admin" }, client))
210
+ .then(({ items: list }) =>
209
211
  // Keep any mailbox created here while the list was loading.
210
212
  setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]))
211
213
  .catch((err) => setError(`${errorMessage(err, "Could not load the existing mailboxes.")} Check the Mailboxes page before creating your own, so you don't create it twice.`))
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import { authApiFetch } from "../../../../lib/util/api.js";
6
+ import { resetNotifications } from "../../notifications/store.js";
6
7
  /**
7
8
  * The cross-tab sign-out channel - must stay identical to `SIGN_OUT_CHANNEL` in
8
9
  * `apps/shared/search/localIndexRpcClient.ts` (the channel `AppShell` listens on and `destroyAllLocalIndexes()`
@@ -69,6 +70,8 @@ async function logOutOfAuthServer(authServerUrl) {
69
70
  */
70
71
  export async function signOutOfConsole(authServerUrl) {
71
72
  markLocalIndexesForDeletion();
73
+ // The tab's notification history is the session's, and its storage outlives it.
74
+ resetNotifications();
72
75
  broadcastSignOut();
73
76
  await logOutOfAuthServer(authServerUrl);
74
77
  window.location.href = authServerUrl ?? "/";
@@ -4,7 +4,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useRef, useState } from "react";
7
- import { ApiRequestError } from "../../../../lib/util/api.js";
7
+ import { actionErrorMessage } from "./elevation.js";
8
8
  import Button from "../../../../lib/components/buttons/Button.js";
9
9
  /** Page size for the async-request lists (the server returns them newest first and caps `limit` at 500). */
10
10
  export const REQUEST_LIST_PAGE_SIZE = 50;
@@ -30,7 +30,7 @@ export function usePagedList(fetchPage, errorMessage) {
30
30
  const loaded = useRef(items);
31
31
  loaded.current = items;
32
32
  function describe(err) {
33
- return err instanceof ApiRequestError ? err.message : errorMessage;
33
+ return actionErrorMessage(err, errorMessage);
34
34
  }
35
35
  async function reload() {
36
36
  const current = ++generation.current;
@@ -25,7 +25,8 @@ export function linkFromInput(input) {
25
25
  if (/\s/.test(text)) {
26
26
  return undefined;
27
27
  }
28
- if (/^[a-z][a-z0-9+.-]*:/i.test(text)) {
28
+ // "example.com:8080/x" and "localhost:3000" name a host and a port, not a scheme.
29
+ if (/^[a-z][a-z0-9+.-]*:/i.test(text) && !/^(?:localhost|[^\s:/@]+\.[^\s:/@]+):\d{1,5}(?:[/?#]|$)/i.test(text)) {
29
30
  return safeDescriptionHref(text);
30
31
  }
31
32
  return safeDescriptionHref(/^[^\s@/]+@[^\s@/]+$/.test(text) ? `mailto:${text}` : `https://${text}`);
@@ -14,6 +14,8 @@ export interface EventDetailsProps {
14
14
  isOwnAddress?: (address: string) => boolean;
15
15
  calendarName?: string;
16
16
  calendarColor?: string;
17
+ /** Whether the reader may change this mailbox's events: `false` for one shared with them view-only, which gets no Modify, Delete or answer. Default `true`. */
18
+ canWrite?: boolean;
17
19
  onClose: () => void;
18
20
  /** Modify: switch to the edit form. */
19
21
  onModify: () => void;
@@ -30,4 +32,4 @@ export interface EventDetailsProps {
30
32
  * An event this reader may only see as a busy block (`occurrence.redacted`: a private or confidential event on a shared calendar) is drawn as just that -
31
33
  * its time and that it is busy - with nothing to change, delete or answer.
32
34
  */
33
- export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;
35
+ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, canWrite, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;