@rapidmx/web-client 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +3 -3
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +2 -2
  33. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  34. package/apps/shared/components/admin/elevation.ts +19 -0
  35. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  36. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  37. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  38. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  40. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  41. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  42. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  43. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  44. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  45. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  46. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  47. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  48. package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
  49. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  50. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  51. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  52. package/apps/shared/components/admin/signOut.ts +3 -0
  53. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  54. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  55. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  56. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  57. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  58. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  59. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  60. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  61. package/apps/shared/components/calendar/ListView.tsx +99 -25
  62. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  63. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  64. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  65. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  66. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  67. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  68. package/apps/shared/components/calendar/timePicker.ts +5 -2
  69. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  70. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  71. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  72. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  73. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  74. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  75. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  76. package/apps/shared/components/layout/AppShell.tsx +9 -0
  77. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  78. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  79. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  80. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  81. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  82. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  83. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  84. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  85. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  86. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  87. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  88. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  89. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  90. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  91. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  92. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  93. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  94. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  95. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  96. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  97. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  98. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  99. package/apps/shared/components/tasks/dueDate.ts +29 -0
  100. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  101. package/apps/shared/mail/decryptedMessages.ts +51 -2
  102. package/apps/shared/mail/listSnapshots.ts +2 -0
  103. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  104. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  105. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  106. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  107. package/apps/shared/notifications/apiErrors.ts +2 -0
  108. package/apps/shared/notifications/preferences.ts +1 -1
  109. package/apps/shared/notifications/store.ts +3 -1
  110. package/apps/shared/plugins/pluginNav.ts +4 -2
  111. package/apps/www/calendar/index.tsx +24 -12
  112. package/apps/www/contacts/[uid].tsx +6 -1
  113. package/apps/www/contacts/index.tsx +66 -12
  114. package/apps/www/index.tsx +91 -28
  115. package/apps/www/messages/[uid].tsx +8 -3
  116. package/apps/www/settings/auto-reply/index.tsx +13 -0
  117. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  118. package/apps/www/settings/encryption/index.tsx +66 -4
  119. package/apps/www/settings/privacy/index.tsx +4 -1
  120. package/apps/www/settings/profile/index.tsx +12 -1
  121. package/apps/www/settings/sharing/index.tsx +11 -1
  122. package/apps/www/settings/signatures/[uid].tsx +6 -4
  123. package/apps/www/tasks/index.tsx +97 -29
  124. package/dist/apps/admin/audit-log/index.js +3 -3
  125. package/dist/apps/admin/branding/index.js +3 -3
  126. package/dist/apps/admin/data-requests/index.js +17 -7
  127. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  128. package/dist/apps/admin/distribution-lists/index.js +3 -3
  129. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  130. package/dist/apps/admin/domains/[uid].js +6 -5
  131. package/dist/apps/admin/domains/index.js +3 -3
  132. package/dist/apps/admin/domains/new/index.js +3 -3
  133. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  134. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  135. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  136. package/dist/apps/admin/index.js +4 -4
  137. package/dist/apps/admin/ingest-queue/index.js +3 -3
  138. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  139. package/dist/apps/admin/quarantine/index.js +4 -4
  140. package/dist/apps/admin/retention-policy/index.js +3 -3
  141. package/dist/apps/admin/signing-certificates/index.js +3 -3
  142. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  143. package/dist/apps/admin/transport-rules/index.js +3 -3
  144. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  145. package/dist/apps/escrow/matters/[uid].js +8 -3
  146. package/dist/apps/escrow/matters/new/index.js +4 -0
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  148. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  149. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  151. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  152. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
  153. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  156. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  157. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
  158. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  159. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  160. package/dist/apps/shared/components/admin/elevation.js +16 -0
  161. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  162. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  163. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  164. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  165. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  166. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  167. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  168. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  169. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  170. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  171. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  172. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  174. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  175. package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
  176. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  177. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  178. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  179. package/dist/apps/shared/components/admin/signOut.js +3 -0
  180. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  181. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  182. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  183. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  184. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  185. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  186. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  187. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  188. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  189. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  190. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  191. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  192. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  193. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  194. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  195. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  196. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  197. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  198. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  199. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  200. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  201. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  202. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  203. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  204. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  205. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  206. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  207. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  208. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  209. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  210. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  211. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  212. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  213. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  214. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  215. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  216. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  217. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  218. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  219. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  221. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  222. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  224. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  226. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  227. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  228. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  229. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  230. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  231. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  232. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  233. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  234. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  235. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  236. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  237. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  238. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  239. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  240. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  241. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  242. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  243. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  244. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  245. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  246. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  247. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  248. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  249. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  250. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  251. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  252. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  253. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  254. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  255. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  256. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  257. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  258. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  259. package/dist/apps/shared/notifications/preferences.js +1 -1
  260. package/dist/apps/shared/notifications/store.js +3 -1
  261. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  262. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  263. package/dist/apps/www/calendar/index.js +24 -12
  264. package/dist/apps/www/contacts/[uid].js +6 -2
  265. package/dist/apps/www/contacts/index.js +62 -11
  266. package/dist/apps/www/index.js +85 -30
  267. package/dist/apps/www/messages/[uid].js +8 -3
  268. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  269. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  270. package/dist/apps/www/settings/encryption/index.js +29 -5
  271. package/dist/apps/www/settings/privacy/index.js +3 -2
  272. package/dist/apps/www/settings/profile/index.js +4 -1
  273. package/dist/apps/www/settings/sharing/index.js +10 -1
  274. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  275. package/dist/apps/www/tasks/index.js +94 -32
  276. package/dist/lib/admin/auditLogApi.js +2 -2
  277. package/dist/lib/admin/distributionListsApi.js +2 -2
  278. package/dist/lib/admin/domainsApi.js +2 -2
  279. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  280. package/dist/lib/admin/escrowScopesApi.js +2 -2
  281. package/dist/lib/admin/pluginsApi.d.ts +31 -2
  282. package/dist/lib/admin/pluginsApi.js +23 -1
  283. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  284. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  285. package/dist/lib/admin/transportRulesApi.js +2 -2
  286. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  287. package/dist/lib/appearance/preferencesApi.js +3 -14
  288. package/dist/lib/auth/session.d.ts +51 -12
  289. package/dist/lib/auth/session.js +214 -38
  290. package/dist/lib/branding/brandingApi.d.ts +3 -3
  291. package/dist/lib/branding/brandingApi.js +9 -21
  292. package/dist/lib/calendar/calendarApi.js +3 -1
  293. package/dist/lib/calendar/eventSearch.js +43 -6
  294. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  295. package/dist/lib/calendar/freeBusyApi.js +13 -5
  296. package/dist/lib/calendar/recurrence.js +51 -3
  297. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  298. package/dist/lib/calendar/useDayKey.js +43 -0
  299. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  300. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  301. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  302. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  303. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  304. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  305. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  306. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  307. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  308. package/dist/lib/components/menus/MenuButton.js +219 -0
  309. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  310. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  311. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  312. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  313. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  314. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  315. package/dist/lib/contacts/contactsApi.js +16 -19
  316. package/dist/lib/contacts/gravatar.d.ts +14 -3
  317. package/dist/lib/contacts/gravatar.js +42 -5
  318. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  319. package/dist/lib/contacts/preparePhoto.js +250 -0
  320. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  321. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  322. package/dist/lib/contacts/vcard.d.ts +3 -0
  323. package/dist/lib/contacts/vcard.js +59 -7
  324. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  325. package/dist/lib/crypto/escrowKeys.js +20 -1
  326. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  327. package/dist/lib/crypto/messageSecurity.js +23 -5
  328. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  329. package/dist/lib/crypto/passwordUnlock.js +13 -2
  330. package/dist/lib/crypto/smime.js +12 -2
  331. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  332. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  333. package/dist/lib/mail/compose/quotedImages.js +35 -9
  334. package/dist/lib/mail/labelsApi.js +2 -2
  335. package/dist/lib/mail/mailApi.d.ts +8 -5
  336. package/dist/lib/mail/mailApi.js +73 -39
  337. package/dist/lib/mail/mailDetailHooks.js +11 -3
  338. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  339. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  340. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  341. package/dist/lib/mail/mailboxImportApi.js +3 -15
  342. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  343. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  344. package/dist/lib/search/queryGrammar.js +12 -4
  345. package/dist/lib/search/searchTier3.d.ts +6 -1
  346. package/dist/lib/search/searchTier3.js +54 -25
  347. package/dist/lib/tasks/tasksApi.js +3 -3
  348. package/dist/lib/util/api.d.ts +26 -4
  349. package/dist/lib/util/api.js +73 -16
  350. package/dist/lib/util/apiQuery.d.ts +11 -0
  351. package/dist/lib/util/apiQuery.js +11 -0
  352. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  353. package/dist/lib/util/settleWithLimit.js +25 -0
  354. package/lib/admin/auditLogApi.ts +2 -2
  355. package/lib/admin/distributionListsApi.ts +2 -2
  356. package/lib/admin/domainsApi.ts +2 -2
  357. package/lib/admin/escrowAuditLogApi.ts +2 -2
  358. package/lib/admin/escrowScopesApi.ts +2 -2
  359. package/lib/admin/pluginsApi.ts +45 -3
  360. package/lib/admin/retentionPolicyApi.ts +4 -0
  361. package/lib/admin/transportRulesApi.ts +2 -2
  362. package/lib/appearance/preferencesApi.ts +3 -14
  363. package/lib/auth/session.ts +239 -39
  364. package/lib/branding/brandingApi.ts +9 -21
  365. package/lib/calendar/calendarApi.ts +3 -1
  366. package/lib/calendar/eventSearch.ts +46 -6
  367. package/lib/calendar/freeBusyApi.ts +14 -6
  368. package/lib/calendar/recurrence.ts +59 -8
  369. package/lib/calendar/useDayKey.ts +45 -0
  370. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  371. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  372. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  373. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  374. package/lib/components/menus/MenuButton.tsx +408 -0
  375. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  376. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  377. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  378. package/lib/contacts/contactsApi.ts +18 -19
  379. package/lib/contacts/gravatar.ts +47 -5
  380. package/lib/contacts/preparePhoto.ts +273 -0
  381. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  382. package/lib/contacts/vcard.ts +60 -7
  383. package/lib/crypto/escrowKeys.ts +21 -1
  384. package/lib/crypto/messageSecurity.ts +33 -9
  385. package/lib/crypto/passwordUnlock.ts +14 -2
  386. package/lib/crypto/smime.ts +13 -2
  387. package/lib/mail/compose/composeQuoting.ts +3 -2
  388. package/lib/mail/compose/quotedImages.ts +39 -9
  389. package/lib/mail/labelsApi.ts +2 -2
  390. package/lib/mail/mailApi.ts +96 -39
  391. package/lib/mail/mailDetailHooks.ts +11 -3
  392. package/lib/mail/mailFilterRulesApi.ts +2 -2
  393. package/lib/mail/mailSignaturesApi.ts +2 -2
  394. package/lib/mail/mailboxImportApi.ts +9 -15
  395. package/lib/mail/messageBodySanitizer.ts +47 -1
  396. package/lib/search/queryGrammar.ts +13 -4
  397. package/lib/search/searchTier3.ts +65 -32
  398. package/lib/tasks/tasksApi.ts +3 -3
  399. package/lib/util/api.ts +99 -16
  400. package/lib/util/apiQuery.ts +13 -0
  401. package/lib/util/settleWithLimit.ts +25 -0
  402. package/package.json +1 -1
