@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
@@ -5,94 +5,337 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { addPlugin, getPluginStatus, listPlugins, lookupPluginPackage, removePlugin, updatePlugin, } from "@rapidmx/react-shared/admin/pluginsApi.js";
8
+ import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, searchPlugins, updatePlugin, } from "@rapidmx/react-shared/admin/pluginsApi.js";
9
9
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
10
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
11
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.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
  /** How often status is refreshed while server copies are still applying a change. */
14
14
  const PENDING_POLL_MS = 5000;
15
+ /** How long status keeps being polled after a saved change, since servers may not have noticed it yet. */
16
+ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
17
+ /** The longest wait between retries while no status has ever been read. */
18
+ const MAX_STATUS_RETRY_MS = 60 * 1000;
15
19
  function errorMessage(err, fallback) {
16
20
  return err instanceof ApiRequestError ? err.message : fallback;
17
21
  }
22
+ /** Runs `action`, resolving why it failed, or `null`. */
23
+ async function attempt(action, failure) {
24
+ try {
25
+ await action();
26
+ return null;
27
+ }
28
+ catch (err) {
29
+ return errorMessage(err, failure);
30
+ }
31
+ }
18
32
  /** Installed plugins with their rollout status, and every plugin action - shared by the Plugins page and the
19
33
  * setup wizard. */
20
34
  export default function PluginsManager() {
21
35
  const [plugins, setPlugins] = useState([]);
22
36
  const [status, setStatus] = useState(null);
37
+ /** Set when the last status refresh failed, so what's shown may be out of date. */
38
+ const [statusStale, setStatusStale] = useState(false);
39
+ /** When polling started by a saved change stops (epoch ms), or 0 when there's none. */
40
+ const [pollUntil, setPollUntil] = useState(0);
41
+ const [updates, setUpdates] = useState(new Map());
23
42
  const [loading, setLoading] = useState(true);
24
43
  const [error, setError] = useState(null);
44
+ /** The installed plugin whose enable just failed, offered a retry next to `error`. */
45
+ const [retryEnableUid, setRetryEnableUid] = useState(null);
25
46
  const [adding, setAdding] = useState(false);
26
47
  const [upgrading, setUpgrading] = useState(null);
27
48
  const [configuring, setConfiguring] = useState(null);
28
49
  const [removing, setRemoving] = useState(null);
29
- const [busyUid, setBusyUid] = useState(null);
50
+ const [confirming, setConfirming] = useState(null);
51
+ /** Installed plugins with an action under way. */
52
+ const [busyUids, setBusyUids] = useState(new Set());
53
+ /** Set when re-reading the list after a change failed, so rows may not show what that change also did. */
54
+ const [listStale, setListStale] = useState(false);
55
+ /** How many times status has been retried while none has ever been read. */
56
+ const [statusRetries, setStatusRetries] = useState(0);
57
+ const statusRequest = useRef(null);
58
+ const setBusy = (uid, busy) => setBusyUids((prev) => {
59
+ const next = new Set(prev);
60
+ if (busy) {
61
+ next.add(uid);
62
+ }
63
+ else {
64
+ next.delete(uid);
65
+ }
66
+ return next;
67
+ });
30
68
  const refreshStatus = useCallback(() => {
31
- // Status is advisory - a failure to read it shouldn't hide the plugin list.
32
- return getPluginStatus()
33
- .then(setStatus)
34
- .catch(() => setStatus(null));
69
+ // One read at a time: a poll that comes due while a slow read is still running shares it.
70
+ statusRequest.current ?? (statusRequest.current = getPluginStatus()
71
+ .then((next) => {
72
+ setStatus(next);
73
+ setStatusStale(false);
74
+ })
75
+ // Status is advisory - a failure to read it shouldn't hide the plugin list, or lose the last status read.
76
+ .catch(() => setStatusStale(true))
77
+ .finally(() => {
78
+ statusRequest.current = null;
79
+ }));
80
+ return statusRequest.current;
81
+ }, []);
82
+ const refreshUpdates = useCallback(() => {
83
+ // Also advisory: a registry that can't be reached just means no update badges.
84
+ return getPluginUpdates()
85
+ .then((list) => setUpdates(new Map(list.map((info) => [info.uid, info]))))
86
+ .catch(() => setUpdates(new Map()));
87
+ }, []);
88
+ /** Re-reads the list, for changes that also installed or enabled other plugins. */
89
+ const reload = useCallback(() => {
90
+ return listPlugins()
91
+ .then((list) => {
92
+ setPlugins(list);
93
+ setListStale(false);
94
+ })
95
+ .catch(() => setListStale(true));
35
96
  }, []);
36
97
  useEffect(() => {
37
98
  Promise.all([listPlugins(), refreshStatus()])
38
- .then(([list]) => setPlugins(list))
99
+ .then(([list]) => {
100
+ setPlugins(list);
101
+ void refreshUpdates();
102
+ })
39
103
  .catch((err) => setError(errorMessage(err, "Could not load plugins.")))
40
104
  .finally(() => setLoading(false));
41
- }, [refreshStatus]);
105
+ }, [refreshStatus, refreshUpdates]);
42
106
  const pending = !!status && status.instances.some((instance) => instance.hash !== status.hash);
