@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
@@ -3,7 +3,6 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import {
8
7
  listMailboxAccess,
9
8
  MailboxAccessMember,
@@ -11,10 +10,11 @@ import {
11
10
  removeMailboxAccess,
12
11
  setMailboxAccess,
13
12
  } from "../../../../../lib/mail/mailboxAccessApi.js";
14
- import Alert from "../../../../../lib/components/feedback/Alert.js";
13
+ import Alert from "../ActionAlert.js";
15
14
  import Button from "../../../../../lib/components/buttons/Button.js";
16
15
  import Modal from "../../../../../lib/components/overlays/Modal.js";
17
16
  import PrincipalPicker from "../../sharing/PrincipalPicker.js";
17
+ import { actionErrorMessage } from "../elevation.js";
18
18
 
19
19
  const ROLE_LABELS: Record<MailboxAccessRole, string> = { viewer: "Read only", manager: "Full access" };
20
20
 
@@ -54,7 +54,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
54
54
  setError(null);
55
55
  listMailboxAccess(mailboxUid)
56
56
  .then(setMembers)
57
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load share settings."))
57
+ .catch((err) => setError(actionErrorMessage(err, "Could not load share settings.")))
58
58
  .finally(() => setLoading(false));
59
59
  }
60
60
 
@@ -67,7 +67,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
67
67
  await setMailboxAccess(mailboxUid, userUid, "manager");
68
68
  reload();
69
69
  } catch (err) {
70
- setError(err instanceof ApiRequestError ? err.message : "Could not grant access.");
70
+ setError(actionErrorMessage(err, "Could not grant access."));
71
71
  } finally {
72
72
  setSaving(false);
73
73
  }
@@ -82,7 +82,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
82
82
  try {
83
83
  await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
84
84
  } catch (err) {
85
- failure = err instanceof ApiRequestError ? err.message : "Could not remove the old entry.";
85
+ failure = actionErrorMessage(err, "Could not remove the old entry.");
86
86
  }
87
87
  }
88
88
  reload();
@@ -102,7 +102,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
102
102
  await removeMailboxAccess(mailboxUid, userOrRoleId);
103
103
  reload();
104
104
  } catch (err) {
105
- setError(err instanceof ApiRequestError ? err.message : "Could not revoke access.");
105
+ setError(actionErrorMessage(err, "Could not revoke access."));
106
106
  } finally {
107
107
  setSaving(false);
108
108
  }
@@ -3,7 +3,6 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { ChangeEvent, FormEvent, useRef, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import {
8
7
  Branding,
9
8
  deleteBrandingIcon,
@@ -14,9 +13,10 @@ import {
14
13
  uploadBrandingLogo,
15
14
  uploadBrandingStylesheet,
16
15
  } from "../../../../../lib/branding/brandingApi.js";
17
- import Alert from "../../../../../lib/components/feedback/Alert.js";
16
+ import Alert from "../ActionAlert.js";
18
17
  import Button from "../../../../../lib/components/buttons/Button.js";
19
18
  import CopyButton from "../../../../../lib/components/buttons/CopyButton.js";
19
+ import { actionErrorMessage } from "../elevation.js";
20
20
 
21
21
  const INPUT_CLASS =
22
22
  "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -74,10 +74,21 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
74
74
  setIconUrlInput(updated.iconUrl ?? "");
75
75
  setStylesheetUrlInput(updated.stylesheetUrl ?? "");
76
76
  })
77
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not update branding."))
77
+ .catch((err) => setError(actionErrorMessage(err, "Could not update branding.")))
78
78
  .finally(() => setAssetBusy(null));
79
79
  }
80
80
 
