@rapidmx/web-client 0.30.0 → 0.32.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 +3 -3
- 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/diagnostics/diagnosticsApi.ts +2 -2
- 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 +195 -46
- 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 +49 -6
- 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/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/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 +91 -28
- 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 +2 -2
- 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/diagnostics/diagnosticsApi.d.ts +2 -2
- 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 +103 -18
- 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.js +49 -6
- 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/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/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 +85 -30
- 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 +31 -2
- package/dist/lib/admin/pluginsApi.js +23 -1
- 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 +45 -3
- 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,13 +4,13 @@ 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 { addPlugin, expectedPlanOf, getPluginStatus, getPluginUpdates, isSecretSetting, isSecretSettingKey, listPluginNamespaces, listPlugins, lookupPluginPackage, planPluginChange, removePlugin, retryPluginPurge, searchPlugins, updatePlugin, uploadPlugin, } from "../../../../../lib/admin/pluginsApi.js";
|
|
7
8
|
import { ApiRequestError } from "../../../../../lib/util/api.js";
|
|
8
|
-
import
|
|
9
|
-
import Alert from "../../../../../lib/components/feedback/Alert.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
|
import { notify } from "../../../notifications/store.js";
|
|
13
|
-
import { isElevationRequired } from "../elevation.js";
|
|
13
|
+
import { actionErrorMessage, isElevationMessage, isElevationRequired } from "../elevation.js";
|
|
14
14
|
import { getAllowPrerelease, setAllowPrerelease } from "./pluginPreferences.js";
|
|
15
15
|
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
16
|
/** How often status is refreshed while server copies are still applying a change. */
|
|
@@ -20,7 +20,7 @@ const AFTER_CHANGE_POLL_MS = 2 * 60 * 1000;
|
|
|
20
20
|
/** The longest wait between retries while no status has ever been read. */
|
|
21
21
|
const MAX_STATUS_RETRY_MS = 60 * 1000;
|
|
22
22
|
function errorMessage(err, fallback) {
|
|
23
|
-
return err
|
|
23
|
+
return actionErrorMessage(err, fallback);
|
|
24
24
|
}
|
|
25
25
|
/** Runs `action`, resolving why it failed, or `null`. */
|
|
26
26
|
async function attempt(action, failure) {
|
|
@@ -44,6 +44,10 @@ function purgeFailure(purge) {
|
|
|
44
44
|
}
|
|
45
45
|
/** A day, in the administrator's own format. */
|
|
46
46
|
const formatDate = (iso) => new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
|
|
47
|
+
/** The file names an uploaded plugin may have: what `npm pack` produces (`<name>-<version>.tgz`), or the same gzipped. */
|
|
48
|
+
const PLUGIN_PACK_NAME = /\.(tgz|tar\.gz|gz)$/i;
|
|
49
|
+
/** What the server answers (409) an upload of a plugin that is already installed, which the administrator may replace. */
|
|
50
|
+
const ALREADY_INSTALLED = /already installed/i;
|
|
47
51
|
const BULK_WORDS = {
|
|
48
52
|
upgrade: { doing: "Upgrading", done: "upgraded" },
|
|
49
53
|
disable: { doing: "Disabling", done: "disabled" },
|
|
@@ -126,6 +130,8 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
126
130
|
const allowPrereleaseRef = useRef(allowPrerelease);
|
|
127
131
|
/** Identifies the latest update check, so a slower earlier one can't replace what it found. */
|
|
128
132
|
const updatesRequest = useRef(0);
|
|
133
|
+
// Set while a bulk action runs, which looks for updates once at its end rather than after each plugin it changes.
|
|
134
|
+
const bulkRunning = useRef(false);
|
|
129
135
|
/** The plugins ticked for a bulk action. */
|
|
130
136
|
const [selected, setSelected] = useState(new Set());
|
|
131
137
|
/** Set while a bulk action runs. */
|
|
@@ -135,6 +141,11 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
135
141
|
const [confirmingBulkUninstall, setConfirmingBulkUninstall] = useState(null);
|
|
136
142
|
/** How many changes (or the lookups that come before them) are waiting on the server. */
|
|
137
143
|
const [working, setWorking] = useState(0);
|
|
144
|
+
/** Set while an uploaded plugin is being sent to the server. */
|
|
145
|
+
const [uploading, setUploading] = useState(false);
|
|
146
|
+
/** An upload that would replace an installed plugin, waiting for the administrator to confirm it. */
|
|
147
|
+
const [replacing, setReplacing] = useState(null);
|
|
148
|
+
const fileInput = useRef(null);
|
|
138
149
|
const statusRequest = useRef(null);
|
|
139
150
|
/** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
|
|
140
151
|
const seenPurges = useRef(new Map());
|
|
@@ -279,7 +290,9 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
279
290
|
return [...next].sort((a, b) => a.name.localeCompare(b.name));
|
|
280
291
|
});
|
|
281
292
|
watchRollout();
|
|
282
|
-
|
|
293
|
+
if (!bulkRunning.current) {
|
|
294
|
+
void refreshUpdates();
|
|
295
|
+
}
|
|
283
296
|
}
|
|
284
297
|
/** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
|
|
285
298
|
* shows why it failed. */
|
|
@@ -370,6 +383,55 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
370
383
|
function upgrade(plugin, packageVersion) {
|
|
371
384
|
return onRow(plugin, () => changeVersion(plugin, packageVersion));
|
|
372
385
|
}
|
|
386
|
+
/** Sends `file` to the server as a plugin to install - replacing the installed plugin of that name only when `replace` is set. A plugin that is
|
|
387
|
+
* already installed isn't an error but a question, asked of the administrator before it is replaced. */
|
|
388
|
+
async function runUpload(file, replace) {
|
|
389
|
+
setUploading(true);
|
|
390
|
+
setError(null);
|
|
391
|
+
setRetryEnableUid(null);
|
|
392
|
+
try {
|
|
393
|
+
const uploaded = await track(uploadPlugin(file, { replace }));
|
|
394
|
+
applied(uploaded);
|
|
395
|
+
notify({
|
|
396
|
+
kind: "success",
|
|
397
|
+
title: `${uploaded.name}@${uploaded.packageVersion} uploaded`,
|
|
398
|
+
message: "The servers restart one at a time to load it.",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
if (!replace && err instanceof ApiRequestError && err.status === 409 && ALREADY_INSTALLED.test(err.message)) {
|
|
403
|
+
const installed = plugins.find((plugin) => err.message.includes(plugin.name));
|
|
404
|
+
setReplacing({
|
|
405
|
+
file,
|
|
406
|
+
question: installed
|
|
407
|
+
? `Replace ${installed.name} ${installed.packageVersion} with the uploaded pack?`
|
|
408
|
+
: "Replace the installed plugin with the uploaded pack?",
|
|
409
|
+
reason: err.message,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
else if (err instanceof ApiRequestError && err.status === 413) {
|
|
413
|
+
setError("That file is too large for the server to accept.");
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
setError(errorMessage(err, "Could not upload the plugin."));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
setUploading(false);
|
|
420
|
+
}
|
|
421
|
+
/** Takes the file the administrator chose, unless it can't be what `npm pack` produces. */
|
|
422
|
+
function onFileChosen(e) {
|
|
423
|
+
const file = e.target.files?.[0];
|
|
424
|
+
// So choosing the same file again (after fixing what was wrong) still counts as a change.
|
|
425
|
+
e.target.value = "";
|
|
426
|
+
if (!file) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!PLUGIN_PACK_NAME.test(file.name)) {
|
|
430
|
+
setError(`${file.name} isn't a plugin pack. Choose the .tgz file that npm pack produces.`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
void runUpload(file, false);
|
|
434
|
+
}
|
|
373
435
|
/** Installs an uninstalled plugin again, at its latest version - as adding it by name would. */
|
|
374
436
|
function reinstall(purge) {
|
|
375
437
|
const displayName = purgeName(purge);
|
|
@@ -435,6 +497,7 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
435
497
|
setError(null);
|
|
436
498
|
setRetryEnableUid(null);
|
|
437
499
|
setBulkFailure(null);
|
|
500
|
+
bulkRunning.current = true;
|
|
438
501
|
await track((async () => {
|
|
439
502
|
for (const [index, plugin] of ordered.entries()) {
|
|
440
503
|
setBulk({ kind, done: index, total: ordered.length, current: plugin.manifest.displayName });
|
|
@@ -448,7 +511,7 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
448
511
|
}
|
|
449
512
|
failed.add(plugin.uid);
|
|
450
513
|
reasons.push(`${plugin.manifest.displayName}: ${problem}`);
|
|
451
|
-
if (problem
|
|
514
|
+
if (isElevationMessage(problem)) {
|
|
452
515
|
for (const untried of ordered.slice(index + 1)) {
|
|
453
516
|
failed.add(untried.uid);
|
|
454
517
|
reasons.push(`${untried.manifest.displayName}: Not attempted.`);
|
|
@@ -457,12 +520,16 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
457
520
|
}
|
|
458
521
|
}
|
|
459
522
|
})());
|
|
523
|
+
bulkRunning.current = false;
|
|
460
524
|
setBulk(null);
|
|
461
525
|
setSelected(failed);
|
|
462
526
|
const succeeded = ordered.length - failed.size;
|
|
463
527
|
if (kind === "uninstall" && succeeded > 0) {
|
|
464
528
|
watchRollout();
|
|
465
529
|
}
|
|
530
|
+
if (kind !== "uninstall" && succeeded > 0) {
|
|
531
|
+
void refreshUpdates();
|
|
532
|
+
}
|
|
466
533
|
if (succeeded > 0) {
|
|
467
534
|
notify({
|
|
468
535
|
kind: "success",
|
|
@@ -515,9 +582,10 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
515
582
|
const retryEnable = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
|
|
516
583
|
const displayNameOf = (name) => plugins.find((plugin) => plugin.name === name)?.manifest.displayName ?? name;
|
|
517
584
|
const SectionHeading = embedded ? "h3" : "h2";
|
|
518
|
-
const addButton = (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setAdding(true), children: "Add by name" }));
|
|
519
585
|
const bulkBusy = bulk !== null;
|
|
520
|
-
|
|
586
|
+
const addButton = (_jsxs("div", { className: "flex flex-wrap gap-2 shrink-0", children: [_jsx("input", { ref: fileInput, type: "file", className: "hidden", accept: ".tgz,.tar.gz,application/gzip,application/x-gzip", "aria-label": "Plugin pack file", tabIndex: -1, onChange: onFileChosen }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", title: "Install a plugin from the .tgz file that npm pack produces", loading: uploading, disabled: uploading || bulkBusy, onClick: () => fileInput.current?.click(), children: uploading ? "Uploading\u2026" : "Upload" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setAdding(true), children: "Add by name" })] }));
|
|
587
|
+
return (_jsxs(_Fragment, { children: [(working > 0 || pending) && _jsx(PluginProgressBar, { label: working > 0 ? "Working on plugins" : "Applying plugin changes" }), embedded ? (_jsx("p", { className: "text-sm text-text-muted mb-5 max-w-3xl", children: "Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Plugins" }), addButton] }), _jsx("p", { className: "text-sm text-text-muted mb-6 max-w-3xl", children: "Plugins add protocols and features to every server. Changes are applied by restarting the servers one at a time, so mail keeps flowing. Plugins run with full access to the server, so only add ones you trust." })] })), _jsxs("label", { className: "flex items-start gap-2 text-sm mb-5 max-w-3xl cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "mt-0.5 h-4 w-4 shrink-0", checked: allowPrerelease, onChange: (e) => changePrerelease(e.target.checked) }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-semibold", children: "Allow pre-release versions" }), _jsx("span", { className: "text-xs text-text-muted", children: "Also offer beta and other pre-release builds, such as 1.0.0-beta.2, as updates and as versions to install. They can be unstable. This is remembered in this browser." })] })] }), bulkFailure && (_jsx(Alert, { children: _jsxs("div", { children: [bulkFailure.title, _jsx("ul", { className: "list-disc pl-5 mt-1", children: bulkFailure.reasons.map((reason) => (_jsx("li", { className: "break-words", children: reason }, reason))) })] }) })), error &&
|
|
588
|
+
(retryEnable ? (_jsxs(Alert, { children: [error, " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0", onClick: () => void toggle(retryEnable), children: "Try again" })] })) : (_jsx(Alert, { children: error }))), _jsx(RolloutBanner, { status: status }), statusStale && status && (_jsx("p", { className: "mb-4 text-xs text-text-muted", children: "Couldn't refresh server status. Showing the last status reported." })), listStale && (_jsxs("p", { className: "mb-4 text-xs text-text-muted", children: ["Couldn't re-read the plugin list, so it may be out of date.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !p-0 !text-xs", onClick: () => void reload(), children: "Retry" })] })), _jsxs("div", { className: "flex items-center justify-between gap-4 mb-2", children: [_jsx(SectionHeading, { className: "text-sm font-bold uppercase tracking-wide", children: "Installed plugins" }), embedded && addButton] }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : plugins.length === 0 && purges.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No plugins installed." })) : (_jsxs("div", { className: "overflow-x-auto", children: [plugins.length > 0 && (_jsxs("div", { role: "toolbar", "aria-label": "Actions for several plugins", className: "flex flex-wrap items-center gap-2 py-2", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm mr-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "h-4 w-4 shrink-0", "aria-label": "Select all plugins", checked: allSelected, disabled: bulkBusy, ref: (input) => {
|
|
521
589
|
if (input) {
|
|
522
590
|
input.indeterminate = selectedPlugins.length > 0 && !allSelected;
|
|
523
591
|
}
|
|
@@ -525,13 +593,15 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
525
593
|
versions: new Map(selectedUpgrades.map((plugin) => [plugin.uid, updates.get(plugin.uid).latestVersion])),
|
|
526
594
|
}), children: ["Upgrade", selectedUpgrades.length > 0 ? ` (${selectedUpgrades.length})` : ""] }), _jsxs(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Disable selected plugins", disabled: bulkBusy || selectedEnabled.length === 0, onClick: () => void runBulk("disable", selectedEnabled), children: ["Disable", selectedEnabled.length > 0 ? ` (${selectedEnabled.length})` : ""] }), _jsxs(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Uninstall selected plugins", disabled: bulkBusy || selectedPlugins.length === 0, onClick: () => setConfirmingBulkUninstall(selectedPlugins), children: ["Uninstall", selectedPlugins.length > 0 ? ` (${selectedPlugins.length})` : ""] }), _jsx(Button, { type: "button", className: "!w-auto ml-auto", "aria-label": "Upgrade all plugins", loading: bulk?.kind === "upgrade", disabled: bulkBusy, onClick: () => void upgradeAll(), children: "Upgrade all" }), bulk && (_jsxs("span", { "aria-live": "polite", className: "basis-full text-xs text-text-muted", children: [BULK_WORDS[bulk.kind].doing, " ", bulk.current, " (", bulk.done + 1, " of ", bulk.total, ")\u2026"] }))] })), _jsxs("table", { role: "table", className: "w-full text-sm border-collapse block lg:table", children: [_jsx("thead", { role: "rowgroup", className: "hidden lg:table-header-group", children: _jsxs("tr", { role: "row", children: [_jsx("th", { role: "columnheader", className: TH_CLASS, children: "Plugin" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Version" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: "Status" }), _jsx("th", { role: "columnheader", className: TH_CLASS, children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsxs("tbody", { role: "rowgroup", className: "block lg:table-row-group", children: [plugins.map((plugin) => {
|
|
527
595
|
const update = updates.get(plugin.uid);
|
|
528
|
-
const
|
|
596
|
+
const uploaded = plugin.source === "upload";
|
|
597
|
+
// An uploaded pack has no registry version to be newer than.
|
|
598
|
+
const latest = !uploaded && update?.updateAvailable ? update.latestVersion : undefined;
|
|
529
599
|
const busy = busyUids.has(plugin.uid) || confirming?.uid === plugin.uid || bulkBusy;
|
|
530
600
|
const requires = Object.keys(plugin.manifest.requires ?? {}).map(displayNameOf);
|
|
531
601
|
const requiredBy = plugins
|
|
532
602
|
.filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
|
|
533
603
|
.map((other) => other.manifest.displayName);
|
|
534
|
-
return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("input", { type: "checkbox", className: "mt-1 h-4 w-4 shrink-0", "aria-label": `Select ${plugin.manifest.displayName}`, checked: selected.has(plugin.uid), disabled: bulkBusy, onChange: () => toggleSelected(plugin.uid) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] })] }) }), _jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`, children: [_jsx("div", { children: plugin.packageVersion }), latest && _jsxs("div", { className: "mt-1 text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1`, children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-2 pb-3`, children: _jsxs("div", { className: "flex flex-col items-start gap-1.5 lg:items-end lg:min-w-[11rem]", children: [_jsxs("div", { className: "flex flex-wrap gap-2 lg:justify-end", children: [latest && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: plugin.enabled ? "secondary" : "primary", type: "button", className: "!w-auto min-w-[5.5rem]", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" })] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 lg:justify-end", children: [_jsx(Button, { variant: "text", type: "button", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
|
|
604
|
+
return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("input", { type: "checkbox", className: "mt-1 h-4 w-4 shrink-0", "aria-label": `Select ${plugin.manifest.displayName}`, checked: selected.has(plugin.uid), disabled: bulkBusy, onChange: () => toggleSelected(plugin.uid) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), uploaded && (_jsxs("div", { className: "text-xs text-text-muted mt-1 break-words", children: [_jsx("span", { className: `${BADGE_CLASS} bg-surface-alt text-text-muted`, children: "Uploaded" }), " ", plugin.uploadFilename, plugin.uploadedAt ? `, ${formatDate(plugin.uploadedAt)}` : ""] })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] })] }) }), _jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`, children: [_jsx("div", { children: plugin.packageVersion }), latest && _jsxs("div", { className: "mt-1 text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1`, children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-2 pb-3`, children: _jsxs("div", { className: "flex flex-col items-start gap-1.5 lg:items-end lg:min-w-[11rem]", children: [_jsxs("div", { className: "flex flex-wrap gap-2 lg:justify-end", children: [latest && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: plugin.enabled ? "secondary" : "primary", type: "button", className: "!w-auto min-w-[5.5rem]", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" })] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 lg:justify-end", children: [_jsx(Button, { variant: "text", type: "button", onClick: () => setUpgrading(plugin), children: uploaded ? "Install the registry version" : "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
|
|
535
605
|
}), purges.map((purge) => (_jsx(UninstalledPluginRow, { purge: purge, busy: busyUids.has(purge.uid), onRetry: () => void retryPurge(purge), onInstall: () => void reinstall(purge) }, purge.uid)))] })] })] })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, prerelease: allowPrerelease, onInstall: (result) => install(result.name, result.version, result.name),
|
|
536
606
|
// The browser only offers an upgrade for a result it matched to one of these same `plugins`.
|
|
537
607
|
onUpgrade: (uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid), packageVersion) }), _jsx(AddPluginModal, { open: adding, prerelease: allowPrerelease, onClose: () => setAdding(false), onAdd: async (name, packageVersion, displayName) => {
|
|
@@ -546,7 +616,11 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
546
616
|
setUpgrading(null);
|
|
547
617
|
}
|
|
548
618
|
return problem;
|
|
549
|
-
} })),
|
|
619
|
+
} })), replacing && (_jsxs(Modal, { open: true, onClose: () => setReplacing(null), title: "Replace plugin?", children: [_jsx("p", { className: "text-sm mb-2", children: replacing.question }), _jsx("p", { className: "text-sm text-text-muted mb-4 break-words", children: replacing.reason }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setReplacing(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
|
|
620
|
+
const { file } = replacing;
|
|
621
|
+
setReplacing(null);
|
|
622
|
+
void runUpload(file, true);
|
|
623
|
+
}, children: "Replace" })] })] })), confirming && (_jsx(ConfirmDependenciesModal, { change: confirming, displayNameOf: displayNameOf, onClose: () => setConfirming(null), onDone: () => setConfirming(null) })), confirmingBulkUninstall && (_jsx(BulkUninstallModal, { plugins: confirmingBulkUninstall, onClose: () => setConfirmingBulkUninstall(null), onConfirm: (purgeData) => {
|
|
550
624
|
const targets = confirmingBulkUninstall;
|
|
551
625
|
setConfirmingBulkUninstall(null);
|
|
552
626
|
void runBulk("uninstall", targets, { purgeData });
|
|
@@ -769,14 +843,19 @@ function ChangeVersionModal({ plugin, prerelease, onClose, onSave, }) {
|
|
|
769
843
|
const [selected, setSelected] = useState(plugin.packageVersion);
|
|
770
844
|
const [error, setError] = useState(null);
|
|
771
845
|
const [busy, setBusy] = useState(false);
|
|
846
|
+
// An uploaded pack isn't a registry version, so any registry version is a change from it - the newest is offered first.
|
|
847
|
+
const uploaded = plugin.source === "upload";
|
|
772
848
|
useEffect(() => {
|
|
773
849
|
lookupPluginPackage(plugin.name, undefined, { prerelease })
|
|
774
850
|
.then((found) => {
|
|
775
851
|
setVersions(found.package.versions);
|
|
776
852
|
setLatest(found.package.latest);
|
|
853
|
+
if (uploaded && found.package.latest) {
|
|
854
|
+
setSelected(found.package.latest);
|
|
855
|
+
}
|
|
777
856
|
})
|
|
778
857
|
.catch((err) => setError(errorMessage(err, "Could not load the available versions.")));
|
|
779
|
-
}, [plugin.name, prerelease]);
|
|
858
|
+
}, [plugin.name, prerelease, uploaded]);
|
|
780
859
|
async function save() {
|
|
781
860
|
setBusy(true);
|
|
782
861
|
setError(null);
|
|
@@ -786,7 +865,7 @@ function ChangeVersionModal({ plugin, prerelease, onClose, onSave, }) {
|
|
|
786
865
|
setBusy(false);
|
|
787
866
|
}
|
|
788
867
|
}
|
|
789
|
-
return (_jsxs(Modal, { open: true, onClose: onClose, title: `${plugin.manifest.displayName} version`, children: [error && _jsx(Alert, { children: error }), versions === null ? (!error && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selected, onChange: (e) => setSelected(e.target.value), children: versions.map((version) => (_jsxs("option", { value: version, children: [version, version === latest ? " (latest)" : "", version === plugin.packageVersion ? " (installed)" : ""] }, version))) })] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy || selected === plugin.packageVersion, onClick: () => void save(), children: "Save" })] })] }))] }));
|
|
868
|
+
return (_jsxs(Modal, { open: true, onClose: onClose, title: uploaded ? `Install the registry version of ${plugin.manifest.displayName}` : `${plugin.manifest.displayName} version`, children: [error && _jsx(Alert, { children: error }), versions === null ? (!error && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [uploaded && (_jsxs("p", { className: "text-sm text-text-muted", children: [plugin.manifest.displayName, " ", plugin.packageVersion, " was uploaded as ", plugin.uploadFilename, ". This replaces it with the version from the registry."] })), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Version" }), _jsx("select", { "aria-label": "Version", className: INPUT_CLASS, value: selected, onChange: (e) => setSelected(e.target.value), children: versions.map((version) => (_jsxs("option", { value: version, children: [version, version === latest ? " (latest)" : "", !uploaded && version === plugin.packageVersion ? " (installed)" : ""] }, version))) })] }), _jsxs("div", { className: "flex gap-2 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy || (!uploaded && selected === plugin.packageVersion), onClick: () => void save(), children: uploaded ? "Install" : "Save" })] })] }))] }));
|
|
790
869
|
}
|
|
791
870
|
/** Lists the plugins a change also installs or enables, and makes the change once confirmed. */
|
|
792
871
|
function ConfirmDependenciesModal({ change, displayNameOf, onClose, onDone, }) {
|
|
@@ -848,6 +927,10 @@ function hostSuggestion(definition, saved, configured) {
|
|
|
848
927
|
* deployment configures; else the setting's own default - or, for a required select with none of those, its first
|
|
849
928
|
* option. Kept as a string for text inputs. */
|
|
850
929
|
function initialValue(definition, saved, configured) {
|
|
930
|
+
if (isSecretSetting(saved)) {
|
|
931
|
+
// 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.
|
|
932
|
+
return "";
|
|
933
|
+
}
|
|
851
934
|
if (configured && isUnsaved(saved)) {
|
|
852
935
|
return configuredValue(definition, configured);
|
|
853
936
|
}
|
|
@@ -929,21 +1012,23 @@ function SettingsModal({ plugin, onClose, onSaved }) {
|
|
|
929
1012
|
setBusy(false);
|
|
930
1013
|
}
|
|
931
1014
|
}
|
|
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
|
|
1015
|
+
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
1016
|
? "Every setting is back to what this deployment's configuration or the plugin's default gives. Save to apply that."
|
|
934
1017
|
: "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
1018
|
}
|
|
936
|
-
function SettingField({ definition, value, configured, onChange, }) {
|
|
1019
|
+
function SettingField({ definition, value, configured, secretSaved, onChange, }) {
|
|
937
1020
|
// What applies without a saved value; one saved here replaces it, and Reset goes back to it.
|
|
938
1021
|
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
|
-
|
|
1022
|
+
// 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
|
|
1023
|
+
// browser's form fill.
|
|
1024
|
+
const secretField = definition.type === "string" && (isSecretSettingKey(definition.key) || secretSaved || !!configured?.secret);
|
|
1025
|
+
const placeholder = secretSaved ? "Saved - type a new value to replace it" : configured?.secret ? "Set by this deployment" : undefined;
|
|
941
1026
|
const help = (_jsxs(_Fragment, { children: [definition.help && _jsx("span", { className: "text-xs text-text-muted", children: definition.help }), note] }));
|
|
942
1027
|
if (definition.type === "boolean") {
|
|
943
1028
|
// Laid out like the admin policy forms' checkboxes: the help sits under the label, beside the box.
|
|
944
1029
|
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
1030
|
}
|
|
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] }));
|
|
1031
|
+
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
1032
|
}
|
|
948
1033
|
/** The word typed to confirm deleting the data of several plugins at once, in place of a plugin's name. */
|
|
949
1034
|
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."
|
|
@@ -4,14 +4,15 @@ 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 { createEscrowScope, listEscrowScopes } from "../../../../../lib/admin/escrowScopesApi.js";
|
|
9
8
|
import { generateEscrowKeyPair } from "../../../../../lib/crypto/escrowKeys.js";
|
|
10
9
|
import { toDatetimeLocal } from "../../../../../lib/util/dateInput.js";
|
|
11
|
-
import Alert from "
|
|
10
|
+
import Alert from "../ActionAlert.js";
|
|
12
11
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
13
12
|
import FormField from "../../../../../lib/components/forms/FormField.js";
|
|
14
|
-
import
|
|
13
|
+
import Modal from "../../../../../lib/components/overlays/Modal.js";
|
|
14
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
15
|
+
import EscrowScopeKeyAndHoldersFields, { emptyEscrowScopeKeyAndHoldersValue, keyValidityError, selfAsHolderError, } from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
|
|
15
16
|
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
17
|
/** How long a download's object URL is kept, since some browsers are still reading it after the click returns. */
|
|
17
18
|
export const DOWNLOAD_URL_LIFETIME_MS = 60000;
|
|
@@ -43,6 +44,8 @@ export default function EscrowSetupStep({ adminUid }) {
|
|
|
43
44
|
const [generated, setGenerated] = useState(null);
|
|
44
45
|
const [downloadedPrivateKey, setDownloadedPrivateKey] = useState(false);
|
|
45
46
|
const [savedPrivateKey, setSavedPrivateKey] = useState(false);
|
|
47
|
+
// The private key leaves the browser as a plain file, so it is only offered after the administrator has read what that means.
|
|
48
|
+
const [confirmingDownload, setConfirmingDownload] = useState(false);
|
|
46
49
|
const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
|
|
47
50
|
const [error, setError] = useState(null);
|
|
48
51
|
const [busy, setBusy] = useState(false);
|
|
@@ -57,6 +60,7 @@ export default function EscrowSetupStep({ adminUid }) {
|
|
|
57
60
|
setGenerated(null);
|
|
58
61
|
setDownloadedPrivateKey(false);
|
|
59
62
|
setSavedPrivateKey(false);
|
|
63
|
+
setConfirmingDownload(false);
|
|
60
64
|
setKeyAndHolders(emptyEscrowScopeKeyAndHoldersValue());
|
|
61
65
|
}
|
|
62
66
|
async function generate() {
|
|
@@ -106,6 +110,11 @@ export default function EscrowSetupStep({ adminUid }) {
|
|
|
106
110
|
setError("The public key, its type, and its fingerprint are all required.");
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
113
|
+
const validityError = generated ? null : keyValidityError(keyAndHolders);
|
|
114
|
+
if (validityError) {
|
|
115
|
+
setError(validityError);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
if (keyAndHolders.holderUserUids.length === 0) {
|
|
110
119
|
setError("At least one holder is required.");
|
|
111
120
|
return;
|
|
@@ -133,15 +142,16 @@ export default function EscrowSetupStep({ adminUid }) {
|
|
|
133
142
|
setName("Escrow");
|
|
134
143
|
}
|
|
135
144
|
catch (err) {
|
|
136
|
-
setError(err
|
|
145
|
+
setError(actionErrorMessage(err, "Could not create the escrow scope."));
|
|
137
146
|
}
|
|
138
147
|
finally {
|
|
139
148
|
setBusy(false);
|
|
140
149
|
}
|
|
141
150
|
}
|
|
142
151
|
const scopeForm = (_jsxs("form", { onSubmit: create, className: "flex flex-col gap-5", children: [_jsx(EscrowScopeKeyAndHoldersFields, { value: keyAndHolders, onChange: setKeyAndHolders, disabled: mode === "generate" && busy, keyReadOnly: mode === "generate" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: busy, disabled: busy, className: "!w-auto", children: "Create escrow scope" }) })] }));
|
|
143
|
-
return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
return (_jsxs("div", { className: "flex flex-col gap-5 max-w-3xl", children: [_jsx("p", { className: "text-sm text-text-muted", children: "Escrow lets a group of trusted holders - for example your legal or compliance team - recover a mailbox\u2019s encrypted mail when they all agree to. It\u2019s optional. Without it, mail that someone encrypts can\u2019t be recovered if they lose their keys, by anyone." }), scopes && scopes.length > 0 && (_jsxs("div", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["Escrow scopes set up: ", scopes.map((scope) => scope.name).join(", "), "."] })), error && _jsx(Alert, { children: error }), _jsxs("fieldset", { className: "flex flex-col gap-2 text-sm", children: [_jsx("legend", { className: "font-semibold mb-1", children: "How do you want to set up escrow?" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "generate", onChange: () => chooseMode("generate") }), "Generate new escrow keys in this browser"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "existing", onChange: () => chooseMode("existing") }), "Use a certificate I already have"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "escrow-mode", checked: mode === "none", onChange: () => chooseMode("none") }), scopes && scopes.length > 0 ? "Don't add another escrow scope" : "Don't use escrow"] })] }), mode !== "none" && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Escrow scope name", htmlFor: "escrowName", children: _jsx("input", { id: "escrowName", type: "text", className: INPUT_CLASS, value: name, disabled: !!generated, onChange: (e) => setName(e.target.value) }) }), mode === "generate" && !generated && (_jsxs(_Fragment, { children: [_jsx(FormField, { label: "Keys valid for", htmlFor: "escrowValidYears", children: _jsx("select", { id: "escrowValidYears", className: INPUT_CLASS, value: validYears, onChange: (e) => setValidYears(Number(e.target.value)), children: [1, 2, 5, 10].map((years) => (_jsxs("option", { value: years, children: [years, " ", years === 1 ? "year" : "years"] }, years))) }) }), _jsx(Button, { type: "button", className: "!w-auto", loading: busy, disabled: busy, onClick: () => void generate(), children: "Generate keys" })] })), mode === "generate" && generated && (_jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsx(Alert, { children: "Download the private key now and give it to your escrow holders. It isn\u2019t stored anywhere else - if it\u2019s lost, no escrowed mail can ever be recovered." }), _jsx(Alert, { children: "You\u2019re downloading this private key, so you must not also be one of its holders: pick holders other than yourself, hand the key over, and delete your copy once they have it. Otherwise one person could both configure escrow and recover mail with it." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setConfirmingDownload(true), children: "Download private key" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => downloadTextFile(`${fileSafe(name)}-certificate.pem`, generated.certificatePem), children: "Download certificate" })] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: savedPrivateKey, disabled: !downloadedPrivateKey, onChange: (e) => setSavedPrivateKey(e.target.checked) }), "I\u2019ve saved the private key somewhere safe"] }), !downloadedPrivateKey && (_jsx("p", { className: "text-xs text-text-muted", children: "Download the private key before continuing." })), _jsxs(Modal, { open: confirmingDownload, onClose: () => setConfirmingDownload(false), title: "Download the private key?", children: [_jsxs("p", { className: "text-sm mb-3", children: ["The file is ", _jsx("strong", { children: "not encrypted" }), ": it lands in this computer\u2019s downloads folder (and its download history), and anyone who obtains it can decrypt every mailbox covered by this escrow scope on their own - the number of required holders does not apply to a copy of the key."] }), _jsx("p", { className: "text-sm mb-5", children: "Move it straight to offline storage or a password manager your escrow holders control, and delete it from here." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDownload(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => {
|
|
153
|
+
downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
|
|
154
|
+
setDownloadedPrivateKey(true);
|
|
155
|
+
setConfirmingDownload(false);
|
|
156
|
+
}, children: "Download unencrypted key" })] })] })] }))] })), mode === "existing" && scopeForm, mode === "generate" && generated && savedPrivateKey && scopeForm] }));
|
|
147
157
|
}
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { useRouter } from "@rapidrest/react/client";
|
|
8
8
|
import { HiCheck } from "react-icons/hi2";
|
|
9
|
-
import {
|
|
9
|
+
import { actionErrorMessage } from "../elevation.js";
|
|
10
10
|
import { getRetentionPolicy } from "../../../../../lib/admin/retentionPolicyApi.js";
|
|
11
11
|
import { getMailboxPolicy } from "../../../../../lib/admin/mailboxPolicyApi.js";
|
|
12
12
|
import { createDomain, listDomains } from "../../../../../lib/admin/domainsApi.js";
|
|
@@ -15,7 +15,8 @@ import { getBranding } from "../../../../../lib/branding/brandingApi.js";
|
|
|
15
15
|
import { getEncryptionPolicy } from "../../../../../lib/crypto/keyvaultApi.js";
|
|
16
16
|
import { listMailboxes } from "../../../../../lib/mail/mailApi.js";
|
|
17
17
|
import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
|
|
18
|
-
import
|
|
18
|
+
import { LIST_PAGE_SIZE, listAllPages } from "../../../mail/listAllPages.js";
|
|
19
|
+
import Alert from "../ActionAlert.js";
|
|
19
20
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
20
21
|
import FormField from "../../../../../lib/components/forms/FormField.js";
|
|
21
22
|
import Modal from "../../../../../lib/components/overlays/Modal.js";
|
|
@@ -43,7 +44,7 @@ function isStepId(value) {
|
|
|
43
44
|
return SETUP_STEPS.some((step) => step.id === value);
|
|
44
45
|
}
|
|
45
46
|
function errorMessage(err, fallback) {
|
|
46
|
-
return err
|
|
47
|
+
return actionErrorMessage(err, fallback);
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* The first-run setup wizard. Each step saves as the administrator works through it; the wizard itself only records
|
|
@@ -203,9 +204,10 @@ function MailboxesStep({ userUid, domain }) {
|
|
|
203
204
|
const [error, setError] = useState(null);
|
|
204
205
|
const [formKey, setFormKey] = useState(0);
|
|
205
206
|
useEffect(() => {
|
|
206
|
-
// The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
// The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own. All pages of them: the
|
|
208
|
+
// administrator's own mailbox may be past the first, and would then be offered to be created again.
|
|
209
|
+
listAllPages((page) => listMailboxes({ limit: LIST_PAGE_SIZE, page, scope: "admin" }, client))
|
|
210
|
+
.then(({ items: list }) =>
|
|
209
211
|
// Keep any mailbox created here while the list was loading.
|
|
210
212
|
setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]))
|
|
211
213
|
.catch((err) => setError(`${errorMessage(err, "Could not load the existing mailboxes.")} Check the Mailboxes page before creating your own, so you don't create it twice.`))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { authApiFetch } from "../../../../lib/util/api.js";
|
|
6
|
+
import { resetNotifications } from "../../notifications/store.js";
|
|
6
7
|
/**
|
|
7
8
|
* The cross-tab sign-out channel - must stay identical to `SIGN_OUT_CHANNEL` in
|
|
8
9
|
* `apps/shared/search/localIndexRpcClient.ts` (the channel `AppShell` listens on and `destroyAllLocalIndexes()`
|
|
@@ -69,6 +70,8 @@ async function logOutOfAuthServer(authServerUrl) {
|
|
|
69
70
|
*/
|
|
70
71
|
export async function signOutOfConsole(authServerUrl) {
|
|
71
72
|
markLocalIndexesForDeletion();
|
|
73
|
+
// The tab's notification history is the session's, and its storage outlives it.
|
|
74
|
+
resetNotifications();
|
|
72
75
|
broadcastSignOut();
|
|
73
76
|
await logOutOfAuthServer(authServerUrl);
|
|
74
77
|
window.location.href = authServerUrl ?? "/";
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useRef, useState } from "react";
|
|
7
|
-
import {
|
|
7
|
+
import { actionErrorMessage } from "./elevation.js";
|
|
8
8
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
9
9
|
/** Page size for the async-request lists (the server returns them newest first and caps `limit` at 500). */
|
|
10
10
|
export const REQUEST_LIST_PAGE_SIZE = 50;
|
|
@@ -30,7 +30,7 @@ export function usePagedList(fetchPage, errorMessage) {
|
|
|
30
30
|
const loaded = useRef(items);
|
|
31
31
|
loaded.current = items;
|
|
32
32
|
function describe(err) {
|
|
33
|
-
return err
|
|
33
|
+
return actionErrorMessage(err, errorMessage);
|
|
34
34
|
}
|
|
35
35
|
async function reload() {
|
|
36
36
|
const current = ++generation.current;
|
|
@@ -25,7 +25,8 @@ export function linkFromInput(input) {
|
|
|
25
25
|
if (/\s/.test(text)) {
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
// "example.com:8080/x" and "localhost:3000" name a host and a port, not a scheme.
|
|
29
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(text) && !/^(?:localhost|[^\s:/@]+\.[^\s:/@]+):\d{1,5}(?:[/?#]|$)/i.test(text)) {
|
|
29
30
|
return safeDescriptionHref(text);
|
|
30
31
|
}
|
|
31
32
|
return safeDescriptionHref(/^[^\s@/]+@[^\s@/]+$/.test(text) ? `mailto:${text}` : `https://${text}`);
|
|
@@ -14,6 +14,8 @@ export interface EventDetailsProps {
|
|
|
14
14
|
isOwnAddress?: (address: string) => boolean;
|
|
15
15
|
calendarName?: string;
|
|
16
16
|
calendarColor?: string;
|
|
17
|
+
/** Whether the reader may change this mailbox's events: `false` for one shared with them view-only, which gets no Modify, Delete or answer. Default `true`. */
|
|
18
|
+
canWrite?: boolean;
|
|
17
19
|
onClose: () => void;
|
|
18
20
|
/** Modify: switch to the edit form. */
|
|
19
21
|
onModify: () => void;
|
|
@@ -30,4 +32,4 @@ export interface EventDetailsProps {
|
|
|
30
32
|
* An event this reader may only see as a busy block (`occurrence.redacted`: a private or confidential event on a shared calendar) is drawn as just that -
|
|
31
33
|
* its time and that it is busy - with nothing to change, delete or answer.
|
|
32
34
|
*/
|
|
33
|
-
export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;
|
|
35
|
+
export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, canWrite, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;
|