43
107
  useEffect(() => {
44
- if (!pending) {
108
+ if (!pending && pollUntil === 0) {
45
109
  return;
46
110
  }
47
111
  const timer = setInterval(() => void refreshStatus(), PENDING_POLL_MS);
48
- return () => clearInterval(timer);
49
- }, [pending, refreshStatus]);
50
- /** Applies a saved change locally and re-reads status, since saving starts a rollout. */
51
- function applied(updated) {
112
+ // Polling for a saved change ends after a while; a rollout that's under way keeps it going.
113
+ const stop = pollUntil > 0 ? setTimeout(() => setPollUntil(0), Math.max(0, pollUntil - Date.now())) : undefined;
114
+ return () => {
115
+ clearInterval(timer);
116
+ clearTimeout(stop);
117
+ };
118
+ }, [pending, pollUntil, refreshStatus]);
119
+ // With no status read at all there's nothing pending to poll for, so keep retrying - less often each time.
120
+ const neverRead = !status && statusStale;
121
+ useEffect(() => {
122
+ if (!neverRead) {
123
+ return;
124
+ }
125
+ const timer = setTimeout(() => void refreshStatus().then(() => setStatusRetries((n) => n + 1)), Math.min(PENDING_POLL_MS * 2 ** statusRetries, MAX_STATUS_RETRY_MS));
126
+ return () => clearTimeout(timer);
127
+ }, [neverRead, statusRetries, refreshStatus]);
128
+ /** Re-reads status, and keeps doing so for a while, since saving starts a rollout servers pick up shortly. */
129
+ function watchRollout() {
130
+ setPollUntil(Date.now() + AFTER_CHANGE_POLL_MS);
131
+ void refreshStatus();
132
+ }
133
+ /** Applies saved changes locally and re-reads status and updates, since saving starts a rollout. */
134
+ function applied(...changed) {
52
135
  setPlugins((prev) => {
53
- const exists = prev.some((plugin) => plugin.uid === updated.uid);
54
- const next = exists ? prev.map((plugin) => (plugin.uid === updated.uid ? updated : plugin)) : [...prev, updated];
55
- return next.sort((a, b) => a.name.localeCompare(b.name));
136
+ let next = prev;
137
+ for (const updated of changed) {
138
+ const exists = next.some((plugin) => plugin.uid === updated.uid);
139
+ next = exists ? next.map((plugin) => (plugin.uid === updated.uid ? updated : plugin)) : [...next, updated];
140
+ }
141
+ return [...next].sort((a, b) => a.name.localeCompare(b.name));
56
142
  });
57
- void refreshStatus();
143
+ watchRollout();
144
+ void refreshUpdates();
58
145
  }