81
+ /** 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
82
+ * encrypted (`https:`) address or one on this server (a path), not `http:`, `data:` or `javascript:`. */
83
+ function runUrl(name: string, field: "logoUrl" | "iconUrl" | "stylesheetUrl", value: string) {
84
+ const address = value.trim();
85
+ if (address !== "" && !/^https:\/\/[^\s/]/i.test(address) && !/^\/(?![/\\])/.test(address)) {
86
+ setError("Use an https:// address, or a path on this server, or upload a file instead.");
87
+ return;
88
+ }
89
+ void runAsset(name, () => updateBranding({ [field]: address }));
90
+ }
91
+
81
92
  function handleLogoFileChange(e: ChangeEvent<HTMLInputElement>) {
82
93
  const file = e.target.files?.[0];
83
94
  if (!file) return;
@@ -125,7 +136,7 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
125
136
  onChange(updated);
126
137
  setSaved(true);
127
138
  } catch (err) {
128
- setError(err instanceof ApiRequestError ? err.message : "Could not save branding.");
139
+ setError(actionErrorMessage(err, "Could not save branding."));
129
140
  } finally {
130
141
  setSaving(false);
131
142
  }
@@ -208,7 +219,7 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
208
219
  className="!w-auto"
209
220
  disabled={assetBusy !== null || logoUrlInput === (branding.logoUrl ?? "")}
210
221
  loading={assetBusy === "logo-url"}
211
- onClick={() => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput }))}
222
+ onClick={() => runUrl("logo-url", "logoUrl", logoUrlInput)}
212
223
  >
213
224
  Set
214
225
  </Button>
@@ -280,7 +291,7 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
280
291
  className="!w-auto"
281
292
  disabled={assetBusy !== null || iconUrlInput === (branding.iconUrl ?? "")}
282
293
  loading={assetBusy === "icon-url"}
283
- onClick={() => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput }))}
294
+ onClick={() => runUrl("icon-url", "iconUrl", iconUrlInput)}
284
295
  >
285
296
  Set
286
297
  </Button>
@@ -331,6 +342,10 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
331
342
  </div>
332
343
  <label className="flex flex-col gap-1.5 text-sm">
333
344
  <span className="font-semibold">Or use an external stylesheet URL</span>
345
+ <span className="text-xs text-text-muted">
346
+ Every visitor&rsquo;s browser loads it, and a stylesheet can read what the page shows: only use an address you control, or
347
+ upload the file instead.
348
+ </span>
334
349
  <div className="flex gap-2">
335
350
  <input
336
351
  aria-label="Stylesheet URL"
@@ -344,9 +359,7 @@ export default function BrandingForm({ branding, onChange, embedded = false }: B
344
359
  className="!w-auto"
345
360
  disabled={assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? "")}
346
361
  loading={assetBusy === "stylesheet-url"}
347
- onClick={() =>
348
- void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput }))
349
- }
362
+ onClick={() => runUrl("stylesheet-url", "stylesheetUrl", stylesheetUrlInput)}
350
363
  >
351
364
  Set
352
365
  </Button>
@@ -3,11 +3,11 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import { DnsRecordCheck, Domain, getDnsSetup, getDomain, verifyDomain } from "../../../../../lib/admin/domainsApi.js";
8
- import Alert from "../../../../../lib/components/feedback/Alert.js";
7
+ import Alert from "../ActionAlert.js";
9
8
  import Button from "../../../../../lib/components/buttons/Button.js";
10
9
  import CopyButton from "../../../../../lib/components/buttons/CopyButton.js";
10
+ import { actionErrorMessage } from "../elevation.js";
11
11
 
12
12
  const RECORD_TYPE_LABELS: Record<DnsRecordCheck["type"], string> = {
13
13
  ownership: "Ownership (TXT)",
@@ -88,7 +88,7 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
88
88
  setDnsSetup(checks);
89
89
  onLoaded?.(d);
90
90
  })
91
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this domain."))
91
+ .catch((err) => setError(actionErrorMessage(err, "Could not load this domain.")))
92
92
  .finally(() => setLoading(false));
93
93
  }
94
94
 
