@rapidmx/web-client 0.30.0 → 0.31.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 (400) 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 +1 -1
  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/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -4,11 +4,11 @@ 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, useRef, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { listLeftoverMailboxes } from "../../../../../lib/admin/leftoverMailboxApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
11
10
  import EraseLeftoverDataDialog from "./EraseLeftoverDataDialog.js";
11
+ import { actionErrorMessage } from "../elevation.js";
12
12
  /** How many deleted mailboxes one request asks for. */
13
13
  const PAGE_SIZE = 50;
14
14
  /** How often the list is refreshed while an erasure is under way. */
@@ -45,7 +45,7 @@ export default function LeftoverMailboxesSection({ refreshIntervalMs = LEFTOVER_
45
45
  }
46
46
  catch (err) {
47
47
  if (current === generation.current) {
48
- setError(err instanceof ApiRequestError ? err.message : "Could not check for deleted mailboxes with remaining data.");
48
+ setError(actionErrorMessage(err, "Could not check for deleted mailboxes with remaining data."));
49
49
  }
50
50
  }
51
51
  }
@@ -61,7 +61,7 @@ export default function LeftoverMailboxesSection({ refreshIntervalMs = LEFTOVER_
61
61
  }
62
62
  catch (err) {
63
63
  if (current === generation.current) {
64
- setError(err instanceof ApiRequestError ? err.message : "Could not load more deleted mailboxes.");
64
+ setError(actionErrorMessage(err, "Could not load more deleted mailboxes."));
65
65
  }
66
66
  }
67
67
  finally {
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { updateMailbox } from "../../../../../lib/mail/mailApi.js";
9
8
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
10
- import Alert from "../../../../../lib/components/feedback/Alert.js";
9
+ import Alert from "../ActionAlert.js";
11
10
  import Button from "../../../../../lib/components/buttons/Button.js";
11
+ import { actionErrorMessage } from "../elevation.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";
13
13
  const SELECT_CLASS = "text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
14
14
  /**
@@ -51,7 +51,7 @@ export default function ResourceSettingsCard({ mailbox, onUpdate }) {
51
51
  setSaved(true);
52
52
  }
53
53
  catch (err) {
54
- setError(err instanceof ApiRequestError ? err.message : "Could not save resource settings.");
54
+ setError(actionErrorMessage(err, "Could not save resource settings."));
55
55
  }
56
56
  finally {
57
57
  setSaving(false);
@@ -4,12 +4,12 @@ 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 { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "../../../../../lib/mail/mailboxAccessApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
11
10
  import Modal from "../../../../../lib/components/overlays/Modal.js";
12
11
  import PrincipalPicker from "../../sharing/PrincipalPicker.js";
12
+ import { actionErrorMessage } from "../elevation.js";
13
13
  const ROLE_LABELS = { viewer: "Read only", manager: "Full access" };
14
14
  function roleLabel(member) {
15
15
  if (member.role === "viewer")
@@ -38,7 +38,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
38
38
  setError(null);
39
39
  listMailboxAccess(mailboxUid)
40
40
  .then(setMembers)
41
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load share settings."))
41
+ .catch((err) => setError(actionErrorMessage(err, "Could not load share settings.")))
42
42
  .finally(() => setLoading(false));
43
43
  }
44
44
  useEffect(reload, [mailboxUid]);
@@ -50,7 +50,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
50
50
  reload();
51
51
  }
52
52
  catch (err) {
53
- setError(err instanceof ApiRequestError ? err.message : "Could not grant access.");
53
+ setError(actionErrorMessage(err, "Could not grant access."));
54
54
  }
55
55
  finally {
56
56
  setSaving(false);
@@ -66,7 +66,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
66
66
  await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
67
67
  }
68
68
  catch (err) {
69
- failure = err instanceof ApiRequestError ? err.message : "Could not remove the old entry.";
69
+ failure = actionErrorMessage(err, "Could not remove the old entry.");
70
70
  }
71
71
  }
72
72
  reload();
@@ -86,7 +86,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
86
86
  reload();
87
87
  }
88
88
  catch (err) {
89
- setError(err instanceof ApiRequestError ? err.message : "Could not revoke access.");
89
+ setError(actionErrorMessage(err, "Could not revoke access."));
90
90
  }
91
91
  finally {
92
92
  setSaving(false);
@@ -4,11 +4,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
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";
8
7
  import { deleteBrandingIcon, deleteBrandingLogo, deleteBrandingStylesheet, updateBranding, uploadBrandingIcon, uploadBrandingLogo, uploadBrandingStylesheet, } from "../../../../../lib/branding/brandingApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
11
10
  import CopyButton from "../../../../../lib/components/buttons/CopyButton.js";
11
+ import { actionErrorMessage } from "../elevation.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";
13
13
  const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
14
14
  /** A header that uses both variables - the snippet the Branding page offers to copy. */
@@ -48,9 +48,19 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
48
48
  setIconUrlInput(updated.iconUrl ?? "");
49
49
  setStylesheetUrlInput(updated.stylesheetUrl ?? "");
50
50
  })