59
- async function toggle(plugin) {
60
- setBusyUid(plugin.uid);
146
+ /** Runs an action on an installed plugin's row, which is busy meanwhile, and shows why it failed. */
147
+ async function onRow(plugin, action) {
148
+ setBusy(plugin.uid, true);
61
149
  setError(null);
150
+ setRetryEnableUid(null);
151
+ const problem = await action();
152
+ if (problem) {
153
+ setError(problem);
154
+ }
155
+ // A change waiting for confirmation keeps the row busy by itself, until the confirmation closes.
156
+ setBusy(plugin.uid, false);
157
+ }
158
+ function toggle(plugin) {
159
+ const displayName = plugin.manifest.displayName;
160
+ if (plugin.enabled) {
161
+ return onRow(plugin, () => attempt(async () => applied(await updatePlugin(plugin.uid, { version: plugin.version, enabled: false })), `Could not disable ${displayName}.`));
162
+ }
163
+ // Enabling a plugin also installs or enables the plugins it requires, so it's previewed like any other change.
164
+ // The installed version is planned from its stored manifest, so this doesn't need the registry. A preview that
165
+ // fails is never skipped - enabling without one could install or enable other plugins the administrator never
166
+ // saw - so the failure is shown with a retry instead.
167
+ return onRow(plugin, async () => {
168
+ const problem = await planned(plugin.name, plugin.packageVersion, displayName, async (plan) => {
169
+ applied(await updatePlugin(plugin.uid, { version: plugin.version, enabled: true, expectedPlan: expectedPlanOf(plan) }));
170
+ if (plan.install.length > 0 || plan.enable.length > 0) {
171
+ void reload();
172
+ }
173
+ }, `Could not enable ${displayName}.`, { uid: plugin.uid });
174
+ if (problem) {
175
+ setRetryEnableUid(plugin.uid);
176
+ }
177
+ return problem;
178
+ });
179
+ }
180
+ /**
181
+ * Checks what installing or changing a plugin also takes before doing it. Resolves why it can't be done, or `null`
182
+ * once it's done - or, when it also installs or enables other plugins, once they're shown for confirmation.
183
+ */
184
+ async function planned(name, packageVersion, displayName, apply, failure, options = {}) {
185
+ let plan;
62
186
  try {
63
- applied(await updatePlugin(plugin.uid, { version: plugin.version, enabled: !plugin.enabled }));
187
+ plan = await planPluginChange(name, packageVersion);
64
188
  }
65
189
  catch (err) {
66
- setError(errorMessage(err, `Could not ${plugin.enabled ? "disable" : "enable"} ${plugin.manifest.displayName}.`));
190
+ return errorMessage(err, failure);
67
191
  }
68
- finally {
69
- setBusyUid(null);
192
+ if (plan.conflicts.length > 0) {
193
+ return `${displayName} ${plan.plugin.version} can't be installed. ${plan.conflicts.join(" ")}`;
70
194
  }
195
+ if (plan.install.length > 0 || plan.enable.length > 0) {
196
+ setConfirming({ displayName, plan, apply, failure, uid: options.uid });
197
+ return null;
198
+ }
199
+ return attempt(() => apply(plan), failure);
71
200
  }
72
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Plugins" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => setAdding(true), children: "+ Add plugin" })] }), _jsx("p", { className: "text-sm text-text-muted mb-5 max-w-3xl", children: "Plugins add protocols and features to every server. Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." }), error && _jsx(Alert, { children: error }), _jsx(RolloutBanner, { status: status }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : plugins.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins installed." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Plugin", "Version", "Status", "Enabled", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: plugins.map((plugin) => (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description }))] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: plugin.packageVersion }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: _jsx("label", { className: "inline-flex items-center gap-2", children: _jsx("input", { type: "checkbox", "aria-label": `Enable ${plugin.manifest.displayName}`, checked: plugin.enabled, disabled: busyUid === plugin.uid, onChange: () => void toggle(plugin) }) }) }), _jsxs("td", { className: "py-2.5 px-2.5 border-b border-border align-top text-right whitespace-nowrap", children: [(plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "text", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: "text", type: "button", className: "!w-auto", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", className: "!w-auto", onClick: () => setRemoving(plugin), children: "Remove" })] })] }, plugin.uid))) })] }) })), _jsx(AddPluginModal, { open: adding, onClose: () => setAdding(false), onAdded: (plugin) => {
73
- setAdding(false);
74
- applied(plugin);
75
- } }), upgrading && (_jsx(ChangeVersionModal, { plugin: upgrading, onClose: () => setUpgrading(null), onSaved: (plugin) => {
76
- setUpgrading(null);
77
- applied(plugin);
78
- } })), configuring && (_jsx(SettingsModal, { plugin: configuring, onClose: () => setConfiguring(null), onSaved: (plugin) => {
201
+ function install(name, packageVersion, displayName) {
202
+ return planned(name, packageVersion, displayName, async (plan) => {
203
+ const result = await addPlugin(name, packageVersion, expectedPlanOf(plan));
204
+ applied(...result.dependencies, result.plugin);
205
+ }, `Could not install ${displayName}.`);
206
+ }
207
+ function changeVersion(plugin, packageVersion) {
208
+ const failure = `Could not upgrade ${plugin.manifest.displayName}.`;
209
+ if (!plugin.enabled) {
210
+ // A disabled plugin stays disabled, so nothing it requires is installed or enabled - there's nothing to preview.
211
+ return attempt(async () => applied(await updatePlugin(plugin.uid, { version: plugin.version, packageVersion })), failure);
212
+ }
213
+ return planned(plugin.name, packageVersion, plugin.manifest.displayName, async (plan) => {
214
+ applied(await updatePlugin(plugin.uid, { version: plugin.version, packageVersion, expectedPlan: expectedPlanOf(plan) }));
215
+ if (plan.install.length > 0 || plan.enable.length > 0) {
216
+ void reload();
217
+ }
218
+ }, failure, { uid: plugin.uid });
219
+ }
220
+ function upgrade(plugin, packageVersion) {
221
+ return onRow(plugin, () => changeVersion(plugin, packageVersion));
222
+ }
223
+ // Retried against the plugin as it's listed now (a newer version, or already enabled elsewhere, hides the retry).
224
+ const retryEnable = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
225
+ const displayNameOf = (name) => plugins.find((plugin) => plugin.name === name)?.manifest.displayName ?? name;
226
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Plugins" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setAdding(true), children: "Add by name" })] }), _jsx("p", { className: "text-sm text-text-muted mb-5 max-w-3xl", children: "Plugins add protocols and features to every server. Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." }), error && (_jsxs(Alert, { children: [error, retryEnable && (_jsxs(_Fragment, { children: [" ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0", onClick: () => void toggle(retryEnable), children: "Try again" })] }))] })), _jsx(RolloutBanner, { status: status }), statusStale && status && (_jsx("p", { className: "mb-4 text-xs text-text-muted", children: "Couldn't refresh server status. Showing the last status reported." })), listStale && (_jsxs("p", { className: "mb-4 text-xs text-text-muted", children: ["Couldn't re-read the plugin list, so it may be out of date.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0 !text-xs", onClick: () => void reload(), children: "Retry" })] })), _jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-2", children: "Installed plugins" }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : plugins.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins installed." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Plugin", "Version", "State", "Servers", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: plugins.map((plugin) => {
227
+ const update = updates.get(plugin.uid);
228
+ const latest = update?.updateAvailable ? update.latestVersion : undefined;
229
+ const busy = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid;
230
+ const requires = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
231
+ const requiredBy = plugins
232
+ .filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
233
+ .map((other) => other.manifest.displayName);
234
+ return (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] }), _jsxs("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: [_jsx("div", { children: plugin.packageVersion }), latest && (_jsxs("div", { className: "mt-1 flex flex-col items-start gap-1", children: [_jsxs("span", { className: "text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto !py-1 !px-2 !text-xs", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })] }))] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: plugin.enabled ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Enabled" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Disabled" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsxs("td", { className: "py-2.5 px-2.5 border-b border-border align-top text-right whitespace-nowrap", children: [_jsx(Button, { variant: "text", type: "button", className: "!w-auto", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" }), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "text", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: "text", type: "button", className: "!w-auto", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", className: "!w-auto", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }, plugin.uid));
235
+ }) })] }) })), _jsx(PluginBrowser, { plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
236
+ // The browser only offers an upgrade for a result it matched to one of these same `plugins`.
237
+ onUpgrade: (uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid), packageVersion) }), _jsx(AddPluginModal, { open: adding, onClose: () => setAdding(false), onAdd: async (name, packageVersion, displayName) => {
238
+ const problem = await install(name, packageVersion, displayName);
239
+ if (!problem) {
240
+ setAdding(false);
241
+ }
242
+ return problem;
243
+ } }), upgrading && (_jsx(ChangeVersionModal, { plugin: upgrading, onClose: () => setUpgrading(null), onSave: async (packageVersion) => {
244
+ const problem = await changeVersion(upgrading, packageVersion);
245
+ if (!problem) {
246
+ setUpgrading(null);
247
+ }
248
+ return problem;
249
+ } })), confirming && (_jsx(ConfirmDependenciesModal, { change: confirming, displayNameOf: displayNameOf, onClose: () => setConfirming(null), onDone: () => setConfirming(null) })), configuring && (_jsx(SettingsModal, { plugin: configuring, onClose: () => setConfiguring(null), onSaved: (plugin) => {
79
250
  setConfiguring(null);
80
251
  applied(plugin);
81
252
  } })), removing && (_jsx(RemoveModal, { plugin: removing, onClose: () => setRemoving(null), onRemoved: () => {
82
253
  const uid = removing.uid;
83
254
  setRemoving(null);
84
255
  setPlugins((prev) => prev.filter((plugin) => plugin.uid !== uid));
85
- void refreshStatus();
256
+ watchRollout();
86
257
  } }))] }));
