@rapidmx/web-client 0.4.0 → 0.6.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 (320) hide show
  1. package/README.md +88 -0
  2. package/apps/admin/audit-log/index.tsx +35 -8
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  6. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  7. package/apps/admin/index.tsx +19 -1
  8. package/apps/admin/ingest-queue/index.tsx +44 -5
  9. package/apps/admin/mailboxes/[uid].tsx +211 -184
  10. package/apps/admin/quarantine/index.tsx +46 -14
  11. package/apps/admin/transport-rules/[uid].tsx +30 -1
  12. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  13. package/apps/admin/transport-rules/index.tsx +24 -6
  14. package/apps/admin/transport-rules/new/index.tsx +30 -1
  15. package/apps/escrow/_layout.tsx +2 -3
  16. package/apps/escrow/audit-log/index.tsx +196 -168
  17. package/apps/escrow/matters/[uid].tsx +621 -521
  18. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  21. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  22. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  24. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  32. package/apps/shared/components/admin/signOut.ts +79 -0
  33. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  34. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  35. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  36. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  37. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  38. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  39. package/apps/shared/components/calendar/allDay.ts +124 -0
  40. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  41. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  42. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  43. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  44. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  45. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  46. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  47. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  48. package/apps/shared/components/layout/AppShell.tsx +123 -33
  49. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  50. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  51. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  52. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  53. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  54. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  55. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  56. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  57. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  58. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  59. package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
  60. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  61. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  62. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  63. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  64. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  66. package/apps/shared/mail/listAllPages.ts +39 -0
  67. package/apps/shared/plugins/pluginNav.ts +73 -0
  68. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  69. package/apps/shared/search/localIndexBuilder.ts +481 -179
  70. package/apps/shared/search/localIndexRpcClient.ts +231 -29
  71. package/apps/shared/search/localIndexSchema.ts +15 -6
  72. package/apps/shared/search/localIndexStorage.ts +98 -0
  73. package/apps/shared/search/localIndexVFS.ts +32 -1
  74. package/apps/shared/search/localIndexWorker.ts +648 -186
  75. package/apps/www/calendar/index.tsx +438 -417
  76. package/apps/www/contacts/[uid].tsx +21 -2
  77. package/apps/www/contacts/index.tsx +116 -12
  78. package/apps/www/index.tsx +1314 -1041
  79. package/apps/www/messages/[uid].tsx +18 -6
  80. package/apps/www/settings/auto-reply/index.tsx +134 -129
  81. package/apps/www/settings/encryption/index.tsx +739 -216
  82. package/apps/www/settings/filters/[uid].tsx +171 -155
  83. package/apps/www/settings/filters/new/index.tsx +8 -1
  84. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  85. package/apps/www/settings/privacy/index.tsx +494 -436
  86. package/apps/www/settings/read-receipts/index.tsx +148 -125
  87. package/apps/www/settings/sharing/index.tsx +17 -14
  88. package/apps/www/tasks/index.tsx +638 -579
  89. package/dist/apps/admin/_404.d.ts +2 -0
  90. package/dist/apps/admin/_500.d.ts +2 -0
  91. package/dist/apps/admin/_layout.d.ts +8 -0
  92. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  93. package/dist/apps/admin/audit-log/index.js +38 -8
  94. package/dist/apps/admin/branding/index.d.ts +3 -0
  95. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  96. package/dist/apps/admin/data-requests/index.js +52 -72
  97. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  98. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  99. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  100. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  101. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  102. package/dist/apps/admin/domains/index.d.ts +3 -0
  103. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  104. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  105. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  106. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  107. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  108. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.d.ts +3 -0
  111. package/dist/apps/admin/index.js +4 -1
  112. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  113. package/dist/apps/admin/ingest-queue/index.js +24 -6
  114. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  115. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  116. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  117. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  118. package/dist/apps/admin/plugins/index.d.ts +3 -0
  119. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  120. package/dist/apps/admin/quarantine/index.js +20 -11
  121. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  122. package/dist/apps/admin/setup/index.d.ts +3 -0
  123. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  124. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  125. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  126. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  127. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  128. package/dist/apps/admin/transport-rules/index.js +22 -6
  129. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  130. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  131. package/dist/apps/escrow/_layout.d.ts +10 -0
  132. package/dist/apps/escrow/_layout.js +3 -3
  133. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  134. package/dist/apps/escrow/audit-log/index.js +26 -1
  135. package/dist/apps/escrow/index.d.ts +3 -0
  136. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  137. package/dist/apps/escrow/matters/[uid].js +77 -42
  138. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  139. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  140. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  141. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  142. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  143. package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
  144. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  145. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  146. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  147. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  148. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  150. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  152. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  153. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  154. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  155. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  156. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  157. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  159. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  160. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  161. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  163. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  164. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  165. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  166. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  167. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  168. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  169. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  170. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  171. package/dist/apps/shared/components/admin/signOut.js +75 -0
  172. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  173. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  174. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  175. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  176. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  177. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  178. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  179. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  180. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  181. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  182. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  183. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  184. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  185. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  186. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  187. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  188. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  189. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
  190. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  191. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  192. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  193. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  194. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  195. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  196. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  197. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  198. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  199. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  200. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  201. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  202. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  203. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  204. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  205. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  206. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  207. package/dist/apps/shared/components/layout/AppShell.js +97 -26
  208. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  209. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  210. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  211. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  212. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  214. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  216. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  217. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  218. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  219. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  222. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  224. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  226. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  227. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  229. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  230. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  231. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  232. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  234. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  235. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
  237. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  238. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  239. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  240. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  241. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  242. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  243. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  244. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  246. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  247. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  248. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  249. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  250. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  251. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  252. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  253. package/dist/apps/shared/mail/listAllPages.js +25 -0
  254. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  255. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  256. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  257. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  258. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  259. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  260. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  261. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  262. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  263. package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
  264. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  265. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  266. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  267. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  268. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  269. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  270. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  271. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  272. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  273. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  274. package/dist/apps/www/_404.d.ts +2 -0
  275. package/dist/apps/www/_500.d.ts +2 -0
  276. package/dist/apps/www/_layout.d.ts +14 -0
  277. package/dist/apps/www/calendar/index.d.ts +3 -0
  278. package/dist/apps/www/calendar/index.js +28 -10
  279. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  280. package/dist/apps/www/contacts/[uid].js +13 -2
  281. package/dist/apps/www/contacts/index.d.ts +3 -0
  282. package/dist/apps/www/contacts/index.js +80 -10
  283. package/dist/apps/www/index.d.ts +3 -0
  284. package/dist/apps/www/index.js +339 -108
  285. package/dist/apps/www/messages/[uid].d.ts +11 -0
  286. package/dist/apps/www/messages/[uid].js +18 -6
  287. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  288. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  289. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  290. package/dist/apps/www/settings/encryption/index.js +514 -97
  291. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  292. package/dist/apps/www/settings/filters/[uid].js +22 -7
  293. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  294. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  295. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  296. package/dist/apps/www/settings/filters/new/index.js +8 -2
  297. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  298. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  299. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  300. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  301. package/dist/apps/www/settings/privacy/index.js +57 -22
  302. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  303. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  304. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  305. package/dist/apps/www/settings/sharing/index.js +7 -11
  306. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  307. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  308. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  309. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  310. package/dist/apps/www/tasks/index.d.ts +3 -0
  311. package/dist/apps/www/tasks/index.js +50 -10
  312. package/package.json +3 -3
  313. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  314. package/apps/www/settings/booking-types/[uid].tsx +0 -300
  315. package/apps/www/settings/booking-types/index.tsx +0 -98
  316. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  317. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  318. package/dist/apps/www/settings/booking-types/[uid].js +0 -129
  319. package/dist/apps/www/settings/booking-types/index.js +0 -29
  320. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export default function NotFoundPage(): React.JSX.Element;
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export default function ErrorPage(): React.JSX.Element;
@@ -0,0 +1,8 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ export interface LayoutProps {
4
+ /** See `apps/www/_layout.tsx`'s `LayoutProps` doc comment - identical mechanism, supplied by
5
+ * `AdminConsoleRoute`'s `fetchProps()` override. */
6
+ branding?: Branding;
7
+ }
8
+ export default function Layout({ children, branding }: PropsWithChildren<LayoutProps>): React.JSX.Element;
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import { AuditLogFilters } from "@rapidmx/react-shared/admin/auditLogApi.js";
3
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
4
+ /** How long typing in a filter box has to pause before the log is re-fetched. */
5
+ export declare const FILTER_DEBOUNCE_MS = 300;
6
+ /** Seeds the filter form from the query string on first render — lets a link (e.g. a future "View audit
7
+ * log" deep link from a mailbox's detail page) land here pre-filtered, same convention as `quarantine`'s
8
+ * `readMailboxUid()`. Filters are also written back to the URL as they change, so the filtered view
9
+ * itself is shareable/bookmarkable. */
10
+ export declare function readFiltersFromUrl(): AuditLogFilters;
11
+ export default function AuditLogPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -3,13 +3,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useState } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { listAuditLog } from "@rapidmx/react-shared/admin/auditLogApi.js";
9
9
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
12
  const PAGE_SIZE = 25;
