@rapidmx/web-client 0.30.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 +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 +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.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 +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,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;
|
|
@@ -34,11 +34,13 @@ function DetailRow({ icon, children }) {
|
|
|
34
34
|
* 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 -
|
|
35
35
|
* its time and that it is busy - with nothing to change, delete or answer.
|
|
36
36
|
*/
|
|
37
|
-
export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }) {
|
|
37
|
+
export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, canWrite = true, onClose, onModify, onSaved, onDeleted, }) {
|
|
38
38
|
const client = useApiClient();
|
|
39
39
|
const [error, setError] = useState(null);
|
|
40
40
|
const [busy, setBusy] = useState(false);
|
|
41
41
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
42
|
+
// An answer to one occurrence of a series waits here for the reader to confirm: the server records it for the entire series.
|
|
43
|
+
const [pendingResponse, setPendingResponse] = useState(null);
|
|
42
44
|
const [requesting, setRequesting] = useState(false);
|
|
43
45
|
// The organizer's own join link for the event's video meeting: `undefined` while it is fetched, `null` once known to be none.
|
|
44
46
|
const [organizerJoinUrl, setOrganizerJoinUrl] = useState(undefined);
|
|
@@ -84,6 +86,7 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
|
|
|
84
86
|
// `@rapidmx/restapi`'s `respond()` route has no occurrence-vs-series scope of its own (see `calendarApi.ts`'s `respondToEvent` doc
|
|
85
87
|
// comment) - it always acts on `occurrence.uid` as a single event document.
|
|
86
88
|
async function handleRespond(responseStatus) {
|
|
89
|
+
setPendingResponse(null);
|
|
87
90
|
setError(null);
|
|
88
91
|
setBusy(true);
|
|
89
92
|
try {
|
|
@@ -133,8 +136,8 @@ export default function EventDetails({ occurrence, isInvited, myResponse, isOwnA
|
|
|
133
136
|
description: occurrence.description,
|
|
134
137
|
descriptionHtml: occurrence.descriptionHtml,
|
|
135
138
|
guestAddresses: occurrence.attendees.map((a) => a.address),
|
|
136
|
-
}, canChange: canChange, canInvite: canInvite, onSent: () => setRequesting(false), onCancel: () => setRequesting(false) }) })), myResponse !== undefined && (_jsxs("div", { className: "flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("span", { className: "text-sm font-medium", children: "Your response" }), _jsx("div", { className: "flex gap-2", children: answers.map(({ response, label }) => {
|
|
139
|
+
}, canChange: canChange, canInvite: canInvite, onSent: () => setRequesting(false), onCancel: () => setRequesting(false) }) })), myResponse !== undefined && canWrite && (_jsxs("div", { className: "flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("span", { className: "text-sm font-medium", children: "Your response" }), _jsx("div", { className: "flex gap-2", children: answers.map(({ response, label }) => {
|
|
137
140
|
const current = myResponse === response;
|
|
138
|
-
return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" "), "aria-pressed": current, disabled: busy, onClick: () => void handleRespond(response), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
|
|
139
|
-
}) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Other attendees\u2019 responses may take a few minutes to update." })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-3 px-5 pb-4", children: [!isInvited && (_jsx(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: onModify, children: "Modify" })), (canChange || canInvite) && !requesting && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setRequesting(true), children: canChange ? "Request a change" : "Add guests" })), !
|
|
141
|
+
return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" "), "aria-pressed": current, disabled: busy, onClick: () => (occurrence.isRecurringOccurrence ? setPendingResponse(response) : void handleRespond(response)), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
|
|
142
|
+
}) })] }), pendingResponse && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsxs("span", { className: "flex-1 min-w-0", children: [pendingResponse === "declined" ? "Declining" : "Your answer", " applies to every event in this series, not just this one", pendingResponse === "declined" ? ", and removes the series from your calendar" : "", "."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setPendingResponse(null), children: "Cancel" }), _jsxs(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: () => void handleRespond(pendingResponse), children: [answers.find((a) => a.response === pendingResponse).label, " the series"] })] })), _jsx("p", { className: "text-xs text-text-muted", children: "Other attendees\u2019 responses may take a few minutes to update." })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-3 px-5 pb-4", children: [!isInvited && canWrite && (_jsx(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: onModify, children: "Modify" })), (canChange || canInvite) && !requesting && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setRequesting(true), children: canChange ? "Request a change" : "Add guests" })), !canWrite ? null : !confirmingDelete ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => setConfirmingDelete(true), children: "Delete" })) : !occurrence.isRecurringOccurrence ? (_jsxs("div", { role: "alert", className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm", children: "Delete this event?" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Yes, delete" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setConfirmingDelete(false), children: "Keep" })] })) : (_jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("occurrence"), children: "Delete this event" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Delete series" })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto ml-auto", onClick: onClose, children: "Close" })] })] }));
|
|
140
143
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { format } from "date-fns";
|
|
8
8
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
9
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
@@ -46,19 +46,25 @@ function wallTimeOfDayMs(instantMs, timezone) {
|
|
|
46
46
|
const wall = toEventWallClock(instantMs, timezone, false);
|
|
47
47
|
return ((wall % MS_PER_DAY) + MS_PER_DAY) % MS_PER_DAY;
|
|
48
48
|
}
|
|
49
|
+
/** The calendar date (`YYYY-MM-DD`) an instant falls on in the frame a series expands in: the UTC date for an all-day event, `timezone`'s wall clock otherwise. */
|
|
50
|
+
function seriesDateKey(iso, allDay, timezone) {
|
|
51
|
+
return allDay ? allDayDateKey(iso) : new Date(toEventWallClock(new Date(iso).getTime(), timezone, false)).toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
const WEEKDAY_ORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
|
49
54
|
/**
|
|
50
55
|
* Rewrites an edited occurrence's `startDate`/`endDate` for a save to the entire series. The modal only
|
|
51
56
|
* ever shows (and edits) *this occurrence's* dates, but the series is stored as its master record starting
|
|
52
57
|
* at the first occurrence - sending the occurrence's dates as-is would move the whole series' start to
|
|
53
|
-
* this occurrence. So
|
|
54
|
-
* start
|
|
58
|
+
* this occurrence. So the change made to the occurrence (its date and its time of day) is applied to the
|
|
59
|
+
* master's own start, and the new duration is kept: moving a Tuesday occurrence to Thursday moves the series
|
|
60
|
+
* by two days, never silently dropping the change. Unchanged times aren't sent at all.
|
|
55
61
|
*
|
|
56
|
-
*
|
|
57
|
-
* browser's
|
|
58
|
-
*
|
|
59
|
-
*
|
|
62
|
+
* The change is measured on the event's own timezone wall clock (the series expands there), not the
|
|
63
|
+
* browser's. The master's new start is converted back from that wall clock, so a master on the other side of
|
|
64
|
+
* a DST change keeps the chosen local time. A weekly rule that the edit left untouched is rotated by the
|
|
65
|
+
* same number of days, so its weekdays move with the start (a rule the form already changed is left as it is).
|
|
60
66
|
*/
|
|
61
|
-
async function toSeriesFields(occurrence, fields, client) {
|
|
67
|
+
async function toSeriesFields(occurrence, fields, formZone, client) {
|
|
62
68
|
const { startDate, endDate, ...rest } = fields;
|
|
63
69
|
if (sameMinute(startDate, occurrence.startDate) && sameMinute(endDate, occurrence.endDate)) {
|
|
64
70
|
// `saveEventSeries()` treats *any* `timezone`/`allDay` in the update as a possible reinterpretation of
|
|
@@ -71,25 +77,32 @@ async function toSeriesFields(occurrence, fields, client) {
|
|
|
71
77
|
const timezone = fields.timezone;
|
|
72
78
|
const newStart = new Date(startDate).getTime();
|
|
73
79
|
const durationMs = new Date(endDate).getTime() - newStart;
|
|
80
|
+
// The days the date moved: on the event's own wall clock when both are timed, otherwise on the clock the form showed the timed side on.
|
|
81
|
+
const frame = fields.allDay || occurrence.allDay ? formZone : timezone;
|
|
82
|
+
const dayShift = Math.round((Date.parse(seriesDateKey(startDate, !!fields.allDay, frame)) - Date.parse(seriesDateKey(occurrence.startDate, occurrence.allDay, frame))) / MS_PER_DAY);
|
|
74
83
|
let masterStart;
|
|
75
84
|
if (fields.allDay) {
|
|
76
|
-
//
|
|
85
|
+
// The master's own date (on its wall clock when it was timed), moved by the days the occurrence moved.
|
|
77
86
|
const masterDateKey = master.allDay
|
|
78
87
|
? allDayDateKey(master.startDate)
|
|
79
88
|
: new Date(toEventWallClock(new Date(master.startDate).getTime(), timezone, false)).toISOString().slice(0, 10);
|
|
80
|
-
masterStart = new Date(allDayInstant(masterDateKey)).getTime();
|
|
89
|
+
masterStart = new Date(allDayInstant(addDaysToKey(masterDateKey, dayShift))).getTime();
|
|
81
90
|
}
|
|
82
91
|
else if (master.allDay) {
|
|
83
|
-
// All-day -> timed: the master's own date at the newly chosen wall-clock time.
|
|
84
|
-
const masterDateWall = new Date(allDayInstant(allDayDateKey(master.startDate))).getTime();
|
|
92
|
+
// All-day -> timed: the master's own date (moved by the days the occurrence moved) at the newly chosen wall-clock time.
|
|
93
|
+
const masterDateWall = new Date(allDayInstant(addDaysToKey(allDayDateKey(master.startDate), dayShift))).getTime();
|
|
85
94
|
masterStart = fromEventWallClock(masterDateWall + wallTimeOfDayMs(newStart, timezone), timezone, false);
|
|
86
95
|
}
|
|
87
96
|
else {
|
|
88
|
-
const
|
|
89
|
-
const delta = ((((rawDelta + MS_PER_DAY / 2) % MS_PER_DAY) + MS_PER_DAY) % MS_PER_DAY) - MS_PER_DAY / 2;
|
|
97
|
+
const delta = toEventWallClock(newStart, timezone, false) - toEventWallClock(new Date(occurrence.startDate).getTime(), timezone, false);
|
|
90
98
|
const masterWall = toEventWallClock(new Date(master.startDate).getTime(), timezone, false);
|
|
91
99
|
masterStart = fromEventWallClock(masterWall + delta, timezone, false);
|
|
92
100
|
}
|
|
101
|
+
const rule = rest.recurrenceRule;
|
|
102
|
+
const original = occurrence.recurrenceRule?.byDay;
|
|
103
|
+
if (rule?.freq === "weekly" && rule.byDay && original && dayShift % 7 !== 0 && rule.byDay.join() === original.join()) {
|
|
104
|
+
rest.recurrenceRule = { ...rule, byDay: rule.byDay.map((day) => WEEKDAY_ORDER[(WEEKDAY_ORDER.indexOf(day) + (dayShift % 7) + 7) % 7]) };
|
|
105
|
+
}
|
|
93
106
|
return {
|
|
94
107
|
...rest,
|
|
95
108
|
startDate: new Date(masterStart).toISOString(),
|
|
@@ -150,6 +163,8 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
150
163
|
targetFolderUid: folderUid,
|
|
151
164
|
};
|
|
152
165
|
});
|
|
166
|
+
// What an existing event's form held when it opened, to tell whether anything was changed since.
|
|
167
|
+
const [pristine] = useState(() => JSON.stringify(values));
|
|
153
168
|
const { attendees, location, videoEnabled } = values;
|
|
154
169
|
function update(patch) {
|
|
155
170
|
setValues((prev) => ({ ...prev, ...patch }));
|
|
@@ -172,6 +187,12 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
172
187
|
// finish the video-conferencing step after it: the modal then stays open on that error, and the retry
|
|
173
188
|
// must update *that* record (its uid and its new version), not re-create it or reuse the stale prop.
|
|
174
189
|
const [savedEvent, setSavedEvent] = useState(null);
|
|
190
|
+
// Whether that stored record is a series saved with `toSeriesFields()`: its master's dates are not the form's (the occurrence's), so a retry
|
|
191
|
+
// must not send them - the first save already applied any change of time.
|
|
192
|
+
const savedAsSeries = useRef(false);
|
|
193
|
+
// A meeting minted for this event whose link onto the event could not be saved. A retry reuses it rather than minting a second one,
|
|
194
|
+
// which would send the invitees a second set of join links and leave this one orphaned.
|
|
195
|
+
const mintedMeeting = useRef(null);
|
|
175
196
|
const [editScope, setEditScope] = useState("occurrence");
|
|
176
197
|
const [error, setError] = useState(null);
|
|
177
198
|
const [saving, setSaving] = useState(false);
|
|
@@ -201,7 +222,9 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
201
222
|
}, [occurrence?.videoMeetingUid, client]);
|
|
202
223
|
// "This event only" detaches a standalone, non-repeating copy - the series' rule doesn't apply to it.
|
|
203
224
|
const editingSingleOccurrence = !!occurrence?.isRecurringOccurrence && editScope === "occurrence";
|
|
204
|
-
dirtyRef.current =
|
|
225
|
+
dirtyRef.current = occurrence
|
|
226
|
+
? JSON.stringify(values) !== pristine
|
|
227
|
+
: !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
|
|
205
228
|
function updateAttendee(index, patch) {
|
|
206
229
|
setValues((prev) => ({ ...prev, attendees: prev.attendees.map((a, i) => (i === index ? { ...a, ...patch } : a)) }));
|
|
207
230
|
}
|
|
@@ -217,6 +240,11 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
217
240
|
],
|
|
218
241
|
}));
|
|
219
242
|
}
|
|
243
|
+
// The weekday the form's start falls on, in the frame the series expands in (`undefined` while the start is not a valid date).
|
|
244
|
+
function startWeekdayOf(v) {
|
|
245
|
+
const ms = v.allDay ? Number.NaN : wallStringToMs(v.start, v.formZone, deviceZone);
|
|
246
|
+
return startWeekdayCode(v.allDay ? v.start : Number.isNaN(ms) ? "" : new Date(ms).toISOString(), v.allDay, v.timezone);
|
|
247
|
+
}
|
|
220
248
|
// Moving the start moves the end with it, so the event keeps its length (a Google Calendar habit, and the way to
|
|
221
249
|
// reschedule without touching two fields).
|
|
222
250
|
function setStart(next) {
|
|
@@ -231,7 +259,13 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
231
259
|
? `${new Date(endMs + shift).toISOString().slice(0, 10)}T00:00`
|
|
232
260
|
: msToWallString(endMs + shift, prev.formZone, deviceZone);
|
|
233
261
|
}
|
|
234
|
-
|
|
262
|
+
// A weekly rule on just the start's weekday ("Weekly on Wednesday") follows the start to its new weekday - left alone it
|
|
263
|
+
// would not include the event's own first day, and the series would begin on the next of the old weekday instead.
|
|
264
|
+
const oldDay = startWeekdayOf(prev);
|
|
265
|
+
const nextDay = startWeekdayOf({ ...prev, start: next });
|
|
266
|
+
const rule = prev.recurrenceRule;
|
|
267
|
+
const followsStart = oldDay && nextDay && rule?.freq === "weekly" && rule.byDay?.length === 1 && rule.byDay[0] === oldDay;
|
|
268
|
+
return { ...prev, start: next, end: nextEnd, recurrenceRule: followsStart ? { ...rule, byDay: [nextDay] } : rule };
|
|
235
269
|
});
|
|
236
270
|
}
|
|
237
271
|
// The recurrence end date is stored in the frame the series expands in (UTC for all-day, local otherwise),
|
|
@@ -331,23 +365,28 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
331
365
|
setVideoError("Add at least one attendee other than yourself to add video conferencing.");
|
|
332
366
|
return { event: saved, failed: true };
|
|
333
367
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
368
|
+
if (!mintedMeeting.current) {
|
|
369
|
+
const result = await createVideoMeeting({
|
|
370
|
+
mailboxUid: saved.mailboxUid,
|
|
371
|
+
title: saved.title.slice(0, MAX_MEETING_TITLE_LENGTH),
|
|
372
|
+
visibility: "private",
|
|
373
|
+
calendarEventUid: saved.uid,
|
|
374
|
+
startTime: saved.startDate,
|
|
375
|
+
endTime: saved.endDate,
|
|
376
|
+
invitees,
|
|
377
|
+
}, client);
|
|
378
|
+
mintedMeeting.current = { uid: result.meeting.uid, organizerJoinUrl: result.organizerJoinUrl ?? null };
|
|
379
|
+
}
|
|
380
|
+
const meeting = mintedMeeting.current;
|
|
343
381
|
const patched = await updateCalendarEvent({
|
|
344
382
|
uid: saved.uid,
|
|
345
383
|
version: saved.version,
|
|
346
384
|
location: VIDEO_LOCATION_PLACEHOLDER,
|
|
347
|
-
videoMeetingUid:
|
|
385
|
+
videoMeetingUid: meeting.uid,
|
|
348
386
|
}, client);
|
|
349
|
-
|
|
350
|
-
|
|
387
|
+
mintedMeeting.current = null;
|
|
388
|
+
setVideoMeetingUid(meeting.uid);
|
|
389
|
+
setOrganizerJoinUrl(meeting.organizerJoinUrl);
|
|
351
390
|
update({ location: VIDEO_LOCATION_PLACEHOLDER });
|
|
352
391
|
return { event: patched, failed: false };
|
|
353
392
|
}
|
|
@@ -455,8 +494,17 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
455
494
|
// A retry after the event was stored but its video meeting couldn't be (see `savedEvent`):
|
|
456
495
|
// update exactly the record the first attempt left behind, whichever branch below created it.
|
|
457
496
|
// A series' own exception/detached-occurrence re-pointing already ran on that attempt and has
|
|
458
|
-
// nothing left to shift
|
|
459
|
-
|
|
497
|
+
// nothing left to shift. For a series the form's dates are those of the edited occurrence, not
|
|
498
|
+
// the master's: sending them would move the whole series to that occurrence, so they (and the
|
|
499
|
+
// zone and all-day flag they go with) stay as the first attempt left them.
|
|
500
|
+
const retryFields = { ...fields };
|
|
501
|
+
if (savedAsSeries.current) {
|
|
502
|
+
delete retryFields.startDate;
|
|
503
|
+
delete retryFields.endDate;
|
|
504
|
+
delete retryFields.timezone;
|
|
505
|
+
delete retryFields.allDay;
|
|
506
|
+
}
|
|
507
|
+
saved = await updateCalendarEvent({ uid: savedEvent.uid, version: savedEvent.version, ...retryFields }, client);
|
|
460
508
|
}
|
|
461
509
|
else if (!occurrence) {
|
|
462
510
|
// The organizer is only ever set on create - an edit keeps the event's own organizer.
|
|
@@ -471,8 +519,9 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
471
519
|
saved = await detachOccurrence(occurrence, fields, client);
|
|
472
520
|
}
|
|
473
521
|
else if (occurrence.isRecurringOccurrence) {
|
|
474
|
-
const series = await saveEventSeries(occurrence, (await toSeriesFields(occurrence, fields, client)), client);
|
|
522
|
+
const series = await saveEventSeries(occurrence, (await toSeriesFields(occurrence, fields, values.formZone, client)), client);
|
|
475
523
|
saved = series;
|
|
524
|
+
savedAsSeries.current = true;
|
|
476
525
|
detachedSyncFailed = !!series.detachedOccurrenceSyncFailed;
|
|
477
526
|
}
|
|
478
527
|
else {
|
|
@@ -507,7 +556,6 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
507
556
|
// stays disabled either way, and the two are told apart below purely from `organizerJoinUrl` itself.
|
|
508
557
|
const joinUrl = joinMeetingUrl(organizerJoinUrl);
|
|
509
558
|
const timeZones = useMemo(() => timeZoneOptions(values.formZone, values.timezone, deviceZone), [values.formZone, values.timezone, deviceZone]);
|
|
510
|
-
const startMs = values.allDay ? Number.NaN : wallStringToMs(values.start, values.formZone, deviceZone);
|
|
511
559
|
const calendarFolderUid = occurrence ? occurrence.folderUid : values.targetFolderUid;
|
|
512
560
|
const controller = {
|
|
513
561
|
occurrence,
|
|
@@ -520,7 +568,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
520
568
|
timeZones,
|
|
521
569
|
organizerAddress: effectiveOrganizerAddress,
|
|
522
570
|
deviceZone,
|
|
523
|
-
startWeekday:
|
|
571
|
+
startWeekday: startWeekdayOf(values),
|
|
524
572
|
editingSingleOccurrence,
|
|
525
573
|
updateAttendee,
|
|
526
574
|
removeAttendee,
|
|
@@ -38,11 +38,13 @@ export declare function ReminderRow({ value, onChange }: {
|
|
|
38
38
|
* "Does not repeat" and the plain repeats as a menu, with "Custom…" opening `RecurrenceEditor`'s full set (an interval, weekdays, an end).
|
|
39
39
|
* Choosing one of the plain repeats keeps the rule's skipped dates; the menu shows "Custom…" for any rule it has no plain entry for.
|
|
40
40
|
*/
|
|
41
|
-
export declare function RecurrenceSelect({ value, onChange, allDay, startWeekday, }: {
|
|
41
|
+
export declare function RecurrenceSelect({ value, onChange, allDay, startWeekday, startDateKey, }: {
|
|
42
42
|
value: RecurrenceRule | null;
|
|
43
43
|
onChange: (rule: RecurrenceRule | null) => void;
|
|
44
44
|
allDay: boolean;
|
|
45
45
|
startWeekday: WeekdayCode | undefined;
|
|
46
|
+
/** The event's start date (`yyyy-MM-dd`), for a custom rule's default end date. */
|
|
47
|
+
startDateKey?: string;
|
|
46
48
|
}): React.JSX.Element;
|
|
47
49
|
/** [start date] [start time] to [end time] [end date], then the "Time zone" link (and its menu), "All day" and how the event repeats. */
|
|
48
50
|
export declare function DateTimeControls({ c }: {
|
|
@@ -124,7 +124,7 @@ function presetOf(rule, startWeekday) {
|
|
|
124
124
|
* "Does not repeat" and the plain repeats as a menu, with "Custom…" opening `RecurrenceEditor`'s full set (an interval, weekdays, an end).
|
|
125
125
|
* Choosing one of the plain repeats keeps the rule's skipped dates; the menu shows "Custom…" for any rule it has no plain entry for.
|
|
126
126
|
*/
|
|
127
|
-
export function RecurrenceSelect({ value, onChange, allDay, startWeekday, }) {
|
|
127
|
+
export function RecurrenceSelect({ value, onChange, allDay, startWeekday, startDateKey, }) {
|
|
128
128
|
// Picking "Custom…" for a rule that is also a plain repeat still opens the editor.
|
|
129
129
|
const [customRequested, setCustomRequested] = useState(false);
|
|
130
130
|
const selected = customRequested && value ? "custom" : presetOf(value, startWeekday);
|
|
@@ -140,7 +140,7 @@ export function RecurrenceSelect({ value, onChange, allDay, startWeekday, }) {
|
|
|
140
140
|
onChange({ ...presetRule(key, startWeekday), exceptions: value?.exceptions ?? [] });
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("select", { id: "event-recurrence", "aria-label": "Recurrence", className: SELECT_CLASS, value: selected, onChange: (e) => choose(e.target.value), children: [_jsx("option", { value: "none", children: "Does not repeat" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "weekly", children: startWeekday ? `Weekly on ${WEEKDAY_NAME[startWeekday]}` : "Weekly" }), _jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Annually" }), _jsx("option", { value: "weekdays", children: "Every weekday (Monday to Friday)" }), _jsx("option", { value: "custom", children: "Custom\u2026" })] }), selected === "custom" && _jsx(RecurrenceEditor, { value: value, onChange: onChange, allDay: allDay, startWeekday: startWeekday, hideToggle: true })] }));
|
|
143
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("select", { id: "event-recurrence", "aria-label": "Recurrence", className: SELECT_CLASS, value: selected, onChange: (e) => choose(e.target.value), children: [_jsx("option", { value: "none", children: "Does not repeat" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "weekly", children: startWeekday ? `Weekly on ${WEEKDAY_NAME[startWeekday]}` : "Weekly" }), _jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Annually" }), _jsx("option", { value: "weekdays", children: "Every weekday (Monday to Friday)" }), _jsx("option", { value: "custom", children: "Custom\u2026" })] }), selected === "custom" && _jsx(RecurrenceEditor, { value: value, onChange: onChange, allDay: allDay, startWeekday: startWeekday, startDateKey: startDateKey, hideToggle: true })] }));
|
|
144
144
|
}
|
|
145
145
|
const START_TIME_OPTIONS = dayTimes().map((time) => ({ value: time, label: formatTimeOfDay(time) }));
|
|
146
146
|
/** [start date] [start time] to [end time] [end date], then the "Time zone" link (and its menu), "All day" and how the event repeats. */
|
|
@@ -149,7 +149,7 @@ export function DateTimeControls({ c }) {
|
|
|
149
149
|
const [zoneOpen, setZoneOpen] = useState(false);
|
|
150
150
|
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsx("input", { type: "date", "aria-label": "Event start date", className: `${SELECT_CLASS} min-w-0`, value: values.start.slice(0, 10), onChange: (e) => c.setStart(`${e.target.value}${values.start.slice(10)}`) }), !values.allDay && (_jsx(TimeComboBox, { "aria-label": "Event start time", className: `${SELECT_CLASS} w-28`, display: formatTimeOfDay(values.start.slice(11, 16)), current: values.start.slice(11, 16), options: START_TIME_OPTIONS,
|
|
151
151
|
// Moving the start moves the end with it, so the event keeps its length: whatever the start is set to.
|
|
152
|
-
onSelect: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`), onType: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`) })), _jsx("span", { className: "text-text-muted", children: "to" }), !values.allDay && (_jsx(TimeComboBox, { "aria-label": "Event end time", className: `${SELECT_CLASS} w-28`, display: formatTimeOfDay(values.end.slice(11, 16)), current: values.end, options: endTimeOptions(values.start).map((option) => ({ value: option.value, label: option.label, hint: option.hint })), onSelect: c.setEnd, onType: (time) => c.setEnd(`${values.end.slice(0, 11)}${time}`) })), _jsx("input", { type: "date", "aria-label": "Event end date", className: `${SELECT_CLASS} min-w-0`, value: values.end.slice(0, 10), onChange: (e) => c.setEnd(`${e.target.value}${values.end.slice(10)}`) })] }), _jsx("button", { type: "button", "aria-expanded": zoneOpen, className: "self-start text-sm text-primary-dark hover:underline", onClick: () => setZoneOpen((open) => !open), children: "Time zone" }), zoneOpen && (_jsx(TimeZonePicker, { "aria-label": "Event time zone", className: `${SELECT_CLASS} max-w-full`, value: values.formZone, zones: c.timeZones, at: zoneDate(values.start), onChange: c.onTimeZoneChange })), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: values.allDay, onChange: (e) => c.onAllDayChange(e.target.checked) }), "All day"] }), !c.editingSingleOccurrence && (_jsx(RecurrenceSelect, { value: values.recurrenceRule, onChange: (rule) => c.update({ recurrenceRule: rule }), allDay: values.allDay, startWeekday: c.startWeekday }))] }));
|
|
152
|
+
onSelect: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`), onType: (time) => c.setStart(`${values.start.slice(0, 11)}${time}`) })), _jsx("span", { className: "text-text-muted", children: "to" }), !values.allDay && (_jsx(TimeComboBox, { "aria-label": "Event end time", className: `${SELECT_CLASS} w-28`, display: formatTimeOfDay(values.end.slice(11, 16)), current: values.end, options: endTimeOptions(values.start).map((option) => ({ value: option.value, label: option.label, hint: option.hint })), onSelect: c.setEnd, onType: (time) => c.setEnd(`${values.end.slice(0, 11)}${time}`) })), _jsx("input", { type: "date", "aria-label": "Event end date", className: `${SELECT_CLASS} min-w-0`, value: values.end.slice(0, 10), onChange: (e) => c.setEnd(`${e.target.value}${values.end.slice(10)}`) })] }), _jsx("button", { type: "button", "aria-expanded": zoneOpen, className: "self-start text-sm text-primary-dark hover:underline", onClick: () => setZoneOpen((open) => !open), children: "Time zone" }), zoneOpen && (_jsx(TimeZonePicker, { "aria-label": "Event time zone", className: `${SELECT_CLASS} max-w-full`, value: values.formZone, zones: c.timeZones, at: zoneDate(values.start), onChange: c.onTimeZoneChange })), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: values.allDay, onChange: (e) => c.onAllDayChange(e.target.checked) }), "All day"] }), !c.editingSingleOccurrence && (_jsx(RecurrenceSelect, { value: values.recurrenceRule, onChange: (rule) => c.update({ recurrenceRule: rule }), allDay: values.allDay, startWeekday: c.startWeekday, startDateKey: values.start.slice(0, 10) }))] }));
|
|
153
153
|
}
|
|
154
154
|
/** The calendar this event is in: its colour and name, a choice between calendars (and mailboxes) when an event is being created. */
|
|
155
155
|
export function CalendarField({ c }) {
|
|
@@ -12,6 +12,6 @@ export interface EventMatchNavProps {
|
|
|
12
12
|
* The Previous / Next match buttons beside an event's card while a search is running: the card moves on to the neighbouring result. They sit
|
|
13
13
|
* inside the dialog (so Tab reaches them and the dialog's focus trap holds) but are positioned against the window - either side of the
|
|
14
14
|
* centered card on a desktop, the bottom corners on a phone, where the card fills the width. The left and right arrow keys do the same unless
|
|
15
|
-
* a field is taking them.
|
|
15
|
+
* a field or a widget (a select, a menu, a slider, ...) is taking them.
|
|
16
16
|
*/
|
|
17
17
|
export default function EventMatchNav({ onPrevious, onNext, canPrevious, canNext, noun }: EventMatchNavProps): React.JSX.Element;
|
|
@@ -6,16 +6,26 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
import { useEffect } from "react";
|
|
7
7
|
import { HiOutlineChevronLeft, HiOutlineChevronRight } from "react-icons/hi2";
|
|
8
8
|
import { isTextEntry } from "../../keyboard/targets.js";
|
|
9
|
+
/** Widgets that take the arrow keys themselves (a menu's items, a radio group, a slider, a list's options, a tab list, ...) and an open popup's trigger. */
|
|
10
|
+
const ARROW_WIDGET_SELECTOR = [
|
|
11
|
+
...["menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "radiogroup", "radio", "listbox", "option"].map((role) => `[role="${role}"]`),
|
|
12
|
+
...["slider", "spinbutton", "tablist", "tab", "tree", "grid", "combobox"].map((role) => `[role="${role}"]`),
|
|
13
|
+
'[aria-haspopup][aria-expanded="true"]',
|
|
14
|
+
].join(",");
|
|
15
|
+
/** Whether the arrow keys pressed at `target` belong to what is there: a field or select, or a widget with arrows of its own. */
|
|
16
|
+
function takesArrowKeys(target) {
|
|
17
|
+
return isTextEntry(target) || (target instanceof Element && target.closest(ARROW_WIDGET_SELECTOR) !== null);
|
|
18
|
+
}
|
|
9
19
|
/**
|
|
10
20
|
* The Previous / Next match buttons beside an event's card while a search is running: the card moves on to the neighbouring result. They sit
|
|
11
21
|
* inside the dialog (so Tab reaches them and the dialog's focus trap holds) but are positioned against the window - either side of the
|
|
12
22
|
* centered card on a desktop, the bottom corners on a phone, where the card fills the width. The left and right arrow keys do the same unless
|
|
13
|
-
* a field is taking them.
|
|
23
|
+
* a field or a widget (a select, a menu, a slider, ...) is taking them.
|
|
14
24
|
*/
|
|
15
25
|
export default function EventMatchNav({ onPrevious, onNext, canPrevious = true, canNext = true, noun = "match" }) {
|
|
16
26
|
useEffect(() => {
|
|
17
27
|
function handleKeyDown(event) {
|
|
18
|
-
if ((event.key !== "ArrowLeft" && event.key !== "ArrowRight") || event.defaultPrevented ||
|
|
28
|
+
if ((event.key !== "ArrowLeft" && event.key !== "ArrowRight") || event.defaultPrevented || takesArrowKeys(event.target)) {
|
|
19
29
|
return;
|
|
20
30
|
}
|
|
21
31
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|