@rapidmx/web-client 0.29.0 → 0.31.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/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +58 -8
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/SwipeRow.tsx +26 -4
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/gestures/useLongPress.ts +138 -0
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +139 -43
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.d.ts +8 -3
- package/dist/apps/shared/components/mail/ConversationList.js +51 -8
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/SwipeRow.d.ts +5 -1
- package/dist/apps/shared/components/mail/SwipeRow.js +17 -3
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/gestures/useLongPress.d.ts +41 -0
- package/dist/apps/shared/gestures/useLongPress.js +112 -0
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +124 -40
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +13 -2
- package/dist/lib/admin/pluginsApi.js +8 -0
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +21 -2
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { listLeftoverMailboxes } from "../../../../../lib/admin/leftoverMailboxApi.js";
|
|
9
|
-
import Alert from "
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
10
|
import EraseLeftoverDataDialog from "./EraseLeftoverDataDialog.js";
|
|
11
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
12
12
|
/** How many deleted mailboxes one request asks for. */
|
|
13
13
|
const PAGE_SIZE = 50;
|
|
14
14
|
/** How often the list is refreshed while an erasure is under way. */
|
|
@@ -45,7 +45,7 @@ export default function LeftoverMailboxesSection({ refreshIntervalMs = LEFTOVER_
|
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
47
|
if (current === generation.current) {
|
|
48
|
-
setError(err
|
|
48
|
+
setError(actionErrorMessage(err, "Could not check for deleted mailboxes with remaining data."));
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -61,7 +61,7 @@ export default function LeftoverMailboxesSection({ refreshIntervalMs = LEFTOVER_
|
|
|
61
61
|
}
|
|
62
62
|
catch (err) {
|
|
63
63
|
if (current === generation.current) {
|
|
64
|
-
setError(err
|
|
64
|
+
setError(actionErrorMessage(err, "Could not load more deleted mailboxes."));
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
finally {
|
|
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { updateMailbox } from "../../../../../lib/mail/mailApi.js";
|
|
9
8
|
import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
|
|
10
|
-
import Alert from "
|
|
9
|
+
import Alert from "../ActionAlert.js";
|
|
11
10
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
12
12
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
13
13
|
const SELECT_CLASS = "text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
14
14
|
/**
|
|
@@ -51,7 +51,7 @@ export default function ResourceSettingsCard({ mailbox, onUpdate }) {
|
|
|
51
51
|
setSaved(true);
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
|
-
setError(err
|
|
54
|
+
setError(actionErrorMessage(err, "Could not save resource settings."));
|
|
55
55
|
}
|
|
56
56
|
finally {
|
|
57
57
|
setSaving(false);
|
|
@@ -4,12 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "../../../../../lib/mail/mailboxAccessApi.js";
|
|
9
|
-
import Alert from "
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
10
|
import Modal from "../../../../../lib/components/overlays/Modal.js";
|
|
12
11
|
import PrincipalPicker from "../../sharing/PrincipalPicker.js";
|
|
12
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
13
13
|
const ROLE_LABELS = { viewer: "Read only", manager: "Full access" };
|
|
14
14
|
function roleLabel(member) {
|
|
15
15
|
if (member.role === "viewer")
|
|
@@ -38,7 +38,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
|
|
|
38
38
|
setError(null);
|
|
39
39
|
listMailboxAccess(mailboxUid)
|
|
40
40
|
.then(setMembers)
|
|
41
|
-
.catch((err) => setError(err
|
|
41
|
+
.catch((err) => setError(actionErrorMessage(err, "Could not load share settings.")))
|
|
42
42
|
.finally(() => setLoading(false));
|
|
43
43
|
}
|
|
44
44
|
useEffect(reload, [mailboxUid]);
|
|
@@ -50,7 +50,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
|
|
|
50
50
|
reload();
|
|
51
51
|
}
|
|
52
52
|
catch (err) {
|
|
53
|
-
setError(err
|
|
53
|
+
setError(actionErrorMessage(err, "Could not grant access."));
|
|
54
54
|
}
|
|
55
55
|
finally {
|
|
56
56
|
setSaving(false);
|
|
@@ -66,7 +66,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
|
|
|
66
66
|
await removeMailboxAccess(mailboxUid, stale.userOrRoleId);
|
|
67
67
|
}
|
|
68
68
|
catch (err) {
|
|
69
|
-
failure = err
|
|
69
|
+
failure = actionErrorMessage(err, "Could not remove the old entry.");
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
reload();
|
|
@@ -86,7 +86,7 @@ export default function ShareAccessCard({ mailboxUid, ownerUserUid, currentUserU
|
|
|
86
86
|
reload();
|
|
87
87
|
}
|
|
88
88
|
catch (err) {
|
|
89
|
-
setError(err
|
|
89
|
+
setError(actionErrorMessage(err, "Could not revoke access."));
|
|
90
90
|
}
|
|
91
91
|
finally {
|
|
92
92
|
setSaving(false);
|
|
@@ -4,11 +4,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useRef, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { deleteBrandingIcon, deleteBrandingLogo, deleteBrandingStylesheet, updateBranding, uploadBrandingIcon, uploadBrandingLogo, uploadBrandingStylesheet, } from "../../../../../lib/branding/brandingApi.js";
|
|
9
|
-
import Alert from "
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
10
|
import CopyButton from "../../../../../lib/components/buttons/CopyButton.js";
|
|
11
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
12
12
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
13
13
|
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
14
14
|
/** A header that uses both variables - the snippet the Branding page offers to copy. */
|
|
@@ -48,9 +48,19 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
|
|
|
48
48
|
setIconUrlInput(updated.iconUrl ?? "");
|
|
49
49
|
setStylesheetUrlInput(updated.stylesheetUrl ?? "");
|
|
50
50
|
})
|
|
51
|
-
.catch((err) => setError(err
|
|
51
|
+
.catch((err) => setError(actionErrorMessage(err, "Could not update branding.")))
|
|
52
52
|
.finally(() => setAssetBusy(null));
|
|
53
53
|
}
|
|
54
|
+
/** Sets a branding asset to an address the administrator typed. Every visitor's browser loads it - the anonymous booking pages too - so it must be an
|
|
55
|
+
* encrypted (`https:`) address or one on this server (a path), not `http:`, `data:` or `javascript:`. */
|
|
56
|
+
function runUrl(name, field, value) {
|
|
57
|
+
const address = value.trim();
|
|
58
|
+
if (address !== "" && !/^https:\/\/[^\s/]/i.test(address) && !/^\/(?![/\\])/.test(address)) {
|
|
59
|
+
setError("Use an https:// address, or a path on this server, or upload a file instead.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
void runAsset(name, () => updateBranding({ [field]: address }));
|
|
63
|
+
}
|
|
54
64
|
function handleLogoFileChange(e) {
|
|
55
65
|
const file = e.target.files?.[0];
|
|
56
66
|
if (!file)
|
|
@@ -104,7 +114,7 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
|
|
|
104
114
|
setSaved(true);
|
|
105
115
|
}
|
|
106
116
|
catch (err) {
|
|
107
|
-
setError(err
|
|
117
|
+
setError(actionErrorMessage(err, "Could not save branding."));
|
|
108
118
|
}
|
|
109
119
|
finally {
|
|
110
120
|
setSaving(false);
|
|
@@ -113,11 +123,11 @@ export default function BrandingForm({ branding, onChange, embedded = false }) {
|
|
|
113
123
|
return (_jsxs("div", { className: "max-w-2xl", children: [!embedded && (_jsxs(_Fragment, { 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. The header appears on the webmail and public pages, not in the admin console; the footer appears in both." })] })), 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(SectionHeading, { 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 () => {
|
|
114
124
|
await deleteBrandingLogo();
|
|
115
125
|
return { ...branding, logoUrl: undefined };
|
|
116
|
-
}), 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: () =>
|
|
126
|
+
}), 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: () => runUrl("logo-url", "logoUrl", logoUrlInput), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { 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 () => {
|
|
117
127
|
await deleteBrandingIcon();
|
|
118
128
|
return { ...branding, iconUrl: undefined };
|
|
119
|
-
}), 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: () =>
|
|
129
|
+
}), 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: () => runUrl("icon-url", "iconUrl", iconUrlInput), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx(SectionHeading, { 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 () => {
|
|
120
130
|
await deleteBrandingStylesheet();
|
|
121
131
|
return { ...branding, stylesheetUrl: undefined };
|
|
122
|
-
}), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () =>
|
|
132
|
+
}), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsx("span", { className: "text-xs text-text-muted", children: "Every visitor\u2019s browser loads it, and a stylesheet can read what the page shows: only use an address you control, or upload the file instead." }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => runUrl("stylesheet-url", "stylesheetUrl", stylesheetUrlInput), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: ["A header replaces the app's own title bar and the icon at the top of the icon rail, and stays at the top of the window. Write", " ", _jsx("code", { children: "{USER_MENU}" }), " where the account menu (avatar, Settings, Sign Out) should go, and ", _jsx("code", { children: "{APP_TITLE}" }), " ", "where the name of the app on screen should show. Both work in the footer too. ", _jsx("code", { children: "{USER_MENU}" }), " is replaced once (the header's, else the footer's); without it the menu sits in a small cell at the right end of the header, so it is never lost. Put it where your styles don't hide it on a phone."] })] }), _jsxs("div", { className: "flex flex-col gap-1.5 text-sm", role: "group", "aria-label": "Header example", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-semibold", children: "Example header" }), _jsx(CopyButton, { value: HEADER_HTML_EXAMPLE, label: "Copy the example header HTML" })] }), _jsx("pre", { className: "overflow-x-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs text-text", children: HEADER_HTML_EXAMPLE })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) }), _jsxs("span", { className: "text-xs text-text-muted", children: [_jsx("code", { children: "{APP_TITLE}" }), " works here as well, and ", _jsx("code", { children: "{USER_MENU}" }), " takes the account menu (opening upward) when the header has none."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
|
|
123
133
|
}
|
|
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { getDnsSetup, getDomain, verifyDomain } from "../../../../../lib/admin/domainsApi.js";
|
|
9
|
-
import Alert from "
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
10
|
import CopyButton from "../../../../../lib/components/buttons/CopyButton.js";
|
|
11
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
12
12
|
const RECORD_TYPE_LABELS = {
|
|
13
13
|
ownership: "Ownership (TXT)",
|
|
14
14
|
mx: "MX",
|
|
@@ -68,7 +68,7 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
|
|
|
68
68
|
setDnsSetup(checks);
|
|
69
69
|
onLoaded?.(d);
|
|
70
70
|
})
|
|
71
|
-
.catch((err) => setError(err
|
|
71
|
+
.catch((err) => setError(actionErrorMessage(err, "Could not load this domain.")))
|
|
72
72
|
.finally(() => setLoading(false));
|
|
73
73
|
}
|
|
74
74
|
useEffect(() => {
|
|
@@ -88,7 +88,7 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
|
|
|
88
88
|
onLoaded?.(d);
|
|
89
89
|
}
|
|
90
90
|
catch (err) {
|
|
91
|
-
setVerifyError(err
|
|
91
|
+
setVerifyError(actionErrorMessage(err, "Could not verify this domain."));
|
|
92
92
|
}
|
|
93
93
|
finally {
|
|
94
94
|
setVerifying(false);
|
|
@@ -4,10 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { updateEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
|
|
9
|
-
import Alert from "
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
10
|
+
import Modal from "../../../../../lib/components/overlays/Modal.js";
|
|
11
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
11
12
|
const SELECT_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
13
|
const TIERS = [
|
|
13
14
|
{ key: "encryptSameOrg", label: "Mail within this server", help: "Messages between mailboxes hosted on this server." },
|
|
@@ -37,8 +38,19 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
|
|
|
37
38
|
const [savedValues, setSavedValues] = useState(policy);
|
|
38
39
|
const dirty = TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]);
|
|
39
40
|
useEffect(() => onDirtyChange?.(dirty), [dirty]);
|
|
40
|
-
|
|
41
|
+
// Tiers this save would newly set to "Never encrypt", which switches encryption off for everyone's mail of that kind: confirmed first.
|
|
42
|
+
const [confirmingProhibit, setConfirmingProhibit] = useState(null);
|
|
43
|
+
function handleSubmit(e) {
|
|
41
44
|
e.preventDefault();
|
|
45
|
+
const newlyProhibited = TIERS.filter((tier) => values[tier.key] === "prohibited" && savedValues[tier.key] !== "prohibited");
|
|
46
|
+
if (newlyProhibited.length > 0) {
|
|
47
|
+
setConfirmingProhibit(newlyProhibited.map((tier) => tier.label));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
void save();
|
|
51
|
+
}
|
|
52
|
+
async function save() {
|
|
53
|
+
setConfirmingProhibit(null);
|
|
42
54
|
setError(null);
|
|
43
55
|
setSaved(false);
|
|
44
56
|
setSaving(true);
|
|
@@ -49,7 +61,7 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
|
|
|
49
61
|
setSaved(true);
|
|
50
62
|
}
|
|
51
63
|
catch (err) {
|
|
52
|
-
setError(err
|
|
64
|
+
setError(actionErrorMessage(err, "Could not save the encryption policy."));
|
|
53
65
|
}
|
|
54
66
|
finally {
|
|
55
67
|
setSaving(false);
|
|
@@ -58,5 +70,5 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
|
|
|
58
70
|
return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" })) : (_jsx("h2", { className: "text-lg font-bold uppercase tracking-wide mb-1", children: "End-to-end encryption" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Encrypted messages can only be read by their senders and recipients - not by this server or its administrators. Choose how encryption applies to each kind of mail." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [TIERS.map((tier) => (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: tier.label }), _jsx("select", { "aria-label": tier.label, className: SELECT_CLASS, value: values[tier.key], onChange: (e) => {
|
|
59
71
|
setSaved(false);
|
|
60
72
|
setValues({ ...values, [tier.key]: e.target.value });
|
|
61
|
-
}, children: STATES.map((state) => (_jsx("option", { value: state.value, children: state.label }, state.value))) }), _jsx("span", { className: "text-xs text-text-muted", children: tier.help })] }, tier.key))), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }));
|
|
73
|
+
}, children: STATES.map((state) => (_jsx("option", { value: state.value, children: state.label }, state.value))) }), _jsx("span", { className: "text-xs text-text-muted", children: tier.help })] }, tier.key))), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: confirmingProhibit !== null, onClose: () => setConfirmingProhibit(null), title: "Turn off encryption?", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Saving sets ", _jsx("strong", { children: confirmingProhibit?.join(", ") }), " to Never encrypt. Nobody on this server can then encrypt that mail end to end, whatever they have chosen for themselves, until an administrator allows it again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingProhibit(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void save(), children: "Save and turn off encryption" })] })] })] }));
|
|
62
74
|
}
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
-
import Alert from "
|
|
7
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
9
9
|
/** Loads a settings object, then renders its form - the loading and error states every settings page shares. */
|
|
10
10
|
export default function LoadedSettingsForm({ load, loadErrorMessage, children }) {
|
|
11
11
|
const [value, setValue] = useState(null);
|
|
@@ -13,7 +13,7 @@ export default function LoadedSettingsForm({ load, loadErrorMessage, children })
|
|
|
13
13
|
useEffect(() => {
|
|
14
14
|
load()
|
|
15
15
|
.then(setValue)
|
|
16
|
-
.catch((err) => setError(err
|
|
16
|
+
.catch((err) => setError(actionErrorMessage(err, loadErrorMessage)));
|
|
17
17
|
}, []);
|
|
18
18
|
if (error) {
|
|
19
19
|
return _jsx(Alert, { children: error });
|
|
@@ -4,17 +4,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
|
|
9
8
|
import { leftoverConflictOf } from "../../../../../lib/admin/leftoverMailboxApi.js";
|
|
10
9
|
import { createMailbox, listMailboxDomains, resolveMailboxOwner } from "../../../../../lib/mail/mailApi.js";
|
|
11
10
|
import { describeTimeZone, deviceTimeZone, zoneClock } from "../../../../../lib/util/timeZone.js";
|
|
12
11
|
import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
|
|
13
|
-
import Alert from "
|
|
12
|
+
import Alert from "../ActionAlert.js";
|
|
14
13
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
15
14
|
import FormField from "../../../../../lib/components/forms/FormField.js";
|
|
16
15
|
import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
|
|
17
16
|
import EraseLeftoverDataDialog from "../mailboxes/EraseLeftoverDataDialog.js";
|
|
17
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
18
18
|
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
19
19
|
const SELECT_CLASS = "text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
20
20
|
/**
|
|
@@ -82,6 +82,11 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
82
82
|
setError("A primary SMTP address is required.");
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
+
// With the domain chosen beside it, a local part with an "@" would make `a@b.com@domain`, which the server refuses without saying why.
|
|
86
|
+
if (constrained && /[@@﹫]/.test(localPart)) {
|
|
87
|
+
setError("The part before the domain can't contain \"@\" - choose the domain from the list instead.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
85
90
|
if (!displayName.trim()) {
|
|
86
91
|
setError("A display name is required.");
|
|
87
92
|
return;
|
|
@@ -125,7 +130,7 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
125
130
|
setLeftover({ address, conflict });
|
|
126
131
|
}
|
|
127
132
|
else {
|
|
128
|
-
setError(err
|
|
133
|
+
setError(actionErrorMessage(err, "Could not create the mailbox."));
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
finally {
|
|
@@ -4,10 +4,10 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
7
|
import { updateMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
|
|
9
|
-
import Alert from "
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
10
|
+
import { actionErrorMessage } from "../elevation.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
13
|
/** Unrounded, so a quota that isn't a whole number of hundredths of a GB (say 4 MB) is shown as it is. */
|
|
@@ -80,7 +80,7 @@ export default function MailboxPolicyForm({ policy, onChange, onDirtyChange, emb
|
|
|
80
80
|
setSaved(true);
|
|
81
81
|
}
|
|
82
82
|
catch (err) {
|
|
83
|
-
setError(err
|
|
83
|
+
setError(actionErrorMessage(err, "Could not save the mailbox policy."));
|
|
84
84
|
}
|
|
85
85
|
finally {
|
|
86
86
|
setSaving(false);
|
|
@@ -4,13 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import Alert from "../../../../../lib/components/feedback/Alert.js";
|
|
7
|
+
import { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, isSecretSetting, isSecretSettingKey, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, retryPluginPurge, searchPlugins, updatePlugin, } from "../../../../../lib/admin/pluginsApi.js";
|
|
8
|
+
import Alert from "../ActionAlert.js";
|
|
10
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
10
|
import Modal from "../../../../../lib/components/overlays/Modal.js";
|
|
12
11
|
import { notify } from "../../../notifications/store.js";
|
|
13
|
-
import { isElevationRequired } from "../elevation.js";
|
|
12
|
+
import { actionErrorMessage, isElevationMessage, isElevationRequired } from "../elevation.js";
|
|
14
13
|
import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
|
|
15
14
|
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
16
15
|
/** How often status is refreshed while server copies are still applying a change. */
|
|
@@ -20,7 +19,7 @@ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
|
|
|
20
19
|
/** The longest wait between retries while no status has ever been read. */
|
|
21
20
|
const MAX_STATUS_RETRY_MS = 60 * 1000;
|
|
22
21
|
function errorMessage(err, fallback) {
|
|
23
|
-
return err
|
|
22
|
+
return actionErrorMessage(err, fallback);
|
|
24
23
|
}
|
|
25
24
|
/** Runs `action`, resolving why it failed, or `null`. */
|
|
26
25
|
async function attempt(action, failure) {
|
|
@@ -126,6 +125,8 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
126
125
|
const allowPrereleaseRef = useRef(allowPrerelease);
|
|
127
126
|
/** Identifies the latest update check, so a slower earlier one can't replace what it found. */
|
|
128
127
|
const updatesRequest = useRef(0);
|
|
128
|
+
// Set while a bulk action runs, which looks for updates once at its end rather than after each plugin it changes.
|
|
129
|
+
const bulkRunning = useRef(false);
|
|
129
130
|
/** The plugins ticked for a bulk action. */
|
|
130
131
|
const [selected, setSelected] = useState(new Set());
|
|
131
132
|
/** Set while a bulk action runs. */
|
|
@@ -279,7 +280,9 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
279
280
|
return [...next].sort((a, b) => a.name.localeCompare(b.name));
|
|
280
281
|
});
|
|
281
282
|
watchRollout();
|
|
282
|
-
|
|
283
|
+
if (!bulkRunning.current) {
|
|
284
|
+
void refreshUpdates();
|
|
285
|
+
}
|
|
283
286
|
}
|
|
284
287
|
/** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
|
|
285
288
|
* shows why it failed. */
|
|
@@ -435,6 +438,7 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
435
438
|
setError(null);
|
|
436
439
|
setRetryEnableUid(null);
|
|
437
440
|
setBulkFailure(null);
|
|
441
|
+
bulkRunning.current = true;
|
|
438
442
|
await track((async () => {
|
|
439
443
|
for (const [index, plugin] of ordered.entries()) {
|
|
440
444
|
setBulk({ kind, done: index, total: ordered.length, current: plugin.manifest.displayName });
|
|
@@ -448,7 +452,7 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
448
452
|
}
|
|
449
453
|
failed.add(plugin.uid);
|
|
450
454
|
reasons.push(`${plugin.manifest.displayName}: ${problem}`);
|
|
451
|
-
if (problem
|
|
455
|
+
if (isElevationMessage(problem)) {
|
|
452
456
|
for (const untried of ordered.slice(index + 1)) {
|
|
453
457
|
failed.add(untried.uid);
|
|
454
458
|
reasons.push(`${untried.manifest.displayName}: Not attempted.`);
|
|
@@ -457,12 +461,16 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
457
461
|
}
|
|
458
462
|
}
|
|
459
463
|
})());
|
|
464
|
+
bulkRunning.current = false;
|
|
460
465
|
setBulk(null);
|
|
461
466
|
setSelected(failed);
|
|
462
467
|
const succeeded = ordered.length - failed.size;
|
|
463
468
|
if (kind === "uninstall" && succeeded > 0) {
|
|
464
469
|
watchRollout();
|
|
465
470
|
}
|
|
471
|
+
if (kind !== "uninstall" && succeeded > 0) {
|
|
472
|
+
void refreshUpdates();
|
|
473
|
+
}
|
|
466
474
|
if (succeeded > 0) {
|
|
467
475
|
notify({
|
|
468
476
|
kind: "success",
|
|
@@ -848,6 +856,10 @@ function hostSuggestion(definition, saved, configured) {
|
|
|
848
856
|
* deployment configures; else the setting's own default - or, for a required select with none of those, its first
|
|
849
857
|
* option. Kept as a string for text inputs. */
|
|
850
858
|
function initialValue(definition, saved, configured) {
|
|
859
|
+
if (isSecretSetting(saved)) {
|
|
860
|
+
// Write-only: the saved secret never reaches the browser, so the field starts empty. Left empty, the placeholder is sent back and the secret stays.
|
|
861
|
+
return "";
|
|
862
|
+
}
|
|
851
863
|
if (configured && isUnsaved(saved)) {
|
|
852
864
|
return configuredValue(definition, configured);
|
|
853
865
|
}
|
|
@@ -929,21 +941,23 @@ function SettingsModal({ plugin, onClose, onSaved }) {
|
|
|
929
941
|
setBusy(false);
|
|
930
942
|
}
|
|
931
943
|
}
|
|
932
|
-
return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} settings`, children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: save, className: "flex flex-col gap-5", children: [definitions.map((definition) => (_jsx(SettingField, { definition: definition, value: values[definition.key], configured: plugin.configured?.[definition.key], onChange: (value) => setValues((prev) => ({ ...prev, [definition.key]: value })) }, definition.key))), _jsxs("div", { className: "sticky -bottom-7 -mx-7 -mb-7 px-7 py-4 bg-surface border-t border-border flex flex-col gap-3", children: [_jsx("p", { className: "text-xs text-text-muted", children: resetting
|
|
944
|
+
return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} settings`, children: [error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: save, className: "flex flex-col gap-5", children: [definitions.map((definition) => (_jsx(SettingField, { definition: definition, value: values[definition.key], configured: plugin.configured?.[definition.key], secretSaved: !resetting && isSecretSetting(plugin.settings[definition.key]), onChange: (value) => setValues((prev) => ({ ...prev, [definition.key]: value })) }, definition.key))), _jsxs("div", { className: "sticky -bottom-7 -mx-7 -mb-7 px-7 py-4 bg-surface border-t border-border flex flex-col gap-3", children: [_jsx("p", { className: "text-xs text-text-muted", children: resetting
|
|
933
945
|
? "Every setting is back to what this deployment's configuration or the plugin's default gives. Save to apply that."
|
|
934
946
|
: "Saving restarts the servers one at a time to apply the new settings." }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mr-auto", onClick: reset, disabled: busy, children: "Reset" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: busy, disabled: busy, children: "Save" })] })] })] })] }));
|
|
935
947
|
}
|
|
936
|
-
function SettingField({ definition, value, configured, onChange, }) {
|
|
948
|
+
function SettingField({ definition, value, configured, secretSaved, onChange, }) {
|
|
937
949
|
// What applies without a saved value; one saved here replaces it, and Reset goes back to it.
|
|
938
950
|
const note = configured && (_jsxs("span", { className: "text-xs text-text-muted", children: ["This deployment's configuration provides ", configured.secret ? "a value" : "the value shown", " unless one is saved here."] }));
|
|
939
|
-
// A secret's value never reaches the browser, so the field only says that it is set
|
|
940
|
-
|
|
951
|
+
// A secret's value never reaches the browser, so the field only says that it is set - and never shows what is typed, or offers it to the
|
|
952
|
+
// browser's form fill.
|
|
953
|
+
const secretField = definition.type === "string" && (isSecretSettingKey(definition.key) || secretSaved || !!configured?.secret);
|
|
954
|
+
const placeholder = secretSaved ? "Saved - type a new value to replace it" : configured?.secret ? "Set by this deployment" : undefined;
|
|
941
955
|
const help = (_jsxs(_Fragment, { children: [definition.help && _jsx("span", { className: "text-xs text-text-muted", children: definition.help }), note] }));
|
|
942
956
|
if (definition.type === "boolean") {
|
|
943
957
|
// Laid out like the admin policy forms' checkboxes: the help sits under the label, beside the box.
|
|
944
958
|
return (_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: value === true, onChange: (e) => onChange(e.target.checked) }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-semibold", children: definition.label }), help] })] }));
|
|
945
959
|
}
|
|
946
|
-
return (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: definition.label }), definition.type === "select" ? (_jsxs("select", { "aria-label": definition.label, className: INPUT_CLASS, value: String(value), onChange: (e) => onChange(e.target.value), children: [!definition.required && _jsx("option", { value: "", children: "Default" }), (definition.options ?? []).map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })) : (_jsx("input", { "aria-label": definition.label, className: INPUT_CLASS, type: definition.type === "number" ? "number" : "text", min: definition.min, max: definition.max, required: definition.required && !configured, placeholder: placeholder, value: String(value), onChange: (e) => onChange(e.target.value) })), help] }));
|
|
960
|
+
return (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: definition.label }), definition.type === "select" ? (_jsxs("select", { "aria-label": definition.label, className: INPUT_CLASS, value: String(value), onChange: (e) => onChange(e.target.value), children: [!definition.required && _jsx("option", { value: "", children: "Default" }), (definition.options ?? []).map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })) : (_jsx("input", { "aria-label": definition.label, className: INPUT_CLASS, type: definition.type === "number" ? "number" : secretField ? "password" : "text", autoComplete: secretField ? "new-password" : undefined, min: definition.min, max: definition.max, required: definition.required && !configured && !secretSaved, placeholder: placeholder, value: String(value), onChange: (e) => onChange(e.target.value) })), help] }));
|
|
947
961
|
}
|
|
948
962
|
/** The word typed to confirm deleting the data of several plugins at once, in place of a plugin's name. */
|
|
949
963
|
const BULK_DELETE_WORD = "delete";
|
|
@@ -4,9 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
-
import { MIN_AUDIT_LOG_RETENTION_DAYS, updateRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
|
|
9
|
-
import Alert from "
|
|
7
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
8
|
+
import { MIN_AUDIT_LOG_RETENTION_DAYS, MIN_MESSAGE_RETENTION_DAYS, updateRetentionPolicy, } from "../../../../../lib/admin/retentionPolicyApi.js";
|
|
9
|
+
import Alert from "../ActionAlert.js";
|
|
10
10
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
11
11
|
import Modal from "../../../../../lib/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";
|
|
@@ -31,13 +31,20 @@ export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, e
|
|
|
31
31
|
const [savedMessageDays, setSavedMessageDays] = useState(policy.messageRetentionDays);
|
|
32
32
|
const [savedAuditDays, setSavedAuditDays] = useState(policy.auditLogRetentionDays);
|
|
33
33
|
const [pendingPatch, setPendingPatch] = useState(null);
|
|
34
|
+
// A period that is still what is stored is neither sent nor held to the floor: it may have been stored before the floor was set.
|
|
35
|
+
const messageChanged = messageRetentionDays !== (savedMessageDays?.toString() ?? "");
|
|
36
|
+
const auditChanged = auditLogRetentionDays !== (savedAuditDays?.toString() ?? "");
|
|
34
37
|
function handleSubmit(e) {
|
|
35
38
|
e.preventDefault();
|
|
36
|
-
// A blank field is sent as `null`, which clears a configured age back to no automatic purge.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// A blank field is sent as `null`, which clears a configured age back to no automatic purge. Only a field that was changed is sent: one left as
|
|
40
|
+
// it is would be refused by a floor that came in after it was stored, and so keep the other from being saved.
|
|
41
|
+
const patch = {};
|
|
42
|
+
if (messageChanged) {
|
|
43
|
+
patch.messageRetentionDays = messageRetentionDays.trim() === "" ? null : Number(messageRetentionDays);
|
|
44
|
+
}
|
|
45
|
+
if (auditChanged) {
|
|
46
|
+
patch.auditLogRetentionDays = auditLogRetentionDays.trim() === "" ? null : Number(auditLogRetentionDays);
|
|
47
|
+
}
|
|
41
48
|
if (shortensRetention(patch.messageRetentionDays, savedMessageDays) ||
|
|
42
49
|
shortensRetention(patch.auditLogRetentionDays, savedAuditDays)) {
|
|
43
50
|
setPendingPatch(patch);
|
|
@@ -59,13 +66,13 @@ export default function RetentionPolicyForm({ policy, onChange, onDirtyChange, e
|
|
|
59
66
|
setSaved(true);
|
|
60
67
|
}
|
|
61
68
|
catch (err) {
|
|
62
|
-
setError(err
|
|
69
|
+
setError(actionErrorMessage(err, "Could not save the retention policy."));
|
|
63
70
|
}
|
|
64
71
|
finally {
|
|
65
72
|
setSaving(false);
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
|
-
return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Retention policy" })) : (_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Retention Policy" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Automatically and permanently deletes mail/audit-log entries older than the configured age. Leave a field blank for no automatic purge. This does not apply to a mailbox that is a custodian on an active legal hold \u2014 a held message is skipped and retried on a later run once the hold is lifted." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message retention (days)" }), _jsx("input", { "aria-label": "Message retention (days)", type: "number", min:
|
|
75
|
+
return (_jsxs("div", { className: "max-w-xl", children: [embedded ? (_jsx("h3", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Retention policy" })) : (_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Retention Policy" })), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Automatically and permanently deletes mail/audit-log entries older than the configured age. Leave a field blank for no automatic purge. This does not apply to a mailbox that is a custodian on an active legal hold \u2014 a held message is skipped and retried on a later run once the hold is lifted." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message retention (days)" }), _jsx("input", { "aria-label": "Message retention (days)", type: "number", min: messageChanged ? MIN_MESSAGE_RETENTION_DAYS : undefined, className: INPUT_CLASS, value: messageRetentionDays, onChange: (e) => setMessageRetentionDays(e.target.value), placeholder: "No automatic purge" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Applies to any message, in any folder, regardless of age. Cannot be set below ", MIN_MESSAGE_RETENTION_DAYS, " days."] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Audit log retention (days)" }), _jsx("input", { "aria-label": "Audit log retention (days)", type: "number", min: auditChanged ? MIN_AUDIT_LOG_RETENTION_DAYS : undefined, className: INPUT_CLASS, value: auditLogRetentionDays, onChange: (e) => setAuditLogRetentionDays(e.target.value), placeholder: "Keep forever" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Cannot be set below ", MIN_AUDIT_LOG_RETENTION_DAYS, " days. Never applies to the tamper-evident escrow audit chain."] })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] }), _jsxs(Modal, { open: pendingPatch !== null, onClose: () => setPendingPatch(null), title: "Delete older data?", children: [shortensRetention(pendingPatch?.messageRetentionDays, savedMessageDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedMessageDays == null
|
|
69
76
|
? "Mail isn't deleted automatically today."
|
|
70
77
|
: `Mail is currently kept for ${savedMessageDays} days.`, " ", "With this change, every message older than ", _jsxs("strong", { children: [pendingPatch?.messageRetentionDays, " days"] }), ", in every mailbox and folder, is permanently deleted on the next purge run - and so on from then on. Mailboxes on an active legal hold are skipped."] })), shortensRetention(pendingPatch?.auditLogRetentionDays, savedAuditDays) && (_jsxs("p", { className: "text-sm mb-3", children: [savedAuditDays == null
|
|
71
78
|
? "Audit-log entries are kept forever today."
|