13
+ /** How long typing in a filter box has to pause before the log is re-fetched. */
14
+ export const FILTER_DEBOUNCE_MS = 300;
13
15
  const FILTER_KEYS = ["mailboxUid", "actorUserUid", "action", "targetType"];
14
16
  /** Seeds the filter form from the query string on first render — lets a link (e.g. a future "View audit
15
17
  * log" deep link from a mailbox's detail page) land here pre-filtered, same convention as `quarantine`'s
@@ -32,26 +34,54 @@ export default function AuditLogPage(props) {
32
34
  return (_jsx(AdminShell, { ...props, active: "auditLog", children: _jsx(AuditLogContent, {}) }));
33
35
  }
34
36
  function AuditLogContent() {
37
+ // `filters` is what the boxes show; `queryFilters` is what was last fetched - it follows `filters` once typing
38
+ // pauses, and stays `null` until the URL's filters are read so no unfiltered fetch goes out first.
35
39
  const [filters, setFilters] = useState({});
40
+ const [queryFilters, setQueryFilters] = useState(null);
36
41
  const [page, setPage] = useState(0);
37
42
  const [entries, setEntries] = useState([]);
38
43
  const [loading, setLoading] = useState(true);
39
44
  const [error, setError] = useState(null);
45
+ const debounceTimer = useRef(undefined);
40
46
  useEffect(() => {
41
- setFilters(readFiltersFromUrl());
47
+ const initial = readFiltersFromUrl();
48
+ setFilters(initial);
49
+ setQueryFilters(initial);
50
+ return () => clearTimeout(debounceTimer.current);
42
51
  }, []);
43
52
  useEffect(() => {
53
+ if (!queryFilters) {
54
+ return;
55
+ }
56
+ // Only the latest filters/page's response is applied, so a slow earlier fetch can't overwrite it.
57
+ let cancelled = false;
44
58
  setLoading(true);
45
59
  setError(null);
46
- listAuditLog(filters, { page, limit: PAGE_SIZE })
47
- .then(setEntries)
48
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load the audit log."))
49
- .finally(() => setLoading(false));
50
- }, [filters, page]);
60
+ listAuditLog(queryFilters, { page, limit: PAGE_SIZE })
61
+ .then((data) => {
62
+ if (!cancelled)
63
+ setEntries(data);
64
+ })
65
+ .catch((err) => {
66
+ if (!cancelled)
67
+ setError(err instanceof ApiRequestError ? err.message : "Could not load the audit log.");
68
+ })
69
+ .finally(() => {
70
+ if (!cancelled)
71
+ setLoading(false);
72
+ });
73
+ return () => {
74
+ cancelled = true;
75
+ };
76
+ }, [queryFilters, page]);
51
77
  function updateFilter(key, value) {
52
78
  const next = { ...filters, [key]: value || undefined };
53
79
  setFilters(next);
54
- setPage(0);
80
+ clearTimeout(debounceTimer.current);
81
+ debounceTimer.current = setTimeout(() => {
82
+ setQueryFilters(next);
83
+ setPage(0);
84
+ }, FILTER_DEBOUNCE_MS);
55
85
  const params = new URLSearchParams();
56
86
  for (const k of FILTER_KEYS) {
57
87
  const v = next[k];
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function BrandingPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function DataRequestsPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -10,6 +10,7 @@ import { listImportRequests, uploadMailboxImport, } from "@rapidmx/react-shared/
10
10
  import { approveErasureRequest, denyErasureRequest, listErasureRequests, } from "@rapidmx/react-shared/mail/erasureRequestApi.js";
11
11
  import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
12
12
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
13
+ import { LoadMoreButton, usePagedList } from "../../shared/components/admin/usePagedList.js";
13
14
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
15
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
15
16
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
@@ -18,43 +19,33 @@ const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]
18
19
  function importFormatFromFilename(filename) {
19
20
  return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
20
21
  }
22
+ /** Request statuses still waiting or running - shown as a neutral in-progress badge, never as success. Includes
23
+ * restapi's `processing` (exports/imports) and `in_progress` (an erasure that is running). */
24
+ const IN_PROGRESS_STATUSES = new Set(["pending", "processing", "in_progress"]);
21
25
  function statusBadgeClass(status) {
22
26
  if (status === "failed" || status === "denied")
23
27
  return "bg-danger-bg text-danger";
24
- if (status === "pending" || status === "processing")
28
+ if (IN_PROGRESS_STATUSES.has(status))
25
29
  return "bg-surface-alt text-text-muted";
26
30
  return "bg-success text-white";
27
31
  }
