@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
@@ -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,
@@ -31,12 +33,14 @@ import {
31
33
  retryPluginPurge,
32
34
  searchPlugins,
33
35
  updatePlugin,
36
+ uploadPlugin,
34
37
  } from "../../../../../lib/admin/pluginsApi.js";
35
- import Alert from "../../../../../lib/components/feedback/Alert.js";
38
+ import { ApiRequestError } from "../../../../../lib/util/api.js";
39
+ import Alert from "../ActionAlert.js";
36
40
  import Button from "../../../../../lib/components/buttons/Button.js";
37
41
  import Modal from "../../../../../lib/components/overlays/Modal.js";
38
42
  import { notify } from "../../../notifications/store.js";
39
- import { isElevationRequired } from "../elevation.js";
43
+ import { actionErrorMessage, isElevationMessage, isElevationRequired } from "../elevation.js";
40
44
  import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
41
45
 
42
46
  const INPUT_CLASS =
@@ -52,7 +56,7 @@ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
52
56
  const MAX_STATUS_RETRY_MS = 60 * 1000;
53
57
 
54
58
  function errorMessage(err: unknown, fallback: string): string {
55
- return err instanceof ApiRequestError ? err.message : fallback;
59
+ return actionErrorMessage(err, fallback);
56
60
  }
57
61
 
58
62
  /** Runs `action`, resolving why it failed, or `null`. */