87
258
  }
88
- /** Shown while any server copy hasn't applied the saved plugin set yet, or when one is in safe mode. */
259
+ const ALL_NAMESPACES = "";
260
+ /** Searches the configured namespaces' registries for plugin packages, with install and upgrade actions. */
261
+ function PluginBrowser({ plugins, onInstall, onUpgrade, }) {
262
+ const [namespaces, setNamespaces] = useState([]);
263
+ const [namespace, setNamespace] = useState(ALL_NAMESPACES);
264
+ const [results, setResults] = useState(null);
265
+ const [searching, setSearching] = useState(false);
266
+ const [busyName, setBusyName] = useState(null);
267
+ const [error, setError] = useState(null);
268
+ useEffect(() => {
269
+ listPluginNamespaces()
270
+ .then(setNamespaces)
271
+ .catch(() => setNamespaces([]));
272
+ }, []);
273
+ async function search(e) {
274
+ e.preventDefault();
275
+ setSearching(true);
276
+ setError(null);
277
+ try {
278
+ setResults(await searchPlugins(namespace || undefined));
279
+ }
280
+ catch (err) {
281
+ setResults(null);
282
+ setError(errorMessage(err, "Could not search for plugins."));
283
+ }
284
+ finally {
285
+ setSearching(false);
286
+ }
287
+ }
288
+ // Keep results in step with installs and upgrades made anywhere on the page.
289
+ const rows = (results ?? []).map((result) => {
290
+ const installed = plugins.find((plugin) => plugin.name === result.name);
291
+ return {
292
+ ...result,
293
+ installedUid: installed?.uid,
294
+ installedVersion: installed?.packageVersion,
295
+ updateAvailable: !!installed && installed.packageVersion !== result.version && result.updateAvailable,
296
+ };
297
+ });
298
+ async function install(result) {
299
+ setBusyName(result.name);
300
+ setError(null);
301
+ setError(await onInstall(result));
302
+ setBusyName(null);
303
+ }
304
+ async function upgrade(result) {
305
+ setBusyName(result.name);
306
+ try {
307
+ await onUpgrade(result.installedUid, result.version);
308
+ }
309
+ finally {
310
+ setBusyName(null);
311
+ }
312
+ }
313
+ return (_jsxs("section", { "aria-labelledby": "plugin-browser-title", className: "mt-8", children: [_jsx("h2", { id: "plugin-browser-title", className: "text-sm font-bold uppercase tracking-wide mb-2", children: "Find plugins" }), _jsxs("form", { onSubmit: (e) => void search(e), className: "flex flex-wrap gap-2 items-end mb-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Namespace" }), _jsxs("select", { "aria-label": "Namespace", className: INPUT_CLASS, value: namespace, onChange: (e) => setNamespace(e.target.value), children: [_jsx("option", { value: ALL_NAMESPACES, children: "All namespaces" }), namespaces.map((ns) => (_jsx("option", { value: ns.name, children: ns.name }, ns.name)))] })] }), _jsx(Button, { type: "submit", className: "!w-auto", loading: searching, disabled: searching, children: "Search" })] }), error && _jsx(Alert, { children: error }), results !== null &&
314
+ (rows.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins found." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Package", "Latest version", "Status", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: rows.map((result) => (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: [_jsx("div", { className: "font-semibold", children: result.name }), result.description && _jsx("div", { className: "text-xs text-text-muted max-w-md", children: result.description })] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top", children: result.version }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top text-text-muted", children: result.updateAvailable
315
+ ? `Installed ${result.installedVersion} - update available`
316
+ : result.installedUid
317
+ ? `Installed ${result.installedVersion}`
318
+ : result.allowed
319
+ ? "Not installed"
320
+ : "Not allowed on this server" }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border align-top text-right", children: result.updateAvailable ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${result.name} to ${result.version}`, disabled: busyName === result.name, onClick: () => void upgrade(result), children: "Upgrade" })) : (!result.installedUid &&
321
+ result.allowed && (_jsx(Button, { type: "button", className: "!w-auto", "aria-label": `Install ${result.name}`, loading: busyName === result.name, disabled: busyName === result.name, onClick: () => void install(result), children: "Install" }))) })] }, result.name))) })] }) })))] }));
322
+ }
323
+ /** Name a server reports an error under when it isn't about any single plugin (e.g. the plugin set as a whole failed). */
324
+ const SERVER_WIDE_ERROR_NAME = "*";
325
+ /**
326
+ * Shown while any server copy hasn't applied the saved plugin set yet, when one is in safe mode, or when one reports a
327
+ * server-wide plugin error - those match no plugin row, so without this they'd never be shown.
328
+ */
89
329
  function RolloutBanner({ status }) {
90
330
  if (!status || status.instances.length === 0) {
91
331
  return null;
92
332
  }
93
333
  const behind = status.instances.filter((instance) => instance.hash !== status.hash).length;
94
334
  const safeMode = status.instances.filter((instance) => instance.safeMode);
95
- return (_jsxs(_Fragment, { children: [behind > 0 && (_jsxs("p", { role: "status", className: "mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Applying changes: ", status.instances.length - behind, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers", " updated. Servers restart one at a time."] })), safeMode.length > 0 && (_jsxs(Alert, { children: [safeMode.map((instance) => instance.instance).join(", "), " started without any plugins because recent starts failed. Fix or disable the failing plugin, and the server will pick up the change."] }))] }));
335
+ const serverWideErrors = status.instances.flatMap((instance) => instance.errors
336
+ .filter((entry) => entry.name === SERVER_WIDE_ERROR_NAME)
337
+ .map((entry) => `${instance.instance}: ${entry.message}`));
338
+ return (_jsxs(_Fragment, { children: [serverWideErrors.length > 0 && (_jsxs(Alert, { children: ["Plugins couldn\u2019t be loaded:", _jsx("ul", { className: "list-disc pl-5 mt-1", children: serverWideErrors.map((message) => (_jsx("li", { className: "break-words", children: message }, message))) })] })), behind > 0 && (_jsxs("p", { role: "status", className: "mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Applying changes: ", status.instances.length - behind, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers", " updated. Servers restart one at a time."] })), safeMode.length > 0 && (_jsxs(Alert, { children: [safeMode.map((instance) => instance.instance).join(", "), " started without any plugins because recent starts failed. Fix or disable the failing plugin, and the server will pick up the change."] }))] }));
96
339
  }
97
340
  function PluginStatusCell({ plugin, status }) {
98
341
  if (!plugin.enabled) {
@@ -106,61 +349,97 @@ function PluginStatusCell({ plugin, status }) {
106
349
  const errors = current.flatMap((instance) => instance.errors.filter((entry) => entry.name === plugin.name).map((entry) => `${instance.instance}: ${entry.message}`));
107
350
  return (_jsxs("div", { children: [_jsxs("span", { className: loaded === status.instances.length ? "text-success font-medium" : "text-text-muted", children: ["Loaded on ", loaded, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers"] }), errors.map((message) => (_jsx("div", { className: "text-xs text-danger mt-1", children: message }, message)))] }));
108
351
  }
109
- function AddPluginModal({ open, onClose, onAdded }) {
352
+ function AddPluginModal({ open, onClose, onAdd, }) {
110
353
  const [name, setName] = useState("");
111
354
  const [lookup, setLookup] = useState(null);
112
355
  const [selectedVersion, setSelectedVersion] = useState("");
113
356
  const [error, setError] = useState(null);
114
357
  const [busy, setBusy] = useState(false);
358
+ const [looking, setLooking] = useState(false);
359
+ /** Identifies the latest lookup, so a slower earlier response can't replace what it found. */
360
+ const lookupToken = useRef(0);
361
+ /** Identifies each time the dialog is opened, so an add that finishes after it was closed can't show its result. */
362
+ const openToken = useRef(0);
363
+ /** Forgets any lookup still in flight. */
364
+ function invalidateLookup() {
365
+ lookupToken.current++;
366
+ setLooking(false);
367
+ }
115
368
  useEffect(() => {
116
369
  if (!open) {
370
+ openToken.current++;
371
+ invalidateLookup();
117
372
  setName("");
118
373
  setLookup(null);
119
374
  setSelectedVersion("");
120
375
  setError(null);
376
+ setBusy(false);
121
377
  }
122
378
  }, [open]);
123
- async function find(e) {
124
- e.preventDefault();
125
- if (!name.trim()) {
126
- setError("Enter a package name.");
127
- return;
128
- }
129
- setBusy(true);
379
+ /** Looks up `packageName` at `packageVersion` (default: latest). `previous` is what a version change started
380
+ * from - kept, with the failure in place of its manifest, when that version can't be read. */
381
+ async function runLookup(packageName, packageVersion, previous) {
382
+ const token = ++lookupToken.current;
383
+ setLooking(true);
130
384
  setError(null);
131
385
  try {
132
- const found = await lookupPluginPackage(name.trim());
133
- setLookup(found);
134
- setSelectedVersion(found.selected.version);
386
+ const found = await lookupPluginPackage(packageName, packageVersion);
387
+ if (token === lookupToken.current) {
388
+ setLookup(found);
389
+ setSelectedVersion(found.selected.version);
390
+ }
135
391
  }
136
392
  catch (err) {
137
- setLookup(null);
138
- setError(errorMessage(err, "Could not look that package up."));
393
+ if (token !== lookupToken.current) {
394
+ return;
395
+ }
396
+ const message = errorMessage(err, "Could not look that package up.");
397
+ if (previous && packageVersion) {
398
+ setLookup({ ...previous, selected: { ...previous.selected, version: packageVersion, manifest: message } });
399
+ }
400
+ else {
401
+ setLookup(null);
402
+ setError(message);
403
+ }
139
404
  }
140
405
  finally {
141
- setBusy(false);
406
+ if (token === lookupToken.current) {
407
+ setLooking(false);
408
+ }
142
409
  }
143
410
  }
411
+ function find(e) {
412
+ e.preventDefault();
413
+ if (!name.trim()) {
414
+ setError("Enter a package name.");
415
+ return;
416
+ }
417
+ void runLookup(name.trim());
418
+ }
419
+ function chooseVersion(version) {
420
+ setSelectedVersion(version);
421
+ // The manifest shown, and whether it's a loadable plugin, depend on the version.
422
+ void runLookup(lookup.package.name, version, lookup);
423
+ }
424
+ const manifest = lookup?.selected.manifest;
144
425
  async function add() {
426
+ const token = openToken.current;
145
427
  setBusy(true);
146
428
  setError(null);
147
- try {
148
- onAdded(await addPlugin(lookup.package.name, selectedVersion));
149
- }
150
- catch (err) {
151
- setError(errorMessage(err, "Could not add the plugin."));
152
- }
153
- finally {
429
+ const name = lookup.package.name;
430
+ const problem = await onAdd(name, selectedVersion, typeof manifest === "object" ? manifest.displayName : name);
431
+ if (token === openToken.current) {
432
+ setError(problem);
154
433
  setBusy(false);
155
434
  }
156
435
  }
157
- const manifest = lookup?.selected.manifest;
158
- return (_jsxs(Modal, { open: open, onClose: onClose, title: "Add plugin", children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: find, className: "flex gap-2 items-end mb-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm flex-1", children: [_jsx("span", { className: "font-semibold", children: "Package name" }), _jsx("input", { "aria-label": "Package name", className: INPUT_CLASS, value: name, placeholder: "@rapidmx/activesync", onChange: (e) => {
436
+ return (_jsxs(Modal, { open: open, onClose: onClose, title: "Add plugin", children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: find, className: "flex gap-2 items-end mb-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm flex-1", children: [_jsx("span", { className: "font-semibold", children: "Package name" }), _jsx("input", { "aria-label": "Package name", className: INPUT_CLASS, value: name, placeholder: "@rapidmx/activesync-plugin", onChange: (e) => {
159
437
  setName(e.target.value);
438
+ invalidateLookup();
160
439
  setLookup(null);
161
- } })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: busy && !lookup, disabled: busy, children: "Find" })] }), lookup && (_jsxs("div", { className: "flex flex-col gap-3", children: [typeof manifest === "string" ? (_jsx(Alert, { children: manifest })) : (_jsxs("div", { className: "text-sm", children: [_jsx("div", { className: "font-semibold", children: manifest?.displayName }), manifest?.description && _jsx("div", { className: "text-text-muted", children: manifest.description })] })), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selectedVersion, onChange: (e) => setSelectedVersion(e.target.value), children: lookup.package.versions.map((version) => (_jsxs("option", { value: version, children: [version, version === lookup.package.latest ? " (latest)" : ""] }, version))) })] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy || typeof manifest === "string", onClick: () => void add(), children: "Add plugin" })] })] }))] }));
440
+ } })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: looking && !lookup, disabled: busy || looking, children: "Find" })] }), lookup && (_jsxs("div", { className: "flex flex-col gap-3", children: [typeof manifest === "string" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-sm font-semibold", children: lookup.package.name }), _jsx(Alert, { children: manifest })] })) : (_jsxs("div", { className: "text-sm", children: [_jsx("div", { className: "font-semibold", children: manifest?.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: lookup.package.name }), manifest?.description && _jsx("div", { className: "text-text-muted", children: manifest.description })] })), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selectedVersion, onChange: (e) => chooseVersion(e.target.value), children: lookup.package.versions.map((version) => (_jsxs("option", { value: version, children: [version, version === lookup.package.latest ? " (latest)" : ""] }, version))) })] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy || looking, disabled: busy || looking || typeof manifest === "string", onClick: () => void add(), children: "Add plugin" })] })] }))] }));
162
441
  }
163
- function ChangeVersionModal({ plugin, onClose, onSaved }) {
442
+ function ChangeVersionModal({ plugin, onClose, onSave, }) {
164
443
  const [versions, setVersions] = useState(null);
165
444
  const [latest, setLatest] = useState();
166
445
  const [selected, setSelected] = useState(plugin.packageVersion);
@@ -175,22 +454,49 @@ function ChangeVersionModal({ plugin, onClose, onSaved }) {
175
454
  .catch((err) => setError(errorMessage(err, "Could not load the available versions.")));
176
455
  }, [plugin.name]);
177
456
  async function save() {
457
+ setBusy(true);
458
+ setError(null);
459
+ const problem = await onSave(selected);
460
+ if (problem) {
461
+ setError(problem);
462
+ setBusy(false);
463
+ }
464
+ }
465
+ return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} version`, children: [error && _jsx(Alert, { children: error }), versions === null ? (!error && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selected, onChange: (e) => setSelected(e.target.value), children: versions.map((version) => (_jsxs("option", { value: version, children: [version, version === latest ? " (latest)" : "", version === plugin.packageVersion ? " (installed)" : ""] }, version))) })] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy || selected === plugin.packageVersion, onClick: () => void save(), children: "Save" })] })] }))] }));
466
+ }
467
+ /** Lists the plugins a change also installs or enables, and makes the change once confirmed. */
468
+ function ConfirmDependenciesModal({ change, displayNameOf, onClose, onDone, }) {
469
+ const [error, setError] = useState(null);
470
+ const [busy, setBusy] = useState(false);
471
+ const { plan } = change;
472
+ async function proceed() {
178
473
  setBusy(true);
179
474
  setError(null);
180
475
  try {
181
- onSaved(await updatePlugin(plugin.uid, { version: plugin.version, packageVersion: selected }));
476
+ await change.apply(plan);
477
+ onDone();
182
478
  }
183
479
  catch (err) {
184
- setError(errorMessage(err, "Could not change the version."));
185
- }
186
- finally {
480
+ setError(errorMessage(err, change.failure));
187
481
  setBusy(false);
188
482
  }
189
483
  }
190
- return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} version`, children: [error && _jsx(Alert, { children: error }), versions === null ? (!error && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selected, onChange: (e) => setSelected(e.target.value), children: versions.map((version) => (_jsxs("option", { value: version, children: [version, version === latest ? " (latest)" : "", version === plugin.packageVersion ? " (installed)" : ""] }, version))) })] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy || selected === plugin.packageVersion, onClick: () => void save(), children: "Save" })] })] }))] }));
484
+ return (_jsxs(Modal, { open: true, onClose: onClose, title: `${change.displayName} requires other plugins`, children: [error && _jsx(Alert, { children: error }), _jsxs("p", { className: "text-sm mb-2", children: [change.displayName, " ", plan.plugin.version, " needs these plugins, so they'll be changed too:"] }), _jsxs("ul", { className: "text-sm list-disc pl-5 mb-4", children: [plan.install.map((dependency) => (_jsxs("li", { children: ["Install ", dependency.manifest.displayName, " ", dependency.version] }, dependency.name))), plan.enable.map((name) => (_jsxs("li", { children: ["Enable ", displayNameOf(name)] }, name)))] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void proceed(), children: "Continue" })] })] }));
485
+ }
486
+ /** Whether a required select has no value to fall back on, so the form has to pick (and save) one. */
487
+ function needsSelection(definition, saved) {
488
+ return (definition.type === "select" &&
489
+ !!definition.required &&
490
+ saved === undefined &&
491
+ definition.default === undefined &&
492
+ (definition.options ?? []).length > 0);
191
493
  }