32
+ /** A status as shown on its badge (`in_progress` -> `in progress`). */
33
+ function statusLabel(status) {
34
+ return status.replace(/_/g, " ");
35
+ }
28
36
  export default function DataRequestsPage(props) {
29
- return (_jsx(AdminShell, { ...props, active: "dataRequests", children: _jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Data Requests" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every GDPR export, mailbox-import, and account-erasure request across every mailbox." })] }), _jsx(ExportRequestsSection, {}), _jsx(ImportRequestsSection, {}), _jsx(ErasureRequestsSection, {})] }) }));
37
+ return (_jsx(AdminShell, { ...props, active: "dataRequests", children: _jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Data Requests" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every GDPR export, mailbox-import, and account-erasure request across every mailbox, newest first." })] }), _jsx(ExportRequestsSection, {}), _jsx(ImportRequestsSection, {}), _jsx(ErasureRequestsSection, {})] }) }));
30
38
  }
31
39
  function ExportRequestsSection() {
32
- const [requests, setRequests] = useState([]);
33
- const [loading, setLoading] = useState(true);
34
- const [loadError, setLoadError] = useState(null);
40
+ // The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before the
41
+ // initial mount fetch does - `usePagedList()` only ever applies the most recently started load.
42
+ const list = usePagedList(listExportRequests, "Could not load export requests.");
35
43
  const [mailboxUid, setMailboxUid] = useState("");
36
44
  const [format, setFormat] = useState("json");
37
45
  const [creating, setCreating] = useState(false);
38
46
  const [createError, setCreateError] = useState(null);
39
- // The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before
40
- // the initial mount fetch does; without this guard the mount fetch's now-stale response would land
41
- // last and silently revert the list. Only the most-recently-issued load's response is ever applied.
42
- const loadSeq = useRef(0);
43
- function loadRequests() {
44
- const seq = ++loadSeq.current;
45
- return listExportRequests()
46
- .then((data) => {
47
- if (seq === loadSeq.current)
48
- setRequests(data);
49
- })
50
- .catch((err) => {
51
- if (seq === loadSeq.current)
52
- setLoadError(err instanceof ApiRequestError ? err.message : "Could not load export requests.");
53
- });
54
- }
55
47
  useEffect(() => {
56
- loadRequests().finally(() => setLoading(false));
57
- // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ void list.reload();
58
49
  }, []);
59
50
  async function handleCreate(e) {
60
51
  e.preventDefault();
@@ -63,7 +54,7 @@ function ExportRequestsSection() {
63
54
  try {
64
55
  await createExportRequest({ format, mailboxUid: mailboxUid.trim() });
65
56
  setMailboxUid("");
66
- await loadRequests();
57
+ await list.reload();
67
58
  }
68
59
  catch (err) {
69
60
  setCreateError(err instanceof ApiRequestError ? err.message : "Could not start this export.");
@@ -72,12 +63,12 @@ function ExportRequestsSection() {
72
63
  setCreating(false);
73
64
  }
74
65
  }
75
- return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Export requests" }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-4 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Export mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: INPUT_CLASS, value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating || !mailboxUid.trim(), children: "Create export" })] }), createError && _jsx(Alert, { children: createError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.mailboxUid, " \u00B7 ", request.format.toUpperCase(), " \u00B7", " ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
66
+ return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Export requests" }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-4 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Export mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: INPUT_CLASS, value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating || !mailboxUid.trim(), children: "Create export" })] }), createError && _jsx(Alert, { children: createError }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.mailboxUid, " \u00B7 ", request.format.toUpperCase(), " \u00B7", " ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more export requests" })] }));
76
67
  }
77
68
  function ImportRequestsSection() {
78
- const [requests, setRequests] = useState([]);
79
- const [loading, setLoading] = useState(true);
80
- const [loadError, setLoadError] = useState(null);
69
+ // Same stale-response guard as ExportRequestsSection - the Upload button is enabled independently of this
70
+ // section's own `loading`.
71
+ const list = usePagedList(listImportRequests, "Could not load import requests.");
81
72
  const [mailboxUid, setMailboxUid] = useState("");
82
73
  const [folders, setFolders] = useState([]);
83
74
  const [targetFolderUid, setTargetFolderUid] = useState("");
@@ -85,25 +76,8 @@ function ImportRequestsSection() {
85
76
  const [uploading, setUploading] = useState(false);
86
77
  const [uploadError, setUploadError] = useState(null);
87
78
  const fileInputRef = useRef(null);
88
- // Same stale-response guard as ExportRequestsSection's loadRequests() - the Upload button is enabled
89
- // independently of this section's own `loading`, so a fast upload-then-reload can resolve before the
90
- // initial mount fetch does.
91
- const loadSeq = useRef(0);
92
- function loadRequests() {
93
- const seq = ++loadSeq.current;
94
- return listImportRequests()
95
- .then((data) => {
96
- if (seq === loadSeq.current)
97
- setRequests(data);
98
- })
99
- .catch((err) => {
100
- if (seq === loadSeq.current)
101
- setLoadError(err instanceof ApiRequestError ? err.message : "Could not load import requests.");
102
- });
103
- }
104
79
  useEffect(() => {
105
- loadRequests().finally(() => setLoading(false));
106
- // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ void list.reload();
107
81
  }, []);
108
82
  function handleLoadFolders() {
109
83
  const uid = mailboxUid.trim();
@@ -130,46 +104,48 @@ function ImportRequestsSection() {
130
104
  setUploadError(null);
131
105
  setUploading(true);
132
106
  uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid.trim() })
133
- .then(() => loadRequests())
107
+ .then(() => list.reload())
134
108
  .catch((err) => setUploadError(err instanceof ApiRequestError ? err.message : "Could not upload this file."))
135
109
  .finally(() => setUploading(false));
136
110
  }
137
- return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Import requests" }), _jsxs("div", { className: "flex items-end gap-3 mb-2 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Import mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value), onBlur: handleLoadFolders })] }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Import destination folder", className: INPUT_CLASS, value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange })] }), folderError && _jsx(Alert, { children: folderError }), uploadError && _jsx(Alert, { children: uploadError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.mailboxUid, " \u00B7 ", request.format.toUpperCase(), " \u00B7", " ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status })] }, request.uid))) }))] }));
111
+ return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Import requests" }), _jsxs("div", { className: "flex items-end gap-3 mb-2 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Import mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value), onBlur: handleLoadFolders })] }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Import destination folder", className: INPUT_CLASS, value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange })] }), folderError && _jsx(Alert, { children: folderError }), uploadError && _jsx(Alert, { children: uploadError }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.mailboxUid, " \u00B7 ", request.format.toUpperCase(), " \u00B7", " ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more import requests" })] }));
138
112
  }