@@ -4,13 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs } 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 { listEscrowScopes } from "../../../../../lib/admin/escrowScopesApi.js";
9
8
  import { updateMailbox } from "../../../../../lib/mail/mailApi.js";
10
9
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
11
- import Alert from "../../../../../lib/components/feedback/Alert.js";
10
+ import Alert from "../ActionAlert.js";
12
11
  import Button from "../../../../../lib/components/buttons/Button.js";
13
12
  import Modal from "../../../../../lib/components/overlays/Modal.js";
13
+ import { actionErrorMessage } from "../elevation.js";
14
14
  const SELECT_CLASS = "text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
15
  /** Enough for any realistic deployment's scopes in one list. */
16
16
  const SCOPE_LIST_LIMIT = 200;
@@ -30,7 +30,7 @@ export default function EscrowScopeCard({ mailbox, onUpdate }) {
30
30
  useEffect(() => {
31
31
  listEscrowScopes({ limit: SCOPE_LIST_LIMIT }, client)
32
32
  .then(setScopes)
33
- .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load escrow scopes."));
33
+ .catch((err) => setLoadError(actionErrorMessage(err, "Could not load escrow scopes.")));
34
34
  }, []);
35
35
  // Changing the scope changes who can recover this mailbox's encrypted mail, so it's confirmed first.
36
36
  function handleSubmit(e) {
@@ -48,7 +48,7 @@ export default function EscrowScopeCard({ mailbox, onUpdate }) {
48
48
  setSaved(true);
49
49
  }
50
50
  catch (err) {
51
- setSaveError(err instanceof ApiRequestError ? err.message : "Could not save the escrow scope.");
51
+ setSaveError(actionErrorMessage(err, "Could not save the escrow scope."));
52
52
  }
53
53
  finally {
54
54
  setSaving(false);
@@ -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);