@@ -109,7 +109,7 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
109
109
  setDnsSetup(checks);
110
110
  onLoaded?.(d);
111
111
  } catch (err) {
112
- setVerifyError(err instanceof ApiRequestError ? err.message : "Could not verify this domain.");
112
+ setVerifyError(actionErrorMessage(err, "Could not verify this domain."));
113
113
  } finally {
114
114
  setVerifying(false);
115
115
  }
@@ -3,10 +3,11 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { FormEvent, useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import { EncryptionPolicy, PolicyState, updateEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
8
- import Alert from "../../../../../lib/components/feedback/Alert.js";
7
+ import Alert from "../ActionAlert.js";
9
8
  import Button from "../../../../../lib/components/buttons/Button.js";
9
+ import Modal from "../../../../../lib/components/overlays/Modal.js";
10
+ import { actionErrorMessage } from "../elevation.js";
10
11
 
11
12
  const SELECT_CLASS =
12
13
  "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -54,8 +55,21 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
54
55
  const dirty: boolean = TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]);
55
56
  useEffect(() => onDirtyChange?.(dirty), [dirty]);
56
57
 
57
- async function handleSubmit(e: FormEvent) {
58
+ // Tiers this save would newly set to "Never encrypt", which switches encryption off for everyone's mail of that kind: confirmed first.
59
+ const [confirmingProhibit, setConfirmingProhibit] = useState<string[] | null>(null);
60
+
61
+ function handleSubmit(e: FormEvent) {
58
62
  e.preventDefault();
63
+ const newlyProhibited = TIERS.filter((tier) => values[tier.key] === "prohibited" && savedValues[tier.key] !== "prohibited");
64
+ if (newlyProhibited.length > 0) {
65
+ setConfirmingProhibit(newlyProhibited.map((tier) => tier.label));
66
+ return;
67
+ }
68
+ void save();
69
+ }
70
+
71
+ async function save() {
72
+ setConfirmingProhibit(null);
59
73
  setError(null);
60
74
  setSaved(false);
61
75
  setSaving(true);
@@ -65,7 +79,7 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
65
79
  setSavedValues(values);
66
80
  setSaved(true);
67
81
  } catch (err) {
68
- setError(err instanceof ApiRequestError ? err.message : "Could not save the encryption policy.");
82
+ setError(actionErrorMessage(err, "Could not save the encryption policy."));
69
83
  } finally {
70
84
  setSaving(false);
71
85
  }
@@ -114,6 +128,21 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
114
128
  </Button>
115
129
  </div>
116
130
  </form>
131
+
132
+ <Modal open={confirmingProhibit !== null} onClose={() => setConfirmingProhibit(null)} title="Turn off encryption?">
133
+ <p className="text-sm mb-3">
134
+ Saving sets <strong>{confirmingProhibit?.join(", ")}</strong> to Never encrypt. Nobody on this server can then encrypt
135
+ that mail end to end, whatever they have chosen for themselves, until an administrator allows it again.
136
+ </p>
137
+ <div className="flex gap-3 justify-end">
138
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingProhibit(null)}>
139
+ Cancel
140
+ </Button>
141
+ <Button type="button" className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger" onClick={() => void save()}>
142
+ Save and turn off encryption
143
+ </Button>
144
+ </div>
145
+ </Modal>
117
146
  </div>
118
147
  );
119
148
  }