139
113
  function ErasureRequestsSection() {
140
- const [requests, setRequests] = useState([]);
141
- const [loading, setLoading] = useState(true);
142
- const [loadError, setLoadError] = useState(null);
143
- const [actionErrors, setActionErrors] = useState({});
144
- const [actingOn, setActingOn] = useState({});
114
+ const list = usePagedList(listErasureRequests, "Could not load erasure requests.");
115
+ const [approveTarget, setApproveTarget] = useState(null);
116
+ const [approving, setApproving] = useState(false);
117
+ const [approveError, setApproveError] = useState(null);
145
118
  const [denyTarget, setDenyTarget] = useState(null);
146
119
  const [denyReason, setDenyReason] = useState("");
147
120
  const [denying, setDenying] = useState(false);
148
121
  const [denyError, setDenyError] = useState(null);
149
- function loadRequests() {
150
- return listErasureRequests()
151
- .then(setRequests)
152
- .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load erasure requests."));
153
- }
154
122
  useEffect(() => {
155
- loadRequests().finally(() => setLoading(false));
156
- // eslint-disable-next-line react-hooks/exhaustive-deps
123
+ void list.reload();
157
124
  }, []);
158
- async function handleApprove(request) {
159
- setActingOn((prev) => ({ ...prev, [request.uid]: true }));
160
- setActionErrors((prev) => ({ ...prev, [request.uid]: "" }));
125
+ function closeApproveModal() {
126
+ setApproveTarget(null);
127
+ setApproveError(null);
128
+ }
129
+ // Escape/Close are ignored while a decision is being sent, so its result (or error) can't land on a modal
130
+ // reopened for a different request.
131
+ function dismissApproveModal() {
132
+ if (!approving)
133
+ closeApproveModal();
134
+ }
135
+ async function handleApprove() {
136
+ setApproveError(null);
137
+ setApproving(true);
161
138
  try {
162
- await approveErasureRequest(request.uid);
163
- await loadRequests();
139
+ // Only invoked from the approve modal below, which only renders once approveTarget is set.
140
+ await approveErasureRequest(approveTarget.uid);
141
+ closeApproveModal();
142
+ await list.reload();
164
143
  }
165
144
  catch (err) {
166
- setActionErrors((prev) => ({
167
- ...prev,
168
- [request.uid]: err instanceof ApiRequestError ? err.message : "Could not approve this request.",
169
- }));
145
+ setApproveError(err instanceof ApiRequestError ? err.message : "Could not approve this request.");
170
146
  }
171
147
  finally {
172
- setActingOn((prev) => ({ ...prev, [request.uid]: false }));
148
+ setApproving(false);
173
149
  }
174
150
  }
175
151
  function closeDenyModal() {
@@ -177,6 +153,10 @@ function ErasureRequestsSection() {
177
153
  setDenyReason("");
178
154
  setDenyError(null);
179
155
  }
156
+ function dismissDenyModal() {
157
+ if (!denying)
158
+ closeDenyModal();
159
+ }
180
160
  async function handleDeny() {
181
161
  setDenyError(null);
182
162
  setDenying(true);
@@ -184,7 +164,7 @@ function ErasureRequestsSection() {
184
164
  // Only invoked from the deny modal below, which only renders once denyTarget is set.
185
165
  await denyErasureRequest(denyTarget.uid, denyReason);
186
166
  closeDenyModal();
187
- await loadRequests();
167
+ await list.reload();
188
168
  }
189
169
  catch (err) {
190
170
  setDenyError(err instanceof ApiRequestError ? err.message : "Could not deny this request.");
@@ -193,5 +173,5 @@ function ErasureRequestsSection() {
193
173
  setDenying(false);
194
174
  }
195
175
  }
196
- return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Self-service only \u2014 a mailbox owner requests their own account's erasure; there is no admin-initiated path here, only review." }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No erasure requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-sm", children: [request.mailboxUid, " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] })), request.status === "completed" && request.purgedCount !== undefined && (_jsxs("span", { className: "text-text-muted", children: [" \u2014 ", request.purgedCount, " records purged"] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), request.status === "pending" && (_jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => handleApprove(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: actingOn[request.uid], onClick: () => setDenyTarget(request), children: "Deny" })] }))] }, request.uid))) })), _jsxs(Modal, { open: denyTarget !== null, onClose: closeDenyModal, title: "Deny erasure request", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Denial reason", className: INPUT_CLASS, value: denyReason, onChange: (e) => setDenyReason(e.target.value) })] }), denyError && _jsx(Alert, { children: denyError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: denying, onClick: closeDenyModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: denying, disabled: denying || !denyReason.trim(), onClick: handleDeny, children: "Deny" })] })] })] }));
176
+ return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Self-service only \u2014 a mailbox owner requests their own account's erasure; there is no admin-initiated path here, only review." }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No erasure requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-sm", children: [request.mailboxUid, " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] })), request.status === "completed" && request.purgedCount !== undefined && (_jsxs("span", { className: "text-text-muted", children: [" \u2014 ", request.purgedCount, " records purged"] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }), request.status === "pending" && (_jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(request), children: "Deny" })] }))] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more erasure requests" }), _jsxs(Modal, { open: approveTarget !== null, onClose: dismissApproveModal, title: "Approve erasure request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Approving permanently erases the mailbox ", _jsx("strong", { className: "break-all", children: approveTarget?.mailboxUid }), " ", "and everything in it - mail, contacts, calendars, tasks, and notes."] }), _jsx("p", { className: "text-sm font-semibold text-danger mb-4", children: "This is irreversible and cannot be undone." }), approveError && _jsx(Alert, { children: approveError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: approving, onClick: closeApproveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: approving, disabled: approving, onClick: handleApprove, children: "Erase mailbox" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: dismissDenyModal, title: "Deny erasure request", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Denial reason", className: INPUT_CLASS, value: denyReason, onChange: (e) => setDenyReason(e.target.value) })] }), denyError && _jsx(Alert, { children: denyError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: denying, onClick: closeDenyModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: denying, disabled: denying || !denyReason.trim(), onClick: handleDeny, children: "Deny" })] })] })] }));
197
177
  }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function DistributionListDetailPage(props: Omit<AdminShellProps, "active"> & {
4
+ params: {
5
+ uid: string;
6
+ };
7
+ }): React.JSX.Element;
@@ -30,5 +30,5 @@ function DistributionListDetailContent({ uid }) {
30
30
  if (error || !list) {
31
31
  return _jsx(Alert, { children: error ?? "Distribution list not found." });
32
32
  }
33
- return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }), _jsx("dd", { children: list.restrictSenders ? "Only members may send" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(list.dateCreated).toLocaleString() })] }) }), _jsx(MemberListCard, { list: list, onUpdate: setList })] }));
33
+ return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }), _jsxs("dd", { children: [list.restrictSenders ? "Only members may send" : "No", list.restrictSenders ? (_jsxs("p", { role: "status", className: "mt-1.5 py-2 px-3 rounded-sm text-xs bg-warning/15 text-text", children: ["Members are recognized only by a verified (DKIM) signature, which requires the mail server\u2019s Authentication-Results configuration (the trusted authserv id,", " ", _jsx("code", { children: "mail:security:trusted_authserv_id" }), "). If that isn\u2019t configured, all mail sent to this list is dropped."] })) : (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Restricting senders to members requires the mail server\u2019s Authentication-Results configuration (the trusted authserv id); without it, all mail to the list is dropped." }))] }), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(list.dateCreated).toLocaleString() })] }) }), _jsx(MemberListCard, { list: list, onUpdate: setList })] }));
34
34
  }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function DistributionListsPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
3
+ export default function NewDistributionListPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function DomainDetailPage(props: Omit<AdminShellProps, "active"> & {
4
+ params: {
5
+ uid: string;
6
+ };
7
+ }): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function DomainsListPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
3
+ export default function NewDomainPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
3
+ export default function EncryptionPolicyPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { EscrowScope, EscrowScopePublicKey } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
3
+ import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
4
+ import { EscrowScopeKeyAndHoldersValue } from "../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
5
+ /**
6
+ * Describes every change to the security-relevant parts of a scope - who holds it, how many holders must approve,
7
+ * and its public key - so the administrator confirms exactly what they're changing before it's saved.
8
+ */
9
+ export declare function describeSecurityChanges(original: EscrowScope, next: EscrowScopeKeyAndHoldersValue): string[];
10
+ /**
11
+ * The `publicKey` to send with an update, or `undefined` when the admin didn't touch any key field - the form only
12
+ * shows dates to the minute, so re-sending a rebuilt key would silently truncate the stored validity window. A date
13
+ * field left as loaded keeps its original epoch-ms value. `revokedAt` isn't editable here: it's carried over when
14
+ * only the validity window changed, and dropped when the key material itself is replaced (a new key isn't revoked).
15
+ */
16
+ export declare function buildPublicKeyUpdate(original: EscrowScope, next: EscrowScopeKeyAndHoldersValue): EscrowScopePublicKey | undefined;
17
+ export default function EscrowScopeDetailPage(props: Omit<AdminShellProps, "active"> & {
18
+ params: {
19
+ uid: string;
20
+ };
21
+ }): React.JSX.Element;
@@ -8,7 +8,7 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
9
9
  import { deleteEscrowScope, getEscrowScope, updateEscrowScope, } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
10
10
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
11
- import EscrowScopeKeyAndHoldersFields from "../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
11
+ import EscrowScopeKeyAndHoldersFields, { selfAsHolderError, } from "../../shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js";
12
12
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
13
13
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
14
14
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
@@ -26,10 +26,75 @@ function toFieldsValue(scope) {
26
26
  notifySubjectOnAccess: scope.notifySubjectOnAccess,
27
27
  };
28
28
  }
29
+ /**
30
+ * Describes every change to the security-relevant parts of a scope - who holds it, how many holders must approve,
31
+ * and its public key - so the administrator confirms exactly what they're changing before it's saved.
32
+ */
33
+ export function describeSecurityChanges(original, next) {
34
+ const changes = [];
35
+ const added = next.holderUserUids.filter((holder) => !original.holderUserUids.includes(holder));
36
+ const removed = original.holderUserUids.filter((holder) => !next.holderUserUids.includes(holder));
37
+ if (added.length > 0) {
38
+ changes.push(`Add holders: ${added.join(", ")}`);
39
+ }
40
+ if (removed.length > 0) {
41
+ changes.push(`Remove holders: ${removed.join(", ")}`);
42
+ }
43
+ if (next.requiredHolders !== original.requiredHolders) {
44
+ changes.push(`Required approvals: ${original.requiredHolders} → ${next.requiredHolders}`);
45
+ }
46
+ if (next.notifySubjectOnAccess !== original.notifySubjectOnAccess) {
47
+ const onOff = (value) => (value ? "on" : "off");
48
+ changes.push(`Notify subject on access: ${onOff(original.notifySubjectOnAccess)} → ${onOff(next.notifySubjectOnAccess)}`);
49
+ }
50
+ const originalKey = toFieldsValue(original);
51
+ if (next.publicKey.trim() !== originalKey.publicKey || next.keyType.trim() !== originalKey.keyType) {
52
+ changes.push("Replace the public key");
53
+ }
54
+ if (next.fingerprint.trim() !== originalKey.fingerprint) {
55
+ changes.push(`Fingerprint: ${originalKey.fingerprint} → ${next.fingerprint.trim()}`);
56
+ }
57
+ if (validityChanged(originalKey, next)) {
58
+ changes.push(`Key validity: ${originalKey.notBefore} – ${originalKey.notAfter} → ${next.notBefore} – ${next.notAfter}`);
59
+ }
60
+ return changes;
61
+ }
62
+ function keyMaterialChanged(originalKey, next) {
63
+ return (next.publicKey.trim() !== originalKey.publicKey ||
64
+ next.keyType.trim() !== originalKey.keyType ||
65
+ next.fingerprint.trim() !== originalKey.fingerprint);
66
+ }
67
+ function validityChanged(originalKey, next) {
68
+ return next.notBefore !== originalKey.notBefore || next.notAfter !== originalKey.notAfter;
69
+ }
70
+ /**
71
+ * The `publicKey` to send with an update, or `undefined` when the admin didn't touch any key field - the form only
72
+ * shows dates to the minute, so re-sending a rebuilt key would silently truncate the stored validity window. A date
73
+ * field left as loaded keeps its original epoch-ms value. `revokedAt` isn't editable here: it's carried over when
74
+ * only the validity window changed, and dropped when the key material itself is replaced (a new key isn't revoked).
75
+ */
76
+ export function buildPublicKeyUpdate(original, next) {
77
+ const originalKey = toFieldsValue(original);
78
+ const materialChanged = keyMaterialChanged(originalKey, next);
79
+ if (!materialChanged && !validityChanged(originalKey, next)) {
80
+ return undefined;
81
+ }
82
+ const key = {
83
+ publicKey: next.publicKey.trim(),
84
+ type: next.keyType.trim(),
85
+ fingerprint: next.fingerprint.trim(),
86
+ notBefore: next.notBefore === originalKey.notBefore ? original.publicKey.notBefore : new Date(next.notBefore).getTime(),
87
+ notAfter: next.notAfter === originalKey.notAfter ? original.publicKey.notAfter : new Date(next.notAfter).getTime(),
88
+ };
89
+ if (!materialChanged && original.publicKey.revokedAt !== undefined) {
90
+ key.revokedAt = original.publicKey.revokedAt;
91
+ }
92
+ return key;
93
+ }
29
94
  export default function EscrowScopeDetailPage(props) {
30
- return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(EscrowScopeDetailContent, { uid: props.params.uid }) }));
95
+ return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(EscrowScopeDetailContent, { uid: props.params.uid, adminUid: props.userUid }) }));
31
96
  }
