@rapidmx/web-client 0.15.1 → 0.17.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/README.md +381 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +166 -164
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +350 -348
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +129 -124
- package/apps/admin/mailboxes/[uid].tsx +271 -224
- package/apps/admin/mailboxes/new/index.tsx +30 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +581 -571
- package/apps/www/contacts/[uid].tsx +112 -109
- package/apps/www/contacts/index.tsx +14 -3
- package/apps/www/index.tsx +2962 -2479
- package/apps/www/messages/[uid].tsx +139 -135
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +136 -133
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +1290 -1287
- package/apps/www/settings/filters/[uid].tsx +179 -176
- package/apps/www/settings/filters/index.tsx +105 -102
- package/apps/www/settings/filters/new/index.tsx +165 -140
- package/apps/www/settings/labels/index.tsx +207 -204
- package/apps/www/settings/privacy/index.tsx +495 -492
- package/apps/www/settings/profile/index.tsx +251 -248
- package/apps/www/settings/read-receipts/index.tsx +150 -147
- package/apps/www/settings/sharing/index.tsx +259 -256
- package/apps/www/settings/signatures/[uid].tsx +175 -172
- package/apps/www/settings/signatures/index.tsx +91 -88
- package/apps/www/settings/signatures/new/index.tsx +138 -135
- package/apps/www/tasks/index.tsx +654 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +5 -2
- package/dist/apps/admin/mailboxes/[uid].js +15 -3
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
- package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +7 -4
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +8 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +542 -207
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useState } from "react";
|
|
8
8
|
import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
9
|
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
@@ -54,4 +54,6 @@ function ReadReceiptsContent() {
|
|
|
54
54
|
}
|
|
55
55
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Read Receipts" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Controls when this mailbox requests a delivery/read receipt on mail it sends, and how it responds to a receipt requested of it. A per-message \u201CRequest a read receipt\u201D checkbox in Compose always overrides the request-side settings below for that one message." }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Request receipts when sending" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptInternal, onChange: (e) => setAlwaysRequestReceiptInternal(e.target.checked) }), "From internal recipients (mail to this organization)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptFederated, onChange: (e) => setAlwaysRequestReceiptFederated(e.target.checked) }), "From federated recipients (partner organizations)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptExternal, onChange: (e) => setAlwaysRequestReceiptExternal(e.target.checked) }), "From external recipients"] })] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Respond automatically to requests" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsInternal, onChange: (e) => setAutoSendReceiptsInternal(e.target.checked) }), "From internal senders"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsFederated, onChange: (e) => setAutoSendReceiptsFederated(e.target.checked) }), "From federated senders (partner organizations)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsExternal, onChange: (e) => setAutoSendReceiptsExternal(e.target.checked) }), "From external senders"] }), _jsx("p", { className: "text-xs text-text-muted", children: "Unchecked means a request from that sender waits in your Inbox for you to send or decline it explicitly." })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
56
56
|
}
|
|
57
|
-
export default
|
|
57
|
+
export default SettingsReadReceiptsPage;
|
|
58
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
59
|
+
export const title = pageTitle("Settings");
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { SettingsShellProps } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
2
3
|
export type SettingsSharingPageProps = Omit<SettingsShellProps, "active">;
|
|
3
|
-
declare
|
|
4
|
-
export default
|
|
4
|
+
declare function SettingsSharingPage(props: SettingsSharingPageProps): React.JSX.Element;
|
|
5
|
+
export default SettingsSharingPage;
|
|
6
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
7
|
+
export declare const title: (props: {
|
|
8
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
9
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { listMailboxAccess, removeMailboxAccess, setMailboxAccess, } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
|
|
@@ -109,4 +109,6 @@ function SharingContent() {
|
|
|
109
109
|
}
|
|
110
110
|
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: "Sharing" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage who else can access ", mailbox?.displayName ?? "this mailbox", "\u2019s mail, calendar, contacts, and tasks. Granting access here covers all of it at once - there\u2019s nothing else to configure per data type."] })] }), status === "loading" && _jsx(SkeletonList, { count: 3 }), status === "forbidden" && (_jsx(Alert, { children: "You don\u2019t have permission to manage sharing for this mailbox." })), status === "error" && error && _jsx(Alert, { children: error }), status === "ready" && (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "People with access" }), members.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No one else has access to this mailbox yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: members.map((member) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { className: "truncate", children: [member.userOrRoleId, member.noEffect && (_jsx("span", { className: "block text-xs text-danger", children: "Not a user - this entry has no effect. Replace it with the person it was meant for, or remove it." }))] }), _jsxs("span", { className: "flex items-center gap-2 shrink-0", children: [member.noEffect && (_jsx(Button, { type: "button", variant: "text", className: "!w-auto", onClick: () => setReplacing(member), children: "Replace with a user" })), _jsxs("select", { "aria-label": `Role for ${member.userOrRoleId}`, className: "text-sm border border-border rounded-sm py-1 px-2 bg-surface", value: member.role, disabled: pendingRoleChange === member.userOrRoleId, onChange: (e) => handleRoleChange(member.userOrRoleId, e.target.value), children: [member.role === "custom" && (_jsx("option", { value: "custom", disabled: true, children: "Custom access" })), Object.keys(ROLE_LABELS).map((role) => (_jsx("option", { value: role, children: ROLE_LABELS[role] }, role)))] }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto text-danger", onClick: () => setRemoveTarget(member), children: "Remove" })] })] }, member.userOrRoleId))) }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: replacing ? `Replace ${replacing.userOrRoleId}` : "Add someone" }), _jsx(PrincipalPicker, { mailboxUid: mailboxUid, roleLabels: ROLE_LABELS, initialPrincipal: replacing?.userOrRoleId, defaultRole: replacing?.role === "manager" ? "manager" : "viewer", placeholder: "Email address or username", onGranted: handleGranted, onCancel: replacing ? () => setReplacing(null) : undefined }, replacing?.userOrRoleId ?? "new")] })] })), _jsxs(Modal, { open: !!removeTarget, onClose: closeRemoveModal, title: "Remove access", children: [_jsxs("p", { className: "text-sm mb-5", children: [removeTarget?.userOrRoleId, " will no longer be able to access ", mailbox?.displayName ?? "this mailbox", "."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: removing, onClick: closeRemoveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: removing, disabled: removing, onClick: handleRemove, children: "Remove" })] })] })] }) }));
|
|
111
111
|
}
|
|
112
|
-
export default
|
|
112
|
+
export default SettingsSharingPage;
|
|
113
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
114
|
+
export const title = pageTitle("Settings");
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { SettingsShellProps } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
2
3
|
export type SignatureDetailPageProps = Omit<SettingsShellProps, "active"> & {
|
|
3
4
|
params: {
|
|
4
5
|
uid: string;
|
|
5
6
|
};
|
|
6
7
|
};
|
|
7
|
-
declare
|
|
8
|
-
export default
|
|
8
|
+
declare function SignatureDetailPage(props: SignatureDetailPageProps): React.JSX.Element;
|
|
9
|
+
export default SignatureDetailPage;
|
|
10
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
11
|
+
export declare const title: (props: {
|
|
12
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
13
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { getMailSignature, listMailSignatures, updateMailSignature } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
@@ -97,4 +97,6 @@ function SignatureDetailContent({ uid }) {
|
|
|
97
97
|
}
|
|
98
98
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("a", { href: `/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 All signatures" }), _jsx("h1", { className: "text-lg font-bold tracking-tight mt-1 mb-5", children: original.name }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6 flex flex-col gap-4", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForNewMessages, onChange: (e) => setIsDefaultForNewMessages(e.target.checked) }), "Use for new messages"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForReplyForward, onChange: (e) => setIsDefaultForReplyForward(e.target.checked) }), "Use for replies and forwards"] })] }), _jsx("div", { className: "h-64", children: _jsx(RichTextEditor, { value: contentHtml, onChange: setContentHtml, fill: true, onUploadImage: handleUploadImage }) }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] })] }) }));
|
|
99
99
|
}
|
|
100
|
-
export default
|
|
100
|
+
export default SignatureDetailPage;
|
|
101
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
102
|
+
export const title = pageTitle("Settings");
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { SettingsShellProps } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
2
3
|
export type SettingsSignaturesPageProps = Omit<SettingsShellProps, "active">;
|
|
3
|
-
declare
|
|
4
|
-
export default
|
|
4
|
+
declare function SettingsSignaturesPage(props: SettingsSignaturesPageProps): React.JSX.Element;
|
|
5
|
+
export default SettingsSignaturesPage;
|
|
6
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
7
|
+
export declare const title: (props: {
|
|
8
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
9
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
@@ -33,4 +33,6 @@ function SignaturesContent() {
|
|
|
33
33
|
.filter(Boolean)
|
|
34
34
|
.join(" · ") }))] }), _jsx("a", { href: `/settings/signatures/${encodeURIComponent(sig.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-primary-dark hover:underline font-medium text-sm", children: "Edit" })] }, sig.uid))) }))] }) }));
|
|
35
35
|
}
|
|
36
|
-
export default
|
|
36
|
+
export default SettingsSignaturesPage;
|
|
37
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
38
|
+
export const title = pageTitle("Settings");
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { SettingsShellProps } from "../../../../shared/components/settings/layout/SettingsShell.js";
|
|
2
3
|
export type NewSignaturePageProps = Omit<SettingsShellProps, "active">;
|
|
3
|
-
declare
|
|
4
|
-
export default
|
|
4
|
+
declare function NewSignaturePage(props: NewSignaturePageProps): React.JSX.Element;
|
|
5
|
+
export default NewSignaturePage;
|
|
6
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
7
|
+
export declare const title: (props: {
|
|
8
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
9
|
+
}) => string;
|
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
7
|
-
import { useNavigate } from "../../../../shared/navigation/
|
|
6
|
+
import { pageTitle } from "../../../../shared/navigation/pageTitle.js";
|
|
7
|
+
import { useNavigate } from "../../../../shared/navigation/index.js";
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { createMailSignature, listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
10
10
|
import { clearPreviousDefaults } from "../signatureDefaults.js";
|
|
@@ -65,4 +65,6 @@ function NewSignatureForm() {
|
|
|
65
65
|
}
|
|
66
66
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-5", children: "New signature" }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6 flex flex-col gap-4", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "Work signature" }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForNewMessages, onChange: (e) => setIsDefaultForNewMessages(e.target.checked) }), "Use for new messages"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForReplyForward, onChange: (e) => setIsDefaultForReplyForward(e.target.checked) }), "Use for replies and forwards"] })] }), _jsx("div", { className: "h-64", children: _jsx(RichTextEditor, { value: contentHtml, onChange: setContentHtml, fill: true, onUploadImage: handleUploadImage }) }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create signature" }), _jsx("a", { href: `/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] })] }) }));
|
|
67
67
|
}
|
|
68
|
-
export default
|
|
68
|
+
export default NewSignaturePage;
|
|
69
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
70
|
+
export const title = pageTitle("Settings");
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { TasksShellProps } from "../../shared/components/tasks/layout/TasksShell.js";
|
|
2
|
-
declare
|
|
3
|
-
export default
|
|
3
|
+
declare function TasksPage(props: TasksShellProps): React.JSX.Element;
|
|
4
|
+
export default TasksPage;
|
|
5
|
+
/** The tab's title: `Brand: Tasks` (see `pageTitle()`). */
|
|
6
|
+
export declare const title: (props: {
|
|
7
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
8
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { endOfWeek, isAfter, isBefore, isToday, parseISO, startOfDay } from "date-fns";
|
|
9
9
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
@@ -292,7 +292,7 @@ function TasksContent() {
|
|
|
292
292
|
const completedTasks = viewFiltered.filter((t) => t.completed);
|
|
293
293
|
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`,
|
|
294
294
|
// `writableMailboxes` only has entries once `mailboxUid` has resolved.
|
|
295
|
-
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)}`, 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) => {
|
|
295
|
+
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) => {
|
|
296
296
|
const items = grouped.get(bucket);
|
|
297
297
|
if (items.length === 0) {
|
|
298
298
|
return null;
|
|
@@ -345,4 +345,6 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete })
|
|
|
345
345
|
}
|
|
346
346
|
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))) })] }) }));
|
|
347
347
|
}
|
|
348
|
-
export default
|
|
348
|
+
export default TasksPage;
|
|
349
|
+
/** The tab's title: `Brand: Tasks` (see `pageTitle()`). */
|
|
350
|
+
export const title = pageTitle("Tasks");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/web-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "RapidMX's webmail and admin console React UI for web-based clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": "https://github.com/rapidmx/web-client",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"trailingComma": "es5"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
+
"@rapidrest/react": ">=2.1.0 <3",
|
|
35
36
|
"react": "19.x",
|
|
36
37
|
"react-dom": "19.x"
|
|
37
38
|
},
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"@dnd-kit/core": "^6.3.1",
|
|
40
41
|
"@journeyapps/wa-sqlite": "^2.0.4",
|
|
41
42
|
"@peculiar/x509": "^2.1.0",
|
|
42
|
-
"@rapidmx/react-shared": "^0.
|
|
43
|
+
"@rapidmx/react-shared": "^0.19.0",
|
|
43
44
|
"@tiptap/core": "^3.31.3",
|
|
44
45
|
"@tiptap/extension-highlight": "^3.31.3",
|
|
45
46
|
"@tiptap/extension-image": "^3.31.3",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"reflect-metadata": "^0.2.2"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
64
|
+
"@rapidrest/react": "^2.1.0",
|
|
63
65
|
"@tailwindcss/vite": "^4.3.3",
|
|
64
66
|
"@testing-library/dom": "^10.4.1",
|
|
65
67
|
"@testing-library/jest-dom": "^7.0.1",
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
/**
|
|
6
|
-
* The web client's client-side router: what lets Mail, Calendar, Contacts, Tasks and Settings replace one another - and a
|
|
7
|
-
* folder replace a folder - without a page load.
|
|
8
|
-
*
|
|
9
|
-
* `@rapidrest/react` has no router of its own: every page is a server-rendered document with its own hydration entry, and
|
|
10
|
-
* hydrates only the page component (never `_layout.tsx`). So the router is a component the pages render themselves - each
|
|
11
|
-
* page's default export is `routedPage(path, Page)` (see `apps/www/_routedPage.tsx`), which renders `AppRouter` with the page
|
|
12
|
-
* as its first content. The first load is unchanged: the server renders the same tree, the browser hydrates it.
|
|
13
|
-
*
|
|
14
|
-
* `AppRouter` keeps ONE `AppChrome` (the icon rail, header, user menu, impersonation banner, compose windows, unlock prompt,
|
|
15
|
-
* idle-key timer, sign-out listener) mounted and swaps only what is inside it. A page's own shell (`MailShell`,
|
|
16
|
-
* `CalendarShell`, ...) still renders `AppShell` as it always did; inside the frame that renders nothing but its children.
|
|
17
|
-
*
|
|
18
|
-
* Navigation:
|
|
19
|
-
*
|
|
20
|
-
* Every `<a href>` to a same-origin path in the route table is intercepted (plain left clicks only - not with a modifier key, not
|
|
21
|
-
* `target`/`download`, not a hash-only link, not one marked `data-full-reload`), so pages just write ordinary links and
|
|
22
|
-
* middle-click, "open in new tab" and copy-link keep working. Anything not in the table - the admin and escrow consoles, plugin
|
|
23
|
-
* pages, other sites - is left to the browser. `useNavigate()` does the same for code (`navigate("/contacts")`).
|
|
24
|
-
*
|
|
25
|
-
* Back and forward work (`popstate`), and the URL is always the real, shareable one (`?mailboxUid=&folderUid=` and all).
|
|
26
|
-
*
|
|
27
|
-
* A route's code is loaded (`route.load()`) before the URL and the page change, so the old page stays up meanwhile (the frame is
|
|
28
|
-
* `aria-busy`); a route that fails to load falls back to a real navigation, which also picks up a new deploy.
|
|
29
|
-
*
|
|
30
|
-
* The next page's code is fetched on hover, focus and pointer-down of a link to it, and for the app rail's pages when the browser
|
|
31
|
-
* is idle.
|
|
32
|
-
*
|
|
33
|
-
* After a page change the frame moves keyboard focus to the content region, scrolls to the top, sets `document.title` and
|
|
34
|
-
* announces the new page to screen readers.
|
|
35
|
-
*
|
|
36
|
-
* The props of every `apps/www` page are the same for all of them (`WwwRoute.fetchProps()`: the user, auth-server URL,
|
|
37
|
-
* branding, plugin navigation, impersonation) except `params`, which the router recomputes from the URL - so the props of the
|
|
38
|
-
* page that was loaded are what every later page gets.
|
|
39
|
-
*/
|
|
40
|
-
import React, { ComponentType, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
41
|
-
import { AppChrome, type AppShellProps } from "../components/layout/AppShell.js";
|
|
42
|
-
import { AppFrameContext, AppFrameContextValue } from "./frameContext.js";
|
|
43
|
-
import { whenIdle, shouldSaveData } from "./idle.js";
|
|
44
|
-
import { RouteDefinition, RouteMatch, matchRoute } from "./routes.js";
|
|
45
|
-
import { NavigateFn, RouterContext, RouterContextValue, RouterLocation, UNKNOWN_LOCATION, readWindowLocation } from "./routerContext.js";
|
|
46
|
-
|
|
47
|
-
export { useLocation, useLocationSearch, useNavigate } from "./routerContext.js";
|
|
48
|
-
export type { NavigateFn, NavigateOptions, RouterLocation } from "./routerContext.js";
|
|
49
|
-
|
|
50
|
-
/** What `routedPage()` gives the router: the page component itself, without the router around it. */
|
|
51
|
-
export type RoutedPageComponent<P = any> = ComponentType<P> & { page: ComponentType<P> };
|
|
52
|
-
|
|
53
|
-
interface CurrentPage {
|
|
54
|
-
Page: ComponentType<any>;
|
|
55
|
-
/** The route template shown. */
|
|
56
|
-
routePath: string;
|
|
57
|
-
active: string;
|
|
58
|
-
params: Record<string, string>;
|
|
59
|
-
/** Changes with the pathname, so a different page (or the same page for a different `:uid`) mounts fresh, while a change
|
|
60
|
-
* of query string alone - a folder - keeps the page and its state. */
|
|
61
|
-
key: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface AppRouterProps {
|
|
65
|
-
routes: readonly RouteDefinition[];
|
|
66
|
-
/** The template of the route this document was rendered for. */
|
|
67
|
-
initialPath: string;
|
|
68
|
-
initialPage: ComponentType<any>;
|
|
69
|
-
/** The props the server rendered the page with. */
|
|
70
|
-
pageProps: Record<string, any>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function pickChromeProps(props: Record<string, any>): Omit<AppShellProps, "active"> {
|
|
74
|
-
const { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance } = props;
|
|
75
|
-
return { userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The anchor a click/pointer event is on or in, if it is a link. */
|
|
79
|
-
function linkOf(event: Event): HTMLAnchorElement | null {
|
|
80
|
-
const target = event.target;
|
|
81
|
-
return target instanceof Element ? target.closest("a[href]") : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The URL of a link the router may take over, `null` for one the browser must handle itself. */
|
|
85
|
-
function routerUrlOf(link: HTMLAnchorElement): URL | null {
|
|
86
|
-
if ((link.target && link.target !== "_self") || link.hasAttribute("download") || link.hasAttribute("data-full-reload")) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
const url = new URL(link.href, window.location.href);
|
|
90
|
-
if (url.origin !== window.location.origin) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
// A link that only moves within the page (`#section`) is the browser's own.
|
|
94
|
-
if (url.hash !== "" && url.pathname === window.location.pathname && url.search === window.location.search) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return url;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export default function AppRouter({ routes, initialPath, initialPage, pageProps }: AppRouterProps) {
|
|
101
|
-
const [current, setCurrent] = useState<CurrentPage>(() => ({
|
|
102
|
-
Page: initialPage,
|
|
103
|
-
routePath: initialPath,
|
|
104
|
-
active: routes.find((route) => route.path === initialPath)?.active ?? "mail",
|
|
105
|
-
params: pageProps.params ?? {},
|
|
106
|
-
key: "initial",
|
|
107
|
-
}));
|
|
108
|
-
const [location, setLocation] = useState<RouterLocation>(UNKNOWN_LOCATION);
|
|
109
|
-
const [pending, setPending] = useState(false);
|
|
110
|
-
const [takeovers, setTakeovers] = useState(0);
|
|
111
|
-
|
|
112
|
-
const currentRef = useRef(current);
|
|
113
|
-
currentRef.current = current;
|
|
114
|
-
/** Identifies the latest navigation; one that finds it changed while loading has been overtaken and does nothing. */
|
|
115
|
-
const navigationRef = useRef(0);
|
|
116
|
-
/** The page components loaded or loading, by route template - one request per page however many links are touched. */
|
|
117
|
-
const pagesRef = useRef(new Map<string, Promise<ComponentType<any>>>([[initialPath, Promise.resolve(initialPage)]]));
|
|
118
|
-
const routesRef = useRef(routes);
|
|
119
|
-
routesRef.current = routes;
|
|
120
|
-
|
|
121
|
-
/** Loads a route's page component (once). Rejects if its code can't be loaded - and then a later attempt tries again. */
|
|
122
|
-
const pageFor = useCallback((route: RouteDefinition): Promise<ComponentType<any>> => {
|
|
123
|
-
let loading = pagesRef.current.get(route.path);
|
|
124
|
-
if (!loading) {
|
|
125
|
-
loading = route.load().then((module) => {
|
|
126
|
-
const page = (module.default as Partial<RoutedPageComponent>).page;
|
|
127
|
-
if (!page) {
|
|
128
|
-
throw new Error(`The page for ${route.path} is not a routedPage().`);
|
|
129
|
-
}
|
|
130
|
-
return page;
|
|
131
|
-
});
|
|
132
|
-
pagesRef.current.set(route.path, loading);
|
|
133
|
-
loading.catch(() => pagesRef.current.delete(route.path));
|
|
134
|
-
}
|
|
135
|
-
return loading;
|
|
136
|
-
}, []);
|
|
137
|
-
|
|
138
|
-
/** The pathname of the page on screen (the address bar's, once a navigation has been committed). */
|
|
139
|
-
const shownPathRef = useRef<string | null>(null);
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Shows `match` (loading its page first if it is a different page from the one on screen), calling `commit` - which puts
|
|
143
|
-
* the URL in the address bar - at the moment the new page replaces the old one. Resolves `false` when the page's code
|
|
144
|
-
* couldn't be loaded, and `true` otherwise, including when a newer navigation overtook this one while it loaded.
|
|
145
|
-
*/
|
|
146
|
-
const show = useCallback(
|
|
147
|
-
async (match: RouteMatch, target: RouterLocation, sequence: number, commit: () => void): Promise<boolean> => {
|
|
148
|
-
const pathChanged = target.pathname !== shownPathRef.current;
|
|
149
|
-
let Page = currentRef.current.Page;
|
|
150
|
-
if (pathChanged && match.route.path !== currentRef.current.routePath) {
|
|
151
|
-
try {
|
|
152
|
-
Page = await pageFor(match.route);
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (sequence !== navigationRef.current) {
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
commit();
|
|
161
|
-
shownPathRef.current = target.pathname;
|
|
162
|
-
if (pathChanged) {
|
|
163
|
-
setCurrent({ Page, routePath: match.route.path, active: match.route.active, params: match.params, key: target.pathname });
|
|
164
|
-
}
|
|
165
|
-
setLocation(target);
|
|
166
|
-
return true;
|
|
167
|
-
},
|
|
168
|
-
[pageFor],
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const navigate = useCallback<NavigateFn>(
|
|
172
|
-
(href, options = {}) => {
|
|
173
|
-
const url = new URL(href, window.location.href);
|
|
174
|
-
const leave = () => (options.replace ? window.location.replace(url.href) : (window.location.href = url.href));
|
|
175
|
-
const match = url.origin === window.location.origin ? matchRoute(routesRef.current, url.pathname) : undefined;
|
|
176
|
-
if (!match) {
|
|
177
|
-
leave();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const target: RouterLocation = { pathname: url.pathname, search: url.search, hash: url.hash };
|
|
181
|
-
const now = readWindowLocation();
|
|
182
|
-
if (target.pathname === now.pathname && target.search === now.search && target.hash === now.hash) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const sequence = ++navigationRef.current;
|
|
186
|
-
setPending(true);
|
|
187
|
-
void show(match, target, sequence, () => {
|
|
188
|
-
window.history[options.replace ? "replaceState" : "pushState"](null, "", target.pathname + target.search + target.hash);
|
|
189
|
-
}).then((ok) => {
|
|
190
|
-
if (!ok) {
|
|
191
|
-
leave();
|
|
192
|
-
}
|
|
193
|
-
if (sequence === navigationRef.current) {
|
|
194
|
-
setPending(false);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
},
|
|
198
|
-
[show],
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// The browser's own location is read after the first render, never during it: the server can't know the query string.
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
const here = readWindowLocation();
|
|
204
|
-
shownPathRef.current = here.pathname;
|
|
205
|
-
setLocation(here);
|
|
206
|
-
}, []);
|
|
207
|
-
|
|
208
|
-
// Back and forward.
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
function handlePopState() {
|
|
211
|
-
const here = readWindowLocation();
|
|
212
|
-
const match = matchRoute(routesRef.current, here.pathname);
|
|
213
|
-
if (!match) {
|
|
214
|
-
window.location.reload();
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const sequence = ++navigationRef.current;
|
|
218
|
-
setPending(true);
|
|
219
|
-
void show(match, here, sequence, () => undefined).then((ok) => {
|
|
220
|
-
if (!ok) {
|
|
221
|
-
window.location.reload();
|
|
222
|
-
}
|
|
223
|
-
if (sequence === navigationRef.current) {
|
|
224
|
-
setPending(false);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
window.addEventListener("popstate", handlePopState);
|
|
229
|
-
return () => window.removeEventListener("popstate", handlePopState);
|
|
230
|
-
}, [show]);
|
|
231
|
-
|
|
232
|
-
// Plain links, and fetching what they lead to before they are clicked.
|
|
233
|
-
useEffect(() => {
|
|
234
|
-
function handleClick(event: MouseEvent) {
|
|
235
|
-
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const link = linkOf(event);
|
|
239
|
-
const url = link && routerUrlOf(link);
|
|
240
|
-
if (!url || !matchRoute(routesRef.current, url.pathname)) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
event.preventDefault();
|
|
244
|
-
navigate(url.pathname + url.search + url.hash);
|
|
245
|
-
}
|
|
246
|
-
function handleIntent(event: Event) {
|
|
247
|
-
const link = linkOf(event);
|
|
248
|
-
const url = link && routerUrlOf(link);
|
|
249
|
-
const match = url && matchRoute(routesRef.current, url.pathname);
|
|
250
|
-
if (match) {
|
|
251
|
-
pageFor(match.route).catch(() => undefined);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
document.addEventListener("click", handleClick);
|
|
255
|
-
document.addEventListener("pointerover", handleIntent);
|
|
256
|
-
document.addEventListener("pointerdown", handleIntent);
|
|
257
|
-
document.addEventListener("focusin", handleIntent);
|
|
258
|
-
return () => {
|
|
259
|
-
document.removeEventListener("click", handleClick);
|
|
260
|
-
document.removeEventListener("pointerover", handleIntent);
|
|
261
|
-
document.removeEventListener("pointerdown", handleIntent);
|
|
262
|
-
document.removeEventListener("focusin", handleIntent);
|
|
263
|
-
};
|
|
264
|
-
}, [navigate, pageFor]);
|
|
265
|
-
|
|
266
|
-
// The app rail's other pages, once the browser has nothing else to do - so that the likeliest next click is instant.
|
|
267
|
-
useEffect(() => {
|
|
268
|
-
if (shouldSaveData()) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const cancels = routesRef.current
|
|
272
|
-
.filter((route) => route.idlePrefetch)
|
|
273
|
-
.map((route) => whenIdle(() => void pageFor(route).catch(() => undefined)));
|
|
274
|
-
return () => cancels.forEach((cancel) => cancel());
|
|
275
|
-
}, [pageFor]);
|
|
276
|
-
|
|
277
|
-
const enterTakeover = useCallback(() => {
|
|
278
|
-
setTakeovers((n) => n + 1);
|
|
279
|
-
return () => setTakeovers((n) => n - 1);
|
|
280
|
-
}, []);
|
|
281
|
-
const frame = useMemo<AppFrameContextValue>(() => ({ enterTakeover }), [enterTakeover]);
|
|
282
|
-
const router = useMemo<RouterContextValue>(() => ({ location, navigate }), [location, navigate]);
|
|
283
|
-
|
|
284
|
-
const Page = current.Page;
|
|
285
|
-
return (
|
|
286
|
-
<RouterContext.Provider value={router}>
|
|
287
|
-
<AppFrameContext.Provider value={frame}>
|
|
288
|
-
<AppChrome
|
|
289
|
-
active={current.active}
|
|
290
|
-
routeKey={current.key}
|
|
291
|
-
busy={pending}
|
|
292
|
-
hideChrome={takeovers > 0}
|
|
293
|
-
{...pickChromeProps(pageProps)}
|
|
294
|
-
>
|
|
295
|
-
<Page key={current.key} {...pageProps} params={current.params} />
|
|
296
|
-
</AppChrome>
|
|
297
|
-
</AppFrameContext.Provider>
|
|
298
|
-
</RouterContext.Provider>
|
|
299
|
-
);
|
|
300
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
|
|
6
|
-
/** How long an idle callback may be held back on a busy page before it runs anyway. */
|
|
7
|
-
export const IDLE_TIMEOUT_MS = 3_000;
|
|
8
|
-
|
|
9
|
-
/** The delay used where the browser has no `requestIdleCallback` (Safari). */
|
|
10
|
-
export const IDLE_FALLBACK_DELAY_MS = 200;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Runs `callback` once the page has finished loading (the window `load` event, which waits for every script and image the page
|
|
14
|
-
* asked for) and the browser has nothing better to do (`requestIdleCallback`), or shortly after where there is no such thing.
|
|
15
|
-
* For work that only makes later things faster - fetching a code chunk, warming a cache - and must never compete with what
|
|
16
|
-
* the user is waiting for. Returns a function that cancels it if it hasn't run yet.
|
|
17
|
-
*/
|
|
18
|
-
export function whenIdle(callback: () => void): () => void {
|
|
19
|
-
let cancelIdle: () => void = () => undefined;
|
|
20
|
-
function schedule() {
|
|
21
|
-
if (typeof window.requestIdleCallback === "function") {
|
|
22
|
-
const handle = window.requestIdleCallback(callback, { timeout: IDLE_TIMEOUT_MS });
|
|
23
|
-
cancelIdle = () => window.cancelIdleCallback(handle);
|
|
24
|
-
} else {
|
|
25
|
-
const timer = setTimeout(callback, IDLE_FALLBACK_DELAY_MS);
|
|
26
|
-
cancelIdle = () => clearTimeout(timer);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (document.readyState === "complete") {
|
|
30
|
-
schedule();
|
|
31
|
-
} else {
|
|
32
|
-
window.addEventListener("load", schedule, { once: true });
|
|
33
|
-
}
|
|
34
|
-
return () => {
|
|
35
|
-
window.removeEventListener("load", schedule);
|
|
36
|
-
cancelIdle();
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** `true` when the user has asked the browser to save data (or is on a very slow connection): work that only speculates
|
|
41
|
-
* about what will be needed - prefetching - should not run then. */
|
|
42
|
-
export function shouldSaveData(): boolean {
|
|
43
|
-
const connection = (navigator as Navigator & { connection?: { saveData?: boolean; effectiveType?: string } }).connection;
|
|
44
|
-
return connection?.saveData === true || connection?.effectiveType === "slow-2g" || connection?.effectiveType === "2g";
|
|
45
|
-
}
|