@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
|
@@ -7,6 +7,7 @@ import { useRef, useState } from "react";
|
|
|
7
7
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
8
8
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
9
9
|
import useIsMobile from "../../../../lib/util/useIsMobile.js";
|
|
10
|
+
import { useMailboxUpdateAccess } from "../../mail/useMailboxUpdateAccess.js";
|
|
10
11
|
import EventDetails from "./EventDetails.js";
|
|
11
12
|
import EventEditor from "./EventEditor.js";
|
|
12
13
|
import EventMatchNav from "./EventMatchNav.js";
|
|
@@ -43,6 +44,10 @@ export default function EventModal({ open, onClose, mailboxUid, folderUid, calen
|
|
|
43
44
|
const [syncWarning, setSyncWarning] = useState(false);
|
|
44
45
|
// Whether the form holds anything a click on the backdrop would throw away (kept up to date by the editor).
|
|
45
46
|
const dirtyRef = useRef(false);
|
|
47
|
+
// Escape or the close button on a form that holds something asks before throwing it away.
|
|
48
|
+
const [confirmingDiscard, setConfirmingDiscard] = useState(false);
|
|
49
|
+
// An event on a calendar shared with the reader view-only cannot be changed, deleted or answered.
|
|
50
|
+
const canWrite = useMailboxUpdateAccess(mailboxOptions?.find((option) => option.mailbox.uid === occurrence?.mailboxUid)?.mailbox);
|
|
46
51
|
if (!open) {
|
|
47
52
|
return null;
|
|
48
53
|
}
|
|
@@ -65,7 +70,7 @@ export default function EventModal({ open, onClose, mailboxUid, folderUid, calen
|
|
|
65
70
|
const width = !isForm ? DETAILS_WIDTH : layout === "expanded" ? (tab === "event" || occurrence ? FORM_WIDTH : TASK_FORM_WIDTH) : QUICK_WIDTH;
|
|
66
71
|
// Leaving the form: an existing event goes back to its details, a new one is done.
|
|
67
72
|
const leaveForm = occurrence ? () => setEditing(false) : onClose;
|
|
68
|
-
return (_jsxs(EventShell, { variant: variant, label: !occurrence ? "New event" : editing ? "Edit event" : "Event details", focusKey: isForm ? layout : "details", anchor: anchor, width: width, onClose: isForm ? leaveForm : onClose, onBackdropPress: isForm ? () => !dirtyRef.current && leaveForm() : undefined, children: [isForm ? (_jsx(EventEditor, { layout: layout, onExpand: () => setExpanded(true), onCancel: leaveForm, onSaved: onSaved, onSyncWarning: () => setSyncWarning(true), dirtyRef: dirtyRef, mailboxUid: mailboxUid, folderUid: folderUid, calendars: calendars, mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: organizerAddress, occurrence: occurrence, initialStart: initialStart, initialEnd: initialEnd, initialAllDay: initialAllDay, quickCreate: occurrence
|
|
73
|
+
return (_jsxs(EventShell, { variant: variant, label: !occurrence ? "New event" : editing ? "Edit event" : "Event details", focusKey: isForm ? layout : "details", anchor: anchor, width: width, onClose: isForm ? () => (dirtyRef.current ? setConfirmingDiscard(true) : leaveForm()) : onClose, onBackdropPress: isForm ? () => !dirtyRef.current && leaveForm() : undefined, children: [isForm ? (_jsx(EventEditor, { layout: layout, onExpand: () => setExpanded(true), onCancel: leaveForm, onSaved: onSaved, onSyncWarning: () => setSyncWarning(true), dirtyRef: dirtyRef, mailboxUid: mailboxUid, folderUid: folderUid, calendars: calendars, mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: organizerAddress, occurrence: occurrence, initialStart: initialStart, initialEnd: initialEnd, initialAllDay: initialAllDay, quickCreate: occurrence
|
|
69
74
|
? undefined
|
|
70
75
|
: {
|
|
71
76
|
tab,
|
|
@@ -74,5 +79,8 @@ export default function EventModal({ open, onClose, mailboxUid, folderUid, calen
|
|
|
74
79
|
setExpanded(false);
|
|
75
80
|
},
|
|
76
81
|
bookingHref,
|
|
77
|
-
} })) : (_jsx(SlideIn, { from: matchNav?.slide?.occurrenceKey === occurrence.occurrenceKey ? matchNav.slide.from : undefined, children: _jsx(EventDetails, { occurrence: occurrence, isInvited: isInvited, myResponse: canRespond ? myAttendee.responseStatus : undefined, isOwnAddress: isOwnAddress, calendarName: calendars?.find((cal) => cal.uid === occurrence.folderUid)?.name, calendarColor: folderColors?.[occurrence.folderUid], onClose: onClose, onModify: () => setEditing(true), onSaved: onSaved, onDeleted: onDeleted }) }, occurrence.occurrenceKey)), !isForm && matchNav && _jsx(EventMatchNav, { onPrevious: matchNav.onPrevious, onNext: matchNav.onNext, canPrevious: matchNav.canPrevious, canNext: matchNav.canNext, noun: matchNav.noun })
|
|
82
|
+
} })) : (_jsx(SlideIn, { from: matchNav?.slide?.occurrenceKey === occurrence.occurrenceKey ? matchNav.slide.from : undefined, children: _jsx(EventDetails, { occurrence: occurrence, isInvited: isInvited, myResponse: canRespond ? myAttendee.responseStatus : undefined, isOwnAddress: isOwnAddress, calendarName: calendars?.find((cal) => cal.uid === occurrence.folderUid)?.name, calendarColor: folderColors?.[occurrence.folderUid], canWrite: canWrite, onClose: onClose, onModify: () => setEditing(true), onSaved: onSaved, onDeleted: onDeleted }) }, occurrence.occurrenceKey)), !isForm && matchNav && _jsx(EventMatchNav, { onPrevious: matchNav.onPrevious, onNext: matchNav.onNext, canPrevious: matchNav.canPrevious, canNext: matchNav.canNext, noun: matchNav.noun }), _jsxs(Modal, { open: confirmingDiscard, onClose: () => setConfirmingDiscard(false), title: "Discard changes?", children: [_jsx("p", { className: "text-sm mb-5", children: "What you entered in this event has not been saved." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDiscard(false), children: "Keep editing" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => {
|
|
83
|
+
setConfirmingDiscard(false);
|
|
84
|
+
leaveForm();
|
|
85
|
+
}, children: "Discard" })] })] })] }));
|
|
78
86
|
}
|
|
@@ -69,7 +69,7 @@ export default function FindATime({ c }) {
|
|
|
69
69
|
}
|
|
70
70
|
}, [startDay]);
|
|
71
71
|
// You, then each guest once (case-insensitively), at most as many as one look-up may ask about.
|
|
72
|
-
const rows = useMemo(() => {
|
|
72
|
+
const { rows, skipped } = useMemo(() => {
|
|
73
73
|
const seen = new Set([organizerAddress.toLowerCase()]);
|
|
74
74
|
const guests = [];
|
|
75
75
|
for (const attendee of values.attendees) {
|
|
@@ -79,7 +79,8 @@ export default function FindATime({ c }) {
|
|
|
79
79
|
guests.push({ address: attendee.address, name: attendee.displayName || attendee.address, you: false });
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const everyone = [{ address: organizerAddress, name: "You", you: true }, ...guests];
|
|
83
|
+
return { rows: everyone.slice(0, FREE_BUSY_MAX_ADDRESSES), skipped: Math.max(0, everyone.length - FREE_BUSY_MAX_ADDRESSES) };
|
|
83
84
|
}, [values.attendees, organizerAddress]);
|
|
84
85
|
const guestCount = rows.length - 1;
|
|
85
86
|
const addresses = rows.map((row) => row.address);
|
|
@@ -168,7 +169,7 @@ export default function FindATime({ c }) {
|
|
|
168
169
|
.map((block) => `${block.tentative ? "Tentatively busy" : "Busy"} ${clockLabel(block.startMs, formZone, deviceZone)} to ${clockLabel(block.endMs, formZone, deviceZone)}`)
|
|
169
170
|
.join(", ");
|
|
170
171
|
}
|
|
171
|
-
return (_jsxs("div", { className: "flex flex-col gap-3 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: () => setDayKey((key) => addDaysToKey(key, -1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u2039" }), _jsx("span", { className: "text-sm font-medium min-w-[9rem] text-center", children: dayLabel }), _jsx("button", { type: "button", "aria-label": "Next day", onClick: () => setDayKey((key) => addDaysToKey(key, 1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u203A" }), _jsx("span", { className: "text-xs text-text-muted", children: describeTimeZone(formZone, zoneDate(dayKey)) })] }), lookUp.status === "error" && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-3 text-sm text-danger", children: [_jsx("span", { children: lookUp.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRetry((count) => count + 1), children: "Try again" })] })), _jsx("div", { ref: scroller, role: "group", "aria-label": "Guests' availability", "aria-busy": lookUp.status === "loading", className: "overflow-x-auto border border-border rounded-md", children: _jsxs("div", { className: "relative", style: { width: NAME_WIDTH + TIMELINE_WIDTH }, children: [_jsxs("div", { className: "flex bg-surface border-b border-border", style: { height: HEADER_HEIGHT }, children: [_jsx("div", { className: "sticky left-0 z-20 shrink-0 bg-surface border-r border-border", style: { width: NAME_WIDTH } }), HOURS.map((hour) => (_jsx("button", { type: "button", "aria-label": `Start at ${hourLabel(hour)}`, title: `Start at ${hourLabel(hour)}`, onClick: () => startAt(hour * 60), className: "shrink-0 text-[10px] text-text-muted hover:bg-surface-alt hover:text-text text-left pl-1", style: { width: HOUR_WIDTH }, children: hourLabel(hour) }, hour)))] }), rows.map((row) => {
|
|
172
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: () => setDayKey((key) => addDaysToKey(key, -1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u2039" }), _jsx("span", { className: "text-sm font-medium min-w-[9rem] text-center", children: dayLabel }), _jsx("button", { type: "button", "aria-label": "Next day", onClick: () => setDayKey((key) => addDaysToKey(key, 1)), className: "w-8 h-8 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: "\u203A" }), _jsx("span", { className: "text-xs text-text-muted", children: describeTimeZone(formZone, zoneDate(dayKey)) })] }), skipped > 0 && (_jsxs("p", { className: "text-xs text-text-muted", children: [skipped, " ", skipped === 1 ? "guest" : "guests", " beyond the first ", FREE_BUSY_MAX_ADDRESSES - 1, " ", skipped === 1 ? "isn’t" : "aren’t", " checked, and ", skipped === 1 ? "is" : "are", " not counted in who is free."] })), lookUp.status === "error" && (_jsxs("div", { role: "alert", className: "flex flex-wrap items-center gap-3 text-sm text-danger", children: [_jsx("span", { children: lookUp.message }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setRetry((count) => count + 1), children: "Try again" })] })), _jsx("div", { ref: scroller, role: "group", "aria-label": "Guests' availability", "aria-busy": lookUp.status === "loading", className: "overflow-x-auto border border-border rounded-md", children: _jsxs("div", { className: "relative", style: { width: NAME_WIDTH + TIMELINE_WIDTH }, children: [_jsxs("div", { className: "flex bg-surface border-b border-border", style: { height: HEADER_HEIGHT }, children: [_jsx("div", { className: "sticky left-0 z-20 shrink-0 bg-surface border-r border-border", style: { width: NAME_WIDTH } }), HOURS.map((hour) => (_jsx("button", { type: "button", "aria-label": `Start at ${hourLabel(hour)}`, title: `Start at ${hourLabel(hour)}`, onClick: () => startAt(hour * 60), className: "shrink-0 text-[10px] text-text-muted hover:bg-surface-alt hover:text-text text-left pl-1", style: { width: HOUR_WIDTH }, children: hourLabel(hour) }, hour)))] }), rows.map((row) => {
|
|
172
173
|
const person = byAddress.get(row.address.toLowerCase());
|
|
173
174
|
const state = person && timeIsValid && covered ? availabilityDuring(person, eventStart, eventEnd) : undefined;
|
|
174
175
|
const hidden = person && person.status !== "available" ? person : undefined;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
|
|
3
|
-
/** The most days one event is listed on, so an event that runs for years does not become thousands of rows. */
|
|
3
|
+
/** The most days one event is listed on (counted from today), so an event that runs for years does not become thousands of rows. */
|
|
4
4
|
export declare const LIST_MAX_DAYS_PER_EVENT = 366;
|
|
5
|
+
/** The days (that have events) rendered at first, and how many more each time the end of the list is reached. */
|
|
6
|
+
export declare const LIST_INITIAL_DAYS = 60;
|
|
7
|
+
export declare const LIST_DAYS_PER_PAGE = 60;
|
|
5
8
|
/** A step through the list's months: `nonce` makes a second press in the same direction a new step. */
|
|
6
9
|
export interface ListStep {
|
|
7
10
|
direction: 1 | -1;
|
|
@@ -24,7 +27,10 @@ export interface ListViewProps {
|
|
|
24
27
|
* nothing at all says so). A multi-day event is listed on every day it covers, as the month grid shows it. Each row is a full-width button that
|
|
25
28
|
* opens the event's card, with the time, title, place (or organizer) and its calendar's colour.
|
|
26
29
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* The days are grouped once per change of `occurrences` (or of the day), not per render, but only the first `LIST_INITIAL_DAYS` of them are rendered:
|
|
31
|
+
* more follow as the end of the list is scrolled to (a daily series alone is thousands of rows). A jump, a step or a search match that lies beyond
|
|
32
|
+
* the rendered days renders up to it first. The list scrolls itself to `focusDate` and, on a `step`, to the neighbouring month.
|
|
29
33
|
*/
|
|
30
|
-
|
|
34
|
+
declare function ListView({ occurrences, folderColors, onSelectEvent, focusDate, jumpNonce, step }: ListViewProps): React.JSX.Element;
|
|
35
|
+
declare const _default: React.MemoExoticComponent<typeof ListView>;
|
|
36
|
+
export default _default;
|
|
@@ -3,12 +3,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { addDays, format, isToday, parseISO, startOfDay } from "date-fns";
|
|
8
8
|
import { allDayDateKey, localDateKey, startsOnDay } from "./allDay.js";
|
|
9
9
|
import { occurrenceMarker, useActiveOccurrenceKey } from "./activeOccurrence.js";
|
|
10
|
-
|
|
10
|
+
import { useDayKey } from "../../../../lib/calendar/useDayKey.js";
|
|
11
|
+
/** The most days one event is listed on (counted from today), so an event that runs for years does not become thousands of rows. */
|
|
11
12
|
export const LIST_MAX_DAYS_PER_EVENT = 366;
|
|
13
|
+
/** The days (that have events) rendered at first, and how many more each time the end of the list is reached. */
|
|
14
|
+
export const LIST_INITIAL_DAYS = 60;
|
|
15
|
+
export const LIST_DAYS_PER_PAGE = 60;
|
|
12
16
|
/** When an occurrence is on show on `day`: its time range, "All day", or - on a later day of a multi-day event - that it continues. */
|
|
13
17
|
function timeLabel(occurrence, day) {
|
|
14
18
|
if (occurrence.allDay) {
|
|
@@ -21,8 +25,8 @@ function timeLabel(occurrence, day) {
|
|
|
21
25
|
const end = new Date(occurrence.endDate);
|
|
22
26
|
return start.getTime() === end.getTime() ? format(start, "h:mma") : `${format(start, "h:mma")} – ${format(end, "h:mma")}`;
|
|
23
27
|
}
|
|
24
|
-
/** The days (local midnights) `occurrence` is on show on
|
|
25
|
-
function daysOf(occurrence) {
|
|
28
|
+
/** The days (local midnights) `occurrence` is on show on from `today` (a local midnight) on: its first to its last, at most `LIST_MAX_DAYS_PER_EVENT` of them. */
|
|
29
|
+
function daysOf(occurrence, today) {
|
|
26
30
|
let first;
|
|
27
31
|
let last;
|
|
28
32
|
if (occurrence.allDay) {
|
|
@@ -37,8 +41,12 @@ function daysOf(occurrence) {
|
|
|
37
41
|
// An event that ends at midnight does not reach into that day.
|
|
38
42
|
last = startOfDay(end > start ? new Date(end.getTime() - 1) : start);
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
// An event is on its first day at least. The days before today are not listed, so the cap counts from today: an event that began years ago and
|
|
45
|
+
// still runs would otherwise be spent before it reached it.
|
|
46
|
+
const lastDay = last < first ? first : last;
|
|
47
|
+
const from = first < today ? today : first;
|
|
48
|
+
const days = [];
|
|
49
|
+
for (let day = from; day <= lastDay && days.length < LIST_MAX_DAYS_PER_EVENT; day = addDays(day, 1)) {
|
|
42
50
|
days.push(day);
|
|
43
51
|
}
|
|
44
52
|
return days;
|
|
@@ -53,22 +61,22 @@ function sortedForDay(occurrences, day) {
|
|
|
53
61
|
* nothing at all says so). A multi-day event is listed on every day it covers, as the month grid shows it. Each row is a full-width button that
|
|
54
62
|
* opens the event's card, with the time, title, place (or organizer) and its calendar's colour.
|
|
55
63
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
64
|
+
* The days are grouped once per change of `occurrences` (or of the day), not per render, but only the first `LIST_INITIAL_DAYS` of them are rendered:
|
|
65
|
+
* more follow as the end of the list is scrolled to (a daily series alone is thousands of rows). A jump, a step or a search match that lies beyond
|
|
66
|
+
* the rendered days renders up to it first. The list scrolls itself to `focusDate` and, on a `step`, to the neighbouring month.
|
|
58
67
|
*/
|
|
59
|
-
|
|
68
|
+
function ListView({ occurrences, folderColors, onSelectEvent, focusDate, jumpNonce, step }) {
|
|
60
69
|
const activeKey = useActiveOccurrenceKey();
|
|
70
|
+
// Today moves on at midnight, with the list open or not.
|
|
71
|
+
const dayKey = useDayKey();
|
|
61
72
|
const scrollerRef = useRef(null);
|
|
73
|
+
const sentinelRef = useRef(null);
|
|
62
74
|
const groups = useMemo(() => {
|
|
63
75
|
const byDay = new Map();
|
|
64
|
-
const
|
|
76
|
+
const today = parseISO(dayKey);
|
|
65
77
|
for (const occurrence of occurrences) {
|
|
66
|
-
for (const day of daysOf(occurrence)) {
|
|
78
|
+
for (const day of daysOf(occurrence, today)) {
|
|
67
79
|
const key = localDateKey(day);
|
|
68
|
-
// The days an event spent before today are not listed.
|
|
69
|
-
if (key < todayKey) {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
80
|
const group = byDay.get(key) ?? { day, items: [] };
|
|
73
81
|
group.items.push(occurrence);
|
|
74
82
|
byDay.set(key, group);
|
|
@@ -77,14 +85,65 @@ export default function ListView({ occurrences, folderColors, onSelectEvent, foc
|
|
|
77
85
|
return Array.from(byDay.entries())
|
|
78
86
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
79
87
|
.map(([key, { day, items }]) => ({ key, day, items: sortedForDay(items, day) }));
|
|
80
|
-
}, [occurrences]);
|
|
88
|
+
}, [occurrences, dayKey]);
|
|
89
|
+
// How many days are rendered. Where there is no IntersectionObserver to say the end was reached, all of them are.
|
|
90
|
+
const [shown, setShown] = useState(LIST_INITIAL_DAYS);
|
|
91
|
+
const rendered = typeof IntersectionObserver === "undefined" ? groups.length : Math.min(shown, groups.length);
|
|
92
|
+
const more = rendered < groups.length;
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const sentinel = sentinelRef.current;
|
|
95
|
+
if (!sentinel) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Watched again after each page, so a sentinel still in view (a tall window) brings the next one.
|
|
99
|
+
const observer = new IntersectionObserver((entries) => {
|
|
100
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
101
|
+
setShown((count) => count + LIST_DAYS_PER_PAGE);
|
|
102
|
+
}
|
|
103
|
+
}, { root: scrollerRef.current, rootMargin: "600px" });
|
|
104
|
+
observer.observe(sentinel);
|
|
105
|
+
return () => observer.disconnect();
|
|
106
|
+
}, [more, shown]);
|
|
107
|
+
// What waits for the days it scrolls to be rendered: one slot per effect that asks, so one cannot replace another's scroll.
|
|
108
|
+
const afterReveal = useRef({});
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const pending = Object.values(afterReveal.current);
|
|
111
|
+
afterReveal.current = {};
|
|
112
|
+
for (const scroll of pending) {
|
|
113
|
+
scroll();
|
|
114
|
+
}
|
|
115
|
+
}, [shown]);
|
|
116
|
+
/** Runs `scroll` once the day at `index` of `groups` is rendered: now, or after the rendered days have grown to reach it. */
|
|
117
|
+
function reveal(slot, index, scroll) {
|
|
118
|
+
if (index < rendered) {
|
|
119
|
+
scroll();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
afterReveal.current[slot] = scroll;
|
|
123
|
+
setShown((count) => Math.max(count, index + LIST_DAYS_PER_PAGE));
|
|
124
|
+
}
|
|
125
|
+
// Both look the element up when the scroll runs, which can be after the list changed (a live refresh) and the element is gone: then there is nothing to scroll to.
|
|
126
|
+
const scrollToDay = (key) => scrollerRef.current?.querySelector(`[data-day="${key}"]`)?.scrollIntoView({ block: "start" });
|
|
81
127
|
const dayElements = () => Array.from(scrollerRef.current.querySelectorAll("[data-day]"));
|
|
82
128
|
// On opening and on a jump (Today, a date picked in the mini calendar): the first day from `focusDate` that has events, else the last one.
|
|
83
129
|
useEffect(() => {
|
|
130
|
+
if (groups.length === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
84
133
|
const wanted = localDateKey(focusDate);
|
|
85
|
-
const
|
|
86
|
-
|
|
134
|
+
const found = groups.findIndex((group) => group.key >= wanted);
|
|
135
|
+
const index = found >= 0 ? found : groups.length - 1;
|
|
136
|
+
reveal("jump", index, () => scrollToDay(groups[index].key));
|
|
87
137
|
}, [jumpNonce]);
|
|
138
|
+
// A search match beyond the rendered days: they grow to it, and it is scrolled to (the page's own scroll to the match found nothing there).
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const index = groups.findIndex((group) => group.items.some((occurrence) => occurrence.occurrenceKey === activeKey));
|
|
141
|
+
if (index >= rendered) {
|
|
142
|
+
reveal("match", index, () => Array.from(scrollerRef.current?.querySelectorAll("[data-occurrence-key]") ?? [])
|
|
143
|
+
.find((el) => el.dataset.occurrenceKey === activeKey)
|
|
144
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" }));
|
|
145
|
+
}
|
|
146
|
+
}, [activeKey]);
|
|
88
147
|
// A step: from the month of the day at the top of the list to the first day of the previous or next month that has any.
|
|
89
148
|
useEffect(() => {
|
|
90
149
|
const days = dayElements();
|
|
@@ -93,19 +152,25 @@ export default function ListView({ occurrences, folderColors, onSelectEvent, foc
|
|
|
93
152
|
}
|
|
94
153
|
const top = scrollerRef.current.getBoundingClientRect().top;
|
|
95
154
|
const current = days.find((el) => el.getBoundingClientRect().bottom > top) ?? days[days.length - 1];
|
|
96
|
-
const months = Array.from(new Set(
|
|
155
|
+
const months = Array.from(new Set(groups.map((group) => group.key.slice(0, 7))));
|
|
97
156
|
const month = months[months.indexOf(current.dataset.day.slice(0, 7)) + step.direction];
|
|
98
157
|
// Past the first or last month with events there is nowhere to go.
|
|
99
|
-
|
|
158
|
+
const index = groups.findIndex((group) => group.key.startsWith(month));
|
|
159
|
+
if (index >= 0) {
|
|
160
|
+
reveal("step", index, () => scrollToDay(groups[index].key));
|
|
161
|
+
}
|
|
100
162
|
}, [step]);
|
|
101
|
-
const thisYear =
|
|
102
|
-
return (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
163
|
+
const thisYear = parseISO(dayKey).getFullYear();
|
|
164
|
+
return (_jsxs("div", { ref: scrollerRef, role: "region", "aria-label": "List", "data-calendar-scroller": true, className: "flex-1 min-h-0 overflow-y-auto", children: [groups.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No upcoming events" })) : (groups.slice(0, rendered).map(({ key, day, items }) => (_jsxs("section", { "data-day": key, "aria-labelledby": `list-day-${key}`, children: [_jsxs("h3", { id: `list-day-${key}`, "aria-current": isToday(day) ? "date" : undefined, className: [
|
|
165
|
+
"sticky top-0 z-10 px-4 py-2 text-sm font-semibold border-b border-border bg-surface-alt text-text border-l-4",
|
|
166
|
+
isToday(day) ? "border-l-primary" : "border-l-transparent",
|
|
167
|
+
].join(" "), children: [format(day, day.getFullYear() === thisYear ? "EEEE, MMMM d" : "EEEE, MMMM d, yyyy"), isToday(day) && (_jsx("span", { "aria-hidden": "true", className: "ml-2 px-1.5 py-0.5 rounded-full bg-primary/15 text-primary-dark text-xs font-medium", children: "Today" }))] }), _jsx("ul", { children: items.map((occurrence) => (_jsx(ListRow, { occurrence: occurrence, day: day, color: folderColors[occurrence.folderUid], onSelect: onSelectEvent,
|
|
168
|
+
// Only the row that is the match is told: a new match renders two rows, not all of them.
|
|
169
|
+
activeKey: activeKey === occurrence.occurrenceKey ? activeKey : null }, occurrence.occurrenceKey))) })] }, key)))), more && _jsx("div", { ref: sentinelRef, "data-list-end": true, "aria-hidden": "true", className: "h-px" })] }));
|
|
106
170
|
}
|
|
107
|
-
|
|
171
|
+
export default memo(ListView);
|
|
172
|
+
const ListRow = memo(function ListRow({ occurrence, day, color, onSelect, activeKey, }) {
|
|
108
173
|
const marker = occurrenceMarker(activeKey, occurrence);
|
|
109
174
|
const place = occurrence.location || occurrence.organizer.displayName || occurrence.organizer.address;
|
|
110
175
|
return (_jsx("li", { className: "border-b border-border/50", children: _jsxs("button", { type: "button", ...marker.attrs, onClick: () => onSelect(occurrence), className: ["w-full min-h-14 px-4 py-2 flex items-center gap-3 text-left hover:bg-surface-alt", marker.className].join(" "), children: [_jsx("span", { className: "w-28 shrink-0 text-xs text-text-muted", children: timeLabel(occurrence, day) }), _jsx("span", { "aria-hidden": "true", className: ["w-3 h-3 rounded-full shrink-0", occurrence.busyStatus === "free" ? "border-2" : ""].join(" "), style: occurrence.busyStatus === "free" ? { borderColor: color } : { backgroundColor: color } }), _jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block text-sm font-medium truncate", children: occurrence.title }), _jsx("span", { className: "block text-xs text-text-muted truncate", children: place })] })] }) }));
|
|
111
|
-
}
|
|
176
|
+
});
|
|
@@ -10,6 +10,9 @@ export interface RecurrenceEditorProps {
|
|
|
10
10
|
/** The weekday the event starts on (in the frame its series expands in - see `allDay.ts`'s
|
|
11
11
|
* `startWeekdayCode()`): the day a new weekly rule, or one switched to Weekly, repeats on. Defaults to Monday. */
|
|
12
12
|
startWeekday?: WeekdayCode;
|
|
13
|
+
/** The event's start date (`yyyy-MM-dd`): "Ends on" starts at today, or at this date when the event starts later - a series that ends before it
|
|
14
|
+
* starts has no occurrences at all. */
|
|
15
|
+
startDateKey?: string;
|
|
13
16
|
/** Leaves out the "Repeats" checkbox, for a caller that already turns repeating on and off itself (the event form's
|
|
14
17
|
* "Does not repeat" menu): what remains is the rule's details, shown only while there is a rule. */
|
|
15
18
|
hideToggle?: boolean;
|
|
@@ -20,4 +23,4 @@ export interface RecurrenceEditorProps {
|
|
|
20
23
|
* live "every ... until/for ..." summary comes from `describeRecurrence()` (built on `rrule`'s own
|
|
21
24
|
* `.toText()`), so it never drifts out of sync with what will actually be submitted.
|
|
22
25
|
*/
|
|
23
|
-
export default function RecurrenceEditor({ value, onChange, allDay, startWeekday, hideToggle }: RecurrenceEditorProps): React.JSX.Element;
|
|
26
|
+
export default function RecurrenceEditor({ value, onChange, allDay, startWeekday, startDateKey, hideToggle }: RecurrenceEditorProps): React.JSX.Element;
|
|
@@ -10,6 +10,9 @@ const FREQ_LABEL = {
|
|
|
10
10
|
monthly: { unit: "month", unitPlural: "months" },
|
|
11
11
|
yearly: { unit: "year", unitPlural: "years" },
|
|
12
12
|
};
|
|
13
|
+
/** The most an interval ("every N days") or a count ("after N occurrences") can be set to. */
|
|
14
|
+
const MAX_INTERVAL = 999;
|
|
15
|
+
const MAX_COUNT = 1000;
|
|
13
16
|
function endConditionOf(rule) {
|
|
14
17
|
if (rule.count)
|
|
15
18
|
return "count";
|
|
@@ -23,7 +26,7 @@ function endConditionOf(rule) {
|
|
|
23
26
|
* live "every ... until/for ..." summary comes from `describeRecurrence()` (built on `rrule`'s own
|
|
24
27
|
* `.toText()`), so it never drifts out of sync with what will actually be submitted.
|
|
25
28
|
*/
|
|
26
|
-
export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO", hideToggle = false }) {
|
|
29
|
+
export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO", startDateKey, hideToggle = false }) {
|
|
27
30
|
function handleEnable(enabled) {
|
|
28
31
|
onChange(enabled ? { freq: "weekly", interval: 1, byDay: [startWeekday], exceptions: [] } : null);
|
|
29
32
|
}
|
|
@@ -55,24 +58,26 @@ export default function RecurrenceEditor({ value, onChange, allDay = false, star
|
|
|
55
58
|
update({ count: 10, until: undefined });
|
|
56
59
|
}
|
|
57
60
|
else {
|
|
58
|
-
|
|
61
|
+
const today = format(new Date(), "yyyy-MM-dd");
|
|
62
|
+
update({ until: recurrenceUntilInstant(startDateKey && startDateKey > today ? startDateKey : today, allDay), count: undefined });
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
|
-
/** A
|
|
62
|
-
* to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN.
|
|
63
|
-
|
|
65
|
+
/** A whole number from 1 to `max` from a number input, or `null` while it's empty (a user clearing the field
|
|
66
|
+
* to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN. A huge one (an event
|
|
67
|
+
* every day, a billion times) would make every view expand it forever. */
|
|
68
|
+
function parsePositive(raw, max) {
|
|
64
69
|
if (raw.trim() === "")
|
|
65
70
|
return null;
|
|
66
71
|
const n = Math.floor(Number(raw));
|
|
67
|
-
return n >= 1 ? n : 1;
|
|
72
|
+
return n >= 1 ? Math.min(n, max) : 1;
|
|
68
73
|
}
|
|
69
74
|
function handleInterval(raw) {
|
|
70
|
-
const interval = parsePositive(raw);
|
|
75
|
+
const interval = parsePositive(raw, MAX_INTERVAL);
|
|
71
76
|
if (interval !== null)
|
|
72
77
|
update({ interval });
|
|
73
78
|
}
|
|
74
79
|
function handleCount(raw) {
|
|
75
|
-
const count = parsePositive(raw);
|
|
80
|
+
const count = parsePositive(raw, MAX_COUNT);
|
|
76
81
|
if (count !== null)
|
|
77
82
|
update({ count });
|
|
78
83
|
}
|
|
@@ -81,10 +86,10 @@ export default function RecurrenceEditor({ value, onChange, allDay = false, star
|
|
|
81
86
|
if (/^\d{4}-\d{2}-\d{2}$/.test(raw))
|
|
82
87
|
update({ until: recurrenceUntilInstant(raw, allDay) });
|
|
83
88
|
}
|
|
84
|
-
return (_jsxs("div", { className: "flex flex-col gap-3", children: [!hideToggle && (_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: value !== null, onChange: (e) => handleEnable(e.target.checked) }), "Repeats"] })), value && (_jsxs("div", { className: "flex flex-col gap-3 pl-6 border-l-2 border-border", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { children: "Every" }), _jsx("input", { type: "number", min: 1, className: `${INPUT_CLASS} w-16`, value: value.interval, onChange: (e) => handleInterval(e.target.value), "aria-label": "Recurrence interval" }), _jsxs("select", { className: SELECT_CLASS, value: value.freq, onChange: (e) => handleFrequency(e.target.value), "aria-label": "Recurrence frequency", children: [_jsx("option", { value: "daily", children: value.interval === 1 ? FREQ_LABEL.daily.unit : FREQ_LABEL.daily.unitPlural }), _jsx("option", { value: "weekly", children: value.interval === 1 ? FREQ_LABEL.weekly.unit : FREQ_LABEL.weekly.unitPlural }), _jsx("option", { value: "monthly", children: value.interval === 1 ? FREQ_LABEL.monthly.unit : FREQ_LABEL.monthly.unitPlural }), _jsx("option", { value: "yearly", children: value.interval === 1 ? FREQ_LABEL.yearly.unit : FREQ_LABEL.yearly.unitPlural })] })] }), value.freq === "weekly" && (_jsx("div", { className: "flex gap-1", children: WEEKDAY_CODES.map((day) => (_jsx("button", { type: "button", onClick: () => toggleDay(day), "aria-pressed": (value.byDay ?? []).includes(day), "aria-label": WEEKDAY_LABELS[day], className: [
|
|
89
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [!hideToggle && (_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: value !== null, onChange: (e) => handleEnable(e.target.checked) }), "Repeats"] })), value && (_jsxs("div", { className: "flex flex-col gap-3 pl-6 border-l-2 border-border", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { children: "Every" }), _jsx("input", { type: "number", min: 1, max: MAX_INTERVAL, className: `${INPUT_CLASS} w-16`, value: value.interval, onChange: (e) => handleInterval(e.target.value), "aria-label": "Recurrence interval" }), _jsxs("select", { className: SELECT_CLASS, value: value.freq, onChange: (e) => handleFrequency(e.target.value), "aria-label": "Recurrence frequency", children: [_jsx("option", { value: "daily", children: value.interval === 1 ? FREQ_LABEL.daily.unit : FREQ_LABEL.daily.unitPlural }), _jsx("option", { value: "weekly", children: value.interval === 1 ? FREQ_LABEL.weekly.unit : FREQ_LABEL.weekly.unitPlural }), _jsx("option", { value: "monthly", children: value.interval === 1 ? FREQ_LABEL.monthly.unit : FREQ_LABEL.monthly.unitPlural }), _jsx("option", { value: "yearly", children: value.interval === 1 ? FREQ_LABEL.yearly.unit : FREQ_LABEL.yearly.unitPlural })] })] }), value.freq === "weekly" && (_jsx("div", { className: "flex gap-1", children: WEEKDAY_CODES.map((day) => (_jsx("button", { type: "button", onClick: () => toggleDay(day), "aria-pressed": (value.byDay ?? []).includes(day), "aria-label": WEEKDAY_LABELS[day], className: [
|
|
85
90
|
"w-9 h-9 rounded-full text-xs font-semibold border",
|
|
86
91
|
(value.byDay ?? []).includes(day)
|
|
87
92
|
? "bg-primary text-white border-primary"
|
|
88
93
|
: "bg-surface text-text-muted border-border hover:border-primary",
|
|
89
|
-
].join(" "), children: _jsx("span", { "aria-hidden": "true", children: WEEKDAY_LABELS[day][0] }) }, day))) })), _jsxs("fieldset", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("legend", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1", children: "Ends" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", checked: endConditionOf(value) === "never", onChange: () => handleEndCondition("never") }), "Never"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "After", checked: endConditionOf(value) === "count", onChange: () => handleEndCondition("count") }), "After", _jsx("input", { type: "number", min: 1, className: `${INPUT_CLASS} w-16`, value: value.count ?? 10, disabled: endConditionOf(value) !== "count", onChange: (e) => handleCount(e.target.value), "aria-label": "Number of occurrences" }), "occurrences"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "On", checked: endConditionOf(value) === "until", onChange: () => handleEndCondition("until") }), "On", _jsx("input", { type: "date", className: INPUT_CLASS, value: value.until ? recurrenceUntilDateKey(value.until, allDay) : "", disabled: endConditionOf(value) !== "until", onChange: (e) => handleUntil(e.target.value), "aria-label": "End date" })] })] }), (value.freq !== "weekly" || (value.byDay ?? []).length > 0) && (_jsxs("p", { className: "text-xs text-text-muted", children: ["Repeats ", describeRecurrence(value), "."] }))] }))] }));
|
|
94
|
+
].join(" "), children: _jsx("span", { "aria-hidden": "true", children: WEEKDAY_LABELS[day][0] }) }, day))) })), _jsxs("fieldset", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("legend", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1", children: "Ends" }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", checked: endConditionOf(value) === "never", onChange: () => handleEndCondition("never") }), "Never"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "After", checked: endConditionOf(value) === "count", onChange: () => handleEndCondition("count") }), "After", _jsx("input", { type: "number", min: 1, max: MAX_COUNT, className: `${INPUT_CLASS} w-16`, value: value.count ?? 10, disabled: endConditionOf(value) !== "count", onChange: (e) => handleCount(e.target.value), "aria-label": "Number of occurrences" }), "occurrences"] }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "radio", name: "recurrence-end", "aria-label": "On", checked: endConditionOf(value) === "until", onChange: () => handleEndCondition("until") }), "On", _jsx("input", { type: "date", className: INPUT_CLASS, value: value.until ? recurrenceUntilDateKey(value.until, allDay) : "", disabled: endConditionOf(value) !== "until", onChange: (e) => handleUntil(e.target.value), "aria-label": "End date" })] })] }), (value.freq !== "weekly" || (value.byDay ?? []).length > 0) && (_jsxs("p", { className: "text-xs text-text-muted", children: ["Repeats ", describeRecurrence(value), "."] }))] }))] }));
|
|
90
95
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { addMinutes, format, startOfDay } from "date-fns";
|
|
3
3
|
import { occursOnDay } from "./allDay.js";
|
|
4
4
|
import { occurrenceMarker, useActiveOccurrenceKey } from "./activeOccurrence.js";
|
|
5
5
|
import { anchorOf } from "./EventShell.js";
|
|
6
|
-
|
|
7
|
-
const SLOT_MINUTES = 30;
|
|
8
|
-
const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
6
|
+
import { HOUR_HEIGHT_PX, SLOTS_PER_DAY, SLOT_MINUTES, blockSpan, laneStyle, layoutLanes, slotStart } from "./gridLayout.js";
|
|
9
7
|
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
10
8
|
/**
|
|
11
9
|
* Outlook's "Split" view: one column per checked calendar, all showing the same single day, so
|
|
@@ -24,27 +22,23 @@ export default function SplitDayView({ day, columns, occurrences, onSelectEvent,
|
|
|
24
22
|
}
|
|
25
23
|
function SplitColumn({ column, dayStart, occurrences, onSelectEvent, onSelectSlot, }) {
|
|
26
24
|
const activeKey = useActiveOccurrenceKey();
|
|
25
|
+
// Clipped to this day, so a multi-day event fills only the part of the day it covers. An all-day event is date-only (see `allDay.ts`), so it
|
|
26
|
+
// always spans the whole local day. Events that overlap in time share the column's width instead of covering one another.
|
|
27
|
+
const spans = occurrences.map((occurrence) => blockSpan(new Date(occurrence.startDate).getTime(), new Date(occurrence.endDate).getTime(), dayStart, occurrence.allDay));
|
|
28
|
+
const lanes = layoutLanes(spans);
|
|
27
29
|
return (_jsxs("div", { className: "flex-1 min-w-0 relative border-l border-border", children: [Array.from({ length: SLOTS_PER_DAY }, (_, i) => {
|
|
28
|
-
const
|
|
29
|
-
return (_jsx("button", { type: "button", onClick: (e) => onSelectSlot(
|
|
30
|
-
}), occurrences.map((occurrence) => {
|
|
30
|
+
const start = slotStart(dayStart, i);
|
|
31
|
+
return (_jsx("button", { type: "button", onClick: (e) => onSelectSlot(start, addMinutes(start, SLOT_MINUTES), column.folderUid, anchorOf(e.currentTarget)), style: { height: HOUR_HEIGHT_PX / 2 }, className: "block w-full border-b border-border/50 text-left", "aria-label": `New event at ${format(start, "h:mm a")} in ${column.name}` }, i));
|
|
32
|
+
}), occurrences.map((occurrence, index) => {
|
|
31
33
|
const start = new Date(occurrence.startDate);
|
|
32
|
-
const
|
|
33
|
-
// Clipped to this day, so a multi-day event fills only the part of the day it covers. An
|
|
34
|
-
// all-day event is date-only (see `allDay.ts`), so it always spans the whole local day.
|
|
35
|
-
const dayEnd = addDays(dayStart, 1).getTime();
|
|
36
|
-
const visibleStart = occurrence.allDay ? dayStart.getTime() : Math.max(start.getTime(), dayStart.getTime());
|
|
37
|
-
const visibleEnd = occurrence.allDay ? dayEnd : Math.min(end.getTime(), dayEnd);
|
|
38
|
-
const top = ((visibleStart - dayStart.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX;
|
|
39
|
-
const height = Math.max(((visibleEnd - visibleStart) / 60000 / 60) * HOUR_HEIGHT_PX, 16);
|
|
34
|
+
const { top, height } = spans[index];
|
|
40
35
|
const isFree = occurrence.busyStatus === "free";
|
|
41
36
|
const marker = occurrenceMarker(activeKey, occurrence);
|
|
42
37
|
return (_jsxs("div", { ...marker.attrs, onClick: () => onSelectEvent(occurrence), style: {
|
|
43
38
|
position: "absolute",
|
|
44
39
|
top,
|
|
45
40
|
height,
|
|
46
|
-
|
|
47
|
-
right: 2,
|
|
41
|
+
...laneStyle(lanes[index]),
|
|
48
42
|
...(isFree ? undefined : { backgroundColor: column.color, color: "#fff" }),
|
|
49
43
|
}, className: [
|
|
50
44
|
"rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer",
|
|
@@ -12,6 +12,7 @@ import { createTask, listTaskLists } from "../../../../lib/tasks/tasksApi.js";
|
|
|
12
12
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
13
13
|
import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
|
|
14
14
|
import { notify } from "../../notifications/store.js";
|
|
15
|
+
import { dueDateInstant } from "../tasks/dueDate.js";
|
|
15
16
|
import { INPUT_CLASS, IconRow, SELECT_CLASS } from "./EventFormParts.js";
|
|
16
17
|
import QuickFaceFrame from "./QuickFaceFrame.js";
|
|
17
18
|
/** A new task's starting values: due on the day of the slot or day that was clicked, all day, with the slot's time ready if a time is wanted. */
|
|
@@ -82,8 +83,8 @@ export default function TaskQuickForm({ c, draft, onDraftChange, tabs, expanded,
|
|
|
82
83
|
folderUid,
|
|
83
84
|
title,
|
|
84
85
|
body: draft.body.trim() || undefined,
|
|
85
|
-
// A date-only due date is
|
|
86
|
-
dueDate: draft.dueDate ?
|
|
86
|
+
// A date-only due date is that day's UTC midnight, the way the Tasks app writes one.
|
|
87
|
+
dueDate: draft.dueDate ? (draft.allDay ? dueDateInstant(draft.dueDate) : parseISO(`${draft.dueDate}T${draft.dueTime}`).toISOString()) : undefined,
|
|
87
88
|
priority: draft.priority,
|
|
88
89
|
reminderDate: draft.reminder ? parseISO(draft.reminder).toISOString() : undefined,
|
|
89
90
|
taskListUid: listUid || undefined,
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useDraggable, useDroppable } from "@dnd-kit/core";
|
|
3
|
-
import {
|
|
3
|
+
import { addMinutes, format, isToday, startOfDay } from "date-fns";
|
|
4
4
|
import { eventDragId, resizeDragId, slotDropId } from "../../../../lib/calendar/calendarDragIds.js";
|
|
5
5
|
import { occursOnDay, startsOnDay } from "./allDay.js";
|
|
6
6
|
import { occurrenceMarker, useActiveOccurrenceKey } from "./activeOccurrence.js";
|
|
7
7
|
import { anchorOf } from "./EventShell.js";
|
|
8
|
-
|
|
9
|
-
const SLOT_MINUTES = 30;
|
|
10
|
-
const SLOTS_PER_DAY = (24 * 60) / SLOT_MINUTES;
|
|
8
|
+
import { HOUR_HEIGHT_PX, SLOTS_PER_DAY, SLOT_MINUTES, blockSpan, laneStyle, layoutLanes, slotStart } from "./gridLayout.js";
|
|
11
9
|
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
12
10
|
/** An hourly time grid (Week, Work Week, or Day view — they share all their rendering/drag logic and
|
|
13
11
|
* differ only in how many day columns are shown), with a day-header row and all-day events in a
|
|
@@ -25,34 +23,37 @@ export default function TimeGridView({ days, occurrences, folderColors, onSelect
|
|
|
25
23
|
}
|
|
26
24
|
function DayColumn({ day, occurrences, folderColors, onSelectEvent, onSelectSlot }) {
|
|
27
25
|
const dayStart = startOfDay(day);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}), occurrences.map((occurrence) => startsOnDay(occurrence, day) ? (_jsx(EventBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, onSelect: onSelectEvent }, occurrence.occurrenceKey)) : (_jsx(ContinuationBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, onSelect: onSelectEvent }, occurrence.occurrenceKey)))] }));
|
|
26
|
+
// Events that overlap in time share the column's width instead of covering one another.
|
|
27
|
+
const lanes = layoutLanes(occurrences.map((occurrence) => blockSpan(new Date(occurrence.startDate).getTime(), new Date(occurrence.endDate).getTime(), dayStart)));
|
|
28
|
+
return (_jsxs("div", { className: ["flex-1 min-w-0 relative border-l border-border", isToday(day) ? "bg-primary/5" : ""].join(" "), children: [Array.from({ length: SLOTS_PER_DAY }, (_, i) => (_jsx(TimeSlot, { start: slotStart(dayStart, i), onSelectSlot: onSelectSlot }, i))), occurrences.map((occurrence, index) => startsOnDay(occurrence, day) ? (_jsx(EventBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, lane: lanes[index], onSelect: onSelectEvent }, occurrence.occurrenceKey)) : (_jsx(ContinuationBlock, { occurrence: occurrence, color: folderColors[occurrence.folderUid], dayStart: dayStart, lane: lanes[index], onSelect: onSelectEvent }, occurrence.occurrenceKey)))] }));
|
|
32
29
|
}
|
|
33
30
|
function TimeSlot({ start, onSelectSlot }) {
|
|
34
31
|
const { setNodeRef, isOver } = useDroppable({ id: slotDropId(start) });
|
|
35
|
-
return (_jsx("button", { ref: setNodeRef, type: "button",
|
|
32
|
+
return (_jsx("button", { ref: setNodeRef, type: "button",
|
|
33
|
+
// 48 of these a day would be hundreds of tab stops in a week: the New event shortcut is the keyboard's way in.
|
|
34
|
+
tabIndex: -1, onClick: (e) => onSelectSlot(start, addMinutes(start, SLOT_MINUTES), anchorOf(e.currentTarget)), style: { height: HOUR_HEIGHT_PX / 2 }, className: ["block w-full border-b border-border/50 text-left", isOver ? "bg-primary/10" : ""].join(" "), "aria-label": `New event at ${format(start, "h:mm a, MMM d")}` }));
|
|
35
|
+
}
|
|
36
|
+
/** Enter or Space on a focused event block opens the event, as a click does (the blocks are not `<button>`s: they hold a nested resize handle). */
|
|
37
|
+
function openOnKey(event, open) {
|
|
38
|
+
if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
open();
|
|
41
|
+
}
|
|
36
42
|
}
|
|
37
43
|
/** A block's position within its day column. A multi-day event is drawn in every day column it
|
|
38
44
|
* overlaps, clipped to that day. */
|
|
39
45
|
function blockGeometry(occurrence, dayStart) {
|
|
40
|
-
|
|
41
|
-
const visibleEnd = Math.min(new Date(occurrence.endDate).getTime(), addDays(dayStart, 1).getTime());
|
|
42
|
-
return {
|
|
43
|
-
top: ((visibleStart - dayStart.getTime()) / 60000 / 60) * HOUR_HEIGHT_PX,
|
|
44
|
-
height: Math.max(((visibleEnd - visibleStart) / 60000 / 60) * HOUR_HEIGHT_PX, 16),
|
|
45
|
-
};
|
|
46
|
+
return blockSpan(new Date(occurrence.startDate).getTime(), new Date(occurrence.endDate).getTime(), dayStart);
|
|
46
47
|
}
|
|
47
48
|
/** A multi-day event's block in a day column after its first: clickable, but not draggable/resizable
|
|
48
49
|
* (its drag ids are already taken by the start day's block). */
|
|
49
|
-
function ContinuationBlock({ occurrence, color, dayStart, onSelect, }) {
|
|
50
|
+
function ContinuationBlock({ occurrence, color, dayStart, lane, onSelect, }) {
|
|
50
51
|
const isFree = occurrence.busyStatus === "free";
|
|
51
52
|
const { top, height } = blockGeometry(occurrence, dayStart);
|
|
52
53
|
const marker = occurrenceMarker(useActiveOccurrenceKey(), occurrence);
|
|
53
|
-
return (_jsx("div", { ...marker.attrs, onClick: () => onSelect(occurrence), style: { position: "absolute", top, height,
|
|
54
|
+
return (_jsx("div", { ...marker.attrs, role: "button", tabIndex: 0, onClick: () => onSelect(occurrence), onKeyDown: (e) => openOnKey(e, () => onSelect(occurrence)), style: { position: "absolute", top, height, ...laneStyle(lane), ...(isFree ? undefined : { backgroundColor: color, color: "#fff" }) }, className: ["rounded-sm px-1.5 py-0.5 text-xs text-left overflow-hidden cursor-pointer", isFree ? "bg-surface-alt text-text-muted" : "", marker.className].join(" "), children: _jsx("div", { className: "font-medium truncate", children: occurrence.title }) }));
|
|
54
55
|
}
|
|
55
|
-
function EventBlock({ occurrence, color, dayStart, onSelect, }) {
|
|
56
|
+
function EventBlock({ occurrence, color, dayStart, lane, onSelect, }) {
|
|
56
57
|
const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ id: eventDragId(occurrence) });
|
|
57
58
|
const { setNodeRef: setResizeRef, listeners: resizeListeners, attributes: resizeAttributes } = useDraggable({
|
|
58
59
|
id: resizeDragId(occurrence),
|
|
@@ -69,8 +70,7 @@ function EventBlock({ occurrence, color, dayStart, onSelect, }) {
|
|
|
69
70
|
position: "absolute",
|
|
70
71
|
top,
|
|
71
72
|
height,
|
|
72
|
-
|
|
73
|
-
right: 2,
|
|
73
|
+
...laneStyle(lane),
|
|
74
74
|
transform: transform ? `translate(${transform.x}px, ${transform.y}px)` : undefined,
|
|
75
75
|
zIndex: isDragging ? 10 : undefined,
|
|
76
76
|
...(isFree ? undefined : { backgroundColor: color, color: "#fff" }),
|
|
@@ -79,5 +79,7 @@ function EventBlock({ occurrence, color, dayStart, onSelect, }) {
|
|
|
79
79
|
isFree ? "bg-surface-alt text-text-muted" : "",
|
|
80
80
|
isDragging ? "opacity-50" : "",
|
|
81
81
|
marker.className,
|
|
82
|
-
].join(" "), ...listeners, ...attributes, children: [_jsx("div", { className: "font-medium truncate", children: occurrence.title }), _jsx("div", { className: "truncate", children: format(start, "h:mma") }), _jsx("div", { ref: setResizeRef, className: "absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize", "aria-label": `Resize "${occurrence.title}"`, ...resizeListeners, ...resizeAttributes
|
|
82
|
+
].join(" "), ...listeners, ...attributes, onKeyDown: (e) => openOnKey(e, () => onSelect(occurrence)), children: [_jsx("div", { className: "font-medium truncate", children: occurrence.title }), _jsx("div", { className: "truncate", children: format(start, "h:mma") }), _jsx("div", { ref: setResizeRef, className: "absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize", "aria-label": `Resize "${occurrence.title}"`, ...resizeListeners, ...resizeAttributes,
|
|
83
|
+
// Dragging is the only way to resize: a tab stop here would do nothing.
|
|
84
|
+
tabIndex: -1 })] }));
|
|
83
85
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Where things sit in the hour grids of the Week, Work Week, Day and Split views: 24 rows of one clock hour each. */
|
|
2
|
+
export declare const HOUR_HEIGHT_PX = 48;
|
|
3
|
+
export declare const SLOT_MINUTES = 30;
|
|
4
|
+
export declare const SLOTS_PER_DAY: number;
|
|
5
|
+
/** The least height a block is drawn with, so a very short event can still be seen and clicked. */
|
|
6
|
+
export declare const MIN_BLOCK_HEIGHT_PX = 16;
|
|
7
|
+
/**
|
|
8
|
+
* The start of the `index`th half-hour row of the day beginning at `dayStart`, by the wall clock - the rows are labelled with clock hours, so on the
|
|
9
|
+
* day a daylight-saving change makes 23 or 25 hours long the row labelled 10 AM must still be 10:00, not ten hours after midnight. (A row whose
|
|
10
|
+
* clock time does not exist that day is the next one that does.)
|
|
11
|
+
*/
|
|
12
|
+
export declare function slotStart(dayStart: Date, index: number): Date;
|
|
13
|
+
/**
|
|
14
|
+
* Where a block for an event from `startMs` to `endMs` goes in the column of the day beginning at `dayStart`: a multi-day event is clipped to the day,
|
|
15
|
+
* and (`wholeDay`, for an all-day event) fills it. Positioned by wall-clock minute, like the rows, so a block lines up with its hour on a 23 or 25
|
|
16
|
+
* hour day too.
|
|
17
|
+
*/
|
|
18
|
+
export declare function blockSpan(startMs: number, endMs: number, dayStart: Date, wholeDay?: boolean): {
|
|
19
|
+
top: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
/** One block's share of the width of its column: the lane it sits in, out of how many its overlapping neighbours need. */
|
|
23
|
+
export interface Lane {
|
|
24
|
+
lane: number;
|
|
25
|
+
lanes: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Splits a column's width between blocks that overlap in time, as Outlook and Google Calendar do, so none is hidden behind another: blocks that
|
|
29
|
+
* overlap (directly or through a chain of others) form a group, and within it each takes the first lane that is free when it starts. Every block of a
|
|
30
|
+
* group gets the group's lane count; a block that overlaps nothing is alone in one lane (the whole width). Results are in the order of `spans`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function layoutLanes(spans: {
|
|
33
|
+
top: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}[]): Lane[];
|
|
36
|
+
/** The horizontal placement of a block in its column: the whole width less a 2px margin, or its lane's share of it. */
|
|
37
|
+
export declare function laneStyle({ lane, lanes }: Lane): {
|
|
38
|
+
left: string | number;
|
|
39
|
+
width?: string;
|
|
40
|
+
right?: number;
|
|
41
|
+
};
|