@rapidmx/web-client 0.15.1 → 0.17.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 (318) hide show
  1. package/README.md +381 -373
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -480
  4. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  5. package/apps/admin/domains/[uid].tsx +166 -164
  6. package/apps/admin/domains/new/index.tsx +3 -1
  7. package/apps/admin/escrow-scopes/[uid].tsx +350 -348
  8. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  9. package/apps/admin/index.tsx +129 -124
  10. package/apps/admin/mailboxes/[uid].tsx +271 -224
  11. package/apps/admin/mailboxes/new/index.tsx +30 -28
  12. package/apps/admin/plugins/index.tsx +15 -15
  13. package/apps/admin/retention-policy/index.tsx +39 -39
  14. package/apps/admin/signing-certificates/index.tsx +343 -343
  15. package/apps/admin/transport-rules/new/index.tsx +3 -1
  16. package/apps/escrow/audit-log/index.tsx +196 -196
  17. package/apps/escrow/matters/[uid].tsx +621 -621
  18. package/apps/escrow/matters/new/index.tsx +3 -1
  19. package/apps/shared/auth/adminAccess.ts +99 -99
  20. package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
  21. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
  22. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  23. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
  24. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  33. package/apps/shared/components/calendar/EventEditor.tsx +5 -10
  34. package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
  35. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  36. package/apps/shared/components/calendar/GuestInput.tsx +100 -0
  37. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  38. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  39. package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
  40. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  41. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  42. package/apps/shared/components/calendar/allDay.ts +124 -124
  43. package/apps/shared/components/calendar/eventForm.ts +2 -2
  44. package/apps/shared/components/calendar/eventFormat.ts +49 -22
  45. package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
  46. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  47. package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
  48. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
  49. package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
  50. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  51. package/apps/shared/components/layout/AppShell.tsx +461 -480
  52. package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
  53. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  54. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  55. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  56. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  57. package/apps/shared/components/layout/UserMenu.tsx +439 -424
  58. package/apps/shared/components/mail/ConversationList.tsx +323 -323
  59. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
  60. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  61. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
  62. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
  64. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  65. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  66. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  67. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
  68. package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
  69. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  70. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  71. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  72. package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
  73. package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
  74. package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
  75. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  76. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  77. package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
  78. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  79. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  80. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  81. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  82. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  83. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  84. package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
  85. package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
  86. package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
  87. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  88. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  89. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  90. package/apps/shared/keyboard/dispatch.ts +124 -124
  91. package/apps/shared/keyboard/format.ts +89 -89
  92. package/apps/shared/keyboard/keymap.ts +114 -114
  93. package/apps/shared/keyboard/registry.ts +65 -65
  94. package/apps/shared/keyboard/targets.ts +79 -79
  95. package/apps/shared/mail/folderCounts.ts +17 -0
  96. package/apps/shared/mail/folderOfType.ts +49 -0
  97. package/apps/shared/mail/folderTree.ts +143 -143
  98. package/apps/shared/mail/listAllPages.ts +39 -39
  99. package/apps/shared/mail/newMailNotifications.ts +171 -171
  100. package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
  101. package/apps/shared/mail/outbox/sendJob.ts +19 -1
  102. package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
  103. package/apps/shared/mail/permanentDelete.ts +160 -0
  104. package/apps/shared/mail/primaryMailbox.ts +71 -0
  105. package/apps/shared/mail/reportNotices.ts +66 -0
  106. package/apps/shared/mail/senderBlocking.ts +141 -0
  107. package/apps/shared/mail/useCollapsedSections.ts +146 -0
  108. package/apps/shared/mail/useMailConnection.ts +205 -203
  109. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  110. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  111. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  112. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  113. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  114. package/apps/shared/navigation/frameContext.tsx +1 -1
  115. package/apps/shared/navigation/index.ts +43 -0
  116. package/apps/shared/navigation/pageTitle.ts +19 -0
  117. package/apps/shared/notifications/store.ts +560 -560
  118. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  119. package/apps/shared/search/crossMailboxSearch.ts +169 -0
  120. package/apps/shared/search/localIndexBuilder.ts +481 -481
  121. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  122. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  123. package/apps/shared/signing/enrollmentView.ts +251 -251
  124. package/apps/shared/signing/useNow.ts +19 -19
  125. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  126. package/apps/shared/styles/app.css +396 -386
  127. package/apps/www/_shell.tsx +82 -0
  128. package/apps/www/calendar/index.tsx +581 -571
  129. package/apps/www/contacts/[uid].tsx +112 -109
  130. package/apps/www/contacts/index.tsx +14 -3
  131. package/apps/www/index.tsx +2962 -2479
  132. package/apps/www/messages/[uid].tsx +139 -135
  133. package/apps/www/settings/appearance/index.tsx +5 -2
  134. package/apps/www/settings/auto-reply/index.tsx +136 -133
  135. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  136. package/apps/www/settings/encryption/index.tsx +1290 -1287
  137. package/apps/www/settings/filters/[uid].tsx +179 -176
  138. package/apps/www/settings/filters/index.tsx +105 -102
  139. package/apps/www/settings/filters/new/index.tsx +165 -140
  140. package/apps/www/settings/labels/index.tsx +207 -204
  141. package/apps/www/settings/privacy/index.tsx +495 -492
  142. package/apps/www/settings/profile/index.tsx +251 -248
  143. package/apps/www/settings/read-receipts/index.tsx +150 -147
  144. package/apps/www/settings/sharing/index.tsx +259 -256
  145. package/apps/www/settings/signatures/[uid].tsx +175 -172
  146. package/apps/www/settings/signatures/index.tsx +91 -88
  147. package/apps/www/settings/signatures/new/index.tsx +138 -135
  148. package/apps/www/tasks/index.tsx +654 -649
  149. package/dist/apps/admin/data-requests/index.js +1 -1
  150. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  151. package/dist/apps/admin/domains/[uid].js +3 -1
  152. package/dist/apps/admin/domains/new/index.js +3 -1
  153. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  154. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  155. package/dist/apps/admin/index.js +5 -2
  156. package/dist/apps/admin/mailboxes/[uid].js +15 -3
  157. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  158. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  159. package/dist/apps/escrow/matters/new/index.js +3 -1
  160. package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
  161. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
  163. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
  165. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  166. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
  167. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  168. package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
  169. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
  170. package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
  171. package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
  172. package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
  173. package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
  174. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
  175. package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
  176. package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
  177. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
  178. package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
  179. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
  180. package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
  181. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
  182. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  183. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  184. package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
  185. package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
  186. package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
  187. package/dist/apps/shared/components/layout/UserMenu.js +6 -5
  188. package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
  189. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  190. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  191. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
  192. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
  193. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  194. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  195. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  196. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  197. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  198. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  199. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
  200. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
  201. package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
  202. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
  203. package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
  204. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
  205. package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
  206. package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
  207. package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
  208. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  209. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  210. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  211. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  212. package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
  213. package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
  214. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  215. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  216. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  217. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  218. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  219. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  220. package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
  221. package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
  222. package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
  223. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  224. package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
  225. package/dist/apps/shared/mail/folderCounts.js +12 -0
  226. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  227. package/dist/apps/shared/mail/folderOfType.js +40 -0
  228. package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
  229. package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
  230. package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
  231. package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
  232. package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
  233. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  234. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  235. package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
  236. package/dist/apps/shared/mail/primaryMailbox.js +61 -0
  237. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  238. package/dist/apps/shared/mail/reportNotices.js +47 -0
  239. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  240. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  241. package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
  242. package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
  243. package/dist/apps/shared/mail/useMailConnection.js +3 -1
  244. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  245. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  246. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  247. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  248. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  249. package/dist/apps/shared/navigation/index.d.ts +16 -0
  250. package/dist/apps/shared/navigation/index.js +34 -0
  251. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  252. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  253. package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
  254. package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
  255. package/dist/apps/shared/styles/app.css +396 -386
  256. package/dist/apps/www/_shell.d.ts +26 -0
  257. package/dist/apps/www/_shell.js +31 -0
  258. package/dist/apps/www/calendar/index.d.ts +7 -2
  259. package/dist/apps/www/calendar/index.js +7 -4
  260. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  261. package/dist/apps/www/contacts/[uid].js +5 -3
  262. package/dist/apps/www/contacts/index.d.ts +7 -2
  263. package/dist/apps/www/contacts/index.js +8 -4
  264. package/dist/apps/www/index.d.ts +7 -2
  265. package/dist/apps/www/index.js +542 -207
  266. package/dist/apps/www/messages/[uid].d.ts +14 -3
  267. package/dist/apps/www/messages/[uid].js +7 -5
  268. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  269. package/dist/apps/www/settings/appearance/index.js +4 -2
  270. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  271. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  272. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  273. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  274. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  275. package/dist/apps/www/settings/encryption/index.js +4 -2
  276. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  277. package/dist/apps/www/settings/filters/[uid].js +4 -2
  278. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  279. package/dist/apps/www/settings/filters/index.js +4 -2
  280. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  281. package/dist/apps/www/settings/filters/new/index.js +28 -5
  282. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  283. package/dist/apps/www/settings/labels/index.js +4 -2
  284. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  285. package/dist/apps/www/settings/privacy/index.js +4 -2
  286. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  287. package/dist/apps/www/settings/profile/index.js +4 -2
  288. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  289. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  290. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  291. package/dist/apps/www/settings/sharing/index.js +4 -2
  292. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  293. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  294. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  295. package/dist/apps/www/settings/signatures/index.js +4 -2
  296. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  297. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  298. package/dist/apps/www/tasks/index.d.ts +7 -2
  299. package/dist/apps/www/tasks/index.js +5 -3
  300. package/package.json +4 -2
  301. package/apps/shared/navigation/AppRouter.tsx +0 -300
  302. package/apps/shared/navigation/idle.ts +0 -45
  303. package/apps/shared/navigation/routerContext.tsx +0 -83
  304. package/apps/shared/navigation/routes.ts +0 -72
  305. package/apps/www/_routedPage.tsx +0 -24
  306. package/apps/www/_routes.ts +0 -37
  307. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  308. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  309. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  310. package/dist/apps/shared/navigation/idle.js +0 -43
  311. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  312. package/dist/apps/shared/navigation/routerContext.js +0 -56
  313. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  314. package/dist/apps/shared/navigation/routes.js +0 -37
  315. package/dist/apps/www/_routedPage.d.ts +0 -12
  316. package/dist/apps/www/_routedPage.js +0 -19
  317. package/dist/apps/www/_routes.d.ts +0 -11
  318. package/dist/apps/www/_routes.js +0 -31
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { listLeftoverMailboxes } from "@rapidmx/react-shared/admin/leftoverMailboxApi.js";
9
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
+ import EraseLeftoverDataDialog from "./EraseLeftoverDataDialog.js";
12
+ /** How many deleted mailboxes one request asks for. */
13
+ const PAGE_SIZE = 50;
14
+ /** How often the list is refreshed while an erasure is under way. */
15
+ export const LEFTOVER_REFRESH_INTERVAL_MS = 5000;
16
+ const HEADING_CLASS = "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border";
17
+ const CELL_CLASS = "py-2.5 px-2.5 border-b border-border align-middle";
18
+ /** Whether the erasure the server last filed for `item` is still to finish. */
19
+ function isErasing(item) {
20
+ return item.erasure?.status === "approved" || item.erasure?.status === "in_progress";
21
+ }
22
+ /**
23
+ * "Deleted mailboxes with remaining data": deleting a mailbox keeps everything in it, and the address can't be used again until
24
+ * that is erased, so this lists what is left (from `GET /mailboxes/leftover`) with an "Erase data" action for each. It shows nothing
25
+ * at all while there is nothing left - the section is only there when it is needed. An erasure that is under way shows as such, and
26
+ * the list refreshes itself until it has finished.
27
+ */
28
+ export default function LeftoverMailboxesSection({ refreshIntervalMs = LEFTOVER_REFRESH_INTERVAL_MS, pollIntervalMs }) {
29
+ const [items, setItems] = useState([]);
30
+ const [next, setNext] = useState(undefined);
31
+ const [error, setError] = useState(null);
32
+ const [loadingMore, setLoadingMore] = useState(false);
33
+ const [target, setTarget] = useState(null);
34
+ // Only the newest reload is applied: a slow earlier one can't put back what a later one already removed.
35
+ const generation = useRef(0);
36
+ async function reload() {
37
+ const current = ++generation.current;
38
+ try {
39
+ const page = await listLeftoverMailboxes({ limit: PAGE_SIZE });
40
+ if (current !== generation.current)
41
+ return;
42
+ setItems(page.items);
43
+ setNext(page.next);
44
+ setError(null);
45
+ }
46
+ catch (err) {
47
+ if (current === generation.current) {
48
+ setError(err instanceof ApiRequestError ? err.message : "Could not check for deleted mailboxes with remaining data.");
49
+ }
50
+ }
51
+ }
52
+ async function loadMore() {
53
+ const current = generation.current;
54
+ setLoadingMore(true);
55
+ try {
56
+ const page = await listLeftoverMailboxes({ limit: PAGE_SIZE, after: next });
57
+ if (current !== generation.current)
58
+ return;
59
+ setItems((existing) => [...existing, ...page.items.filter((item) => !existing.some((seen) => seen.mailboxUid === item.mailboxUid))]);
60
+ setNext(page.next);
61
+ }
62
+ catch (err) {
63
+ if (current === generation.current) {
64
+ setError(err instanceof ApiRequestError ? err.message : "Could not load more deleted mailboxes.");
65
+ }
66
+ }
67
+ finally {
68
+ setLoadingMore(false);
69
+ }
70
+ }
71
+ useEffect(() => {
72
+ void reload();
73
+ }, []);
74
+ const anyErasing = items.some(isErasing);
75
+ useEffect(() => {
76
+ if (!anyErasing) {
77
+ return;
78
+ }
79
+ const timer = setInterval(() => void reload(), refreshIntervalMs);
80
+ return () => clearInterval(timer);
81
+ }, [anyErasing, refreshIntervalMs]);
82
+ // Only there when something is left (or the check failed).
83
+ const section = items.length > 0 || error !== null ? (_jsxs("section", { className: "mt-10", "aria-labelledby": "leftover-heading", children: [_jsx("h2", { id: "leftover-heading", className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-2", children: "Deleted mailboxes with remaining data" }), _jsx("p", { className: "text-sm text-text-muted mb-3", children: "Deleting a mailbox keeps everything in it, and its address can\u2019t be used for a new mailbox until that data is erased. Erasing is permanent." }), error && _jsx(Alert, { children: error }), items.length > 0 && (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("caption", { className: "sr-only", children: "Deleted mailboxes with remaining data" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: HEADING_CLASS, children: "Address" }), _jsx("th", { scope: "col", className: HEADING_CLASS, children: "Folders" }), _jsx("th", { scope: "col", className: HEADING_CLASS, children: "Messages" }), _jsx("th", { scope: "col", className: HEADING_CLASS, children: "Erasure" }), _jsx("th", { scope: "col", className: "border-b border-border", children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { children: items.map((item) => (_jsxs("tr", { children: [_jsx("td", { className: `${CELL_CLASS} break-all`, children: item.mailboxUid }), _jsx("td", { className: CELL_CLASS, children: item.folderCount }), _jsx("td", { className: CELL_CLASS, children: item.messageCount }), _jsx("td", { className: CELL_CLASS, children: isErasing(item) ? (_jsx("span", { className: "inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Erasing" })) : (_jsx("span", { className: "text-text-muted", children: "Not erased" })) }), _jsx("td", { className: `${CELL_CLASS} text-right`, children: _jsx(Button, { type: "button", variant: "secondary", className: isErasing(item) ? "!w-auto" : "!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger", "aria-label": `${isErasing(item) ? "Show the erasure of" : "Erase data of"} ${item.mailboxUid}`, onClick: () => setTarget(item), children: isErasing(item) ? "Show progress" : "Erase data" }) })] }, item.mailboxUid))) })] }) })), next !== undefined && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mt-3", loading: loadingMore, disabled: loadingMore, onClick: () => void loadMore(), children: "Load more" }))] })) : null;
84
+ // The dialog outlives the row it was opened from - erasing the last one empties the list while its "erased" message is still up - and
85
+ // keeps its place among the fragment's children whether or not the section is there, so it is not rebuilt (and does not lose where it
86
+ // is) when the section goes.
87
+ const dialog = target && (_jsx(EraseLeftoverDataDialog, { address: target.mailboxUid, counts: { folders: target.folderCount, messages: target.messageCount }, resume: isErasing(target) && target.erasure ? { uid: target.erasure.uid, status: target.erasure.status } : undefined, pollIntervalMs: pollIntervalMs, onErased: () => void reload(), onClose: () => {
88
+ setTarget(null);
89
+ void reload();
90
+ } }, target.mailboxUid));
91
+ return (_jsxs(_Fragment, { children: [section, dialog] }));
92
+ }
@@ -12,9 +12,9 @@ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
12
12
  const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