@@ -3,8 +3,8 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { ReactNode, useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
- import Alert from "../../../../../lib/components/feedback/Alert.js";
6
+ import { actionErrorMessage } from "../elevation.js";
7
+ import Alert from "../ActionAlert.js";
8
8
 
9
9
  export interface LoadedSettingsFormProps<T> {
10
10
  load: () => Promise<T>;
@@ -21,7 +21,7 @@ export default function LoadedSettingsForm<T>({ load, loadErrorMessage, children
21
21
  useEffect(() => {
22
22
  load()
23
23
  .then(setValue)
24
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : loadErrorMessage));
24
+ .catch((err) => setError(actionErrorMessage(err, loadErrorMessage)));
25
25
  }, []);
26
26
 
27
27
  if (error) {
@@ -3,17 +3,17 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { FormEvent, useEffect, useRef, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
8
7
  import { LeftoverConflict, leftoverConflictOf } from "../../../../../lib/admin/leftoverMailboxApi.js";
9
8
  import { createMailbox, listMailboxDomains, Mailbox, resolveMailboxOwner, ResolvedPrincipal } from "../../../../../lib/mail/mailApi.js";
10
9
  import { describeTimeZone, deviceTimeZone, zoneClock } from "../../../../../lib/util/timeZone.js";
11
10
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
12
- import Alert from "../../../../../lib/components/feedback/Alert.js";
11
+ import Alert from "../ActionAlert.js";
13
12
  import Button from "../../../../../lib/components/buttons/Button.js";
14
13
  import FormField from "../../../../../lib/components/forms/FormField.js";
15
14
  import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
16
15
  import EraseLeftoverDataDialog from "../mailboxes/EraseLeftoverDataDialog.js";
16
+ import { actionErrorMessage } from "../elevation.js";
17
17
 
18
18
  const INPUT_CLASS =
19
19
  "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -98,6 +98,11 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
98
98
  setError("A primary SMTP address is required.");
99
99
  return;
100
100
  }
101
+ // With the domain chosen beside it, a local part with an "@" would make `a@b.com@domain`, which the server refuses without saying why.
102
+ if (constrained && /[@@﹫]/.test(localPart)) {
103
+ setError("The part before the domain can't contain \"@\" - choose the domain from the list instead.");
104
+ return;
105
+ }
101
106
  if (!displayName.trim()) {
102
107
  setError("A display name is required.");
103
108
  return;
@@ -143,7 +148,7 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
143
148
  // Not a plain failure: the address is taken by what a deleted mailbox left, which can be erased right here.
144
149
  setLeftover({ address, conflict });
145
150
  } else {
146
- setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
151
+ setError(actionErrorMessage(err, "Could not create the mailbox."));
147
152
  }
148
153
  } finally {
149
154
  setSaving(false);
@@ -3,10 +3,10 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { FormEvent, useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import { MailboxPolicy, updateMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
8
- import Alert from "../../../../../lib/components/feedback/Alert.js";
7
+ import Alert from "../ActionAlert.js";
9
8
  import Button from "../../../../../lib/components/buttons/Button.js";
9
+ import { actionErrorMessage } from "../elevation.js";
10
10
 
11
11
  const INPUT_CLASS =
12
12
  "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
@@ -109,7 +109,7 @@ export default function MailboxPolicyForm({ policy, onChange, onDirtyChange, emb
109
109
  setBaseline(current);
110
110
  setSaved(true);
111
111
  } catch (err) {
112
- setError(err instanceof ApiRequestError ? err.message : "Could not save the mailbox policy.");
112
+ setError(actionErrorMessage(err, "Could not save the mailbox policy."));
113
113
  } finally {
114
114
  setSaving(false);
115
115
  }
@@ -3,12 +3,13 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { FormEvent, useCallback, useEffect, useId, useRef, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
6
  import {
8
7
  addPlugin,
9
8
  expectedPlanOf,
10
9
  getPluginStatus,
11
10
  getPluginUpdates,
11
+ isSecretSetting,
12
+ isSecretSettingKey,
12
13
  listPluginNamespaces,
13
14
  listPlugins,
14
15
  lookupPluginPackage,
@@ -21,6 +22,7 @@ import {
21
22
  PluginPurgeInfo,
22
23
  PluginPurgeState,
23
24
  PluginRegistryLookup,
25
+ PluginSavedSetting,
24
26
  PluginSearchResult,
25
27
  PluginSettingDefinition,
26
28
  PluginSettingValue,
@@ -32,11 +34,11 @@ import {
32
34
  searchPlugins,
33
35
  updatePlugin,
34
36
  } from "../../../../../lib/admin/pluginsApi.js";
35
- import Alert from "../../../../../lib/components/feedback/Alert.js";
37
+ import Alert from "../ActionAlert.js";
36
38
  import Button from "../../../../../lib/components/buttons/Button.js";
37
39
  import Modal from "../../../../../lib/components/overlays/Modal.js";
38
40
  import { notify } from "../../../notifications/store.js";
39
- import { isElevationRequired } from "../elevation.js";
41
+ import { actionErrorMessage, isElevationMessage, isElevationRequired } from "../elevation.js";
40
42
  import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
41
43
 
42
44
  const INPUT_CLASS =
@@ -52,7 +54,7 @@ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
52
54
  const MAX_STATUS_RETRY_MS = 60 * 1000;
53
55
 
54
56
  function errorMessage(err: unknown, fallback: string): string {
55
- return err instanceof ApiRequestError ? err.message : fallback;
57
+ return actionErrorMessage(err, fallback);
56
58
  }
57
59
 
58
60
  /** Runs `action`, resolving why it failed, or `null`. */
@@ -212,6 +214,8 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
212
214
  const allowPrereleaseRef = useRef(allowPrerelease);
213
215
  /** Identifies the latest update check, so a slower earlier one can't replace what it found. */
214
216
  const updatesRequest = useRef(0);
217
+ // Set while a bulk action runs, which looks for updates once at its end rather than after each plugin it changes.
218
+ const bulkRunning = useRef(false);
215
219
  /** The plugins ticked for a bulk action. */
216
220
  const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());
217
221
  /** Set while a bulk action runs. */
@@ -378,7 +382,9 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
378
382
  return [...next].sort((a, b) => a.name.localeCompare(b.name));
379
383
  });
380
384
  watchRollout();
381
- void refreshUpdates();
385
+ if (!bulkRunning.current) {
386
+ void refreshUpdates();
387
+ }
382
388
  }
383
389
 
384
390
  /** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
@@ -579,6 +585,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
579
585
  setError(null);
580
586
  setRetryEnableUid(null);
581
587
  setBulkFailure(null);
588
+ bulkRunning.current = true;
582
589
  await track(
583
590
  (async () => {
584
591
  for (const [index, plugin] of ordered.entries()) {
@@ -594,7 +601,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
594
601
  }
595
602
  failed.add(plugin.uid);
596
603
  reasons.push(`${plugin.manifest.displayName}: ${problem}`);
597
- if (problem === ELEVATION_MESSAGE) {
604
+ if (isElevationMessage(problem)) {
598
605
  for (const untried of ordered.slice(index + 1)) {
599
606
  failed.add(untried.uid);
600
607
  reasons.push(`${untried.manifest.displayName}: Not attempted.`);
@@ -604,12 +611,16 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
604
611
  }
605
612
  })(),
606
613
  );
614
+ bulkRunning.current = false;
607
615
  setBulk(null);
608
616
  setSelected(failed);
609
617
  const succeeded: number = ordered.length - failed.size;
610
618
  if (kind === "uninstall" && succeeded > 0) {
611
619
  watchRollout();
612
620
  }
621
+ if (kind !== "uninstall" && succeeded > 0) {
622
+ void refreshUpdates();
623
+ }
613
624
  if (succeeded > 0) {
614
625
  notify({
615
626
  kind: "success",
@@ -1675,7 +1686,7 @@ function ConfirmDependenciesModal({
1675
1686
  }
1676
1687
 
1677
1688
  /** Whether a required select has no value to fall back on, so the form has to pick (and save) one. */
1678
- function needsSelection(definition: PluginSettingDefinition, saved: PluginSettingValue | undefined): boolean {
1689
+ function needsSelection(definition: PluginSettingDefinition, saved: PluginSavedSetting | undefined): boolean {
1679
1690
  return (
1680
1691
  definition.type === "select" &&
1681
1692
  !!definition.required &&
@@ -1693,7 +1704,7 @@ const HOST_PLACEHOLDER = "<host>";
1693
1704
  * saved yet - nothing, an empty value, or the placeholder itself, which an older server stores when it installs the
1694
1705
  * plugin. It's the default with this console's host filled in, and is only stored once the form is saved.
1695
1706
  */
1696
- function suggestedHost(definition: PluginSettingDefinition, saved: PluginSettingValue | undefined): string | undefined {
1707
+ function suggestedHost(definition: PluginSettingDefinition, saved: PluginSavedSetting | undefined): string | undefined {
1697
1708
  if (typeof definition.default !== "string" || !definition.default.includes(HOST_PLACEHOLDER) || typeof window === "undefined") {
1698
1709
  return undefined;
1699
1710
  }
@@ -1710,12 +1721,12 @@ function configuredValue(definition: PluginSettingDefinition, configured: Plugin
1710
1721
  }
1711
1722
 
1712
1723
  /** Whether a saved value is absent - nothing, or the empty value a plugin's own "" default is saved as. */
1713
- function isUnsaved(saved: PluginSettingValue | undefined): boolean {
1724
+ function isUnsaved(saved: PluginSavedSetting | undefined): boolean {
1714
1725
  return saved === undefined || saved === "";
1715
1726
  }
1716
1727
 
1717
1728
  /** The suggestion for the host, unless the deployment configures the setting, which is offered instead. */
1718
- function hostSuggestion(definition: PluginSettingDefinition, saved: PluginSettingValue | undefined, configured: PluginConfiguredSetting | undefined): string | undefined {
1729
+ function hostSuggestion(definition: PluginSettingDefinition, saved: PluginSavedSetting | undefined, configured: PluginConfiguredSetting | undefined): string | undefined {
1719
1730
  return configured ? undefined : suggestedHost(definition, saved);
1720
1731
  }
1721
1732
 
@@ -1724,9 +1735,13 @@ function hostSuggestion(definition: PluginSettingDefinition, saved: PluginSettin
1724
1735
  * option. Kept as a string for text inputs. */
1725
1736
  function initialValue(
1726
1737
  definition: PluginSettingDefinition,
1727
- saved: PluginSettingValue | undefined,
1738
+ saved: PluginSavedSetting | undefined,
1728
1739
  configured?: PluginConfiguredSetting,
1729
1740
  ): PluginSettingValue | "" {
1741
+ if (isSecretSetting(saved)) {
1742
+ // 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.
1743
+ return "";
1744
+ }
1730
1745
  if (configured && isUnsaved(saved)) {
1731
1746
  return configuredValue(definition, configured);
1732
1747
  }
@@ -1768,12 +1783,12 @@ function SettingsModal({ plugin, onClose, onSaved }: { plugin: Plugin; onClose:
1768
1783
  // A required select with nothing to fall back on, and a setting offering this console's host, are sent as the
1769
1784
  // form shows them. What the deployment configures is never saved as a copy, which would outrank it when it changes
1770
1785
  // - a field that shows it is sent as nothing.
1771
- const settings: Record<string, PluginSettingValue | null> = {};
1786
+ const settings: Record<string, PluginSavedSetting | null> = {};
1772
1787
  let changed = false;
1773
1788
  for (const definition of definitions) {
1774
1789
  const key: string = definition.key;
1775
1790
  const configured: PluginConfiguredSetting | undefined = plugin.configured?.[key];
1776
- const saved: PluginSettingValue | undefined = plugin.settings[key];
1791
+ const saved: PluginSavedSetting | undefined = plugin.settings[key];
1777
1792
  const value = values[key];
1778
1793
  if (resetting && value === initialValue(definition, undefined, configured)) {
1779
1794
  // What a fresh install saves: only a default naming this console's host has to be, for the plugin to work.
@@ -1820,6 +1835,7 @@ function SettingsModal({ plugin, onClose, onSaved }: { plugin: Plugin; onClose:
1820
1835
  definition={definition}
1821
1836
  value={values[definition.key]}
1822
1837
  configured={plugin.configured?.[definition.key]}
1838
+ secretSaved={!resetting && isSecretSetting(plugin.settings[definition.key])}
1823
1839
  onChange={(value) => setValues((prev) => ({ ...prev, [definition.key]: value }))}
1824
1840
  />
1825
1841
  ))}
@@ -1852,11 +1868,14 @@ function SettingField({
1852
1868
  definition,
1853
1869
  value,
1854
1870
  configured,
1871
+ secretSaved,
1855
1872
  onChange,
1856
1873
  }: {
1857
1874
  definition: PluginSettingDefinition;
1858
1875
  value: PluginSettingValue | "";
1859
1876
  configured?: PluginConfiguredSetting;
1877
+ /** A secret is saved: the server never sends it, so the field is write-only - typing replaces it, leaving it empty keeps it. */
1878
+ secretSaved: boolean;
1860
1879
  onChange: (value: PluginSettingValue | "") => void;
1861
1880
  }) {
1862
1881
  // What applies without a saved value; one saved here replaces it, and Reset goes back to it.
@@ -1865,8 +1884,10 @@ function SettingField({
1865
1884
  This deployment&apos;s configuration provides {configured.secret ? "a value" : "the value shown"} unless one is saved here.
1866
1885
  </span>
1867
1886
  );
1868
- // A secret's value never reaches the browser, so the field only says that it is set.
1869
- const placeholder: string | undefined = configured?.secret ? "Set by this deployment" : undefined;
1887
+ // 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
1888
+ // browser's form fill.
1889
+ const secretField: boolean = definition.type === "string" && (isSecretSettingKey(definition.key) || secretSaved || !!configured?.secret);
1890
+ const placeholder: string | undefined = secretSaved ? "Saved - type a new value to replace it" : configured?.secret ? "Set by this deployment" : undefined;
1870
1891
  const help = (
1871
1892
  <>
1872
1893
  {definition.help && <span className="text-xs text-text-muted">{definition.help}</span>}
@@ -1901,10 +1922,11 @@ function SettingField({
1901
1922
  <input
1902
1923
  aria-label={definition.label}
1903
1924
  className={INPUT_CLASS}
1904
- type={definition.type === "number" ? "number" : "text"}
1925
+ type={definition.type === "number" ? "number" : secretField ? "password" : "text"}
1926
+ autoComplete={secretField ? "new-password" : undefined}
1905
1927
  min={definition.min}
1906
1928
  max={definition.max}
1907
- required={definition.required && !configured}
1929
+ required={definition.required && !configured && !secretSaved}
1908
1930
  placeholder={placeholder}
1909
1931
  value={String(value)}
1910
1932
  onChange={(e) => onChange(e.target.value)}
@@ -3,9 +3,15 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React, { FormEvent, useEffect, useState } from "react";
6
- import { ApiRequestError } from "../../../../../lib/util/api.js";
7
- import { MIN_AUDIT_LOG_RETENTION_DAYS, RetentionPolicy, RetentionPolicyUpdate, updateRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
8
- import Alert from "../../../../../lib/components/feedback/Alert.js";
6
+ import { actionErrorMessage } from "../elevation.js";
7
+ import {
8
+ MIN_AUDIT_LOG_RETENTION_DAYS,
9
+ MIN_MESSAGE_RETENTION_DAYS,
10
+ RetentionPolicy,
11
+ RetentionPolicyUpdate,
12
+ updateRetentionPolicy,
13
+ } from "../../../../../lib/admin/retentionPolicyApi.js";
14
+ import Alert from "../ActionAlert.js";
9
15
  import Button from "../../../../../lib/components/buttons/Button.js";
10
16
  import Modal from "../../../../../lib/components/overlays/Modal.js";
11
17
 
@@ -49,14 +55,21 @@ export default function RetentionPolicyForm({
49
55
  const [savedMessageDays, setSavedMessageDays] = useState(policy.messageRetentionDays);
50
56
  const [savedAuditDays, setSavedAuditDays] = useState(policy.auditLogRetentionDays);
51
57
  const [pendingPatch, setPendingPatch] = useState<RetentionPolicyUpdate | null>(null);
58
+ // 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.
59
+ const messageChanged = messageRetentionDays !== (savedMessageDays?.toString() ?? "");
60
+ const auditChanged = auditLogRetentionDays !== (savedAuditDays?.toString() ?? "");
52
61
 
53
62
  function handleSubmit(e: FormEvent) {
54
63
  e.preventDefault();
55
- // A blank field is sent as `null`, which clears a configured age back to no automatic purge.
56
- const patch: RetentionPolicyUpdate = {
57
- messageRetentionDays: messageRetentionDays.trim() === "" ? null : Number(messageRetentionDays),
58
- auditLogRetentionDays: auditLogRetentionDays.trim() === "" ? null : Number(auditLogRetentionDays),
59
- };
64
+ // 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
65
+ // it is would be refused by a floor that came in after it was stored, and so keep the other from being saved.
66
+ const patch: RetentionPolicyUpdate = {};
67
+ if (messageChanged) {
68
+ patch.messageRetentionDays = messageRetentionDays.trim() === "" ? null : Number(messageRetentionDays);
69
+ }
70
+ if (auditChanged) {
71
+ patch.auditLogRetentionDays = auditLogRetentionDays.trim() === "" ? null : Number(auditLogRetentionDays);
72
+ }
60
73
  if (
61
74
  shortensRetention(patch.messageRetentionDays, savedMessageDays) ||
62
75
  shortensRetention(patch.auditLogRetentionDays, savedAuditDays)
@@ -80,7 +93,7 @@ export default function RetentionPolicyForm({
80
93
  setSavedSnapshot(snapshot);
81
94
  setSaved(true);
82
95
  } catch (err) {
83
- setError(err instanceof ApiRequestError ? err.message : "Could not save the retention policy.");
96
+ setError(actionErrorMessage(err, "Could not save the retention policy."));
84
97
  } finally {
85
98
  setSaving(false);
86
99
  }
@@ -109,14 +122,14 @@ export default function RetentionPolicyForm({
109
122
  <input
110
123
  aria-label="Message retention (days)"
111
124
  type="number"
112
- min={1}
125
+ min={messageChanged ? MIN_MESSAGE_RETENTION_DAYS : undefined}
113
126
  className={INPUT_CLASS}
114
127
  value={messageRetentionDays}
115
128
  onChange={(e) => setMessageRetentionDays(e.target.value)}
116
129
  placeholder="No automatic purge"
117
130
  />
118
131
  <span className="text-xs text-text-muted">
119
- Applies to any message, in any folder, regardless of age.
132
+ Applies to any message, in any folder, regardless of age. Cannot be set below {MIN_MESSAGE_RETENTION_DAYS} days.
120
133
  </span>
121
134
  </label>
122
135
  <label className="flex flex-col gap-1.5 text-sm">
@@ -124,7 +137,7 @@ export default function RetentionPolicyForm({
124
137
  <input
125
138
  aria-label="Audit log retention (days)"
126
139
  type="number"
127
- min={MIN_AUDIT_LOG_RETENTION_DAYS}
140
+ min={auditChanged ? MIN_AUDIT_LOG_RETENTION_DAYS : undefined}
128
141
  className={INPUT_CLASS}
129
142
  value={auditLogRetentionDays}
130
143
  onChange={(e) => setAuditLogRetentionDays(e.target.value)}