51
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not update branding."))
51
+ .catch((err) => setError(actionErrorMessage(err, "Could not update branding.")))
52
52
  .finally(() => setAssetBusy(null));
53
53
  }
54
+ /** Sets a branding asset to an address the administrator typed. Every visitor's browser loads it - the anonymous booking pages too - so it must be an
55
+ * encrypted (`https:`) address or one on this server (a path), not `http:`, `data:` or `javascript:`. */
56
+ function runUrl(name, field, value) {
57
+ const address = value.trim();
58
+ if (address !== "" && !/^https:\/\/[^\s/]/i.test(address) && !/^\/(?![/\\])/.test(address)) {
59
+ setError("Use an https:// address, or a path on this server, or upload a file instead.");
60
+ return;
61
+ }
62
+ void runAsset(name, () => updateBranding({ [field]: address }));
63
+ }
54
64
  function handleLogoFileChange(e) {
55
65
  const file = e.target.files?.[0];
56
66
  if (!file)
@@ -104,7 +114,7 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
104
114
  setSaved(true);
105
115
  }
106
116
  catch (err) {
107
- setError(err instanceof ApiRequestError ? err.message : "Could not save branding.");
117
+ setError(actionErrorMessage(err, "Could not save branding."));
108
118
  }
109
119
  finally {
110
120
  setSaving(false);
@@ -113,11 +123,11 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
113
123
  return (_jsxs("div", { className: "max-w-2xl", children: [!embedded && (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors. The header appears on the webmail and public pages, not in the admin console; the footer appears in both." })] })), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload logo", className: "hidden", onChange: handleLogoFileChange }), branding.logoUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "logo-delete", onClick: () => void runAsset("logo-delete", async () => {
114
124
  await deleteBrandingLogo();
115
125
  return { ...branding, logoUrl: undefined };
116
- }), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
126
+ }), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => runUrl("logo-url", "logoUrl", logoUrlInput), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
117
127
  await deleteBrandingIcon();
118
128
  return { ...branding, iconUrl: undefined };
119
- }), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
129
+ }), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => runUrl("icon-url", "iconUrl", iconUrlInput), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
120
130
  await deleteBrandingStylesheet();
121
131
  return { ...branding, stylesheetUrl: undefined };