192
- /** A setting's current form value: its saved value, else its default. Kept as a string for text inputs. */
494
+ /** A setting's current form value: its saved value, else its default - or, for a required select with neither, its
495
+ * first option. Kept as a string for text inputs. */
193
496
  function initialValue(definition, saved) {
497
+ if (needsSelection(definition, saved)) {
498
+ return definition.options[0].value;
499
+ }
194
500
  const value = saved ?? definition.default;
195
501
  if (definition.type === "boolean") {
196
502
  return value === true;
@@ -198,27 +504,33 @@ function initialValue(definition, saved) {
198
504
  return value === undefined ? "" : String(value);
199
505
  }
200
506
  function SettingsModal({ plugin, onClose, onSaved }) {
201
- const definitions = plugin.manifest.settings ?? [];
507
+ // Only opened from the Settings button, which is only shown for a plugin that declares settings.
508
+ const definitions = plugin.manifest.settings;
202
509
  const initial = useRef(Object.fromEntries(definitions.map((d) => [d.key, initialValue(d, plugin.settings[d.key])])));
203
510
  const [values, setValues] = useState(initial.current);
204
511
  const [error, setError] = useState(null);
205
512
  const [busy, setBusy] = useState(false);
206
513
  async function save(e) {
207
514
  e.preventDefault();
515
+ // The server replaces the whole settings object, so every setting is sent. One left alone is sent as it's saved
516
+ // - `null` when nothing is, so it keeps following the plugin's default rather than pinning the default shown.
517
+ // A required select with nothing to fall back on is sent as the form picked it.
208
518
  const settings = {};
519
+ let changed = false;
209
520
  for (const definition of definitions) {
210
- const value = values[definition.key];
211
- if (definition.type === "number") {
212
- if (value === "") {
213
- settings[definition.key] = null;
214
- continue;
215
- }
216
- // A number input only ever reports a valid number or an empty string.
217
- settings[definition.key] = Number(value);
218
- }
219
- else {
220
- settings[definition.key] = value === "" ? null : value;
521
+ const key = definition.key;
522
+ const value = values[key];
523
+ if (value === initial.current[key] && !needsSelection(definition, plugin.settings[key])) {
524
+ settings[key] = plugin.settings[key] ?? null;
525
+ continue;
221
526
  }
527
+ changed = true;
528
+ // A number input only ever reports a valid number or an empty string.
529
+ settings[key] = value === "" ? null : definition.type === "number" ? Number(value) : value;
530
+ }
531
+ if (!changed) {
532
+ onClose();
533
+ return;
222
534
  }
223
535
  setBusy(true);
224
536
  setError(null);
@@ -252,9 +564,9 @@ function RemoveModal({ plugin, onClose, onRemoved }) {
252
564
  onRemoved();
253
565
  }
254
566
  catch (err) {
255
- setError(errorMessage(err, "Could not remove the plugin."));
567
+ setError(errorMessage(err, "Could not uninstall the plugin."));
256
568
  setBusy(false);
257
569
  }
258
570
  }
259
- return (_jsxs(Modal, { open: true, onClose: onClose, title: `Remove ${plugin.manifest.displayName}?`, children: [error && _jsx(Alert, { children: error }), _jsx("p", { className: "text-sm mb-4", children: "The servers stop running this plugin after they restart. Data it stored stays in the database, and adding the plugin again brings it back." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void remove(), children: "Remove" })] })] }));
571
+ return (_jsxs(Modal, { open: true, onClose: onClose, title: `Uninstall ${plugin.manifest.displayName}?`, children: [error && _jsx(Alert, { children: error }), _jsx("p", { className: "text-sm mb-4", children: "The servers stop running this plugin after they restart. Data it stored stays in the database, and adding the plugin again brings it back." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void remove(), children: "Uninstall" })] })] }));
260
572
  }