@rapidmx/web-client 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/audit-log/index.tsx +3 -3
- package/apps/admin/branding/index.tsx +3 -3
- package/apps/admin/data-requests/index.tsx +18 -7
- package/apps/admin/distribution-lists/[uid].tsx +3 -3
- package/apps/admin/distribution-lists/index.tsx +3 -3
- package/apps/admin/distribution-lists/new/index.tsx +3 -3
- package/apps/admin/domains/[uid].tsx +6 -5
- package/apps/admin/domains/index.tsx +3 -3
- package/apps/admin/domains/new/index.tsx +3 -3
- package/apps/admin/escrow-scopes/[uid].tsx +11 -5
- package/apps/admin/escrow-scopes/index.tsx +3 -3
- package/apps/admin/escrow-scopes/new/index.tsx +9 -3
- package/apps/admin/index.tsx +4 -4
- package/apps/admin/ingest-queue/index.tsx +3 -3
- package/apps/admin/mailboxes/[uid].tsx +15 -7
- package/apps/admin/quarantine/index.tsx +4 -4
- package/apps/admin/retention-policy/index.tsx +3 -3
- package/apps/admin/signing-certificates/index.tsx +3 -3
- package/apps/admin/transport-rules/[uid].tsx +4 -4
- package/apps/admin/transport-rules/index.tsx +3 -3
- package/apps/admin/transport-rules/new/index.tsx +3 -3
- package/apps/escrow/matters/[uid].tsx +26 -2
- package/apps/escrow/matters/new/index.tsx +4 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
- package/apps/shared/calendar/useCalendarReminders.ts +2 -0
- package/apps/shared/components/admin/ActionAlert.tsx +32 -0
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +3 -3
- package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +2 -2
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
- package/apps/shared/components/admin/elevation.ts +19 -0
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
- package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
- package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
- package/apps/shared/components/admin/settings/PluginsManager.tsx +195 -46
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
- package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
- package/apps/shared/components/admin/signOut.ts +3 -0
- package/apps/shared/components/admin/usePagedList.tsx +2 -2
- package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
- package/apps/shared/components/calendar/EventDetails.tsx +34 -8
- package/apps/shared/components/calendar/EventEditor.tsx +90 -40
- package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
- package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
- package/apps/shared/components/calendar/EventModal.tsx +25 -1
- package/apps/shared/components/calendar/FindATime.tsx +10 -2
- package/apps/shared/components/calendar/ListView.tsx +99 -25
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
- package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
- package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
- package/apps/shared/components/calendar/gridLayout.ts +94 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
- package/apps/shared/components/calendar/timePicker.ts +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +15 -4
- package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
- package/apps/shared/components/contacts/ContactForm.tsx +61 -17
- package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
- package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
- package/apps/shared/components/contacts/participantDetails.ts +18 -1
- package/apps/shared/components/layout/AppShell.tsx +9 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
- package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
- package/apps/shared/components/mail/ConversationList.tsx +49 -6
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
- package/apps/shared/components/mail/MenuButton.tsx +3 -400
- package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
- package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
- package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
- package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
- package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
- package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
- package/apps/shared/components/mail/writableMailboxes.ts +2 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
- package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
- package/apps/shared/components/tasks/dueDate.ts +29 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
- package/apps/shared/mail/decryptedMessages.ts +51 -2
- package/apps/shared/mail/listSnapshots.ts +2 -0
- package/apps/shared/mail/outbox/sendDecision.ts +14 -3
- package/apps/shared/mail/outbox/sendJob.ts +5 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
- package/apps/shared/mail/usePermanentDelete.tsx +3 -1
- package/apps/shared/notifications/apiErrors.ts +2 -0
- package/apps/shared/notifications/preferences.ts +1 -1
- package/apps/shared/notifications/store.ts +3 -1
- package/apps/shared/plugins/pluginNav.ts +4 -2
- package/apps/www/calendar/index.tsx +24 -12
- package/apps/www/contacts/[uid].tsx +6 -1
- package/apps/www/contacts/index.tsx +66 -12
- package/apps/www/index.tsx +91 -28
- package/apps/www/messages/[uid].tsx +8 -3
- package/apps/www/settings/auto-reply/index.tsx +13 -0
- package/apps/www/settings/blocked-senders/index.tsx +4 -0
- package/apps/www/settings/encryption/index.tsx +66 -4
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/profile/index.tsx +12 -1
- package/apps/www/settings/sharing/index.tsx +11 -1
- package/apps/www/settings/signatures/[uid].tsx +6 -4
- package/apps/www/tasks/index.tsx +97 -29
- package/dist/apps/admin/audit-log/index.js +3 -3
- package/dist/apps/admin/branding/index.js +3 -3
- package/dist/apps/admin/data-requests/index.js +17 -7
- package/dist/apps/admin/distribution-lists/[uid].js +3 -3
- package/dist/apps/admin/distribution-lists/index.js +3 -3
- package/dist/apps/admin/distribution-lists/new/index.js +3 -3
- package/dist/apps/admin/domains/[uid].js +6 -5
- package/dist/apps/admin/domains/index.js +3 -3
- package/dist/apps/admin/domains/new/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
- package/dist/apps/admin/escrow-scopes/index.js +3 -3
- package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
- package/dist/apps/admin/index.js +4 -4
- package/dist/apps/admin/ingest-queue/index.js +3 -3
- package/dist/apps/admin/mailboxes/[uid].js +15 -7
- package/dist/apps/admin/quarantine/index.js +4 -4
- package/dist/apps/admin/retention-policy/index.js +3 -3
- package/dist/apps/admin/signing-certificates/index.js +3 -3
- package/dist/apps/admin/transport-rules/[uid].js +4 -4
- package/dist/apps/admin/transport-rules/index.js +3 -3
- package/dist/apps/admin/transport-rules/new/index.js +3 -3
- package/dist/apps/escrow/matters/[uid].js +8 -3
- package/dist/apps/escrow/matters/new/index.js +4 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
- package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
- package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
- package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +2 -2
- package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +2 -2
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
- package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
- package/dist/apps/shared/components/admin/elevation.js +16 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +103 -18
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
- package/dist/apps/shared/components/admin/signOut.js +3 -0
- package/dist/apps/shared/components/admin/usePagedList.js +2 -2
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
- package/dist/apps/shared/components/calendar/EventModal.js +10 -2
- package/dist/apps/shared/components/calendar/FindATime.js +4 -3
- package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
- package/dist/apps/shared/components/calendar/ListView.js +92 -27
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
- package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
- package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
- package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/calendar/timePicker.js +5 -2
- package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
- package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
- package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
- package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
- package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
- package/dist/apps/shared/components/layout/AppShell.js +9 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
- package/dist/apps/shared/components/mail/ConversationList.js +49 -6
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
- package/dist/apps/shared/components/mail/MenuButton.js +2 -211
- package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
- package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
- package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
- package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
- package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
- package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
- package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
- package/dist/apps/shared/components/tasks/dueDate.js +26 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
- package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
- package/dist/apps/shared/mail/decryptedMessages.js +46 -2
- package/dist/apps/shared/mail/listSnapshots.js +2 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
- package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
- package/dist/apps/shared/notifications/apiErrors.js +2 -0
- package/dist/apps/shared/notifications/preferences.d.ts +1 -1
- package/dist/apps/shared/notifications/preferences.js +1 -1
- package/dist/apps/shared/notifications/store.js +3 -1
- package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
- package/dist/apps/shared/plugins/pluginNav.js +4 -2
- package/dist/apps/www/calendar/index.js +24 -12
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.js +62 -11
- package/dist/apps/www/index.js +85 -30
- package/dist/apps/www/messages/[uid].js +8 -3
- package/dist/apps/www/settings/auto-reply/index.js +13 -1
- package/dist/apps/www/settings/blocked-senders/index.js +4 -0
- package/dist/apps/www/settings/encryption/index.js +29 -5
- package/dist/apps/www/settings/privacy/index.js +3 -2
- package/dist/apps/www/settings/profile/index.js +4 -1
- package/dist/apps/www/settings/sharing/index.js +10 -1
- package/dist/apps/www/settings/signatures/[uid].js +6 -4
- package/dist/apps/www/tasks/index.js +94 -32
- package/dist/lib/admin/auditLogApi.js +2 -2
- package/dist/lib/admin/distributionListsApi.js +2 -2
- package/dist/lib/admin/domainsApi.js +2 -2
- package/dist/lib/admin/escrowAuditLogApi.js +2 -2
- package/dist/lib/admin/escrowScopesApi.js +2 -2
- package/dist/lib/admin/pluginsApi.d.ts +31 -2
- package/dist/lib/admin/pluginsApi.js +23 -1
- package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
- package/dist/lib/admin/retentionPolicyApi.js +3 -0
- package/dist/lib/admin/transportRulesApi.js +2 -2
- package/dist/lib/appearance/preferencesApi.d.ts +1 -1
- package/dist/lib/appearance/preferencesApi.js +3 -14
- package/dist/lib/auth/session.d.ts +51 -12
- package/dist/lib/auth/session.js +214 -38
- package/dist/lib/branding/brandingApi.d.ts +3 -3
- package/dist/lib/branding/brandingApi.js +9 -21
- package/dist/lib/calendar/calendarApi.js +3 -1
- package/dist/lib/calendar/eventSearch.js +43 -6
- package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
- package/dist/lib/calendar/freeBusyApi.js +13 -5
- package/dist/lib/calendar/recurrence.js +51 -3
- package/dist/lib/calendar/useDayKey.d.ts +7 -0
- package/dist/lib/calendar/useDayKey.js +43 -0
- package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
- package/dist/lib/components/avatar/ContactAvatar.js +37 -8
- package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
- package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
- package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
- package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
- package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
- package/dist/lib/components/forms/ImageEditBadge.js +62 -0
- package/dist/lib/components/menus/MenuButton.d.ts +78 -0
- package/dist/lib/components/menus/MenuButton.js +219 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
- package/dist/lib/components/navigation/BottomTabBar.js +22 -3
- package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
- package/dist/lib/components/overlays/PopoverPortal.js +3 -2
- package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
- package/dist/lib/contacts/contactsApi.d.ts +5 -2
- package/dist/lib/contacts/contactsApi.js +16 -19
- package/dist/lib/contacts/gravatar.d.ts +14 -3
- package/dist/lib/contacts/gravatar.js +42 -5
- package/dist/lib/contacts/preparePhoto.d.ts +24 -0
- package/dist/lib/contacts/preparePhoto.js +250 -0
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
- package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
- package/dist/lib/contacts/vcard.d.ts +3 -0
- package/dist/lib/contacts/vcard.js +59 -7
- package/dist/lib/crypto/escrowKeys.d.ts +8 -0
- package/dist/lib/crypto/escrowKeys.js +20 -1
- package/dist/lib/crypto/messageSecurity.d.ts +7 -4
- package/dist/lib/crypto/messageSecurity.js +23 -5
- package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
- package/dist/lib/crypto/passwordUnlock.js +13 -2
- package/dist/lib/crypto/smime.js +12 -2
- package/dist/lib/mail/compose/composeQuoting.js +3 -2
- package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
- package/dist/lib/mail/compose/quotedImages.js +35 -9
- package/dist/lib/mail/labelsApi.js +2 -2
- package/dist/lib/mail/mailApi.d.ts +8 -5
- package/dist/lib/mail/mailApi.js +73 -39
- package/dist/lib/mail/mailDetailHooks.js +11 -3
- package/dist/lib/mail/mailFilterRulesApi.js +2 -2
- package/dist/lib/mail/mailSignaturesApi.js +2 -2
- package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
- package/dist/lib/mail/mailboxImportApi.js +3 -15
- package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
- package/dist/lib/mail/messageBodySanitizer.js +44 -1
- package/dist/lib/search/queryGrammar.js +12 -4
- package/dist/lib/search/searchTier3.d.ts +6 -1
- package/dist/lib/search/searchTier3.js +54 -25
- package/dist/lib/tasks/tasksApi.js +3 -3
- package/dist/lib/util/api.d.ts +26 -4
- package/dist/lib/util/api.js +73 -16
- package/dist/lib/util/apiQuery.d.ts +11 -0
- package/dist/lib/util/apiQuery.js +11 -0
- package/dist/lib/util/settleWithLimit.d.ts +5 -0
- package/dist/lib/util/settleWithLimit.js +25 -0
- package/lib/admin/auditLogApi.ts +2 -2
- package/lib/admin/distributionListsApi.ts +2 -2
- package/lib/admin/domainsApi.ts +2 -2
- package/lib/admin/escrowAuditLogApi.ts +2 -2
- package/lib/admin/escrowScopesApi.ts +2 -2
- package/lib/admin/pluginsApi.ts +45 -3
- package/lib/admin/retentionPolicyApi.ts +4 -0
- package/lib/admin/transportRulesApi.ts +2 -2
- package/lib/appearance/preferencesApi.ts +3 -14
- package/lib/auth/session.ts +239 -39
- package/lib/branding/brandingApi.ts +9 -21
- package/lib/calendar/calendarApi.ts +3 -1
- package/lib/calendar/eventSearch.ts +46 -6
- package/lib/calendar/freeBusyApi.ts +14 -6
- package/lib/calendar/recurrence.ts +59 -8
- package/lib/calendar/useDayKey.ts +45 -0
- package/lib/components/avatar/ContactAvatar.tsx +46 -8
- package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
- package/lib/components/forms/CameraCaptureModal.tsx +133 -0
- package/lib/components/forms/ImageEditBadge.tsx +121 -0
- package/lib/components/menus/MenuButton.tsx +408 -0
- package/lib/components/navigation/BottomTabBar.tsx +30 -8
- package/lib/components/overlays/PopoverPortal.tsx +3 -2
- package/lib/components/pickers/TimeZonePicker.tsx +17 -1
- package/lib/contacts/contactsApi.ts +18 -19
- package/lib/contacts/gravatar.ts +47 -5
- package/lib/contacts/preparePhoto.ts +273 -0
- package/lib/contacts/useContactPhotoSrc.ts +177 -0
- package/lib/contacts/vcard.ts +60 -7
- package/lib/crypto/escrowKeys.ts +21 -1
- package/lib/crypto/messageSecurity.ts +33 -9
- package/lib/crypto/passwordUnlock.ts +14 -2
- package/lib/crypto/smime.ts +13 -2
- package/lib/mail/compose/composeQuoting.ts +3 -2
- package/lib/mail/compose/quotedImages.ts +39 -9
- package/lib/mail/labelsApi.ts +2 -2
- package/lib/mail/mailApi.ts +96 -39
- package/lib/mail/mailDetailHooks.ts +11 -3
- package/lib/mail/mailFilterRulesApi.ts +2 -2
- package/lib/mail/mailSignaturesApi.ts +2 -2
- package/lib/mail/mailboxImportApi.ts +9 -15
- package/lib/mail/messageBodySanitizer.ts +47 -1
- package/lib/search/queryGrammar.ts +13 -4
- package/lib/search/searchTier3.ts +65 -32
- package/lib/tasks/tasksApi.ts +3 -3
- package/lib/util/api.ts +99 -16
- package/lib/util/apiQuery.ts +13 -0
- package/lib/util/settleWithLimit.ts +25 -0
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ import { getMailbox } from "../../../../lib/mail/mailApi.js";
|
|
|
18
18
|
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
19
19
|
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
20
20
|
import KeyEnrollmentGate from "../../../shared/components/layout/KeyEnrollmentGate.js";
|
|
21
|
+
import { MIN_PASSWORD_LENGTH } from "../../../shared/components/layout/RecoveryCodeUnlock.js";
|
|
21
22
|
import { useUnlockPrompt } from "../../../shared/components/layout/UnlockPromptProvider.js";
|
|
22
23
|
import { destroyLocalIndex } from "../../../shared/search/localIndexRpcClient.js";
|
|
23
24
|
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
@@ -29,7 +30,6 @@ import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/s
|
|
|
29
30
|
import { checkEnrollmentNow, forgetEnrollment, recordEnrollmentResult, useEnrollmentSnapshot, watchCurrentEnrollment, watchEnrollment, } from "../../../shared/signing/enrollmentTracker.js";
|
|
30
31
|
import { STALE_REQUEST_TEXT, beforeRequestText, healthWarning, isInstalling, timeOf } from "../../../shared/signing/enrollmentView.js";
|
|
31
32
|
import { useSigningEnrollmentInfo } from "../../../shared/signing/signingInfo.js";
|
|
32
|
-
const MIN_PASSWORD_LENGTH = 8;
|
|
33
33
|
/** Mirrors restapi's `BaseKeyVaultRoute` `MAX_MASTER_KEY_WRAPS` - `addMasterKeyWrap()` refuses a vault
|
|
34
34
|
* already holding this many wraps (escrow included). */
|
|
35
35
|
export const MAX_MASTER_KEY_WRAPS = 20;
|
|
@@ -185,6 +185,7 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
185
185
|
// Distinguishes the "Save your new recovery codes" screen's copy for the two different actions that
|
|
186
186
|
// land on it - a plain regeneration only invalidates old recovery codes, while a rotation also
|
|
187
187
|
// invalidates every other unlock method, which the copy needs to say plainly.
|
|
188
|
+
const [confirmingRotation, setConfirmingRotation] = useState(false);
|
|
188
189
|
const [recoveryCodesReason, setRecoveryCodesReason] = useState("regenerate");
|
|
189
190
|
const [codesSaved, setCodesSaved] = useState(false);
|
|
190
191
|
const [codesCopied, setCodesCopied] = useState(false);
|
|
@@ -228,6 +229,7 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
228
229
|
const hasEscrowWrap = vault?.masterKeyWraps.some((w) => w.method === "escrow") ?? false;
|
|
229
230
|
const [wrappingEscrow, setWrappingEscrow] = useState(false);
|
|
230
231
|
const [escrowError, setEscrowError] = useState(null);
|
|
232
|
+
const [pendingEscrow, setPendingEscrow] = useState(null);
|
|
231
233
|
/**
|
|
232
234
|
* This mailbox's unlocked keys *right now* - never a copy read at render time, which a lock (idle
|
|
233
235
|
* timeout, another tab's sign-out) may since have destroyed. Prompts for the password when locked;
|
|
@@ -270,13 +272,29 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
270
272
|
}
|
|
271
273
|
return err instanceof ApiRequestError ? err.message : fallback;
|
|
272
274
|
}
|
|
275
|
+
/** Looks up the certificate the server says this mailbox's escrow scope holds, and asks the owner to confirm it
|
|
276
|
+
* before the master key is wrapped to it: nothing else vouches for that certificate, and whoever holds its
|
|
277
|
+
* private key can read this mailbox's encrypted mail. */
|
|
273
278
|
async function handleWrapEscrow() {
|
|
274
279
|
setEscrowError(null);
|
|
275
280
|
setWrappingEscrow(true);
|
|
276
281
|
try {
|
|
277
282
|
// Only reachable when mailbox.escrowScopeId is set (see the render guard below).
|
|
283
|
+
await verifiedUnlockedKeys();
|
|
284
|
+
setPendingEscrow(await getEscrowInfo(mailboxUid, client));
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
setEscrowError(errorMessage(err, "Could not add escrow protection for this mailbox."));
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
setWrappingEscrow(false);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function handleConfirmEscrow(escrowInfo) {
|
|
294
|
+
setPendingEscrow(null);
|
|
295
|
+
setWrappingEscrow(true);
|
|
296
|
+
try {
|
|
278
297
|
const { current } = await verifiedUnlockedKeys();
|
|
279
|
-
const escrowInfo = await getEscrowInfo(mailboxUid, client);
|
|
280
298
|
const wrap = await buildEscrowWrap(current.masterKey, escrowInfo.escrowScopeId, fromBase64(escrowInfo.publicKey.publicKey));
|
|
281
299
|
await addMasterKeyWrap(mailboxUid, wrap, undefined, client);
|
|
282
300
|
await loadVault();
|
|
@@ -635,7 +653,7 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
635
653
|
* working the instant this succeeds, since `rekey()` replaces `masterKeyWraps` wholesale - the whole
|
|
636
654
|
* point, for a captured-wrap scenario where it's unclear which method was compromised.
|
|
637
655
|
*/
|
|
638
|
-
|
|
656
|
+
function handleRotateKeys(e) {
|
|
639
657
|
e.preventDefault();
|
|
640
658
|
if (rotationBlockedBySigning) {
|
|
641
659
|
// The form's controls are disabled in this state too; this also covers an implicit submit.
|
|
@@ -650,6 +668,12 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
650
668
|
return;
|
|
651
669
|
}
|
|
652
670
|
setActionError(null);
|
|
671
|
+
// One press must not end every other unlock method and every other device's password: say so, and wait for a yes.
|
|
672
|
+
setConfirmingRotation(true);
|
|
673
|
+
}
|
|
674
|
+
async function rotateKeys() {
|
|
675
|
+
setConfirmingRotation(false);
|
|
676
|
+
setActionError(null);
|
|
653
677
|
setEscrowError(null);
|
|
654
678
|
setRotating(true);
|
|
655
679
|
let keys;
|
|
@@ -775,12 +799,12 @@ function EncryptionContent({ canManageKeys }) {
|
|
|
775
799
|
: null;
|
|
776
800
|
const certificateEnrollment = certificateMode === "issued" && enrollmentResult?.status !== "issued" ? null : certificateMode === "expired" ? null : enrollmentResult;
|
|
777
801
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Encryption" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage how ", mailbox.displayName, " unlocks its encryption keys on this and other devices."] })] }), loadError && _jsx(Alert, { children: loadError }), actionError && _jsx(Alert, { children: actionError }), !canManageKeys && (_jsx("p", { className: "text-sm text-text-muted", children: "Only this mailbox\u2019s owner, signed in as themselves, can change its encryption keys and unlock methods." })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Encryption keys" }), displayedKeys.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-sm", children: displayedKeys.map((key) => (_jsxs("li", { className: "font-mono text-xs", children: [key.useType === "sign" ? "Signing" : "Encryption", " key: ", key.fingerprint, key.revokedAt &&
|
|
778
|
-
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] }))] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
802
|
+
(key.revocationReason === "superseded" ? (_jsx("span", { className: "text-text-muted", children: " (superseded)" })) : (_jsx("span", { className: "text-danger", children: " (revoked)" })))] }, key.fingerprint))) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No keys enrolled yet." }))] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Digital signatures" }), activeSigningKey && signingStatus !== "pending" && (_jsx("p", { className: ["text-sm text-text-muted", certificateMode ? "mb-3" : ""].join(" "), children: "Enabled \u2014 outgoing mail from this mailbox is signed with a publicly-trusted certificate." })), certificateMode && (_jsxs("div", { className: "flex flex-col gap-2", children: [certificateMode === "failed" && signingError && _jsx(Alert, { children: signingError }), _jsx(SigningCertificateCard, { mode: certificateMode, enrollment: certificateEnrollment, snapshot: enrollment, info: signingInfo, address: mailbox.primarySmtpAddress, keyNotAfter: certificateMode === "expired" ? expiredSigningKey?.notAfter : activeSigningKey?.notAfter, canRequest: canManageKeys, requesting: signingStatus === "enrolling", onCheck: canManageKeys ? () => void checkEnrollmentNow(mailboxUid) : undefined, onRequest: handleEnrollSigning })] })), !activeSigningKey && !certificateMode && !canManageKeys ? (_jsx("p", { className: "text-sm text-text-muted", children: "Not enabled." })) : activeSigningKey || certificateMode ? null : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: "Lets recipients verify that mail from this mailbox is genuinely from you. Optional \u2014 encryption already works without it." }), beforeRequestText(signingInfo, mailbox.primarySmtpAddress) && (_jsx("p", { className: "text-xs text-text-muted", children: beforeRequestText(signingInfo, mailbox.primarySmtpAddress) })), healthWarning(signingInfo) && _jsx(Alert, { children: healthWarning(signingInfo) }), enrollment?.gone && _jsx(Alert, { children: STALE_REQUEST_TEXT }), signingError && _jsx(Alert, { children: signingError }), signingInfo?.backend !== "none" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: signingStatus === "enrolling", disabled: signingStatus !== "idle", onClick: handleEnrollSigning, children: "Enable digital signatures" }))] }))] }), mailbox.escrowScopeId && (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Escrow" }), escrowError && _jsx(Alert, { children: escrowError }), hasEscrowWrap ? (_jsx("p", { className: "text-sm text-text-muted", children: "This mailbox is under legal/compliance escrow \u2014 an authorized holder in your organization can recover its encrypted mail if needed. This does not weaken protection against anyone else." })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Your organization has assigned this mailbox to an escrow scope, but nothing has been protected yet \u2014 an authorized holder cannot recover this mailbox\u2019s encrypted mail until you complete this step.", " ", "This does not weaken protection against anyone else."] }), canManageKeys && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: wrappingEscrow, disabled: wrappingEscrow, onClick: handleWrapEscrow, children: "Add escrow protection" }))] })), _jsxs(Modal, { open: pendingEscrow !== null, onClose: () => setPendingEscrow(null), title: "Confirm escrow certificate", children: [_jsx("p", { className: "text-sm mb-3", children: "Anyone holding the private key for this certificate will be able to recover this mailbox\u2019s encrypted mail. Confirm it is the escrow certificate your organization expects before continuing." }), _jsxs("dl", { className: "text-sm mb-5 flex flex-col gap-1", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "break-all", children: pendingEscrow?.escrowScopeId })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate fingerprint" }), _jsx("dd", { className: "font-mono break-all", children: pendingEscrow?.publicKey.fingerprint })] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingEscrow(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleConfirmEscrow(pendingEscrow), children: "Trust this certificate" })] })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Unlock methods" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Removing a method here stops it from being usable to unlock this mailbox going forward, but it is ", _jsx("strong", { children: "not" }), " full revocation \u2014 anyone who already captured a wrapped copy and knows its secret could still use it. For real revocation (e.g. after a lost device), rotate your keys entirely below instead."] }), vault && vault.masterKeyWraps.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-2", children: [...passwordWraps, ...recoveryWraps, ...otherWraps].map((wrap) => {
|
|
779
803
|
const key = `${wrap.method}:${wrap.methodId ?? ""}`;
|
|
780
804
|
return (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { children: METHOD_LABELS[wrap.method] ?? wrap.method }), wrap.method !== "escrow" &&
|
|
781
805
|
canManageKeys &&
|
|
782
806
|
(canRemoveWrap(wrap) ? (_jsx(Button, { type: "button", variant: "text", className: "!w-auto text-danger", loading: removingMethod === key, disabled: removingMethod !== null, onClick: () => setPendingRemoval(wrap), children: "Remove" })) : (_jsx("span", { className: "text-xs text-text-muted", children: ownUnlockWrapCount > 1 ? "Needed to unlock" : "Your only unlock method" })))] }, key));
|
|
783
|
-
}) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No unlock methods on file." })), _jsxs(Modal, { open: pendingRemoval !== null, onClose: () => setPendingRemoval(null), title: "Remove unlock method", children: [_jsx("p", { className: "text-sm mb-5", children: "Remove this unlock method? It will no longer unlock this mailbox\u2019s encrypted mail on any device." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingRemoval(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => handleRemove(pendingRemoval), children: "Remove method" })] })] })] }), canManageKeys && (_jsxs(_Fragment, { children: [passwordWraps.length === 0 ? (vault && (_jsxs("form", { onSubmit: handleAddPassword, className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Add a password" }), _jsx("input", { type: "password", "aria-label": "New password", placeholder: "New password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: newPassword, onChange: (e) => setNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm new password", placeholder: "Confirm new password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmNewPassword, onChange: (e) => setConfirmNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: addingPassword, disabled: addingPassword, className: "!w-auto", children: "Add password" }) })] }))) : (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Changing your password" }), _jsx("p", { className: "text-xs text-text-muted", children: "Only one password can unlock this mailbox. To change it, rotate your keys below - that sets a new password and replaces every other unlock method." })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Recovery codes" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Regenerating replaces all of your existing recovery codes \u2014 old ones stop working immediately." }), _jsx(Button, { type: "button", variant: "secondary", loading: regenerating, disabled: regenerating || !vault, onClick: handleRegenerateRecoveryCodes, className: "!w-auto", children: "Regenerate recovery codes" })] }), _jsxs("form", { onSubmit: handleRotateKeys, className: "flex flex-col gap-2 border-t border-border pt-6", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Rotate keys" }), _jsx("p", { className: "text-xs text-text-muted mb-1", children: "Real revocation, for when a device or an unlock method may have been compromised. Re-protects your existing encryption key under a brand new master key - your signing/ encryption keypair itself doesn\u2019t change, so mail you\u2019ve already sent or received still decrypts normally. Every current unlock method (password, recovery codes, and anything else on file) stops working immediately; you\u2019ll set a new password and get new recovery codes below." }), rotationBlockedBySigning && (_jsx("p", { className: "text-xs text-text-muted", children: signingStatus === "checking"
|
|
807
|
+
}) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "No unlock methods on file." })), _jsxs(Modal, { open: pendingRemoval !== null, onClose: () => setPendingRemoval(null), title: "Remove unlock method", children: [_jsx("p", { className: "text-sm mb-5", children: "Remove this unlock method? It will no longer unlock this mailbox\u2019s encrypted mail on any device." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingRemoval(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => handleRemove(pendingRemoval), children: "Remove method" })] })] })] }), canManageKeys && (_jsxs(_Fragment, { children: [passwordWraps.length === 0 ? (vault && (_jsxs("form", { onSubmit: handleAddPassword, className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Add a password" }), _jsx("input", { type: "password", "aria-label": "New password", placeholder: "New password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: newPassword, onChange: (e) => setNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm new password", placeholder: "Confirm new password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmNewPassword, onChange: (e) => setConfirmNewPassword(e.target.value), disabled: addingPassword, autoComplete: "new-password" }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: addingPassword, disabled: addingPassword, className: "!w-auto", children: "Add password" }) })] }))) : (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Changing your password" }), _jsx("p", { className: "text-xs text-text-muted", children: "Only one password can unlock this mailbox. To change it, rotate your keys below - that sets a new password and replaces every other unlock method." })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Recovery codes" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Regenerating replaces all of your existing recovery codes \u2014 old ones stop working immediately." }), _jsx(Button, { type: "button", variant: "secondary", loading: regenerating, disabled: regenerating || !vault, onClick: handleRegenerateRecoveryCodes, className: "!w-auto", children: "Regenerate recovery codes" })] }), _jsxs(Modal, { open: confirmingRotation, onClose: () => setConfirmingRotation(false), title: "Rotate your keys?", children: [_jsx("p", { className: "text-sm mb-3", children: "Every other way of unlocking this mailbox - its password, its recovery codes, and every other device\u2019s saved password - stops working the moment this finishes, and cannot be brought back. You will be shown new recovery codes once." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingRotation(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void rotateKeys(), children: "Yes, rotate my keys" })] })] }), _jsxs("form", { onSubmit: handleRotateKeys, className: "flex flex-col gap-2 border-t border-border pt-6", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Rotate keys" }), _jsx("p", { className: "text-xs text-text-muted mb-1", children: "Real revocation, for when a device or an unlock method may have been compromised. Re-protects your existing encryption key under a brand new master key - your signing/ encryption keypair itself doesn\u2019t change, so mail you\u2019ve already sent or received still decrypts normally. Every current unlock method (password, recovery codes, and anything else on file) stops working immediately; you\u2019ll set a new password and get new recovery codes below." }), rotationBlockedBySigning && (_jsx("p", { className: "text-xs text-text-muted", children: signingStatus === "checking"
|
|
784
808
|
? "Checking whether a signing certificate enrollment is still in progress..."
|
|
785
809
|
: "Rotation is unavailable while a signing certificate enrollment is in progress - rotating now would lose the key being enrolled. Try again once it finishes, or cancel the enrollment." })), signingStatus === "pending" && signingEnrollmentId && (_jsxs("div", { className: "flex flex-col gap-2", children: [cancelEnrollmentError && _jsx(Alert, { children: cancelEnrollmentError }), _jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: cancelingEnrollment, disabled: cancelingEnrollment, onClick: () => handleCancelEnrollment(signingEnrollmentId), children: "Cancel enrollment" }) })] })), _jsx("input", { type: "password", "aria-label": "New password for rotated keys", placeholder: "New password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: rotationPassword, onChange: (e) => setRotationPassword(e.target.value), disabled: rotating || rotationBlockedBySigning, autoComplete: "new-password" }), _jsx("input", { type: "password", "aria-label": "Confirm new password for rotated keys", placeholder: "Confirm new password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: rotationConfirmPassword, onChange: (e) => setRotationConfirmPassword(e.target.value), disabled: rotating || rotationBlockedBySigning, autoComplete: "new-password" }), _jsx("div", { children: _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto text-danger", loading: rotating, disabled: rotating || rotationBlockedBySigning, children: "Rotate keys now" }) })] })] })), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Session timeout" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Automatically destroys your unlocked keys on this device after this much time with no activity anywhere in the app - not just Mail or Settings. Applies the next time you open or reload the app." }), _jsx("select", { "aria-label": "Session timeout", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: idleTimeoutMinutes, onChange: handleIdleTimeoutChange, children: IDLE_TIMEOUT_OPTIONS_MINUTES.map((minutes) => (_jsx("option", { value: minutes, children: idleTimeoutLabel(minutes) }, minutes))) })] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Local search index size" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["How much decrypted mail this device keeps in a local encrypted search index, so recent search results and inbox previews work instantly without contacting the server every time. Defaults to ", localIndexDefaultByteBudgetLabel, " on this device. Applies the next time this mailbox\u2019s index rebuilds (e.g. the next time you unlock it) \u2014 lowering it doesn\u2019t delete anything already indexed elsewhere, it just narrows what this device keeps a local copy of."] }), _jsx("select", { "aria-label": "Local search index size", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: localIndexByteBudget, onChange: handleLocalIndexByteBudgetChange, children: LOCAL_INDEX_SIZE_OPTIONS.map((option) => (_jsx("option", { value: option.bytes, children: option.label }, option.bytes))) })] }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "This session" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Removes your unlocked keys from this browser tab\u2019s memory right now, without affecting any other device. You\u2019ll be asked to unlock again the next time you read or send encrypted mail here." }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", onClick: handleDestroyKeysNow, children: "Destroy keys on this device now" })] })] }) }));
|
|
786
810
|
}
|
|
@@ -16,6 +16,7 @@ import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
|
16
16
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
17
17
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
18
18
|
import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
19
|
+
import GravatarSetting from "../../../shared/components/settings/GravatarSetting.js";
|
|
19
20
|
const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]);
|
|
20
21
|
function importFormatFromFilename(filename) {
|
|
21
22
|
return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
|
|
@@ -49,7 +50,7 @@ function PrivacyContent({ userUid }) {
|
|
|
49
50
|
const ownedMailboxes = mailboxes.filter((mb) => mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
|
|
50
51
|
const ownMailbox = ownedMailboxes.length === 1 ? ownedMailboxes[0] : undefined;
|
|
51
52
|
const selectedIsOwn = selected.uid === ownMailbox?.uid;
|
|
52
|
-
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
|
|
53
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), _jsx(GravatarSetting, {}), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
|
|
53
54
|
}
|
|
54
55
|
function OwnMailboxOnlyNotice({ ownedMailboxCount, ownMailboxLabel }) {
|
|
55
56
|
let detail;
|
|
@@ -158,7 +159,7 @@ function ImportSection({ mailboxUid }) {
|
|
|
158
159
|
return;
|
|
159
160
|
}
|
|
160
161
|
setUploading(true);
|
|
161
|
-
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid })
|
|
162
|
+
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid }, client)
|
|
162
163
|
.then(() => loadRequests())
|
|
163
164
|
.catch((err) => notifyApiError(err, "Couldn't upload the file"))
|
|
164
165
|
.finally(() => setUploading(false));
|
|
@@ -119,7 +119,10 @@ function ProfileContent() {
|
|
|
119
119
|
}, "aria-describedby": "profile-time-zone-help" }), timezone !== deviceZone && (_jsxs(Button, { type: "button", variant: "text", className: "!w-auto", onClick: () => {
|
|
120
120
|
setTimezone(deviceZone);
|
|
121
121
|
setSaved(false);
|
|
122
|
-
}, children: ["Use this device\u2019s time zone (", describeTimeZone(deviceZone), ")"] }))] }), zoneWasPreselected && (
|
|
122
|
+
}, children: ["Use this device\u2019s time zone (", describeTimeZone(deviceZone), ")"] }))] }), zoneWasPreselected && (_jsxs("p", { className: "mt-1 text-xs text-text-muted", children: ["Your mailbox has no time zone chosen yet (it uses UTC); this device\u2019s is preselected, and Save sets it - whatever else you change.", " ", _jsx(Button, { type: "button", variant: "text", className: "!w-auto !inline !p-0 !text-xs", onClick: () => {
|
|
123
|
+
setTimezone(DEFAULT_TIME_ZONE);
|
|
124
|
+
setSaved(false);
|
|
125
|
+
}, children: "Keep UTC" })] })), _jsx("p", { id: "profile-time-zone-help", className: "mt-1 text-xs text-text-muted", children: "How calendar times and reminders are shown and scheduled for this mailbox, such as when an event or a reminder falls." })] }), _jsxs(FormField, { label: "Free/busy visibility", htmlFor: "profile-free-busy", children: [_jsx("select", { id: "profile-free-busy", className: SELECT_CLASS, value: freeBusy, disabled: !canChangeFreeBusy, onChange: (e) => {
|
|
123
126
|
setFreeBusy(e.target.value);
|
|
124
127
|
setSaved(false);
|
|
125
128
|
}, "aria-describedby": "profile-free-busy-help", children: FREE_BUSY_CHOICES.map((choice) => (_jsx("option", { value: choice.value, children: choice.label }, choice.value))) }), _jsx("p", { id: "profile-free-busy-help", className: "mt-1 text-xs text-text-muted", children: FREE_BUSY_CHOICES.find((choice) => choice.value === freeBusy).help }), _jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Who can see the hours you are busy when they look for a time to meet - never the title, place or guests of an event." }), !canChangeFreeBusy && (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "This mailbox is shared with you: only its owner can change who sees its free/busy times." }))] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
@@ -4,7 +4,7 @@ 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 { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
|
-
import { useEffect, useState } from "react";
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
9
9
|
import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "../../../../lib/mail/mailboxAccessApi.js";
|
|
10
10
|
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
@@ -40,15 +40,24 @@ function SharingContent() {
|
|
|
40
40
|
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved (a mailbox-less caller
|
|
41
41
|
// gets `<MailboxProvisioning />` instead) - same established precedent as the Labels/Focused Inbox
|
|
42
42
|
// settings pages, so none of the handlers below need their own `!mailboxUid` guard.
|
|
43
|
+
// Only the latest request's answer is shown (a reload after a change can overtake a slower earlier one).
|
|
44
|
+
const latestLoad = useRef(0);
|
|
43
45
|
function reload() {
|
|
46
|
+
const load = ++latestLoad.current;
|
|
44
47
|
setStatus("loading");
|
|
45
48
|
setError(null);
|
|
46
49
|
listMailboxAccess(mailboxUid)
|
|
47
50
|
.then((result) => {
|
|
51
|
+
if (load !== latestLoad.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
48
54
|
setMembers(result);
|
|
49
55
|
setStatus("ready");
|
|
50
56
|
})
|
|
51
57
|
.catch((err) => {
|
|
58
|
+
if (load !== latestLoad.current) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
52
61
|
// A 403 here is the live, server-verified "can this caller manage this mailbox's sharing"
|
|
53
62
|
// signal - the same permission threshold (ACLAction.UPDATE) this route itself requires, so
|
|
54
63
|
// no separate probe call is needed to decide which state to render.
|
|
@@ -63,10 +63,7 @@ function SignatureDetailContent({ uid }) {
|
|
|
63
63
|
setSaving(true);
|
|
64
64
|
setSaved(false);
|
|
65
65
|
try {
|
|
66
|
-
|
|
67
|
-
const existing = await listMailSignatures(mailboxUid, { limit: 100 });
|
|
68
|
-
await clearPreviousDefaults(existing, original.uid, isDefaultForNewMessages, isDefaultForReplyForward);
|
|
69
|
-
}
|
|
66
|
+
// This signature is saved first: were it refused (a 409, a 400) after the others had been cleared, the mailbox would be left with no default at all.
|
|
70
67
|
const updated = await updateMailSignature({
|
|
71
68
|
uid: original.uid,
|
|
72
69
|
version: original.version,
|
|
@@ -76,6 +73,11 @@ function SignatureDetailContent({ uid }) {
|
|
|
76
73
|
isDefaultForReplyForward,
|
|
77
74
|
});
|
|
78
75
|
setOriginal(updated);
|
|
76
|
+
if (isDefaultForNewMessages || isDefaultForReplyForward) {
|
|
77
|
+
// The signature's own mailbox, not the one the page happens to have selected.
|
|
78
|
+
const existing = await listMailSignatures(original.mailboxUid, { limit: 100 });
|
|
79
|
+
await clearPreviousDefaults(existing, original.uid, isDefaultForNewMessages, isDefaultForReplyForward);
|
|
80
|
+
}
|
|
79
81
|
setSaved(true);
|
|
80
82
|
}
|
|
81
83
|
catch (err) {
|
|
@@ -5,11 +5,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
-
import { endOfWeek, isAfter, isBefore, isToday,
|
|
8
|
+
import { endOfWeek, isAfter, isBefore, isToday, startOfDay } from "date-fns";
|
|
9
9
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
10
10
|
import { createTask, deleteTask, listTasks, setTaskCompleted, setTaskMyDay, } from "../../../lib/tasks/tasksApi.js";
|
|
11
11
|
import { listFlaggedMessages } from "../../../lib/mail/flaggedMessages.js";
|
|
12
12
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
13
|
+
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
14
|
+
import { dueDateInstant, parseDueDate } from "../../shared/components/tasks/dueDate.js";
|
|
13
15
|
import TasksShell, { useTasksShell } from "../../shared/components/tasks/layout/TasksShell.js";
|
|
14
16
|
import TasksSidebar from "../../shared/components/tasks/TasksSidebar.js";
|
|
15
17
|
import TasksToolbar from "../../shared/components/tasks/TasksToolbar.js";
|
|
@@ -27,14 +29,6 @@ const INPUT_CLASS = "text-sm py-1.5 px-2 border border-border rounded-sm bg-surf
|
|
|
27
29
|
function TasksPage(props) {
|
|
28
30
|
return (_jsx(TasksShell, { ...props, children: _jsx(TasksContent, {}) }));
|
|
29
31
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Parses a task's `dueDate` - `parseISO()` rather than `new Date()`, since `new Date("2026-06-16")` reads a
|
|
32
|
-
* date-only value as UTC midnight, which is the *previous* local day anywhere west of UTC. A full
|
|
33
|
-
* timestamp parses identically either way.
|
|
34
|
-
*/
|
|
35
|
-
function parseDueDate(value) {
|
|
36
|
-
return parseISO(value);
|
|
37
|
-
}
|
|
38
32
|
const BUCKET_ORDER = ["Overdue", "Today", "This Week", "Later", "No due date"];
|
|
39
33
|
/** Buckets a task by its due date relative to `now`, matching the Outlook/To-Do convention this list mirrors. */
|
|
40
34
|
function bucketFor(task, now) {
|
|
@@ -94,7 +88,10 @@ function TasksContent() {
|
|
|
94
88
|
// Registered only while the form is rendered (`canAddTask`), so its field is there.
|
|
95
89
|
useShortcut(SHORTCUTS.tasks.create, () => addTaskRef.current.focus(), { enabled: canAddTask });
|
|
96
90
|
const addTaskHint = useShortcutProps("Add a task", SHORTCUTS.tasks.create, canAddTask);
|
|
91
|
+
// Which load is the latest: a slower, older one (another folder's, from before the mailbox changed) must not replace the list a newer one set.
|
|
92
|
+
const loadRun = useRef(0);
|
|
97
93
|
function reload() {
|
|
94
|
+
const run = ++loadRun.current;
|
|
98
95
|
if (!folderUid) {
|
|
99
96
|
setTasks([]);
|
|
100
97
|
setTruncated(false);
|
|
@@ -105,11 +102,21 @@ function TasksContent() {
|
|
|
105
102
|
setError(null);
|
|
106
103
|
return listAllPages((page) => listTasks(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
107
104
|
.then((result) => {
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
if (run === loadRun.current) {
|
|
106
|
+
setTasks(result.items);
|
|
107
|
+
setTruncated(result.truncated);
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.catch((err) => {
|
|
111
|
+
if (run === loadRun.current) {
|
|
112
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load tasks.");
|
|
113
|
+
}
|
|
110
114
|
})
|
|
111
|
-
.
|
|
112
|
-
|
|
115
|
+
.finally(() => {
|
|
116
|
+
if (run === loadRun.current) {
|
|
117
|
+
setLoading(false);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
113
120
|
}
|
|
114
121
|
useEffect(() => {
|
|
115
122
|
void reload();
|
|
@@ -122,12 +129,29 @@ function TasksContent() {
|
|
|
122
129
|
if (view.type !== "flagged" || !mailboxUid) {
|
|
123
130
|
return;
|
|
124
131
|
}
|
|
132
|
+
// Set once the view or mailbox this load was for is no longer the one shown, so its answer is dropped.
|
|
133
|
+
let stale = false;
|
|
125
134
|
setFlaggedLoading(true);
|
|
126
135
|
setFlaggedError(null);
|
|
127
136
|
listFlaggedMessages(mailboxUid, client)
|
|
128
|
-
.then(
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
.then((messages) => {
|
|
138
|
+
if (!stale) {
|
|
139
|
+
setFlaggedMessages(messages);
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
if (!stale) {
|
|
144
|
+
setFlaggedError(err instanceof ApiRequestError ? err.message : "Could not load flagged email.");
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.finally(() => {
|
|
148
|
+
if (!stale) {
|
|
149
|
+
setFlaggedLoading(false);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return () => {
|
|
153
|
+
stale = true;
|
|
154
|
+
};
|
|
131
155
|
}, [view, mailboxUid]);
|
|
132
156
|
async function handleCreate(e) {
|
|
133
157
|
e.preventDefault();
|
|
@@ -151,8 +175,8 @@ function TasksContent() {
|
|
|
151
175
|
mailboxUid: target,
|
|
152
176
|
folderUid: targetFolderUid,
|
|
153
177
|
title: title.trim(),
|
|
154
|
-
// The date input's "yyyy-MM-dd" is
|
|
155
|
-
dueDate: dueDate ?
|
|
178
|
+
// The date input's "yyyy-MM-dd" is stored as that day's UTC midnight - see `dueDateInstant()`.
|
|
179
|
+
dueDate: dueDate ? dueDateInstant(dueDate) : undefined,
|
|
156
180
|
priority,
|
|
157
181
|
}, client);
|
|
158
182
|
if (target === mailboxUid) {
|
|
@@ -174,7 +198,24 @@ function TasksContent() {
|
|
|
174
198
|
setCreating(false);
|
|
175
199
|
}
|
|
176
200
|
}
|
|
201
|
+
// The tasks whose completion is being saved. The control is disabled meanwhile, and the ref (current at once, unlike state) turns away a second click that
|
|
202
|
+
// lands before the page has re-rendered: it would send the same stale `version` and be refused, rolling the task back to a state the server has left.
|
|
203
|
+
const togglingRef = useRef(new Set());
|
|
204
|
+
const [toggling, setToggling] = useState(new Set());
|
|
205
|
+
function setTogglePending(uid, pending) {
|
|
206
|
+
if (pending) {
|
|
207
|
+
togglingRef.current.add(uid);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
togglingRef.current.delete(uid);
|
|
211
|
+
}
|
|
212
|
+
setToggling(new Set(togglingRef.current));
|
|
213
|
+
}
|
|
177
214
|
async function handleToggle(task) {
|
|
215
|
+
if (togglingRef.current.has(task.uid)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
setTogglePending(task.uid, true);
|
|
178
219
|
const nextCompleted = !task.completed;
|
|
179
220
|
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: nextCompleted } : t)));
|
|
180
221
|
try {
|
|
@@ -185,6 +226,9 @@ function TasksContent() {
|
|
|
185
226
|
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? task : t)));
|
|
186
227
|
notifyApiError(err, "Couldn't update the task");
|
|
187
228
|
}
|
|
229
|
+
finally {
|
|
230
|
+
setTogglePending(task.uid, false);
|
|
231
|
+
}
|
|
188
232
|
}
|
|
189
233
|
async function handleDelete(task) {
|
|
190
234
|
try {
|
|
@@ -230,8 +274,24 @@ function TasksContent() {
|
|
|
230
274
|
}
|
|
231
275
|
}, [tasks, view, userUid]);
|
|
232
276
|
const checkedTasks = viewFiltered.filter((t) => checkedUids.has(t.uid));
|
|
277
|
+
// A bulk action sends every checked row's own version: pressed again before it finishes, it would send stale ones and raise a stream of 409s.
|
|
278
|
+
const bulkRunning = useRef(false);
|
|
279
|
+
function singleFlight(run) {
|
|
280
|
+
return async () => {
|
|
281
|
+
if (bulkRunning.current) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
bulkRunning.current = true;
|
|
285
|
+
try {
|
|
286
|
+
await run();
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
bulkRunning.current = false;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
233
293
|
// The bulk handlers try every task and report each failure: the same failure repeated is one pop-up with a count.
|
|
234
|
-
|
|
294
|
+
const handleBulkComplete = singleFlight(async () => {
|
|
235
295
|
for (const task of checkedTasks) {
|
|
236
296
|
try {
|
|
237
297
|
await setTaskCompleted(task, true, client);
|
|
@@ -242,8 +302,8 @@ function TasksContent() {
|
|
|
242
302
|
}
|
|
243
303
|
setCheckedUids(new Set());
|
|
244
304
|
await reload();
|
|
245
|
-
}
|
|
246
|
-
|
|
305
|
+
});
|
|
306
|
+
const handleBulkAddToMyDay = singleFlight(async () => {
|
|
247
307
|
for (const task of checkedTasks) {
|
|
248
308
|
try {
|
|
249
309
|
await setTaskMyDay(task, true, client);
|
|
@@ -254,7 +314,7 @@ function TasksContent() {
|
|
|
254
314
|
}
|
|
255
315
|
setCheckedUids(new Set());
|
|
256
316
|
await reload();
|
|
257
|
-
}
|
|
317
|
+
});
|
|
258
318
|
async function handleBulkDelete() {
|
|
259
319
|
for (const task of checkedTasks) {
|
|
260
320
|
try {
|
|
@@ -280,6 +340,8 @@ function TasksContent() {
|
|
|
280
340
|
void handleBulkDelete();
|
|
281
341
|
}
|
|
282
342
|
}
|
|
343
|
+
// Re-worked when the day changes, so a page left open past midnight moves Today's tasks to Overdue.
|
|
344
|
+
const dayKey = useDayKey();
|
|
283
345
|
const grouped = useMemo(() => {
|
|
284
346
|
const now = new Date();
|
|
285
347
|
const map = new Map(BUCKET_ORDER.map((b) => [b, []]));
|
|
@@ -290,17 +352,17 @@ function TasksContent() {
|
|
|
290
352
|
map.get(bucketFor(task, now)).push(task);
|
|
291
353
|
}
|
|
292
354
|
return map;
|
|
293
|
-
}, [viewFiltered]);
|
|
355
|
+
}, [viewFiltered, dayKey]);
|
|
294
356
|
const completedTasks = viewFiltered.filter((t) => t.completed);
|
|
295
357
|
return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row", children: [_jsx(TasksSidebar, { mailboxUid: mailboxUid, tasks: tasks, userUid: userUid, active: view, onSelect: handleSelectView }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col overflow-y-auto", children: [_jsx(TasksToolbar, { viewMode: viewMode, onViewModeChange: setViewMode, selectedCount: checkedTasks.length, onComplete: handleBulkComplete, onDelete: () => setPendingDelete({ type: "bulk" }), onAddToMyDay: handleBulkAddToMyDay }), _jsxs("div", { role: "region", "aria-label": "Tasks list", className: "max-w-2xl mx-auto w-full flex flex-col gap-6 p-6", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Tasks" }), error && _jsx(Alert, { children: error }), truncated && view.type !== "flagged" && (_jsxs(Alert, { children: ["This folder has more tasks than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] })), view.type === "flagged" ? (_jsx(FlaggedEmailList, { loading: flaggedLoading, error: flaggedError, messages: flaggedMessages })) : (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: handleCreate, className: "flex flex-wrap items-center gap-2 bg-surface border border-border rounded-md p-2", children: [_jsx("input", { ref: addTaskRef, type: "text", "aria-label": "Add a task", ...addTaskHint, className: `${INPUT_CLASS} min-w-40 basis-40 flex-1`, value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Add a task" }), _jsx("input", { type: "date", "aria-label": "Due date", className: INPUT_CLASS, value: dueDate, onChange: (e) => setDueDate(e.target.value) }), _jsxs("select", { "aria-label": "Priority", className: INPUT_CLASS, value: priority, onChange: (e) => setPriority(e.target.value), children: [_jsx("option", { value: "low", children: "Low" }), _jsx("option", { value: "normal", children: "Normal" }), _jsx("option", { value: "high", children: "High" })] }), writableMailboxes.length > 1 && (_jsx("select", { "aria-label": "Task mailbox", className: `${INPUT_CLASS} max-w-40`,
|
|
296
358
|
// `writableMailboxes` only has entries once `mailboxUid` has resolved.
|
|
297
|
-
value: targetMailboxUid ?? mailboxUid, onChange: (e) => setTargetMailboxUid(e.target.value), children: writableMailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid === userUid ? "" : " (shared)"] }, mb.uid))) })), _jsx(Button, { type: "submit", loading: creating, disabled: creating, className: "!w-auto shrink-0", children: "Add" })] }), createError && _jsx(Alert, { children: createError }), addedElsewhere && (_jsxs("p", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["\u201C", addedElsewhere.title, "\u201D was added to", " ", mailboxes.find((mb) => mb.uid === addedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/tasks?mailboxUid=${encodeURIComponent(addedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s tasks" })] })), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : viewFiltered.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No tasks yet." })) : viewMode === "grid" ? (_jsx(TaskTable, { tasks: viewFiltered, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: requestDelete })) : (_jsxs(_Fragment, { children: [BUCKET_ORDER.map((bucket) => {
|
|
359
|
+
value: targetMailboxUid ?? mailboxUid, onChange: (e) => setTargetMailboxUid(e.target.value), children: writableMailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid === userUid ? "" : " (shared)"] }, mb.uid))) })), _jsx(Button, { type: "submit", loading: creating, disabled: creating, className: "!w-auto shrink-0", children: "Add" })] }), createError && _jsx(Alert, { children: createError }), addedElsewhere && (_jsxs("p", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["\u201C", addedElsewhere.title, "\u201D was added to", " ", mailboxes.find((mb) => mb.uid === addedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/tasks?mailboxUid=${encodeURIComponent(addedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s tasks" })] })), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : viewFiltered.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No tasks yet." })) : viewMode === "grid" ? (_jsx(TaskTable, { tasks: viewFiltered, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, toggling: toggling, onDelete: requestDelete })) : (_jsxs(_Fragment, { children: [BUCKET_ORDER.map((bucket) => {
|
|
298
360
|
const items = grouped.get(bucket);
|
|
299
361
|
if (items.length === 0) {
|
|
300
362
|
return null;
|
|
301
363
|
}
|
|
302
|
-
return (_jsx(TaskGroup, { label: bucket, tasks: items, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: requestDelete }, bucket));
|
|
303
|
-
}), completedTasks.length > 0 && (_jsx(TaskGroup, { label: "Completed", tasks: completedTasks, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: requestDelete }))] }))] }))] })] }), _jsxs(Modal, { open: pendingDelete !== null, onClose: () => setPendingDelete(null), title: "Delete tasks", children: [_jsx("p", { className: "text-sm mb-5", children: pendingDelete?.type === "single"
|
|
364
|
+
return (_jsx(TaskGroup, { label: bucket, tasks: items, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, toggling: toggling, onDelete: requestDelete }, bucket));
|
|
365
|
+
}), completedTasks.length > 0 && (_jsx(TaskGroup, { label: "Completed", tasks: completedTasks, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, toggling: toggling, onDelete: requestDelete }))] }))] }))] })] }), _jsxs(Modal, { open: pendingDelete !== null, onClose: () => setPendingDelete(null), title: "Delete tasks", children: [_jsx("p", { className: "text-sm mb-5", children: pendingDelete?.type === "single"
|
|
304
366
|
? `Delete "${pendingDelete.task.title}"?`
|
|
305
367
|
: `Delete ${checkedTasks.length} selected ${checkedTasks.length === 1 ? "task" : "tasks"}?` }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingDelete(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleConfirmDelete, children: "Delete" })] })] })] }));
|
|
306
368
|
}
|
|
@@ -318,16 +380,16 @@ function FlaggedEmailList({ loading, error, messages }) {
|
|
|
318
380
|
}
|
|
319
381
|
/** A circular completion-toggle control matching Outlook's visual language (not a native checkbox), while
|
|
320
382
|
* staying a real accessible toggle. */
|
|
321
|
-
function CompletionToggle({ task, onToggle }) {
|
|
322
|
-
return (_jsx("button", { type: "button", role: "checkbox", "aria-checked": task.completed, "aria-label": `Mark "${task.title}" as ${task.completed ? "not complete" : "complete"}`, onClick: () => onToggle(task), className: [
|
|
383
|
+
function CompletionToggle({ task, onToggle, pending }) {
|
|
384
|
+
return (_jsx("button", { type: "button", role: "checkbox", "aria-checked": task.completed, disabled: pending, "aria-label": `Mark "${task.title}" as ${task.completed ? "not complete" : "complete"}`, onClick: () => onToggle(task), className: [
|
|
323
385
|
"w-4 h-4 rounded-full border shrink-0 flex items-center justify-center text-[10px]",
|
|
324
386
|
task.completed ? "bg-primary border-primary text-white" : "border-border text-transparent",
|
|
325
387
|
].join(" "), children: "\u2713" }));
|
|
326
388
|
}
|
|
327
|
-
function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, onDelete }) {
|
|
328
|
-
return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle }), _jsx("span", { className: ["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), task.priority !== "normal" && (_jsx("span", { className: ["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] })), task.dueDate && (_jsx("span", { className: "text-xs text-text-muted shrink-0", children: parseDueDate(task.dueDate).toLocaleDateString() })), _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" })] }, task.uid))) })] }));
|
|
389
|
+
function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, toggling, onDelete }) {
|
|
390
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle, pending: toggling.has(task.uid) }), _jsx("span", { className: ["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), task.priority !== "normal" && (_jsx("span", { className: ["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] })), task.dueDate && (_jsx("span", { className: "text-xs text-text-muted shrink-0", children: parseDueDate(task.dueDate).toLocaleDateString() })), _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" })] }, task.uid))) })] }));
|
|
329
391
|
}
|
|
330
|
-
function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }) {
|
|
392
|
+
function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, toggling, onDelete }) {
|
|
331
393
|
const allChecked = tasks.length > 0 && tasks.every((t) => checkedUids.has(t.uid));
|
|
332
394
|
function toggleAll() {
|
|
333
395
|
if (allChecked) {
|
|
@@ -345,7 +407,7 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete })
|
|
|
345
407
|
}
|
|
346
408
|
}
|
|
347
409
|
}
|
|
348
|
-
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle }) }), _jsx("td", { className: ["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), _jsx("td", { className: "px-3 py-2 text-text-muted", children: task.dueDate ? parseDueDate(task.dueDate).toLocaleDateString() : "" }), _jsx("td", { className: ["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" }) })] }, task.uid))) })] }) }));
|
|
410
|
+
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle, pending: toggling.has(task.uid) }) }), _jsx("td", { className: ["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), _jsx("td", { className: "px-3 py-2 text-text-muted", children: task.dueDate ? parseDueDate(task.dueDate).toLocaleDateString() : "" }), _jsx("td", { className: ["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" }) })] }, task.uid))) })] }) }));
|
|
349
411
|
}
|
|
350
412
|
export default TasksPage;
|
|
351
413
|
/** The tab's title: `Brand: Tasks` (see `pageTitle()`). */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* own server-internal `recordAuditLog()`.
|
|
10
10
|
*/
|
|
11
11
|
import { withClient } from "../util/api.js";
|
|
12
|
-
import { buildQuery } from "../util/apiQuery.js";
|
|
12
|
+
import { buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
13
13
|
/** `client`, given below, is an explicit `ApiClient` from `createApiClient()` (e.g. one account of a multi-account
|
|
14
14
|
* app) to call instead of the default global `apiFetch()` - see `withClient()`'s own doc comment in `util/api.ts`.
|
|
15
15
|
* Omitted (the default), this behaves exactly as before. */
|
|
@@ -23,5 +23,5 @@ export function listAuditLog(filters = {}, params = {}, client) {
|
|
|
23
23
|
extra.action = filters.action;
|
|
24
24
|
if (filters.targetType)
|
|
25
25
|
extra.targetType = filters.targetType;
|
|
26
|
-
return withClient(client, `/mail/audit-log?${buildQuery(params, extra)}`);
|
|
26
|
+
return withClient(client, `/mail/audit-log?${buildQuery(params, { ...extra, ...sortQuery({ dateCreated: "DESC", uid: "ASC" }) })}`);
|
|
27
27
|
}
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* trusted-role-only, no self-service creation or per-list delegated ownership (v1).
|
|
8
8
|
*/
|
|
9
9
|
import { withClient } from "../util/api.js";
|
|
10
|
-
import { buildQuery } from "../util/apiQuery.js";
|
|
10
|
+
import { buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
11
11
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
12
12
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
13
13
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
14
14
|
export function listDistributionLists(params = {}, client) {
|
|
15
|
-
return withClient(client, `/mail/distribution-lists?${buildQuery(params)}`);
|
|
15
|
+
return withClient(client, `/mail/distribution-lists?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
|
|
16
16
|
}
|
|
17
17
|
export function getDistributionList(uid, client) {
|
|
18
18
|
return withClient(client, `/mail/distribution-lists/${encodeURIComponent(uid)}`);
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* admin-scoped entities.
|
|
9
9
|
*/
|
|
10
10
|
import { withClient } from "../util/api.js";
|
|
11
|
-
import { buildQuery } from "../util/apiQuery.js";
|
|
11
|
+
import { buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
12
12
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
13
13
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
14
14
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
15
15
|
export function listDomains(params = {}, client) {
|
|
16
|
-
return withClient(client, `/mail/domains?${buildQuery(params)}`);
|
|
16
|
+
return withClient(client, `/mail/domains?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
|
|
17
17
|
}
|
|
18
18
|
export function getDomain(uid, client) {
|
|
19
19
|
return withClient(client, `/mail/domains/${encodeURIComponent(uid)}`);
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
* enforces it and a non-trusted caller simply gets a 403 `ApiRequestError` back.
|
|
13
13
|
*/
|
|
14
14
|
import { withClient } from "../util/api.js";
|
|
15
|
-
import { buildQuery } from "../util/apiQuery.js";
|
|
15
|
+
import { buildQuery, sortQuery } from "../util/apiQuery.js";
|
|
16
16
|
/** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
|
|
17
17
|
* account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
|
|
18
18
|
* own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
|
|
19
19
|
export function listAuditLogEntries(params = {}, client) {
|
|
20
|
-
return withClient(client, `/escrow/audit-log?${buildQuery(params)}`);
|
|
20
|
+
return withClient(client, `/escrow/audit-log?${buildQuery(params, sortQuery({ sequence: "DESC" }))}`);
|
|
21
21
|
}
|
|
22
22
|
export function getAuditLogEntry(uid, client) {
|
|
23
23
|
return withClient(client, `/escrow/audit-log/${encodeURIComponent(uid)}`);
|