122
- }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: ["A header replaces the app's own title bar and the icon at the top of the icon rail, and stays at the top of the window. Write", " ", _jsx("code", { children: "{USER_MENU}" }), " where the account menu (avatar, Settings, Sign Out) should go, and ", _jsx("code", { children: "{APP_TITLE}" }), " ", "where the name of the app on screen should show. Both work in the footer too. ", _jsx("code", { children: "{USER_MENU}" }), " is replaced once (the header's, else the footer's); without it the menu sits in a small cell at the right end of the header, so it is never lost. Put it where your styles don't hide it on a phone."] })] }), _jsxs("div", { className: "flex flex-col gap-1.5 text-sm", role: "group", "aria-label": "Header example", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-semibold", children: "Example header" }), _jsx(CopyButton, { value: HEADER_HTML_EXAMPLE, label: "Copy the example header HTML" })] }), _jsx("pre", { className: "overflow-x-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs text-text", children: HEADER_HTML_EXAMPLE })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: [_jsx("code", { children: "{APP_TITLE}" }), " works here as well, and ", _jsx("code", { children: "{USER_MENU}" }), " takes the account menu (opening upward) when the header has none."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
132
+ }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsx("span", { className: "text-xs text-text-muted", children: "Every visitor\u2019s browser loads it, and a stylesheet can read what the page shows: only use an address you control, or upload the file instead." }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => runUrl("stylesheet-url", "stylesheetUrl", stylesheetUrlInput), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: ["A header replaces the app's own title bar and the icon at the top of the icon rail, and stays at the top of the window. Write", " ", _jsx("code", { children: "{USER_MENU}" }), " where the account menu (avatar, Settings, Sign Out) should go, and ", _jsx("code", { children: "{APP_TITLE}" }), " ", "where the name of the app on screen should show. Both work in the footer too. ", _jsx("code", { children: "{USER_MENU}" }), " is replaced once (the header's, else the footer's); without it the menu sits in a small cell at the right end of the header, so it is never lost. Put it where your styles don't hide it on a phone."] })] }), _jsxs("div", { className: "flex flex-col gap-1.5 text-sm", role: "group", "aria-label": "Header example", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-semibold", children: "Example header" }), _jsx(CopyButton, { value: HEADER_HTML_EXAMPLE, label: "Copy the example header HTML" })] }), _jsx("pre", { className: "overflow-x-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs text-text", children: HEADER_HTML_EXAMPLE })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: [_jsx("code", { children: "{APP_TITLE}" }), " works here as well, and ", _jsx("code", { children: "{USER_MENU}" }), " takes the account menu (opening upward) when the header has none."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
123
133
  }
@@ -4,11 +4,11 @@ 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 { getDnsSetup, getDomain, verifyDomain } from "../../../../../lib/admin/domainsApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
11
10
  import CopyButton from "../../../../../lib/components/buttons/CopyButton.js";
11
+ import { actionErrorMessage } from "../elevation.js";
12
12
  const RECORD_TYPE_LABELS = {
13
13
  ownership: "Ownership (TXT)",
14
14
  mx: "MX",
@@ -68,7 +68,7 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
68
68
  setDnsSetup(checks);
69
69
  onLoaded?.(d);
70
70
  })
71
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this domain."))
71
+ .catch((err) => setError(actionErrorMessage(err, "Could not load this domain.")))
72
72
  .finally(() => setLoading(false));
73
73
  }