32
- function EscrowScopeDetailContent({ uid }) {
97
+ function EscrowScopeDetailContent({ uid, adminUid }) {
33
98
  const [original, setOriginal] = useState(null);
34
99
  const [name, setName] = useState("");
35
100
  const [description, setDescription] = useState("");
@@ -41,6 +106,7 @@ function EscrowScopeDetailContent({ uid }) {
41
106
  const [confirmingDelete, setConfirmingDelete] = useState(false);
42
107
  const [deleting, setDeleting] = useState(false);
43
108
  const [deleteError, setDeleteError] = useState(null);
109
+ const [pendingChanges, setPendingChanges] = useState(null);
44
110
  useEffect(() => {
45
111
  setLoading(true);
46
112
  setError(null);
@@ -79,6 +145,20 @@ function EscrowScopeDetailContent({ uid }) {
79
145
  setError("Required holders must be between 1 and the number of holders.");
80
146
  return;
81
147
  }
148
+ const selfError = selfAsHolderError(fields.holderUserUids, adminUid, original.holderUserUids);
149
+ if (selfError) {
150
+ setError(selfError);
151
+ return;
152
+ }
153
+ const changes = describeSecurityChanges(original, fields);
154
+ if (changes.length > 0) {
155
+ setPendingChanges(changes);
156
+ return;
157
+ }
158
+ await save();
159
+ }
160
+ async function save() {
161
+ setPendingChanges(null);
82
162
  setSaving(true);
83
163
  setSaved(false);
84
164
  try {
@@ -86,14 +166,10 @@ function EscrowScopeDetailContent({ uid }) {
86
166
  uid: original.uid,
87
167
  version: original.version,
88
168
  name: name.trim(),
89
- description: description.trim() || undefined,
90
- publicKey: {
91
- publicKey: fields.publicKey.trim(),
92
- type: fields.keyType.trim(),
93
- fingerprint: fields.fingerprint.trim(),
94
- notBefore: new Date(fields.notBefore).getTime(),
95
- notAfter: new Date(fields.notAfter).getTime(),
96
- },
169
+ // `null` (not omitted) so clearing the field actually clears the stored description - an update is
170
+ // a merge, and an absent field is left as it was.
171
+ description: (description.trim() || null),
172
+ publicKey: buildPublicKeyUpdate(original, fields),
97
173
  holderUserUids: fields.holderUserUids,
98
174
  requiredHolders: fields.requiredHolders,
99
175
  notifySubjectOnAccess: fields.notifySubjectOnAccess,
@@ -109,7 +185,10 @@ function EscrowScopeDetailContent({ uid }) {
109
185
  setSaving(false);
110
186
  }
111
187
  }
188
+ // Ignored while deleting (Escape/Close; Cancel is disabled then too).
112
189
  function closeDeleteModal() {
190
+ if (deleting)
191
+ return;
113
192
  setConfirmingDelete(false);
114
193
  }
115
194
  // Only ever invoked from the delete-confirmation modal below, which itself only renders once
@@ -137,5 +216,5 @@ function EscrowScopeDetailContent({ uid }) {
137
216
  if (!original || !fields) {
138
217
  return _jsx(Alert, { children: "Escrow scope not found." });
139
218
  }
140
- return (_jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 mb-1", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/escrow-scopes", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All escrow scopes" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: original.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete scope" })] }), 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-5 mt-4", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("input", { id: "description", type: "text", className: INPUT_CLASS, value: description, onChange: (e) => setDescription(e.target.value) }) })] }), _jsx(EscrowScopeKeyAndHoldersFields, { value: fields, onChange: setFields }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete escrow scope", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: original.name }), "? This cannot be undone, and fails if any Matter still references this scope."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] })] }));
219
+ return (_jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 mb-1", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/escrow-scopes", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All escrow scopes" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: original.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete scope" })] }), 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-5 mt-4", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("input", { id: "description", type: "text", className: INPUT_CLASS, value: description, onChange: (e) => setDescription(e.target.value) }) })] }), _jsx(EscrowScopeKeyAndHoldersFields, { value: fields, onChange: setFields }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] }), _jsxs(Modal, { open: pendingChanges !== null, onClose: () => setPendingChanges(null), title: "Confirm escrow scope changes", children: [_jsxs("p", { className: "text-sm mb-3", children: ["These changes decide who can recover escrowed mail under ", _jsx("strong", { children: original.name }), ":"] }), _jsx("ul", { className: "list-disc pl-5 text-sm flex flex-col gap-1 mb-3", children: pendingChanges?.map((change) => (_jsx("li", { className: "break-all", children: change }, change))) }), _jsx("p", { className: "text-xs text-text-muted", children: "The server refuses changes while an access request under this scope is still awaiting approval." }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingChanges(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void save(), children: "Confirm and save" })] })] }), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete escrow scope", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: original.name }), "? This cannot be undone, and fails if any Matter still references this scope."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] })] }));
141
220
  }