@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.
- package/README.md +88 -0
- package/apps/admin/audit-log/index.tsx +35 -8
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +19 -1
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/transport-rules/[uid].tsx +30 -1
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
- package/apps/admin/transport-rules/index.tsx +24 -6
- package/apps/admin/transport-rules/new/index.tsx +30 -1
- package/apps/escrow/_layout.tsx +2 -3
- package/apps/escrow/audit-log/index.tsx +196 -168
- package/apps/escrow/matters/[uid].tsx +621 -521
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
- package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
- package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/EventModal.tsx +717 -549
- package/apps/shared/components/calendar/MonthView.tsx +173 -139
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
- package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
- package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
- package/apps/shared/components/calendar/allDay.ts +124 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -348
- package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
- package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
- package/apps/shared/components/contacts/contactKeys.ts +78 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +123 -33
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
- package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
- package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
- package/apps/shared/components/mail/pinnedSigners.ts +124 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -0
- package/apps/shared/components/mail/writableMailboxes.ts +101 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
- package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
- package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/plugins/pluginNav.ts +73 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
- package/apps/shared/search/localIndexBuilder.ts +481 -179
- package/apps/shared/search/localIndexRpcClient.ts +231 -29
- package/apps/shared/search/localIndexSchema.ts +15 -6
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +32 -1
- package/apps/shared/search/localIndexWorker.ts +648 -186
- package/apps/www/calendar/index.tsx +438 -417
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +116 -12
- package/apps/www/index.tsx +1314 -1041
- package/apps/www/messages/[uid].tsx +18 -6
- package/apps/www/settings/auto-reply/index.tsx +134 -129
- package/apps/www/settings/encryption/index.tsx +739 -216
- package/apps/www/settings/filters/[uid].tsx +171 -155
- package/apps/www/settings/filters/new/index.tsx +8 -1
- package/apps/www/settings/focused-inbox/index.tsx +150 -148
- package/apps/www/settings/privacy/index.tsx +494 -436
- package/apps/www/settings/read-receipts/index.tsx +148 -125
- package/apps/www/settings/sharing/index.tsx +17 -14
- package/apps/www/tasks/index.tsx +638 -579
- package/dist/apps/admin/_404.d.ts +2 -0
- package/dist/apps/admin/_500.d.ts +2 -0
- package/dist/apps/admin/_layout.d.ts +8 -0
- package/dist/apps/admin/audit-log/index.d.ts +11 -0
- package/dist/apps/admin/audit-log/index.js +38 -8
- package/dist/apps/admin/branding/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
- package/dist/apps/admin/domains/[uid].d.ts +7 -0
- package/dist/apps/admin/domains/index.d.ts +3 -0
- package/dist/apps/admin/domains/new/index.d.ts +3 -0
- package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
- package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
- package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.d.ts +3 -0
- package/dist/apps/admin/index.js +4 -1
- package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
- package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
- package/dist/apps/admin/plugins/index.d.ts +3 -0
- package/dist/apps/admin/quarantine/index.d.ts +6 -0
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/retention-policy/index.d.ts +3 -0
- package/dist/apps/admin/setup/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
- package/dist/apps/admin/transport-rules/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.d.ts +10 -0
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.d.ts +9 -0
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/index.d.ts +3 -0
- package/dist/apps/escrow/matters/[uid].d.ts +7 -0
- package/dist/apps/escrow/matters/[uid].js +77 -42
- package/dist/apps/escrow/matters/new/index.d.ts +3 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
- package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
- package/dist/apps/shared/components/calendar/EventModal.js +142 -21
- package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
- package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
- package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
- package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
- package/dist/apps/shared/components/layout/AppShell.js +97 -26
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
- package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
- package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
- package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
- package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
- package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
- package/dist/apps/shared/plugins/pluginNav.js +33 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
- package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
- package/dist/apps/shared/search/localIndexBuilder.js +292 -37
- package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
- package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
- package/dist/apps/shared/search/localIndexSchema.js +12 -6
- package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
- package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
- package/dist/apps/shared/search/localIndexVFS.js +68 -40
- package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
- package/dist/apps/shared/search/localIndexWorker.js +519 -173
- package/dist/apps/shared/search/searchTier2.d.ts +30 -0
- package/dist/apps/www/_404.d.ts +2 -0
- package/dist/apps/www/_500.d.ts +2 -0
- package/dist/apps/www/_layout.d.ts +14 -0
- package/dist/apps/www/calendar/index.d.ts +3 -0
- package/dist/apps/www/calendar/index.js +28 -10
- package/dist/apps/www/contacts/[uid].d.ts +13 -0
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.d.ts +3 -0
- package/dist/apps/www/contacts/index.js +80 -10
- package/dist/apps/www/index.d.ts +3 -0
- package/dist/apps/www/index.js +339 -108
- package/dist/apps/www/messages/[uid].d.ts +11 -0
- package/dist/apps/www/messages/[uid].js +18 -6
- package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/encryption/index.d.ts +7 -0
- package/dist/apps/www/settings/encryption/index.js +514 -97
- package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
- package/dist/apps/www/settings/filters/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- package/dist/apps/www/settings/labels/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.d.ts +4 -0
- package/dist/apps/www/settings/sharing/index.js +7 -11
- package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
- package/dist/apps/www/settings/signatures/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
- package/dist/apps/www/tasks/index.d.ts +3 -0
- package/dist/apps/www/tasks/index.js +50 -10
- package/package.json +3 -3
- package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
- package/apps/www/settings/booking-types/[uid].tsx +0 -300
- package/apps/www/settings/booking-types/index.tsx +0 -98
- package/apps/www/settings/booking-types/new/index.tsx +0 -212
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
- package/dist/apps/www/settings/booking-types/[uid].js +0 -129
- package/dist/apps/www/settings/booking-types/index.js +0 -29
- package/dist/apps/www/settings/booking-types/new/index.js +0 -86
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listEscrowScopes } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
|
|
9
|
+
import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
13
|
+
const SELECT_CLASS = "text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
14
|
+
/** Enough for any realistic deployment's scopes in one list. */
|
|
15
|
+
const SCOPE_LIST_LIMIT = 200;
|
|
16
|
+
/**
|
|
17
|
+
* Assigns a mailbox to an escrow scope (or none), which decides whose holders can recover its encrypted mail. Only
|
|
18
|
+
* shown in the admin console, whose trusted-role gate the server enforces again for `escrowScopeId`.
|
|
19
|
+
*/
|
|
20
|
+
export default function EscrowScopeCard({ mailbox, onUpdate }) {
|
|
21
|
+
const [scopes, setScopes] = useState(null);
|
|
22
|
+
const [loadError, setLoadError] = useState(null);
|
|
23
|
+
const [escrowScopeId, setEscrowScopeId] = useState(mailbox.escrowScopeId ?? "");
|
|
24
|
+
const [saving, setSaving] = useState(false);
|
|
25
|
+
const [saveError, setSaveError] = useState(null);
|
|
26
|
+
const [saved, setSaved] = useState(false);
|
|
27
|
+
const [confirming, setConfirming] = useState(false);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
listEscrowScopes({ limit: SCOPE_LIST_LIMIT })
|
|
30
|
+
.then(setScopes)
|
|
31
|
+
.catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load escrow scopes."));
|
|
32
|
+
}, []);
|
|
33
|
+
// Changing the scope changes who can recover this mailbox's encrypted mail, so it's confirmed first.
|
|
34
|
+
function handleSubmit(e) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setConfirming(true);
|
|
37
|
+
}
|
|
38
|
+
async function save() {
|
|
39
|
+
setConfirming(false);
|
|
40
|
+
setSaveError(null);
|
|
41
|
+
setSaved(false);
|
|
42
|
+
setSaving(true);
|
|
43
|
+
try {
|
|
44
|
+
const updated = await updateMailbox({ uid: mailbox.uid, version: mailbox.version, escrowScopeId: escrowScopeId || null });
|
|
45
|
+
onUpdate(updated);
|
|
46
|
+
setSaved(true);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
setSaveError(err instanceof ApiRequestError ? err.message : "Could not save the escrow scope.");
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setSaving(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const current = mailbox.escrowScopeId ?? "";
|
|
56
|
+
const unknownCurrent = !!current && !!scopes && !scopes.some((scope) => scope.uid === current);
|
|
57
|
+
return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Escrow scope" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "The holders of this scope can jointly recover this mailbox\u2019s encrypted mail, for example for legal discovery. With no scope, nobody can." }), loadError && _jsx(Alert, { children: loadError }), saveError && _jsx(Alert, { children: saveError }), saved && !saveError && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), scopes && (_jsxs("form", { onSubmit: handleSubmit, className: "flex items-end gap-3 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Scope" }), _jsxs("select", { "aria-label": "Escrow scope", className: SELECT_CLASS, value: escrowScopeId, onChange: (e) => {
|
|
58
|
+
setEscrowScopeId(e.target.value);
|
|
59
|
+
setSaved(false);
|
|
60
|
+
}, children: [_jsx("option", { value: "", children: "No escrow" }), unknownCurrent && _jsxs("option", { value: current, children: ["Unknown scope (", current, ")"] }), scopes.map((scope) => (_jsx("option", { value: scope.uid, children: scope.name }, scope.uid)))] })] }), _jsx(Button, { type: "submit", className: "!w-auto", loading: saving, disabled: saving || escrowScopeId === current, children: "Save escrow scope" })] })), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Change escrow scope", children: [_jsxs("p", { className: "text-sm mb-3", children: ["This changes who can jointly recover ", _jsx("strong", { className: "break-all", children: mailbox.primarySmtpAddress }), "\u2019s encrypted mail."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "From" }), _jsx("dd", { className: "break-all", children: _jsx(ScopeSummary, { uid: current, scopes: scopes ?? [] }) }), _jsx("dt", { className: "text-text-muted", children: "To" }), _jsx("dd", { className: "break-all", children: _jsx(ScopeSummary, { uid: escrowScopeId, scopes: scopes ?? [] }) })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirming(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void save(), children: "Confirm and save" })] })] })] }));
|
|
61
|
+
}
|
|
62
|
+
/** A scope's name, holders and required approvals - or what having no (or an unlisted) scope means. */
|
|
63
|
+
function ScopeSummary({ uid, scopes }) {
|
|
64
|
+
if (!uid) {
|
|
65
|
+
return _jsx("span", { children: "No escrow - nobody can recover this mailbox\u2019s encrypted mail" });
|
|
66
|
+
}
|
|
67
|
+
const scope = scopes.find((candidate) => candidate.uid === uid);
|
|
68
|
+
if (!scope) {
|
|
69
|
+
return _jsxs("span", { children: ["Unknown scope (", uid, ")"] });
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs("span", { children: [_jsx("strong", { children: scope.name }), " - holders ", scope.holderUserUids.join(", "), "; ", scope.requiredHolders, " of", " ", scope.holderUserUids.length, " must approve"] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
export interface ResourceSettingsCardProps {
|
|
4
|
+
mailbox: Mailbox;
|
|
5
|
+
/** Called with the freshly-saved mailbox after a successful save — mirrors `MemberListCard`'s
|
|
6
|
+
* identical push-up-rather-than-reload pattern, since the caller already has everything the
|
|
7
|
+
* response would return. */
|
|
8
|
+
onUpdate: (mailbox: Mailbox) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Lets an admin view and edit a resource mailbox's booking settings — same load/edit/PUT/reload
|
|
12
|
+
* interaction shape as `ShareAccessCard`, adapted the same way `MemberListCard` already adapted it:
|
|
13
|
+
* these seven fields live directly on the `Mailbox` the parent detail page already loaded (not a
|
|
14
|
+
* separate resource fetched by its own endpoint, unlike a mailbox's ACL), so this receives the
|
|
15
|
+
* already-loaded `mailbox` and pushes the `PUT` response back up rather than fetching independently.
|
|
16
|
+
* Only rendered by the detail page when `mailbox.isResource` is already `true` — a mailbox becomes a
|
|
17
|
+
* resource at creation time (`apps/admin/mailboxes/new`), not retroactively from here.
|
|
18
|
+
*/
|
|
19
|
+
export default function ResourceSettingsCard({ mailbox, onUpdate }: ResourceSettingsCardProps): React.JSX.Element;
|
|
@@ -39,11 +39,11 @@ export default function ResourceSettingsCard({ mailbox, onUpdate }) {
|
|
|
39
39
|
uid: mailbox.uid,
|
|
40
40
|
version: mailbox.version,
|
|
41
41
|
resourceType,
|
|
42
|
-
resourceCapacity: resourceCapacity ? Number(resourceCapacity) :
|
|
42
|
+
resourceCapacity: resourceCapacity ? Number(resourceCapacity) : null,
|
|
43
43
|
autoAcceptBookings,
|
|
44
44
|
allowConflicts,
|
|
45
|
-
bookingWindowDays: bookingWindowDays ? Number(bookingWindowDays) :
|
|
46
|
-
maxDurationMinutes: maxDurationMinutes ? Number(maxDurationMinutes) :
|
|
45
|
+
bookingWindowDays: bookingWindowDays ? Number(bookingWindowDays) : null,
|
|
46
|
+
maxDurationMinutes: maxDurationMinutes ? Number(maxDurationMinutes) : null,
|
|
47
47
|
});
|
|
48
48
|
onUpdate(updated);
|
|
49
49
|
setSaved(true);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ShareAccessCardProps {
|
|
3
|
+
mailboxUid: string;
|
|
4
|
+
/** The mailbox's owner - their own grant is shown but can't be revoked from here. */
|
|
5
|
+
ownerUserUid?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Lets an admin (or, eventually, the mailbox owner) view and edit a mailbox's delegate access — the
|
|
9
|
+
* mechanism behind Exchange-style shared mailboxes. `records` on a mailbox's own ACL doubles as both the
|
|
10
|
+
* owner's grant (if any) and every delegate's — see `BaseMailboxRoute`'s doc comment in `@rapidmx/restapi`.
|
|
11
|
+
*/
|
|
12
|
+
export default function ShareAccessCard({ mailboxUid, ownerUserUid }: ShareAccessCardProps): React.JSX.Element;
|
|
@@ -8,18 +8,21 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
|
8
8
|
import { getMailboxAcl, grantMailboxAccess, revokeMailboxAccess } from "@rapidmx/react-shared/mail/mailApi.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
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
11
12
|
const DEFAULT_DELEGATE_ACTIONS = ["read", "list", "count", "exists"];
|
|
12
13
|
/**
|
|
13
14
|
* Lets an admin (or, eventually, the mailbox owner) view and edit a mailbox's delegate access — the
|
|
14
15
|
* mechanism behind Exchange-style shared mailboxes. `records` on a mailbox's own ACL doubles as both the
|
|
15
16
|
* owner's grant (if any) and every delegate's — see `BaseMailboxRoute`'s doc comment in `@rapidmx/restapi`.
|
|
16
17
|
*/
|
|
17
|
-
export default function ShareAccessCard({ mailboxUid }) {
|
|
18
|
+
export default function ShareAccessCard({ mailboxUid, ownerUserUid }) {
|
|
18
19
|
const [acl, setAcl] = useState(null);
|
|
19
20
|
const [loading, setLoading] = useState(true);
|
|
20
21
|
const [error, setError] = useState(null);
|
|
22
|
+
const [notice, setNotice] = useState(null);
|
|
21
23
|
const [newUserUid, setNewUserUid] = useState("");
|
|
22
24
|
const [saving, setSaving] = useState(false);
|
|
25
|
+
const [revokeTarget, setRevokeTarget] = useState(null);
|
|
23
26
|
function reload() {
|
|
24
27
|
setLoading(true);
|
|
25
28
|
setError(null);
|
|
@@ -37,8 +40,19 @@ export default function ShareAccessCard({ mailboxUid }) {
|
|
|
37
40
|
}
|
|
38
41
|
setSaving(true);
|
|
39
42
|
setError(null);
|
|
43
|
+
setNotice(null);
|
|
40
44
|
try {
|
|
41
|
-
|
|
45
|
+
// `grantMailboxAccess()` replaces any existing record for this uid, so granting read access to someone
|
|
46
|
+
// who already has more (e.g. the owner) would silently downgrade them. Merge with their current actions.
|
|
47
|
+
const current = await getMailboxAcl(mailboxUid);
|
|
48
|
+
const existing = current.records.find((record) => record.userOrRoleId === userUid)?.actions ?? [];
|
|
49
|
+
const missing = DEFAULT_DELEGATE_ACTIONS.filter((action) => !existing.includes(action));
|
|
50
|
+
if (missing.length === 0) {
|
|
51
|
+
setNotice(`${userUid} already has this access.`);
|
|
52
|
+
setNewUserUid("");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await grantMailboxAccess(mailboxUid, userUid, [...existing, ...missing]);
|
|
42
56
|
setNewUserUid("");
|
|
43
57
|
reload();
|
|
44
58
|
}
|
|
@@ -49,9 +63,13 @@ export default function ShareAccessCard({ mailboxUid }) {
|
|
|
49
63
|
setSaving(false);
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
|
-
|
|
66
|
+
// Only ever invoked from the revoke-confirmation modal below, which only renders once `revokeTarget` is set.
|
|
67
|
+
async function handleRevoke() {
|
|
68
|
+
const userOrRoleId = revokeTarget;
|
|
69
|
+
setRevokeTarget(null);
|
|
53
70
|
setSaving(true);
|
|
54
71
|
setError(null);
|
|
72
|
+
setNotice(null);
|
|
55
73
|
try {
|
|
56
74
|
await revokeMailboxAccess(mailboxUid, userOrRoleId);
|
|
57
75
|
reload();
|
|
@@ -63,5 +81,5 @@ export default function ShareAccessCard({ mailboxUid }) {
|
|
|
63
81
|
setSaving(false);
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
|
-
return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Shared access" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Grants read access to this mailbox for another user, independent of ownership." }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [(acl?.records ?? []).length === 0 && (_jsx("li", { className: "text-sm text-text-muted", children: "No grants on this mailbox yet." })), acl?.records.map((record) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: record.userOrRoleId }), _jsx("div", { className: "text-xs text-text-muted", children: record.actions.join(", ") })] }), _jsx("button", { type: "button", onClick: () =>
|
|
84
|
+
return (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Shared access" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Grants read access to this mailbox for another user, independent of ownership." }), error && _jsx(Alert, { children: error }), notice && !error && _jsx("div", { className: "mb-4 text-sm text-text-muted", children: notice }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("ul", { className: "flex flex-col gap-2 mb-4", children: [(acl?.records ?? []).length === 0 && (_jsx("li", { className: "text-sm text-text-muted", children: "No grants on this mailbox yet." })), acl?.records.map((record) => (_jsxs("li", { className: "flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: record.userOrRoleId }), _jsx("div", { className: "text-xs text-text-muted", children: record.actions.join(", ") })] }), record.userOrRoleId === ownerUserUid ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Owner" })) : (_jsx("button", { type: "button", onClick: () => setRevokeTarget(record.userOrRoleId), disabled: saving, className: "text-sm text-danger hover:underline disabled:opacity-55", children: "Revoke" }))] }, record.userOrRoleId)))] })), _jsxs("form", { onSubmit: handleGrant, className: "flex gap-2", children: [_jsx("input", { type: "text", value: newUserUid, onChange: (e) => setNewUserUid(e.target.value), placeholder: "User uid to grant access to", className: "flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary" }), _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Grant" })] }), _jsxs(Modal, { open: revokeTarget !== null, onClose: () => setRevokeTarget(null), title: "Revoke access", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Revoke ", _jsx("strong", { className: "break-all", children: revokeTarget }), "\u2019s access to this mailbox? They lose it immediately."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRevokeTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleRevoke(), children: "Revoke" })] })] })] }));
|
|
67
85
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
3
|
+
/** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
|
|
4
|
+
export declare function isSvgFile(file: File): boolean;
|
|
5
|
+
/** The branding editor, shared by the Branding page and the setup wizard. */
|
|
6
|
+
export default function BrandingForm({ branding, onChange }: {
|
|
7
|
+
branding: Branding;
|
|
8
|
+
onChange: (b: Branding) => void;
|
|
9
|
+
}): React.JSX.Element;
|
|
@@ -10,6 +10,11 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
|
10
10
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
11
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
12
12
|
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
/** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
|
|
14
|
+
export function isSvgFile(file) {
|
|
15
|
+
return file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
|
|
16
|
+
}
|
|
17
|
+
const RASTER_IMAGE_ACCEPT = "image/png,image/jpeg,image/gif,image/webp,image/x-icon,image/vnd.microsoft.icon";
|
|
13
18
|
/** The branding editor, shared by the Branding page and the setup wizard. */
|
|
14
19
|
export default function BrandingForm({ branding, onChange }) {
|
|
15
20
|
const [companyName, setCompanyName] = useState(branding.companyName);
|
|
@@ -43,7 +48,10 @@ export default function BrandingForm({ branding, onChange }) {
|
|
|
43
48
|
const file = e.target.files?.[0];
|
|
44
49
|
if (!file)
|
|
45
50
|
return;
|
|
46
|
-
if (file
|
|
51
|
+
if (isSvgFile(file)) {
|
|
52
|
+
setError(`"${file.name}" is an SVG — upload a PNG, JPEG, GIF, or WebP logo instead.`);
|
|
53
|
+
}
|
|
54
|
+
else if (file.size > MAX_UPLOAD_BYTES) {
|
|
47
55
|
setError(`"${file.name}" is too large — logos must be 5MB or smaller.`);
|
|
48
56
|
}
|
|
49
57
|
else {
|
|
@@ -55,7 +63,10 @@ export default function BrandingForm({ branding, onChange }) {
|
|
|
55
63
|
const file = e.target.files?.[0];
|
|
56
64
|
if (!file)
|
|
57
65
|
return;
|
|
58
|
-
if (file
|
|
66
|
+
if (isSvgFile(file)) {
|
|
67
|
+
setError(`"${file.name}" is an SVG — upload a PNG, JPEG, GIF, WebP, or ICO icon instead.`);
|
|
68
|
+
}
|
|
69
|
+
else if (file.size > MAX_UPLOAD_BYTES) {
|
|
59
70
|
setError(`"${file.name}" is too large — icons must be 5MB or smaller.`);
|
|
60
71
|
}
|
|
61
72
|
else {
|
|
@@ -92,10 +103,10 @@ export default function BrandingForm({ branding, onChange }) {
|
|
|
92
103
|
setSaving(false);
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
|
-
return (_jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept:
|
|
106
|
+
return (_jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload logo", className: "hidden", onChange: handleLogoFileChange }), branding.logoUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "logo-delete", onClick: () => void runAsset("logo-delete", async () => {
|
|
96
107
|
await deleteBrandingLogo();
|
|
97
108
|
return { ...branding, logoUrl: undefined };
|
|
98
|
-
}), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept:
|
|
109
|
+
}), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: RASTER_IMAGE_ACCEPT, "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
|
|
99
110
|
await deleteBrandingIcon();
|
|
100
111
|
return { ...branding, iconUrl: undefined };
|
|
101
112
|
}), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Domain } from "@rapidmx/react-shared/admin/domainsApi.js";
|
|
3
|
+
export interface DomainDnsSetupProps {
|
|
4
|
+
uid: string;
|
|
5
|
+
/** Called with the domain every time it's (re)loaded - e.g. after "Verify now". */
|
|
6
|
+
onLoaded?: (domain: Domain) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A domain's status, its ownership TXT record with "Verify now", and the DNS setup checklist - shared by the domain
|
|
10
|
+
* detail page and the setup wizard.
|
|
11
|
+
*/
|
|
12
|
+
export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps): React.JSX.Element;
|
|
@@ -25,11 +25,12 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
|
|
|
25
25
|
const [loading, setLoading] = useState(true);
|
|
26
26
|
const [error, setError] = useState(null);
|
|
27
27
|
const [verifying, setVerifying] = useState(false);
|
|
28
|
+
const [verifyError, setVerifyError] = useState(null);
|
|
28
29
|
const [copied, setCopied] = useState(false);
|
|
29
30
|
function reload(id) {
|
|
30
31
|
setLoading(true);
|
|
31
32
|
setError(null);
|
|
32
|
-
Promise.all([getDomain(id), getDnsSetup(id)])
|
|
33
|
+
return Promise.all([getDomain(id), getDnsSetup(id)])
|
|
33
34
|
.then(([d, checks]) => {
|
|
34
35
|
setDomain(d);
|
|
35
36
|
setDnsSetup(checks);
|
|
@@ -39,18 +40,23 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
|
|
|
39
40
|
.finally(() => setLoading(false));
|
|
40
41
|
}
|
|
41
42
|
useEffect(() => {
|
|
42
|
-
reload(uid);
|
|
43
|
+
void reload(uid);
|
|
43
44
|
}, [uid]);
|
|
44
|
-
// Only ever invoked from the "Verify now" button below, which only renders once `domain` is loaded.
|
|
45
|
+
// Only ever invoked from the "Verify now" button below, which only renders once `domain` is loaded. Any failure -
|
|
46
|
+
// verifying, or refreshing afterwards - is shown next to the button with the panel kept, so it can be retried
|
|
47
|
+
// (e.g. once a DNS record has propagated) instead of the whole panel being replaced by an error.
|
|
45
48
|
async function handleVerify() {
|
|
46
49
|
setVerifying(true);
|
|
47
|
-
|
|
50
|
+
setVerifyError(null);
|
|
48
51
|
try {
|
|
49
52
|
await verifyDomain(domain.uid);
|
|
50
|
-
|
|
53
|
+
const [d, checks] = await Promise.all([getDomain(domain.uid), getDnsSetup(domain.uid)]);
|
|
54
|
+
setDomain(d);
|
|
55
|
+
setDnsSetup(checks);
|
|
56
|
+
onLoaded?.(d);
|
|
51
57
|
}
|
|
52
58
|
catch (err) {
|
|
53
|
-
|
|
59
|
+
setVerifyError(err instanceof ApiRequestError ? err.message : "Could not verify this domain.");
|
|
54
60
|
}
|
|
55
61
|
finally {
|
|
56
62
|
setVerifying(false);
|
|
@@ -73,5 +79,5 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
|
|
|
73
79
|
return _jsx(Alert, { children: error ?? "Domain not found." });
|
|
74
80
|
}
|
|
75
81
|
const ownershipValue = dnsSetup.find((c) => c.type === "ownership")?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`;
|
|
76
|
-
return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap", children: ownershipValue }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => handleCopy(ownershipValue), children: copied ? "Copied" : "Copy" })] }), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Recommended value"].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: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: RECORD_TYPE_LABELS[check.type] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: _jsx("code", { className: "text-xs", children: check.recommendedValue ?? "—" }) })] }, check.type))) })] }) })] }))] }));
|
|
82
|
+
return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap", children: ownershipValue }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => handleCopy(ownershipValue), children: copied ? "Copied" : "Copy" })] }), verifyError && (_jsx("div", { className: "mt-3", children: _jsx(Alert, { children: verifyError }) })), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: verifyError ? "Try again" : "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Recommended value"].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: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: RECORD_TYPE_LABELS[check.type] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: _jsx("code", { className: "text-xs", children: check.recommendedValue ?? "—" }) })] }, check.type))) })] }) })] }))] }));
|
|
77
83
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { EncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
3
|
+
/** Whether end-to-end encryption can be used at all under `policy` - i.e. any tier allows it. */
|
|
4
|
+
export declare function isEncryptionEnabled(policy: EncryptionPolicy): boolean;
|
|
5
|
+
export interface EncryptionPolicyFormProps {
|
|
6
|
+
policy: EncryptionPolicy;
|
|
7
|
+
onChange: (policy: EncryptionPolicy) => void;
|
|
8
|
+
/** Told whether the form has edits that haven't been saved. */
|
|
9
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
/** The end-to-end encryption policy editor, shared by the Encryption Policy page and the setup wizard. */
|
|
12
|
+
export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange }: EncryptionPolicyFormProps): React.JSX.Element;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useState } from "react";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { updateEncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
@@ -28,11 +28,15 @@ export function isEncryptionEnabled(policy) {
|
|
|
28
28
|
return TIERS.some((tier) => policy[tier.key] !== "prohibited");
|
|
29
29
|
}
|
|
30
30
|
/** The end-to-end encryption policy editor, shared by the Encryption Policy page and the setup wizard. */
|
|
31
|
-
export default function EncryptionPolicyForm({ policy, onChange }) {
|
|
31
|
+
export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange }) {
|
|
32
32
|
const [values, setValues] = useState(policy);
|
|
33
33
|
const [error, setError] = useState(null);
|
|
34
34
|
const [saving, setSaving] = useState(false);
|
|
35
35
|
const [saved, setSaved] = useState(false);
|
|
36
|
+
// Edits are unsaved until they match what was loaded or last saved.
|
|
37
|
+
const [savedValues, setSavedValues] = useState(policy);
|
|
38
|
+
const dirty = TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]);
|
|
39
|
+
useEffect(() => onDirtyChange?.(dirty), [dirty]);
|
|
36
40
|
async function handleSubmit(e) {
|
|
37
41
|
e.preventDefault();
|
|
38
42
|
setError(null);
|
|
@@ -41,6 +45,7 @@ export default function EncryptionPolicyForm({ policy, onChange }) {
|
|
|
41
45
|
try {
|
|
42
46
|
const updated = await updateEncryptionPolicy(values);
|
|
43
47
|
onChange(updated);
|
|
48
|
+
setSavedValues(values);
|
|
44
49
|
setSaved(true);
|
|
45
50
|
}
|
|
46
51
|
catch (err) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
export interface LoadedSettingsFormProps<T> {
|
|
3
|
+
load: () => Promise<T>;
|
|
4
|
+
/** Shown when loading fails with something other than an API error message. */
|
|
5
|
+
loadErrorMessage: string;
|
|
6
|
+
children: (value: T, onChange: (value: T) => void) => ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** Loads a settings object, then renders its form - the loading and error states every settings page shares. */
|
|
9
|
+
export default function LoadedSettingsForm<T>({ load, loadErrorMessage, children }: LoadedSettingsFormProps<T>): React.JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
export interface MailboxCreateFormProps {
|
|
4
|
+
onCreated: (mailbox: Mailbox) => void;
|
|
5
|
+
/** Initial field values, e.g. the signed-in admin's own mailbox in the setup wizard. */
|
|
6
|
+
defaults?: {
|
|
7
|
+
localPart?: string;
|
|
8
|
+
domain?: string;
|
|
9
|
+
displayName?: string;
|
|
10
|
+
ownerUserUid?: string;
|
|
11
|
+
};
|
|
12
|
+
submitLabel?: string;
|
|
13
|
+
cancelHref?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The "New mailbox" form, shared by the New Mailbox page and the setup wizard. The quota starts at the mailbox
|
|
17
|
+
* policy's default.
|
|
18
|
+
*/
|
|
19
|
+
export default function MailboxCreateForm({ onCreated, defaults, submitLabel, cancelHref }: MailboxCreateFormProps): React.JSX.Element;
|
|
@@ -69,6 +69,12 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
69
69
|
setError("A display name is required.");
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
+
// restapi refuses these (400): a display name shown in From must not look like an address. The fullwidth and
|
|
73
|
+
// small look-alike @ signs are refused too (restapi's `AT_SIGN_LIKE`).
|
|
74
|
+
if (/[@@﹫\r\n]/.test(displayName)) {
|
|
75
|
+
setError("A display name can't contain \"@\" (or a look-alike) or line breaks.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
72
78
|
setSaving(true);
|
|
73
79
|
try {
|
|
74
80
|
// ownerUserUid left blank creates a true ownerless shared mailbox (e.g. support@example.com);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { MailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.js";
|
|
3
|
+
export interface MailboxPolicyFormProps {
|
|
4
|
+
policy: MailboxPolicy;
|
|
5
|
+
onChange: (policy: MailboxPolicy) => void;
|
|
6
|
+
/** Told whether the form has edits that haven't been saved. */
|
|
7
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
/** The mailbox defaults editor, shared by the Mailbox Policy page and the setup wizard. */
|
|
10
|
+
export default function MailboxPolicyForm({ policy, onChange, onDirtyChange }: MailboxPolicyFormProps): React.JSX.Element;
|
|
@@ -3,42 +3,64 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useState } from "react";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { updateMailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.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
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
12
12
|
const GB = 1000000000;
|
|
13
|
+
/** Unrounded, so a quota that isn't a whole number of hundredths of a GB (say 4 MB) is shown as it is. */
|
|
13
14
|
function toGb(bytes) {
|
|
14
|
-
return String(
|
|
15
|
+
return String(bytes / GB);
|
|
15
16
|
}
|
|
16
17
|
/** The mailbox defaults editor, shared by the Mailbox Policy page and the setup wizard. */
|
|
17
|
-
export default function MailboxPolicyForm({ policy, onChange }) {
|
|
18
|
+
export default function MailboxPolicyForm({ policy, onChange, onDirtyChange }) {
|
|
18
19
|
const [defaultQuotaGb, setDefaultQuotaGb] = useState(toGb(policy.defaultQuotaBytes));
|
|
19
20
|
const [autoProvisionEnabled, setAutoProvisionEnabled] = useState(policy.autoProvisionEnabled);
|
|
20
21
|
const [autoProvisionQuotaGb, setAutoProvisionQuotaGb] = useState(toGb(policy.autoProvisionQuotaBytes));
|
|
21
22
|
const [error, setError] = useState(null);
|
|
22
23
|
const [saving, setSaving] = useState(false);
|
|
23
24
|
const [saved, setSaved] = useState(false);
|
|
25
|
+
// Edits are unsaved until they match what was loaded or last saved.
|
|
26
|
+
const [baseline, setBaseline] = useState({ defaultQuotaGb, autoProvisionEnabled, autoProvisionQuotaGb });
|
|
27
|
+
const dirty = defaultQuotaGb !== baseline.defaultQuotaGb ||
|
|
28
|
+
autoProvisionEnabled !== baseline.autoProvisionEnabled ||
|
|
29
|
+
autoProvisionQuotaGb !== baseline.autoProvisionQuotaGb;
|
|
30
|
+
useEffect(() => onDirtyChange?.(dirty), [dirty]);
|
|
24
31
|
async function handleSubmit(e) {
|
|
25
32
|
e.preventDefault();
|
|
26
33
|
setError(null);
|
|
27
34
|
setSaved(false);
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
// Only fields the administrator changed are sent, so a quota left alone keeps its exact stored bytes.
|
|
36
|
+
const patch = {};
|
|
37
|
+
for (const [field, text, base] of [
|
|
38
|
+
["defaultQuotaBytes", defaultQuotaGb, baseline.defaultQuotaGb],
|
|
39
|
+
["autoProvisionQuotaBytes", autoProvisionQuotaGb, baseline.autoProvisionQuotaGb],
|
|
40
|
+
]) {
|
|
41
|
+
if (text === base) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const bytes = Math.round(Number(text) * GB);
|
|
45
|
+
if (!(bytes >= 1)) {
|
|
46
|
+
setError("Quotas must be more than 0 GB.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
patch[field] = bytes;
|
|
50
|
+
}
|
|
51
|
+
if (autoProvisionEnabled !== baseline.autoProvisionEnabled) {
|
|
52
|
+
patch.autoProvisionEnabled = autoProvisionEnabled;
|
|
53
|
+
}
|
|
54
|
+
const current = { defaultQuotaGb, autoProvisionEnabled, autoProvisionQuotaGb };
|
|
55
|
+
if (Object.keys(patch).length === 0) {
|
|
56
|
+
setSaved(true);
|
|
32
57
|
return;
|
|
33
58
|
}
|
|
34
59
|
setSaving(true);
|
|
35
60
|
try {
|
|
36
|
-
const updated = await updateMailboxPolicy(
|
|
37
|
-
defaultQuotaBytes: Math.round(defaultQuota * GB),
|
|
38
|
-
autoProvisionEnabled,
|
|
39
|
-
autoProvisionQuotaBytes: Math.round(autoQuota * GB),
|
|
40
|
-
});
|
|
61
|
+
const updated = await updateMailboxPolicy(patch);
|
|
41
62
|
onChange(updated);
|
|
63
|
+
setBaseline(current);
|
|
42
64
|
setSaved(true);
|
|
43
65
|
}
|
|
44
66
|
catch (err) {
|