74
74
  useEffect(() => {
@@ -88,7 +88,7 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
88
88
  onLoaded?.(d);
89
89
  }
90
90
  catch (err) {
91
- setVerifyError(err instanceof ApiRequestError ? err.message : "Could not verify this domain.");
91
+ setVerifyError(actionErrorMessage(err, "Could not verify this domain."));
92
92
  }
93
93
  finally {
94
94
  setVerifying(false);
@@ -4,10 +4,11 @@ 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
7
  import { updateEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
10
+ import Modal from "../../../../../lib/components/overlays/Modal.js";
11
+ import { actionErrorMessage } from "../elevation.js";
11
12
  const SELECT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
12
13
  const TIERS = [
13
14
  { key: "encryptSameOrg", label: "Mail within this server", help: "Messages between mailboxes hosted on this server." },
@@ -37,8 +38,19 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
37
38
  const [savedValues, setSavedValues] = useState(policy);
38
39
  const dirty = TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]);
39
40
  useEffect(() => onDirtyChange?.(dirty), [dirty]);
40
- async function handleSubmit(e) {
41
+ // Tiers this save would newly set to "Never encrypt", which switches encryption off for everyone's mail of that kind: confirmed first.
42
+ const [confirmingProhibit, setConfirmingProhibit] = useState(null);
43
+ function handleSubmit(e) {
41
44
  e.preventDefault();
45
+ const newlyProhibited = TIERS.filter((tier) => values[tier.key] === "prohibited" && savedValues[tier.key] !== "prohibited");
46
+ if (newlyProhibited.length > 0) {
47
+ setConfirmingProhibit(newlyProhibited.map((tier) => tier.label));
48
+ return;
49
+ }
50
+ void save();
51
+ }
52
+ async function save() {
53
+ setConfirmingProhibit(null);
42
54
  setError(null);
43
55
  setSaved(false);
44
56
  setSaving(true);
@@ -49,7 +61,7 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
49
61
  setSaved(true);
50
62
  }
51
63
  catch (err) {
52
- setError(err instanceof ApiRequestError ? err.message : "Could not save the encryption policy.");
64
+ setError(actionErrorMessage(err, "Could not save the encryption policy."));
53
65
  }
54
66
  finally {
55
67
  setSaving(false);
@@ -58,5 +70,5 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
58
70
  return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" })) : (_jsx("h2", { className: "text-lg font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Encrypted messages can only be read by their senders and recipients - not by this server or its administrators. Choose how encryption applies to each kind of mail." }), 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: [TIERS.map((tier) => (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: tier.label }), _jsx("select", { "aria-label": tier.label, className: SELECT_CLASS, value: values[tier.key], onChange: (e) => {
59
71
  setSaved(false);
60
72
  setValues({ ...values, [tier.key]: e.target.value });
61
- }, children: STATES.map((state) => (_jsx("option", { value: state.value, children: state.label }, state.value))) }), _jsx("span", { className: "text-xs text-text-muted", children: tier.help })] }, tier.key))), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }));
73
+ }, children: STATES.map((state) => (_jsx("option", { value: state.value, children: state.label }, state.value))) }), _jsx("span", { className: "text-xs text-text-muted", children: tier.help })] }, tier.key))), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: confirmingProhibit !== null, onClose: () => setConfirmingProhibit(null), title: "Turn off encryption?", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Saving sets ", _jsx("strong", { children: confirmingProhibit?.join(", ") }), " to Never encrypt. Nobody on this server can then encrypt that mail end to end, whatever they have chosen for themselves, until an administrator allows it again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingProhibit(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void save(), children: "Save and turn off encryption" })] })] })] }));
62
74
  }
@@ -4,8 +4,8 @@ import { jsx as _jsx, Fragment as _Fragment } 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 Alert from "../../../../../lib/components/feedback/Alert.js";
7
+ import { actionErrorMessage } from "../elevation.js";
8
+ import Alert from "../ActionAlert.js";
9
9
  /** Loads a settings object, then renders its form - the loading and error states every settings page shares. */
10
10
  export default function LoadedSettingsForm({ load, loadErrorMessage, children }) {
11
11
  const [value, setValue] = useState(null);
@@ -13,7 +13,7 @@ export default function LoadedSettingsForm({ load, loadErrorMessage, children })
13
13
  useEffect(() => {
14
14
  load()
15
15
  .then(setValue)
16
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : loadErrorMessage));
16
+ .catch((err) => setError(actionErrorMessage(err, loadErrorMessage)));
17
17
  }, []);