@@ -82,6 +86,21 @@ function purgeFailure(purge: PluginPurgeInfo): string {
82
86
  /** A day, in the administrator's own format. */
83
87
  const formatDate = (iso: string): string => new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
84
88
 
89
+ /** The file names an uploaded plugin may have: what `npm pack` produces (`<name>-<version>.tgz`), or the same gzipped. */
90
+ const PLUGIN_PACK_NAME = /\.(tgz|tar\.gz|gz)$/i;
91
+
92
+ /** What the server answers (409) an upload of a plugin that is already installed, which the administrator may replace. */
93
+ const ALREADY_INSTALLED = /already installed/i;
94
+
95
+ /** An upload the administrator is being asked to let replace the plugin of the same name. */
96
+ interface PendingReplace {
97
+ file: File;
98
+ /** What the replacement would do, as a question. */
99
+ question: string;
100
+ /** Why the server refused, in its words. */
101
+ reason: string;
102
+ }
103
+
85
104
  /** A change waiting for the administrator to confirm the other plugins it also installs or enables. */
86
105
  interface PendingChange {
87
106
  displayName: string;
@@ -212,6 +231,8 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
212
231
  const allowPrereleaseRef = useRef(allowPrerelease);
213
232
  /** Identifies the latest update check, so a slower earlier one can't replace what it found. */
214
233
  const updatesRequest = useRef(0);
234
+ // Set while a bulk action runs, which looks for updates once at its end rather than after each plugin it changes.
235
+ const bulkRunning = useRef(false);
215
236
  /** The plugins ticked for a bulk action. */
216
237
  const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());
217
238
  /** Set while a bulk action runs. */
@@ -221,6 +242,11 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
221
242
  const [confirmingBulkUninstall, setConfirmingBulkUninstall] = useState<Plugin[] | null>(null);
222
243
  /** How many changes (or the lookups that come before them) are waiting on the server. */
223
244
  const [working, setWorking] = useState(0);
245
+ /** Set while an uploaded plugin is being sent to the server. */
246
+ const [uploading, setUploading] = useState(false);
247
+ /** An upload that would replace an installed plugin, waiting for the administrator to confirm it. */
248
+ const [replacing, setReplacing] = useState<PendingReplace | null>(null);
249
+ const fileInput = useRef<HTMLInputElement>(null);
224
250
  const statusRequest = useRef<Promise<void> | null>(null);
225
251
  /** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
226
252
  const seenPurges = useRef<Map<string, PluginPurgeState>>(new Map());
@@ -378,7 +404,9 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
378
404
  return [...next].sort((a, b) => a.name.localeCompare(b.name));
379
405
  });
380
406
  watchRollout();
381
- void refreshUpdates();
407
+ if (!bulkRunning.current) {
408
+ void refreshUpdates();
409
+ }
382
410
  }
383
411
 
384
412
  /** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
@@ -511,6 +539,54 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
511
539
  return onRow(plugin, () => changeVersion(plugin, packageVersion));
512
540
  }
513
541
 
542
+ /** Sends `file` to the server as a plugin to install - replacing the installed plugin of that name only when `replace` is set. A plugin that is
543
+ * already installed isn't an error but a question, asked of the administrator before it is replaced. */
544
+ async function runUpload(file: File, replace: boolean) {
545
+ setUploading(true);
546
+ setError(null);
547
+ setRetryEnableUid(null);
548
+ try {
549
+ const uploaded: Plugin = await track(uploadPlugin(file, { replace }));
550
+ applied(uploaded);
551
+ notify({
552
+ kind: "success",
553
+ title: `${uploaded.name}@${uploaded.packageVersion} uploaded`,
554
+ message: "The servers restart one at a time to load it.",
555
+ });
556
+ } catch (err) {
557
+ if (!replace && err instanceof ApiRequestError && err.status === 409 && ALREADY_INSTALLED.test(err.message)) {
558
+ const installed: Plugin | undefined = plugins.find((plugin) => err.message.includes(plugin.name));
559
+ setReplacing({
560
+ file,
561
+ question: installed
562
+ ? `Replace ${installed.name} ${installed.packageVersion} with the uploaded pack?`
563
+ : "Replace the installed plugin with the uploaded pack?",
564
+ reason: err.message,
565
+ });
566
+ } else if (err instanceof ApiRequestError && err.status === 413) {
567
+ setError("That file is too large for the server to accept.");
568
+ } else {
569
+ setError(errorMessage(err, "Could not upload the plugin."));
570
+ }
571
+ }
572
+ setUploading(false);
573
+ }
574
+
575
+ /** Takes the file the administrator chose, unless it can't be what `npm pack` produces. */
576
+ function onFileChosen(e: React.ChangeEvent<HTMLInputElement>) {
577
+ const file: File | undefined = e.target.files?.[0];
578
+ // So choosing the same file again (after fixing what was wrong) still counts as a change.
579
+ e.target.value = "";
580
+ if (!file) {
581
+ return;
582
+ }
583
+ if (!PLUGIN_PACK_NAME.test(file.name)) {
584
+ setError(`${file.name} isn't a plugin pack. Choose the .tgz file that npm pack produces.`);
585
+ return;
586
+ }
587
+ void runUpload(file, false);
588
+ }
589
+
514
590
  /** Installs an uninstalled plugin again, at its latest version - as adding it by name would. */
515
591
  function reinstall(purge: PluginPurgeInfo) {
516
592
  const displayName: string = purgeName(purge);
@@ -579,6 +655,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
579
655
  setError(null);
580
656
  setRetryEnableUid(null);
581
657
  setBulkFailure(null);
658
+ bulkRunning.current = true;
582
659
  await track(
583
660
  (async () => {
584
661
  for (const [index, plugin] of ordered.entries()) {
@@ -594,7 +671,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
594
671
  }
595
672
  failed.add(plugin.uid);
596
673
  reasons.push(`${plugin.manifest.displayName}: ${problem}`);
597
- if (problem === ELEVATION_MESSAGE) {
674
+ if (isElevationMessage(problem)) {
598
675
  for (const untried of ordered.slice(index + 1)) {
599
676
  failed.add(untried.uid);
600
677
  reasons.push(`${untried.manifest.displayName}: Not attempted.`);
@@ -604,12 +681,16 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
604
681
  }
605
682
  })(),
606
683
  );
684
+ bulkRunning.current = false;
607
685
  setBulk(null);
608
686
  setSelected(failed);
609
687
  const succeeded: number = ordered.length - failed.size;
610
688
  if (kind === "uninstall" && succeeded > 0) {
611
689
  watchRollout();
612
690
  }
691
+ if (kind !== "uninstall" && succeeded > 0) {
692
+ void refreshUpdates();
693
+ }
613
694
  if (succeeded > 0) {
614
695
  notify({
615
696
  kind: "success",
@@ -671,12 +752,34 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
671
752
  const displayNameOf = (name: string): string => plugins.find((plugin) => plugin.name === name)?.manifest.displayName ?? name;
672
753
 
673
754
  const SectionHeading = embedded ? "h3" : "h2";
755
+ const bulkBusy: boolean = bulk !== null;
674
756
  const addButton = (
675
- <Button type="button" variant="secondary" className="!w-auto shrink-0" onClick={() => setAdding(true)}>
676
- Add by name
677
- </Button>
757
+ <div className="flex flex-wrap gap-2 shrink-0">
758
+ <input
759
+ ref={fileInput}
760
+ type="file"
761
+ className="hidden"
762
+ accept=".tgz,.tar.gz,application/gzip,application/x-gzip"
763
+ aria-label="Plugin pack file"
764
+ tabIndex={-1}
765
+ onChange={onFileChosen}
766
+ />
767
+ <Button
768
+ type="button"
769
+ variant="secondary"
770
+ className="!w-auto"
771
+ title="Install a plugin from the .tgz file that npm pack produces"
772
+ loading={uploading}
773
+ disabled={uploading || bulkBusy}
774
+ onClick={() => fileInput.current?.click()}
775
+ >
776
+ {uploading ? "Uploading\u2026" : "Upload"}
777
+ </Button>
778
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setAdding(true)}>
779
+ Add by name
780
+ </Button>
781
+ </div>
678
782
  );
679
- const bulkBusy: boolean = bulk !== null;
680
783
 
681
784
  return (
682
785
  <>
@@ -730,24 +833,18 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
730
833
  </div>
731
834
  </Alert>
732
835
  )}
733
- {error && (
734
- <Alert>
735
- {error}
736
- {retryEnable && (
737
- <>
738
- {" "}
739
- <Button
740
- type="button"
741
- variant="text"
742
- className="!w-auto !p-0"
743
- onClick={() => void toggle(retryEnable)}
744
- >
745
- Try again
746
- </Button>
747
- </>
748
- )}
749
- </Alert>
750
- )}
836
+ {/* A message on its own is a plain string child, which is what lets the alert offer to confirm the identity again. */}
837
+ {error &&
838
+ (retryEnable ? (
839
+ <Alert>
840
+ {error}{" "}
841
+ <Button type="button" variant="text" className="!w-auto !p-0" onClick={() => void toggle(retryEnable)}>
842
+ Try again
843
+ </Button>
844
+ </Alert>
845
+ ) : (
846
+ <Alert>{error}</Alert>
847
+ ))}
751
848
  <RolloutBanner status={status} />
752
849
  {statusStale && status && (
753
850
  <p className="mb-4 text-xs text-text-muted">Couldn&apos;t refresh server status. Showing the last status reported.</p>
@@ -860,7 +957,9 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
860
957
  <tbody role="rowgroup" className="block lg:table-row-group">
861
958
  {plugins.map((plugin) => {
862
959
  const update: PluginUpdateInfo | undefined = updates.get(plugin.uid);
863
- const latest: string | undefined = update?.updateAvailable ? update.latestVersion : undefined;
960
+ const uploaded: boolean = plugin.source === "upload";
961
+ // An uploaded pack has no registry version to be newer than.
962
+ const latest: string | undefined = !uploaded && update?.updateAvailable ? update.latestVersion : undefined;
864
963
  const busy: boolean = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid || bulkBusy;
865
964
  const requires: string[] = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
866
965
  const requiredBy: string[] = plugins
@@ -884,6 +983,13 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
884
983
  {plugin.manifest.description && (
885
984
  <div className="text-xs text-text-muted mt-1 max-w-md">{plugin.manifest.description}</div>
886
985
  )}
986
+ {uploaded && (
987
+ <div className="text-xs text-text-muted mt-1 break-words">
988
+ <span className={`${BADGE_CLASS} bg-surface-alt text-text-muted`}>Uploaded</span>{" "}
989
+ {plugin.uploadFilename}
990
+ {plugin.uploadedAt ? `, ${formatDate(plugin.uploadedAt)}` : ""}
991
+ </div>
992
+ )}
887
993
  {requires.length > 0 && <div className="text-xs text-text-muted mt-1">Requires: {requires.join(", ")}</div>}
888
994
  {requiredBy.length > 0 && (
889
995
  <div className="text-xs text-text-muted mt-1">Required by: {requiredBy.join(", ")}</div>
@@ -931,7 +1037,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
931
1037
  </div>
932
1038
  <div className="flex flex-wrap gap-x-4 lg:justify-end">
933
1039
  <Button variant="text" type="button" onClick={() => setUpgrading(plugin)}>
934
- Change version
1040
+ {uploaded ? "Install the registry version" : "Change version"}
935
1041
  </Button>
936
1042
  <Button variant="text" type="button" onClick={() => setRemoving(plugin)}>
937
1043
  Uninstall
@@ -991,6 +1097,28 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
991
1097
  }}
992
1098
  />
993
1099
  )}
1100
+ {replacing && (
1101
+ <Modal open onClose={() => setReplacing(null)} title="Replace plugin?">
1102
+ <p className="text-sm mb-2">{replacing.question}</p>
1103
+ <p className="text-sm text-text-muted mb-4 break-words">{replacing.reason}</p>
1104
+ <div className="flex gap-2 justify-end">
1105
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => setReplacing(null)}>
1106
+ Cancel
1107
+ </Button>
1108
+ <Button
1109
+ type="button"
1110
+ className="!w-auto"
1111
+ onClick={() => {
1112
+ const { file } = replacing;
1113
+ setReplacing(null);
1114
+ void runUpload(file, true);
1115
+ }}
1116
+ >
1117
+ Replace
1118
+ </Button>
1119
+ </div>
1120
+ </Modal>
1121
+ )}
994
1122
  {confirming && (
995
1123
  <ConfirmDependenciesModal
996
1124
  change={confirming}
@@ -1559,15 +1687,20 @@ function ChangeVersionModal({
1559
1687
  const [selected, setSelected] = useState(plugin.packageVersion);
1560
1688
  const [error, setError] = useState<string | null>(null);
1561
1689
  const [busy, setBusy] = useState(false);
1690
+ // An uploaded pack isn't a registry version, so any registry version is a change from it - the newest is offered first.
1691
+ const uploaded: boolean = plugin.source === "upload";
1562
1692
 
1563
1693
  useEffect(() => {
1564
1694
  lookupPluginPackage(plugin.name, undefined, { prerelease })
1565
1695
  .then((found) => {
1566
1696
  setVersions(found.package.versions);
1567
1697
  setLatest(found.package.latest);
1698
+ if (uploaded && found.package.latest) {
1699
+ setSelected(found.package.latest);
1700
+ }
1568
1701
  })
1569
1702
  .catch((err) => setError(errorMessage(err, "Could not load the available versions.")));
1570
- }, [plugin.name, prerelease]);
1703
+ }, [plugin.name, prerelease, uploaded]);
1571
1704
 
1572
1705
  async function save() {
1573
1706
  setBusy(true);
@@ -1580,12 +1713,17 @@ function ChangeVersionModal({
1580
1713
  }
1581
1714
 
1582
1715
  return (
1583
- <Modal open onClose={onClose} title={`${plugin.manifest.displayName} version`}>
1716
+ <Modal open onClose={onClose} title={uploaded ? `Install the registry version of ${plugin.manifest.displayName}` : `${plugin.manifest.displayName} version`}>
1584
1717
  {error && <Alert>{error}</Alert>}
1585
1718
  {versions === null ? (
1586
1719
  !error && <p className="text-sm text-text-muted">Loading&hellip;</p>
1587
1720
  ) : (
1588
1721
  <div className="flex flex-col gap-3">
1722
+ {uploaded && (
1723
+ <p className="text-sm text-text-muted">
1724
+ {plugin.manifest.displayName} {plugin.packageVersion} was uploaded as {plugin.uploadFilename}. This replaces it with the version from the registry.
1725
+ </p>
1726
+ )}
1589
1727
  <label className="flex flex-col gap-1.5 text-sm">
1590
1728
  <span className="font-semibold">Version</span>
1591
1729
  <select aria-label="Version" className={INPUT_CLASS} value={selected} onChange={(e) => setSelected(e.target.value)}>
@@ -1593,7 +1731,7 @@ function ChangeVersionModal({
1593
1731
  <option key={version} value={version}>
1594
1732
  {version}
1595
1733
  {version === latest ? " (latest)" : ""}
1596
- {version === plugin.packageVersion ? " (installed)" : ""}
1734
+ {!uploaded && version === plugin.packageVersion ? " (installed)" : ""}
1597
1735
  </option>
1598
1736
  ))}
1599
1737
  </select>
@@ -1606,10 +1744,10 @@ function ChangeVersionModal({
1606
1744
  type="button"
1607
1745
  className="!w-auto"
1608
1746
  loading={busy}
1609
- disabled={busy || selected === plugin.packageVersion}
1747
+ disabled={busy || (!uploaded && selected === plugin.packageVersion)}
1610
1748
  onClick={() => void save()}
1611
1749
  >
1612
- Save
1750
+ {uploaded ? "Install" : "Save"}
1613
1751
  </Button>
1614
1752
  </div>
1615
1753
  </div>
@@ -1675,7 +1813,7 @@ function ConfirmDependenciesModal({
1675
1813
  }
1676
1814
 
1677
1815
  /** 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 {
1816
+ function needsSelection(definition: PluginSettingDefinition, saved: PluginSavedSetting | undefined): boolean {
1679
1817
  return (
1680
1818
  definition.type === "select" &&
1681
1819
  !!definition.required &&
@@ -1693,7 +1831,7 @@ const HOST_PLACEHOLDER = "<host>";
1693
1831
  * saved yet - nothing, an empty value, or the placeholder itself, which an older server stores when it installs the
1694
1832
  * plugin. It's the default with this console's host filled in, and is only stored once the form is saved.
1695
1833
  */
1696
- function suggestedHost(definition: PluginSettingDefinition, saved: PluginSettingValue | undefined): string | undefined {
1834
+ function suggestedHost(definition: PluginSettingDefinition, saved: PluginSavedSetting | undefined): string | undefined {
1697
1835
  if (typeof definition.default !== "string" || !definition.default.includes(HOST_PLACEHOLDER) || typeof window === "undefined") {
1698
1836
  return undefined;
1699
1837
  }
@@ -1710,12 +1848,12 @@ function configuredValue(definition: PluginSettingDefinition, configured: Plugin
1710
1848
  }
1711
1849
 
1712
1850
  /** 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 {
1851
+ function isUnsaved(saved: PluginSavedSetting | undefined): boolean {
1714
1852
  return saved === undefined || saved === "";
1715
1853
  }
1716
1854
 
1717
1855
  /** 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 {
1856
+ function hostSuggestion(definition: PluginSettingDefinition, saved: PluginSavedSetting | undefined, configured: PluginConfiguredSetting | undefined): string | undefined {
1719
1857
  return configured ? undefined : suggestedHost(definition, saved);
1720
1858
  }
1721
1859
 
@@ -1724,9 +1862,13 @@ function hostSuggestion(definition: PluginSettingDefinition, saved: PluginSettin
1724
1862
  * option. Kept as a string for text inputs. */
1725
1863
  function initialValue(
1726
1864
  definition: PluginSettingDefinition,
1727
- saved: PluginSettingValue | undefined,
1865
+ saved: PluginSavedSetting | undefined,
1728
1866
  configured?: PluginConfiguredSetting,
1729
1867
  ): PluginSettingValue | "" {
1868
+ if (isSecretSetting(saved)) {
1869
+ // 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.
1870
+ return "";
1871
+ }
1730
1872
  if (configured && isUnsaved(saved)) {
1731
1873
  return configuredValue(definition, configured);
1732
1874
  }
@@ -1768,12 +1910,12 @@ function SettingsModal({ plugin, onClose, onSaved }: { plugin: Plugin; onClose:
1768
1910
  // A required select with nothing to fall back on, and a setting offering this console's host, are sent as the
1769
1911
  // form shows them. What the deployment configures is never saved as a copy, which would outrank it when it changes
1770
1912
  // - a field that shows it is sent as nothing.
1771
- const settings: Record<string, PluginSettingValue | null> = {};
1913
+ const settings: Record<string, PluginSavedSetting | null> = {};
1772
1914
  let changed = false;
1773
1915
  for (const definition of definitions) {
1774
1916
  const key: string = definition.key;
1775
1917
  const configured: PluginConfiguredSetting | undefined = plugin.configured?.[key];
1776
- const saved: PluginSettingValue | undefined = plugin.settings[key];
1918
+ const saved: PluginSavedSetting | undefined = plugin.settings[key];
1777
1919
  const value = values[key];
1778
1920
  if (resetting && value === initialValue(definition, undefined, configured)) {
1779
1921
  // What a fresh install saves: only a default naming this console's host has to be, for the plugin to work.
@@ -1820,6 +1962,7 @@ function SettingsModal({ plugin, onClose, onSaved }: { plugin: Plugin; onClose:
1820
1962
  definition={definition}
1821
1963
  value={values[definition.key]}
1822
1964
  configured={plugin.configured?.[definition.key]}
1965
+ secretSaved={!resetting && isSecretSetting(plugin.settings[definition.key])}
1823
1966
  onChange={(value) => setValues((prev) => ({ ...prev, [definition.key]: value }))}
1824
1967
  />
1825
1968
  ))}
@@ -1852,11 +1995,14 @@ function SettingField({
1852
1995
  definition,
1853
1996
  value,
1854
1997
  configured,
1998
+ secretSaved,
1855
1999
  onChange,
1856
2000
  }: {
1857
2001
  definition: PluginSettingDefinition;
1858
2002
  value: PluginSettingValue | "";
1859
2003
  configured?: PluginConfiguredSetting;
2004
+ /** A secret is saved: the server never sends it, so the field is write-only - typing replaces it, leaving it empty keeps it. */
2005
+ secretSaved: boolean;
1860
2006
  onChange: (value: PluginSettingValue | "") => void;
1861
2007
  }) {
1862
2008
  // What applies without a saved value; one saved here replaces it, and Reset goes back to it.
@@ -1865,8 +2011,10 @@ function SettingField({
1865
2011
  This deployment&apos;s configuration provides {configured.secret ? "a value" : "the value shown"} unless one is saved here.
1866
2012
  </span>
1867
2013
  );
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;
2014
+ // 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
2015
+ // browser's form fill.
2016
+ const secretField: boolean = definition.type === "string" && (isSecretSettingKey(definition.key) || secretSaved || !!configured?.secret);
2017
+ const placeholder: string | undefined = secretSaved ? "Saved - type a new value to replace it" : configured?.secret ? "Set by this deployment" : undefined;
1870
2018
  const help = (
1871
2019
  <>
1872
2020
  {definition.help && <span className="text-xs text-text-muted">{definition.help}</span>}
@@ -1901,10 +2049,11 @@ function SettingField({
1901
2049
  <input
1902
2050
  aria-label={definition.label}
1903
2051
  className={INPUT_CLASS}
1904
- type={definition.type === "number" ? "number" : "text"}
2052
+ type={definition.type === "number" ? "number" : secretField ? "password" : "text"}
2053
+ autoComplete={secretField ? "new-password" : undefined}
1905
2054
  min={definition.min}
1906
2055
  max={definition.max}
1907
- required={definition.required && !configured}
2056
+ required={definition.required && !configured && !secretSaved}
1908
2057
  placeholder={placeholder}
1909
2058
  value={String(value)}
1910
2059
  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)}