13
13
  const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
14
14
  /** A header that uses both variables - the snippet the Branding page offers to copy. */
15
- export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
16
- <span>{APP_TITLE}</span>
17
- <span>{USER_MENU}</span>
15
+ export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
16
+ <span>{APP_TITLE}</span>
17
+ <span>{USER_MENU}</span>
18
18
  </div>`;
19
19
  /** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
20
20
  export function isSvgFile(file) {
@@ -6,12 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  import { useEffect, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { getMailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.js";
9
+ import { leftoverConflictOf } from "@rapidmx/react-shared/admin/leftoverMailboxApi.js";
9
10
  import { createMailbox, listMailboxDomains, resolveMailboxOwner } from "@rapidmx/react-shared/mail/mailApi.js";
10
11
  import { deviceTimeZone } from "@rapidmx/react-shared/util/timeZone.js";
11
12
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
13
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
13
14
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
14
15
  import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
16
+ import EraseLeftoverDataDialog from "../mailboxes/EraseLeftoverDataDialog.js";
15
17
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
16
18
  const SELECT_CLASS = "text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
17
19
  /**
@@ -40,6 +42,10 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
40
42
  const [maxDurationMinutes, setMaxDurationMinutes] = useState("");
41
43
  const [error, setError] = useState(null);
42
44
  const [saving, setSaving] = useState(false);
45
+ // The address a deleted mailbox left data at, when creating a mailbox there was refused for that - and what to do about it.
46
+ const [leftover, setLeftover] = useState(null);
47
+ const [erasing, setErasing] = useState(false);
48
+ const formRef = useRef(null);
43
49
  // An empty `mail:domains` list (the default) means this server enforces no domain restriction —
44
50
  // the address field stays free text, exactly as before. Once an admin configures at least one
45
51
  // domain, every mailbox this server creates must be on one of them (enforced server-side too, in
@@ -61,10 +67,15 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
61
67
  .catch(() => undefined);
62
68
  }, []);
63
69
  const constrained = domains.length > 0;
70
+ const address = constrained ? `${localPart.trim()}@${domain}` : primarySmtpAddress.trim();
71
+ // What was said about one address is not said about another: typing a different one takes the explanation away.
72
+ useEffect(() => {
73
+ setLeftover((current) => (current && current.address !== address ? null : current));
74
+ }, [address]);
64
75
  async function handleSubmit(e) {
65
76
  e.preventDefault();
66
77
  setError(null);
67
- const address = constrained ? `${localPart.trim()}@${domain}` : primarySmtpAddress.trim();
78
+ setLeftover(null);
68
79
  if (constrained ? !localPart.trim() : !primarySmtpAddress.trim()) {
69
80
  setError("A primary SMTP address is required.");
70
81
  return;
@@ -106,13 +117,24 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
106
117
  onCreated(mailbox);
107
118
  }
108
119
  catch (err) {
109
- setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
120
+ const conflict = leftoverConflictOf(err);
121
+ if (conflict) {
122
+ // Not a plain failure: the address is taken by what a deleted mailbox left, which can be erased right here.
123
+ setLeftover({ address, conflict });
124
+ }
125
+ else {
126
+ setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
127
+ }
110
128
  }
111
129
  finally {
112
130
  setSaving(false);
113
131
  }
114
132
  }
115
- return (_jsxs(_Fragment, { children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "bg-surface border border-border rounded-md p-6", children: [constrained ? (_jsx(FormField, { label: "Local part", htmlFor: "localPart", children: _jsxs("div", { className: "flex gap-2 items-center", children: [_jsx("input", { id: "localPart", type: "text", className: INPUT_CLASS, value: localPart, onChange: (e) => setLocalPart(e.target.value), placeholder: "support" }), _jsx("span", { className: "text-text-muted shrink-0", children: "@" }), _jsx("select", { "aria-label": "Domain", className: SELECT_CLASS, value: domain, onChange: (e) => setDomain(e.target.value), children: domains.map((d) => (_jsx("option", { value: d, children: d }, d))) })] }) })) : (_jsx(FormField, { label: "Primary SMTP address", htmlFor: "primarySmtpAddress", children: _jsx("input", { id: "primarySmtpAddress", type: "email", className: INPUT_CLASS, value: primarySmtpAddress, onChange: (e) => setPrimarySmtpAddress(e.target.value), placeholder: "support@example.com" }) })), _jsx(FormField, { label: "Display name", htmlFor: "displayName", children: _jsx("input", { id: "displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Support" }) }), _jsxs("div", { className: "mb-4", children: [_jsx("span", { className: "block text-sm font-semibold mb-1.5 text-text", children: "Owner" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "radio", name: "ownerMode", checked: ownerMode === "shared", onChange: () => {
133
+ return (_jsxs(_Fragment, { children: [error && _jsx(Alert, { children: error }), leftover && (_jsxs("div", { role: "alert", className: "py-3 px-3.5 rounded-sm text-sm mb-5 bg-danger-bg text-danger", children: [_jsx("p", { className: "font-semibold mb-1", children: leftover.conflict.reason === "mailbox-data-erasing"
134
+ ? "The data left at this address is being erased."
135
+ : "This address still has data from a deleted mailbox." }), _jsx("p", { className: "mb-3", children: leftover.conflict.reason === "mailbox-data-erasing"
136
+ ? "A new mailbox can be created there once the erasure has finished."
137
+ : "Deleting a mailbox keeps everything in it, and a new mailbox can't take the address until that data is erased." }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setErasing(true), children: leftover.conflict.reason === "mailbox-data-erasing" ? "Show the erasure" : "Erase the leftover data" })] })), erasing && leftover && (_jsx(EraseLeftoverDataDialog, { address: leftover.address, resume: leftover.conflict.reason === "mailbox-data-erasing" ? leftover.conflict.erasure : undefined, doneLabel: "Create mailbox", onDone: () => formRef.current?.requestSubmit(), onClose: () => setErasing(false) }, leftover.address)), _jsxs("form", { ref: formRef, onSubmit: handleSubmit, className: "bg-surface border border-border rounded-md p-6", children: [constrained ? (_jsx(FormField, { label: "Local part", htmlFor: "localPart", children: _jsxs("div", { className: "flex gap-2 items-center", children: [_jsx("input", { id: "localPart", type: "text", className: INPUT_CLASS, value: localPart, onChange: (e) => setLocalPart(e.target.value), placeholder: "support" }), _jsx("span", { className: "text-text-muted shrink-0", children: "@" }), _jsx("select", { "aria-label": "Domain", className: SELECT_CLASS, value: domain, onChange: (e) => setDomain(e.target.value), children: domains.map((d) => (_jsx("option", { value: d, children: d }, d))) })] }) })) : (_jsx(FormField, { label: "Primary SMTP address", htmlFor: "primarySmtpAddress", children: _jsx("input", { id: "primarySmtpAddress", type: "email", className: INPUT_CLASS, value: primarySmtpAddress, onChange: (e) => setPrimarySmtpAddress(e.target.value), placeholder: "support@example.com" }) })), _jsx(FormField, { label: "Display name", htmlFor: "displayName", children: _jsx("input", { id: "displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Support" }) }), _jsxs("div", { className: "mb-4", children: [_jsx("span", { className: "block text-sm font-semibold mb-1.5 text-text", children: "Owner" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "radio", name: "ownerMode", checked: ownerMode === "shared", onChange: () => {
116
138
  setOwnerMode("shared");
117
139
  setResolvedOwner(null);
118
140
  } }), "Shared mailbox (no single owner)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "radio", name: "ownerMode", checked: ownerMode === "owned", onChange: () => setOwnerMode("owned") }), "Owned by a specific person"] }), ownerMode === "owned" &&
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useRef, useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { HiCheck } from "react-icons/hi2";
8
9
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
10
  import { getRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
@@ -49,6 +50,7 @@ function errorMessage(err, fallback) {
49
50
  * except the domain can be passed over and configured later from the admin console.
50
51
  */
51
52
  export default function SetupWizard({ userUid }) {
53
+ const { navigate } = useRouter();
52
54
  const [step, setStep] = useState(null);
53
55
  const [status, setStatus] = useState();
54
56
  const [domains, setDomains] = useState([]);
@@ -141,7 +143,7 @@ export default function SetupWizard({ userUid }) {
141
143
  // Let a step still being recorded land first, so it can't arrive after setup is marked finished.
142
144
  await progress.current.running;
143
145
  await completeSetup();
144
- window.location.href = "/admin";
146
+ void navigate("/admin");
145
147
  }
146
148
  catch (err) {
147
149
  setError(errorMessage(err, "Could not finish setup."));
@@ -131,6 +131,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
131
131
  timezone: occurrence?.timezone ?? deviceZone,
132
132
  attendees: occurrence?.attendees ?? [],
133
133
  guestDraft: "",
134
+ guestInvalid: [],
134
135
  recurrenceRule: occurrence?.recurrenceRule ?? null,
135
136
  reminderMinutes: occurrence?.reminderMinutesBeforeStart?.toString() ?? "",
136
137
  busyStatus: occurrence?.busyStatus ?? "busy",
@@ -195,12 +196,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
195
196
  }, [occurrence?.videoMeetingUid]);
196
197
  // "This event only" detaches a standalone, non-repeating copy - the series' rule doesn't apply to it.
197
198
  const editingSingleOccurrence = !!occurrence?.isRecurringOccurrence && editScope === "occurrence";
198
- dirtyRef.current = !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || hasDescriptionText(values.descriptionHtml));
199
- function addGuests(text) {
200
- const merged = mergeGuests(values.attendees, text);
201
- update({ attendees: merged.attendees, guestDraft: merged.invalid.join(" ") });
202
- return merged.invalid;
203
- }
199
+ dirtyRef.current = !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
204
200
  function updateAttendee(index, patch) {
205
201
  setValues((prev) => ({ ...prev, attendees: prev.attendees.map((a, i) => (i === index ? { ...a, ...patch } : a)) }));
206
202
  }
@@ -382,8 +378,8 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
382
378
  setError("A title is required.");
383
379
  return;
384
380
  }
385
- // An address typed into the guests field but not added yet is added now - or, not being one, stops the save.
386
- const merged = mergeGuests(values.attendees, values.guestDraft);
381
+ // An address typed into the guests field but not added yet is added now - or, not being one (nor what was already flagged there), stops the save.
382
+ const merged = mergeGuests(values.attendees, [...values.guestInvalid, values.guestDraft].join(", "), [effectiveOrganizerAddress]);
387
383
  if (merged.invalid.length > 0) {
388
384
  setError(`“${merged.invalid[0]}” isn’t a valid email address.`);
389
385
  return;
@@ -394,7 +390,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
394
390
  return;
395
391
  }
396
392
  if (values.guestDraft) {
397
- update({ attendees: merged.attendees, guestDraft: "" });
393
+ update({ attendees: merged.attendees, guestDraft: "", guestInvalid: [] });
398
394
  }
399
395
  let startDate;
400
396
  let endDate;
@@ -521,7 +517,6 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
521
517
  deviceZone,
522
518
  startWeekday: startWeekdayCode(values.allDay ? values.start : Number.isNaN(startMs) ? "" : new Date(startMs).toISOString(), values.allDay, values.timezone),
523
519
  editingSingleOccurrence,
524
- addGuests,
525
520
  updateAttendee,
526
521
  removeAttendee,
527
522
  addResource,
@@ -17,8 +17,9 @@ export declare function VideoConferencingRow({ c }: {
17
17
  c: EventFormController;
18
18
  }): React.JSX.Element;
19
19
  /**
20
- * The guests field: an address box (Enter, a comma or a semicolon adds what is typed) and what has been added - chips in the quick popover,
21
- * rows with a role, the answer so far and a remove button in the full form, which also offers rooms and equipment.
20
+ * The guests field: compose's recipient field (`GuestInput`), which looks a name up among the contacts and the server's mailboxes and lists and
21
+ * turns what is picked, typed or pasted into chips - drawn in the field itself in the quick popover. In the full form the guests are rows with a
22
+ * role, the answer so far and a remove button instead (the field then draws only what is not an address), and rooms and equipment can be added.
22
23
  */
23
24
  export declare function GuestsField({ c, compact }: {
24
25
  c: EventFormController;
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
6
6
  import { useRef, useState } from "react";
7
7
  import { HiOutlineBars3BottomLeft, HiOutlineBell, HiOutlineMapPin, HiOutlineUserGroup, HiOutlineVideoCamera, HiOutlineXMark } from "react-icons/hi2";
8
8
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
9
+ import GuestInput from "./GuestInput.js";
9
10
  import LazyDescriptionEditor from "./LazyDescriptionEditor.js";
10
11
  import RecurrenceEditor from "./RecurrenceEditor.js";
11
12
  import ResourcePicker from "./ResourcePicker.js";
@@ -31,27 +32,15 @@ export function VideoConferencingRow({ c }) {
31
32
  return (_jsxs(IconRow, { icon: _jsx(HiOutlineVideoCamera, { size: 20 }), children: [_jsxs("label", { className: "flex items-center gap-2 text-sm py-1.5", children: [_jsx("input", { type: "checkbox", checked: values.videoEnabled, onChange: (e) => c.update({ videoEnabled: e.target.checked }) }), "Add video conferencing"] }), values.videoEnabled && (_jsx("p", { className: "text-xs text-text-muted mb-1", children: "Changing attendees after enabling video conferencing won\u2019t update meeting invitees \u2014 turn this off and back on to reissue links to the current attendee list." })), c.videoError && (_jsx("p", { role: "alert", className: "text-xs text-danger mb-1", children: c.videoError })), c.videoMeetingUid && (_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: !c.joinUrl, onClick: () => window.open(c.joinUrl, "_blank", "noopener,noreferrer"), children: "Join video call" }), !c.joinUrl && (_jsx("span", { className: "text-xs text-text-muted", children: c.organizerJoinUrl === undefined ? "Loading the join link…" : "This meeting’s join link isn’t available." }))] }))] }));
32
33
  }
33
34
  /**
34
- * The guests field: an address box (Enter, a comma or a semicolon adds what is typed) and what has been added - chips in the quick popover,
35
- * rows with a role, the answer so far and a remove button in the full form, which also offers rooms and equipment.
35
+ * The guests field: compose's recipient field (`GuestInput`), which looks a name up among the contacts and the server's mailboxes and lists and
36
+ * turns what is picked, typed or pasted into chips - drawn in the field itself in the quick popover. In the full form the guests are rows with a
37
+ * role, the answer so far and a remove button instead (the field then draws only what is not an address), and rooms and equipment can be added.
36
38
  */
37
39
  export function GuestsField({ c, compact }) {
38
40
  const { values } = c;
39
- const [hint, setHint] = useState(null);
40
41
  const [pickerOpen, setPickerOpen] = useState(false);
41
42
  const pickerAnchor = useRef(null);
42
- function commit() {
43
- const invalid = c.addGuests(values.guestDraft);
44
- setHint(invalid.length > 0 ? `“${invalid[0]}” isn’t a valid email address.` : null);
45
- }
46
- return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("input", { type: "text", "aria-label": "Add guests", placeholder: "Add guests", className: compact ? QUIET_INPUT_CLASS : INPUT_CLASS, value: values.guestDraft, onChange: (e) => {
47
- setHint(null);
48
- c.update({ guestDraft: e.target.value });
49
- }, onKeyDown: (e) => {
50
- if (e.key === "Enter" || e.key === "," || e.key === ";") {
51
- e.preventDefault();
52
- commit();
53
- }
54
- } }), hint && (_jsx("p", { role: "alert", className: "text-xs text-danger", children: hint })), compact ? (values.attendees.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-1.5", children: values.attendees.map((attendee, i) => (_jsxs("li", { className: "flex items-center gap-1 text-xs rounded-full bg-surface-alt pl-2.5 pr-1 py-0.5", children: [_jsx("span", { className: "truncate max-w-[16rem]", children: attendee.address }), _jsx("button", { type: "button", onClick: () => c.removeAttendee(i), className: "w-4 h-4 flex items-center justify-center rounded-full text-text-muted hover:text-text", "aria-label": `Remove attendee ${i + 1}`, children: _jsx(HiOutlineXMark, { size: 12, "aria-hidden": "true" }) })] }, i))) }))) : (_jsxs(_Fragment, { children: [values.attendees.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: values.attendees.map((attendee, i) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "w-7 h-7 shrink-0 rounded-full bg-primary/15 text-primary-dark text-xs font-semibold flex items-center justify-center uppercase", children: (attendee.displayName || attendee.address).charAt(0) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm truncate", children: attendee.displayName || attendee.address }), _jsxs("div", { className: "text-xs text-text-muted truncate", children: [attendee.displayName ? `${attendee.address} · ` : "", RESPONSE_STATUS_LABEL[attendee.responseStatus]] })] }), _jsx("select", { className: SELECT_CLASS, value: attendee.role, onChange: (e) => c.updateAttendee(i, { role: e.target.value }), "aria-label": `Attendee role ${i + 1}`, children: ATTENDEE_ROLES.map((role) => (_jsx("option", { value: role, children: role }, role))) }), _jsx("button", { type: "button", onClick: () => c.removeAttendee(i), className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": `Remove attendee ${i + 1}`, children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }, i))) })), _jsx("button", { ref: pickerAnchor, type: "button", onClick: () => setPickerOpen(true), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add room/equipment" }), pickerOpen && (_jsx(ResourcePicker, { anchorRef: pickerAnchor, onClose: () => setPickerOpen(false), onSelect: (mailbox) => {
43
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(GuestInput, { id: "event-guests", label: "Add guests", placeholder: "Add guests", variant: compact ? "quiet" : "box", listedElsewhere: !compact, guests: values.attendees, invalid: values.guestInvalid, draft: values.guestDraft, skipAddresses: [c.organizerAddress], mailboxUid: values.targetMailboxUid, onChange: (next) => c.update({ attendees: next.guests, guestInvalid: next.invalid, guestDraft: next.draft }) }), compact ? null : (_jsxs(_Fragment, { children: [values.attendees.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: values.attendees.map((attendee, i) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "w-7 h-7 shrink-0 rounded-full bg-primary/15 text-primary-dark text-xs font-semibold flex items-center justify-center uppercase", children: (attendee.displayName || attendee.address).charAt(0) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm truncate", children: attendee.displayName || attendee.address }), _jsxs("div", { className: "text-xs text-text-muted truncate", children: [attendee.displayName ? `${attendee.address} · ` : "", RESPONSE_STATUS_LABEL[attendee.responseStatus]] })] }), _jsx("select", { className: SELECT_CLASS, value: attendee.role, onChange: (e) => c.updateAttendee(i, { role: e.target.value }), "aria-label": `Attendee role ${i + 1}`, children: ATTENDEE_ROLES.map((role) => (_jsx("option", { value: role, children: role }, role))) }), _jsx("button", { type: "button", onClick: () => c.removeAttendee(i), className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": `Remove attendee ${i + 1}`, children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }, i))) })), _jsx("button", { ref: pickerAnchor, type: "button", onClick: () => setPickerOpen(true), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add room/equipment" }), pickerOpen && (_jsx(ResourcePicker, { anchorRef: pickerAnchor, onClose: () => setPickerOpen(false), onSelect: (mailbox) => {
55
44
  c.addResource(mailbox);
56
45
  setPickerOpen(false);
57
46
  }, excludeAddresses: c.guestAddresses }))] }))] }));
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import type { Attendee } from "@rapidmx/react-shared/calendar/calendarApi.js";
3
+ import type { ContactSuggestionOptions, RecipientSuggestion } from "@rapidmx/react-shared/mail/directoryApi.js";
4
+ declare const VARIANT_CLASS: {
5
+ /** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
6
+ quiet: string;
7
+ /** A bordered box, as the full form's fields are. */
8
+ box: string;
9
+ };
10
+ /** What a guests field holds: the guests added, what was committed but is not an address (kept in the field, flagged, until it is fixed or removed),
11
+ * and what is still being typed. */
12
+ export interface GuestEntries {
13
+ guests: Attendee[];
14
+ invalid: string[];
15
+ draft: string;
16
+ }
17
+ export interface GuestInputProps extends GuestEntries {
18
+ /** The input's id, and the prefix of its suggestions' ids. */
19
+ id: string;
20
+ /** The input's accessible name. */
21
+ label: string;
22
+ placeholder?: string;
23
+ variant: keyof typeof VARIANT_CLASS;
24
+ /** The guests are listed elsewhere (rows with a role and an answer): the field draws chips only for what is not an address. */
25
+ listedElsewhere?: boolean;
26
+ /** Addresses that are never added - the organizer's, or the people already invited. */
27
+ skipAddresses?: string[];
28
+ /** The mailbox the event is on, whose contacts are suggested too. */
29
+ mailboxUid?: string;
30
+ onChange: (next: GuestEntries) => void;
31
+ /** Loads suggestions; defaults to the lookup compose's recipient fields use. */
32
+ fetchSuggestions?: (query: string, options: ContactSuggestionOptions) => Promise<RecipientSuggestion[]>;
33
+ debounceMs?: number;
34
+ }
35
+ /**
36
+ * The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
37
+ * caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
38
+ * Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
39
+ * a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
40
+ * (as compose sends to one), which the mail server delivers to its members.
41
+ *
42
+ * The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
43
+ */
44
+ export default function GuestInput({ id, label, placeholder, variant, listedElsewhere, skipAddresses, mailboxUid, guests, invalid, draft, onChange, fetchSuggestions, debounceMs, }: GuestInputProps): React.JSX.Element;
45
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import RecipientInput from "../mail/compose/RecipientInput.js";
3
+ import { applyGuestChips, guestChip } from "./eventFormat.js";
4
+ /** Above the event dialog's own backdrop (`z-[1000]`), which the suggestions would otherwise sit under. */
5
+ const DIALOG_SUGGESTIONS_Z_INDEX = 1100;
6
+ const VARIANT_CLASS = {
7
+ /** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
8
+ quiet: "w-full text-sm py-1 border-b border-transparent hover:border-border focus-within:border-primary",
9
+ /** A bordered box, as the full form's fields are. */
10
+ box: "w-full text-sm py-1.5 px-3 border border-border rounded-md bg-surface text-text focus-within:border-primary",
11
+ };
12
+ /** What `RecipientInput` calls `onChange` with; the entries come through `onEdit`. */
13
+ const IGNORED = () => undefined;
14
+ /**
15
+ * The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
16
+ * caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
17
+ * Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
18
+ * a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
19
+ * (as compose sends to one), which the mail server delivers to its members.
20
+ *
21
+ * The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
22
+ */
23
+ export default function GuestInput({ id, label, placeholder, variant, listedElsewhere, skipAddresses, mailboxUid, guests, invalid, draft, onChange, fetchSuggestions, debounceMs, }) {
24
+ const value = [...guests.map(guestChip), ...invalid, draft].filter((part) => part.length > 0).join(", ");
25
+ const listed = new Set(listedElsewhere ? guests.map(guestChip) : []);
26
+ return (_jsx(RecipientInput, { id: id, label: label, ariaLabel: label, listLabel: "Guests", placeholder: placeholder, className: VARIANT_CLASS[variant], dropdownZIndex: DIALOG_SUGGESTIONS_Z_INDEX, value: value, initialPending: draft, mailboxUid: mailboxUid, hideChip: (chip) => listed.has(chip), fetchSuggestions: fetchSuggestions, debounceMs: debounceMs, onChange: IGNORED, onEdit: (chips, pending) => {
27
+ const merged = applyGuestChips(guests, chips, skipAddresses);
28
+ onChange({ guests: merged.attendees, invalid: merged.invalid, draft: pending });
29
+ } }));
30
+ }
@@ -13,6 +13,7 @@ import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
13
13
  import { notifyApiError } from "../../notifications/apiErrors.js";
14
14
  import { notify } from "../../notifications/store.js";
15
15
  import { INPUT_CLASS } from "./EventFormParts.js";
16
+ import GuestInput from "./GuestInput.js";
16
17
  import LazyDescriptionEditor from "./LazyDescriptionEditor.js";
17
18
  import { descriptionProblem, initialDescriptionHtml } from "./eventDialogFields.js";
18
19
  import { mergeGuests } from "./eventFormat.js";
@@ -35,41 +36,29 @@ export default function RequestChangeForm({ subject, canChange, canInvite, onSen
35
36
  const [start, setStart] = useState(subject.startDate ? toDatetimeLocal(subject.startDate) : "");
36
37
  const [end, setEnd] = useState(subject.endDate ? toDatetimeLocal(subject.endDate) : "");
37
38
  const [descriptionHtml, setDescriptionHtml] = useState(() => initialDescriptionHtml(subject));
38
- const [added, setAdded] = useState([]);
39
- const [guestDraft, setGuestDraft] = useState("");
39
+ // The guests to add (not the ones already invited: `subject.guestAddresses` are skipped), what is flagged in the field, and what is being typed.
40
+ const [entries, setEntries] = useState({ guests: [], invalid: [], draft: "" });
40
41
  const [error, setError] = useState(null);
41
42
  const [sending, setSending] = useState(false);
42
43
  const heading = canChange ? "Request a change" : "Add guests";
43
- /** Adds what was typed into the guests box, returning the guests as they now stand; a mistyped address stays in the box and is said. */
44
+ /** Adds what was typed into the guests box, returning the guests to add as they now stand; something that is not an address is said. */
44
45
  function addTypedGuests() {
45
- const invited = [...subject.guestAddresses, ...added.map((guest) => guest.address)].map((address) => ({ address }));
46
- const merged = mergeGuests(invited.map((guest) => ({ ...guest, role: "required", responseStatus: "needsAction", isOrganizer: false })), guestDraft);
46
+ const merged = mergeGuests(entries.guests, [...entries.invalid, entries.draft].join(", "), subject.guestAddresses);
47
47
  if (merged.invalid.length > 0) {
48
48
  setError(`“${merged.invalid[0]}” isn’t a valid email address.`);
49
- setGuestDraft(merged.invalid.join(" "));
50
49
  return undefined;
51
50
  }
52
- setGuestDraft("");
53
- return merged.attendees.slice(invited.length).map((guest) => ({ address: guest.address }));
54
- }
55
- function handleGuestKey(event) {
56
- if (event.key === "Enter" || event.key === "," || event.key === ";") {
57
- event.preventDefault();
58
- setError(null);
59
- const more = addTypedGuests();
60
- if (more) {
61
- setAdded((current) => [...current, ...more]);
62
- }
63
- }
51
+ setEntries({ guests: merged.attendees, invalid: [], draft: "" });
52
+ return merged.attendees;
64
53
  }
65
54
  async function handleSubmit(event) {
66
55
  event.preventDefault();
67
56
  setError(null);
68
- const typed = canInvite ? addTypedGuests() : [];
69
- if (!typed) {
57
+ const added = canInvite ? addTypedGuests() : [];
58
+ if (!added) {
70
59
  return;
71
60
  }
72
- const guests = [...added, ...typed];
61
+ const guests = added.map((guest) => (guest.displayName ? { address: guest.address, displayName: guest.displayName } : { address: guest.address }));
73
62
  const request = {};
74
63
  if (canChange) {
75
64
  if (title.trim() !== subject.title.trim()) {
@@ -134,5 +123,5 @@ export default function RequestChangeForm({ subject, canChange, canInvite, onSen
134
123
  setSending(false);
135
124
  }
136
125
  }
137
- return (_jsxs("form", { onSubmit: (event) => void handleSubmit(event), "aria-label": heading, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold", children: heading }), _jsx("button", { type: "button", "aria-label": "Cancel request", onClick: onCancel, className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Your request goes to the organizer. Nothing on your calendar changes until their update arrives; when the organizer allows it, it is applied for you." }), error && _jsx(Alert, { children: error }), canChange && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Title", _jsx("input", { type: "text", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Location", _jsx("input", { type: "text", className: INPUT_CLASS, value: location, onChange: (e) => setLocation(e.target.value) })] }), canChangeTime && (_jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Starts", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: start, onChange: (e) => setStart(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Ends", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: end, onChange: (e) => setEnd(e.target.value) })] })] })), !canChangeTime && (_jsx("p", { className: "text-xs text-text-muted", children: subject.recurring ? "A change here applies to the whole series; its times can only be changed by the organizer." : "The time of this event can only be changed by the organizer." })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-xs font-medium", children: "Description" }), _jsx(LazyDescriptionEditor, { value: descriptionHtml, onChange: setDescriptionHtml })] })] })), canInvite && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Guests to add", _jsx("input", { type: "text", className: INPUT_CLASS, placeholder: "name@example.com", value: guestDraft, onChange: (e) => setGuestDraft(e.target.value), onKeyDown: handleGuestKey })] }), added.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-1.5", children: added.map((guest, index) => (_jsxs("li", { className: "flex items-center gap-1 text-xs rounded-full bg-surface-alt pl-2.5 pr-1 py-0.5", children: [_jsx("span", { className: "truncate max-w-[16rem]", children: guest.address }), _jsx("button", { type: "button", "aria-label": `Remove ${guest.address}`, onClick: () => setAdded((current) => current.filter((_, i) => i !== index)), className: "w-4 h-4 flex items-center justify-center rounded-full text-text-muted hover:text-text", children: _jsx(HiOutlineXMark, { size: 12, "aria-hidden": "true" }) })] }, guest.address))) }))] })), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "submit", loading: sending, disabled: sending, className: "!w-auto", children: "Send request" }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto", disabled: sending, onClick: onCancel, children: "Cancel" })] })] }));
126
+ return (_jsxs("form", { onSubmit: (event) => void handleSubmit(event), "aria-label": heading, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold", children: heading }), _jsx("button", { type: "button", "aria-label": "Cancel request", onClick: onCancel, className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Your request goes to the organizer. Nothing on your calendar changes until their update arrives; when the organizer allows it, it is applied for you." }), error && _jsx(Alert, { children: error }), canChange && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Title", _jsx("input", { type: "text", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Location", _jsx("input", { type: "text", className: INPUT_CLASS, value: location, onChange: (e) => setLocation(e.target.value) })] }), canChangeTime && (_jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Starts", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: start, onChange: (e) => setStart(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Ends", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: end, onChange: (e) => setEnd(e.target.value) })] })] })), !canChangeTime && (_jsx("p", { className: "text-xs text-text-muted", children: subject.recurring ? "A change here applies to the whole series; its times can only be changed by the organizer." : "The time of this event can only be changed by the organizer." })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-xs font-medium", children: "Description" }), _jsx(LazyDescriptionEditor, { value: descriptionHtml, onChange: setDescriptionHtml })] })] })), canInvite && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: "request-guests", className: "text-xs font-medium", children: "Guests to add" }), _jsx(GuestInput, { id: "request-guests", label: "Guests to add", placeholder: "Name or name@example.com", variant: "box", skipAddresses: subject.guestAddresses, ...entries, onChange: setEntries })] })), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "submit", loading: sending, disabled: sending, className: "!w-auto", children: "Send request" }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto", disabled: sending, onClick: onCancel, children: "Cancel" })] })] }));
138
127
  }
@@ -22,6 +22,8 @@ export interface EventFormValues {
22
22
  attendees: Attendee[];
23
23
  /** What has been typed into the guests field but not added yet. */
24
24
  guestDraft: string;
25
+ /** What was committed to the guests field (Enter, a comma, leaving it) but is not an address: kept there, flagged, until it is fixed or removed. */
26
+ guestInvalid: string[];
25
27
  recurrenceRule: RecurrenceRule | null;
26
28
  /** Minutes before the start, as typed; blank means no reminder. */
27
29
  reminderMinutes: string;
@@ -57,8 +59,6 @@ export interface EventFormController {
57
59
  /** The address the event is organized from (the chosen mailbox's, for a new event). */
58
60
  organizerAddress: string;
59
61
  deviceZone: string;
60
- /** Adds the addresses in `text`; returns the ones that are not valid addresses. */
61
- addGuests: (text: string) => string[];
62
62
  updateAttendee: (index: number, patch: Partial<Attendee>) => void;
63
63
  removeAttendee: (index: number) => void;
64
64
  addResource: (mailbox: Mailbox) => void;
@@ -33,12 +33,24 @@ export type ReminderUnit = (typeof REMINDER_UNITS)[number]["unit"];
33
33
  export declare function bestReminderUnit(minutes: number): (typeof REMINDER_UNITS)[number];
34
34
  /** "Notify 30 minutes before" / "Notify 1 hour before" / "Notify at the start" for a stored reminder, or "No notification". */
35
35
  export declare function describeReminder(minutes: number | undefined): string;
36
- /** Splits what was typed into the guests field into its addresses: commas, semicolons and white space separate them. */
37
- export declare function splitGuestInput(text: string): string[];
38
- /** The loosest address check that is still worth making before an invitation is sent: something, an @, something. */
39
- export declare function isValidGuestAddress(address: string): boolean;
40
36
  /** An attendee as the guests field adds one. */
41
- export declare function newGuest(address: string): Attendee;
37
+ export declare function newGuest(address: string, displayName?: string): Attendee;
38
+ /** A guest as the guests field shows it: `Name <address>`, or the bare address for one without a name (the form compose's recipient fields use). */
39
+ export declare function guestChip(guest: {
40
+ address: string;
41
+ displayName?: string;
42
+ }): string;
43
+ /**
44
+ * The guests the guests field's chips name, given the guests `current`ly on the list: a chip for someone already there keeps that guest as
45
+ * they are (their role and answer, whatever the case of the address), a chip for a new address adds a required guest with the chip's name, and
46
+ * an address twice (or one of `skipAddresses` - the organizer's, or the people already invited) is added once. The chips are the field's whole
47
+ * list, so a guest without a chip is removed. What is not an address comes back as `invalid`, to stay in the field flagged; several addresses
48
+ * separated by white space in one chip ("a@x.com b@x.com", as a paste can be) are taken as several.
49
+ */
50
+ export declare function applyGuestChips(current: Attendee[], chips: string[], skipAddresses?: string[]): {
51
+ attendees: Attendee[];
52
+ invalid: string[];
53
+ };
42
54
  /** The wall-clock string a form input holds ("YYYY-MM-DDTHH:mm") read as `zone`'s clock, as an instant. `deviceZone` is the zone the browser
43
55
  * itself parses such a string in, which needs no conversion. */
44
56
  export declare function wallStringToMs(value: string, zone: string, deviceZone: string): number;
@@ -55,9 +67,10 @@ export declare function localDateFromKey(key: string): Date;
55
67
  export declare function formatWhen(start: Date, end: Date, allDay: boolean, now?: Date): string;
56
68
  /** `formatWhen()` for a stored event: instants for a timed one, the date-only keys (with the exclusive end) for an all-day one. */
57
69
  export declare function formatStoredWhen(startDate: string, endDate: string, allDay: boolean, now?: Date): string;
58
- /** Adds the addresses typed into the guests field to `current`: an address already on the list (in any case) is skipped, and what is not an
59
- * address comes back as `invalid` for the field to keep. */
60
- export declare function mergeGuests(current: Attendee[], text: string): {
70
+ /** Adds the guests typed or pasted into the guests field to `current` - recipients separated by commas or semicolons, each an address or
71
+ * `Name <address>` (see `applyGuestChips()`, which keeps who is already there): an address already on the list (in any case) or in
72
+ * `skipAddresses` is skipped, and what is not an address comes back as `invalid` for the field to keep. */
73
+ export declare function mergeGuests(current: Attendee[], text: string, skipAddresses?: string[]): {
61
74
  attendees: Attendee[];
62
75
  invalid: string[];
63
76
  };