18
18
  if (error) {
19
19
  return _jsx(Alert, { children: error });
@@ -4,17 +4,17 @@ 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, useRef, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
9
8
  import { leftoverConflictOf } from "../../../../../lib/admin/leftoverMailboxApi.js";
10
9
  import { createMailbox, listMailboxDomains, resolveMailboxOwner } from "../../../../../lib/mail/mailApi.js";
11
10
  import { describeTimeZone, deviceTimeZone, zoneClock } from "../../../../../lib/util/timeZone.js";
12
11
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
13
- import Alert from "../../../../../lib/components/feedback/Alert.js";
12
+ import Alert from "../ActionAlert.js";
14
13
  import Button from "../../../../../lib/components/buttons/Button.js";
15
14
  import FormField from "../../../../../lib/components/forms/FormField.js";
16
15
  import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
17
16
  import EraseLeftoverDataDialog from "../mailboxes/EraseLeftoverDataDialog.js";
17
+ import { actionErrorMessage } from "../elevation.js";
18
18
  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";
19
19
  const SELECT_CLASS = "text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
20
20
  /**
@@ -82,6 +82,11 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
82
82
  setError("A primary SMTP address is required.");
83
83
  return;
84
84
  }
85
+ // With the domain chosen beside it, a local part with an "@" would make `a@b.com@domain`, which the server refuses without saying why.
86
+ if (constrained && /[@@﹫]/.test(localPart)) {
87
+ setError("The part before the domain can't contain \"@\" - choose the domain from the list instead.");
88
+ return;
89
+ }
85
90
  if (!displayName.trim()) {
86
91
  setError("A display name is required.");
87
92
  return;
@@ -125,7 +130,7 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
125
130
  setLeftover({ address, conflict });
126
131
  }
127
132
  else {
128
- setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
133
+ setError(actionErrorMessage(err, "Could not create the mailbox."));
129
134
  }
130
135
  }
131
136
  finally {
@@ -4,10 +4,10 @@ import { jsxs as _jsxs, jsx as _jsx, 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 { updateMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
10
+ import { actionErrorMessage } from "../elevation.js";
11
11
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
12
12
  const GB = 1000000000;
13
13
  /** Unrounded, so a quota that isn't a whole number of hundredths of a GB (say 4 MB) is shown as it is. */
@@ -80,7 +80,7 @@ export default function MailboxPolicyForm({ policy, onChange, onDirtyChange, emb
80
80
  setSaved(true);
81
81
  }
82
82
  catch (err) {
83
- setError(err instanceof ApiRequestError ? err.message : "Could not save the mailbox policy.");
83
+ setError(actionErrorMessage(err, "Could not save the mailbox policy."));
84
84
  }
85
85
  finally {
86
86
  setSaving(false);
@@ -4,13 +4,12 @@ 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 { 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";
7
+ import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, isSecretSetting, isSecretSettingKey, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, retryPluginPurge, searchPlugins, updatePlugin, } from "../../../../../lib/admin/pluginsApi.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
11
10
  import Modal from "../../../../../lib/components/overlays/Modal.js";
12
11
  import { notify } from "../../../notifications/store.js";
13
- import { isElevationRequired } from "../elevation.js";
12
+ import { actionErrorMessage, isElevationMessage, isElevationRequired } from "../elevation.js";
14
13
  import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
15
14
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
16
15
  /** How often status is refreshed while server copies are still applying a change. */
@@ -20,7 +19,7 @@ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
20
19
  /** The longest wait between retries while no status has ever been read. */
21
20
  const MAX_STATUS_RETRY_MS = 60 * 1000;
22
21
  function errorMessage(err, fallback) {
23
- return err instanceof ApiRequestError ? err.message : fallback;
22
+ return actionErrorMessage(err, fallback);
24
23
  }
25
24
  /** Runs `action`, resolving why it failed, or `null`. */
26
25
  async function attempt(action, failure) {
@@ -126,6 +125,8 @@ export default function PluginsManager({ embedded = false } = {}) {
126
125
  const allowPrereleaseRef = useRef(allowPrerelease);
127
126
  /** Identifies the latest update check, so a slower earlier one can't replace what it found. */
128
127
  const updatesRequest = useRef(0);
128
+ // Set while a bulk action runs, which looks for updates once at its end rather than after each plugin it changes.
129
+ const bulkRunning = useRef(false);
129
130
  /** The plugins ticked for a bulk action. */
130
131
  const [selected, setSelected] = useState(new Set());
131
132
  /** Set while a bulk action runs. */
@@ -279,7 +280,9 @@ export default function PluginsManager({ embedded = false } = {}) {
279
280
  return [...next].sort((a, b) => a.name.localeCompare(b.name));
280
281
  });
281
282
  watchRollout();
282
- void refreshUpdates();
283
+ if (!bulkRunning.current) {
284
+ void refreshUpdates();
285
+ }
283
286
  }
284
287
  /** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
285
288
  * shows why it failed. */
@@ -435,6 +438,7 @@ export default function PluginsManager({ embedded = false } = {}) {
435
438
  setError(null);
436
439
  setRetryEnableUid(null);
437
440
  setBulkFailure(null);
441
+ bulkRunning.current = true;
438
442
  await track((async () => {
439
443
  for (const [index, plugin] of ordered.entries()) {
440
444
  setBulk({ kind, done: index, total: ordered.length, current: plugin.manifest.displayName });
@@ -448,7 +452,7 @@ export default function PluginsManager({ embedded = false } = {}) {
448
452
  }
449
453
  failed.add(plugin.uid);
450
454
  reasons.push(`${plugin.manifest.displayName}: ${problem}`);
451
- if (problem === ELEVATION_MESSAGE) {
455
+ if (isElevationMessage(problem)) {
452
456
  for (const untried of ordered.slice(index + 1)) {
453
457
  failed.add(untried.uid);
454
458
  reasons.push(`${untried.manifest.displayName}: Not attempted.`);
@@ -457,12 +461,16 @@ export default function PluginsManager({ embedded = false } = {}) {
457
461
  }
458
462
  }
459
463
  })());
464
+ bulkRunning.current = false;
460
465
  setBulk(null);
461
466
  setSelected(failed);
462
467
  const succeeded = ordered.length - failed.size;
463
468
  if (kind === "uninstall" && succeeded > 0) {
464
469
  watchRollout();
465
470
  }
471
+ if (kind !== "uninstall" && succeeded > 0) {
472
+ void refreshUpdates();
473
+ }
466
474
  if (succeeded > 0) {
467
475
  notify({
468
476
  kind: "success",
@@ -848,6 +856,10 @@ function hostSuggestion(definition, saved, configured) {
848
856
  * deployment configures; else the setting's own default - or, for a required select with none of those, its first
849
857
  * option. Kept as a string for text inputs. */
850
858
  function initialValue(definition, saved, configured) {
859
+ if (isSecretSetting(saved)) {
860
+ // 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.
861
+ return "";
862
+ }
851
863
  if (configured && isUnsaved(saved)) {
852
864
  return configuredValue(definition, configured);
853
865
  }
@@ -929,21 +941,23 @@ function SettingsModal({ plugin, onClose, onSaved }) {
929
941
  setBusy(false);
930
942
  }
931
943
  }
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
944
+ 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
945
  ? "Every setting is back to what this deployment's configuration or the plugin's default gives. Save to apply that."
934
946
  : "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
947
  }
936
- function SettingField({ definition, value, configured, onChange, }) {
948
+ function SettingField({ definition, value, configured, secretSaved, onChange, }) {
937
949
  // What applies without a saved value; one saved here replaces it, and Reset goes back to it.
938
950
  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;
951
+ // 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
952
+ // browser's form fill.
953
+ const secretField = definition.type === "string" && (isSecretSettingKey(definition.key) || secretSaved || !!configured?.secret);
954
+ const placeholder = secretSaved ? "Saved - type a new value to replace it" : configured?.secret ? "Set by this deployment" : undefined;
941
955
  const help = (_jsxs(_Fragment, { children: [definition.help && _jsx("span", { className: "text-xs text-text-muted", children: definition.help }), note] }));
942
956
  if (definition.type === "boolean") {
943
957
  // Laid out like the admin policy forms' checkboxes: the help sits under the label, beside the box.
944
958
  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
959
  }
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] }));
960
+ 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
961
  }
948
962
  /** The word typed to confirm deleting the data of several plugins at once, in place of a plugin's name. */
949
963
  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."