@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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listLeftoverMailboxes } from "@rapidmx/react-shared/admin/leftoverMailboxApi.js";
|
|
9
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
|
+
import EraseLeftoverDataDialog from "./EraseLeftoverDataDialog.js";
|
|
12
|
+
/** How many deleted mailboxes one request asks for. */
|
|
13
|
+
const PAGE_SIZE = 50;
|
|
14
|
+
/** How often the list is refreshed while an erasure is under way. */
|
|
15
|
+
export const LEFTOVER_REFRESH_INTERVAL_MS = 5000;
|
|
16
|
+
const HEADING_CLASS = "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border";
|
|
17
|
+
const CELL_CLASS = "py-2.5 px-2.5 border-b border-border align-middle";
|
|
18
|
+
/** Whether the erasure the server last filed for `item` is still to finish. */
|
|
19
|
+
function isErasing(item) {
|
|
20
|
+
return item.erasure?.status === "approved" || item.erasure?.status === "in_progress";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* "Deleted mailboxes with remaining data": deleting a mailbox keeps everything in it, and the address can't be used again until
|
|
24
|
+
* that is erased, so this lists what is left (from `GET /mailboxes/leftover`) with an "Erase data" action for each. It shows nothing
|
|
25
|
+
* at all while there is nothing left - the section is only there when it is needed. An erasure that is under way shows as such, and
|
|
26
|
+
* the list refreshes itself until it has finished.
|
|
27
|
+
*/
|
|
28
|
+
export default function LeftoverMailboxesSection({ refreshIntervalMs = LEFTOVER_REFRESH_INTERVAL_MS, pollIntervalMs }) {
|
|
29
|
+
const [items, setItems] = useState([]);
|
|
30
|
+
const [next, setNext] = useState(undefined);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
33
|
+
const [target, setTarget] = useState(null);
|
|
34
|
+
// Only the newest reload is applied: a slow earlier one can't put back what a later one already removed.
|
|
35
|
+
const generation = useRef(0);
|
|
36
|
+
async function reload() {
|
|
37
|
+
const current = ++generation.current;
|
|
38
|
+
try {
|
|
39
|
+
const page = await listLeftoverMailboxes({ limit: PAGE_SIZE });
|
|
40
|
+
if (current !== generation.current)
|
|
41
|
+
return;
|
|
42
|
+
setItems(page.items);
|
|
43
|
+
setNext(page.next);
|
|
44
|
+
setError(null);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (current === generation.current) {
|
|
48
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not check for deleted mailboxes with remaining data.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function loadMore() {
|
|
53
|
+
const current = generation.current;
|
|
54
|
+
setLoadingMore(true);
|
|
55
|
+
try {
|
|
56
|
+
const page = await listLeftoverMailboxes({ limit: PAGE_SIZE, after: next });
|
|
57
|
+
if (current !== generation.current)
|
|
58
|
+
return;
|
|
59
|
+
setItems((existing) => [...existing, ...page.items.filter((item) => !existing.some((seen) => seen.mailboxUid === item.mailboxUid))]);
|
|
60
|
+
setNext(page.next);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (current === generation.current) {
|
|
64
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load more deleted mailboxes.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
setLoadingMore(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
void reload();
|
|
73
|
+
}, []);
|
|
74
|
+
const anyErasing = items.some(isErasing);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!anyErasing) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const timer = setInterval(() => void reload(), refreshIntervalMs);
|
|
80
|
+
return () => clearInterval(timer);
|
|
81
|
+
}, [anyErasing, refreshIntervalMs]);
|
|
82
|
+
// Only there when something is left (or the check failed).
|
|
83
|
+
const section = items.length > 0 || error !== null ? (_jsxs("section", { className: "mt-10", "aria-labelledby": "leftover-heading", children: [_jsx("h2", { id: "leftover-heading", className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-2", children: "Deleted mailboxes with remaining data" }), _jsx("p", { className: "text-sm text-text-muted mb-3", children: "Deleting a mailbox keeps everything in it, and its address can\u2019t be used for a new mailbox until that data is erased. Erasing is permanent." }), error && _jsx(Alert, { children: error }), items.length > 0 && (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("caption", { className: "sr-only", children: "Deleted mailboxes with remaining data" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: HEADING_CLASS, children: "Address" }), _jsx("th", { scope: "col", className: HEADING_CLASS, children: "Folders" }), _jsx("th", { scope: "col", className: HEADING_CLASS, children: "Messages" }), _jsx("th", { scope: "col", className: HEADING_CLASS, children: "Erasure" }), _jsx("th", { scope: "col", className: "border-b border-border", children: _jsx("span", { className: "sr-only", children: "Actions" }) })] }) }), _jsx("tbody", { children: items.map((item) => (_jsxs("tr", { children: [_jsx("td", { className: `${CELL_CLASS} break-all`, children: item.mailboxUid }), _jsx("td", { className: CELL_CLASS, children: item.folderCount }), _jsx("td", { className: CELL_CLASS, children: item.messageCount }), _jsx("td", { className: CELL_CLASS, children: isErasing(item) ? (_jsx("span", { className: "inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Erasing" })) : (_jsx("span", { className: "text-text-muted", children: "Not erased" })) }), _jsx("td", { className: `${CELL_CLASS} text-right`, children: _jsx(Button, { type: "button", variant: "secondary", className: isErasing(item) ? "!w-auto" : "!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger", "aria-label": `${isErasing(item) ? "Show the erasure of" : "Erase data of"} ${item.mailboxUid}`, onClick: () => setTarget(item), children: isErasing(item) ? "Show progress" : "Erase data" }) })] }, item.mailboxUid))) })] }) })), next !== undefined && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mt-3", loading: loadingMore, disabled: loadingMore, onClick: () => void loadMore(), children: "Load more" }))] })) : null;
|
|
84
|
+
// The dialog outlives the row it was opened from - erasing the last one empties the list while its "erased" message is still up - and
|
|
85
|
+
// keeps its place among the fragment's children whether or not the section is there, so it is not rebuilt (and does not lose where it
|
|
86
|
+
// is) when the section goes.
|
|
87
|
+
const dialog = target && (_jsx(EraseLeftoverDataDialog, { address: target.mailboxUid, counts: { folders: target.folderCount, messages: target.messageCount }, resume: isErasing(target) && target.erasure ? { uid: target.erasure.uid, status: target.erasure.status } : undefined, pollIntervalMs: pollIntervalMs, onErased: () => void reload(), onClose: () => {
|
|
88
|
+
setTarget(null);
|
|
89
|
+
void reload();
|
|
90
|
+
} }, target.mailboxUid));
|
|
91
|
+
return (_jsxs(_Fragment, { children: [section, dialog] }));
|
|
92
|
+
}
|
|
@@ -12,9 +12,9 @@ import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
|
12
12
|
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
13
13
|
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
14
14
|
/** A header that uses both variables - the snippet the Branding page offers to copy. */
|
|
15
|
-
export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
|
|
16
|
-
<span>{APP_TITLE}</span>
|
|
17
|
-
<span>{USER_MENU}</span>
|
|
15
|
+
export const HEADER_HTML_EXAMPLE = `<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
|
|
16
|
+
<span>{APP_TITLE}</span>
|
|
17
|
+
<span>{USER_MENU}</span>
|
|
18
18
|
</div>`;
|
|
19
19
|
/** SVG logo/icon uploads are refused (the server refuses them too): an SVG served from this origin can carry script. */
|
|
20
20
|
export function isSvgFile(file) {
|
|
@@ -6,12 +6,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { getMailboxPolicy } from "@rapidmx/react-shared/admin/mailboxPolicyApi.js";
|
|
9
|
+
import { leftoverConflictOf } from "@rapidmx/react-shared/admin/leftoverMailboxApi.js";
|
|
9
10
|
import { createMailbox, listMailboxDomains, resolveMailboxOwner } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
11
|
import { deviceTimeZone } from "@rapidmx/react-shared/util/timeZone.js";
|
|
11
12
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
13
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
13
14
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
14
15
|
import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
|
|
16
|
+
import EraseLeftoverDataDialog from "../mailboxes/EraseLeftoverDataDialog.js";
|
|
15
17
|
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
16
18
|
const SELECT_CLASS = "text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
17
19
|
/**
|
|
@@ -40,6 +42,10 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
40
42
|
const [maxDurationMinutes, setMaxDurationMinutes] = useState("");
|
|
41
43
|
const [error, setError] = useState(null);
|
|
42
44
|
const [saving, setSaving] = useState(false);
|
|
45
|
+
// The address a deleted mailbox left data at, when creating a mailbox there was refused for that - and what to do about it.
|
|
46
|
+
const [leftover, setLeftover] = useState(null);
|
|
47
|
+
const [erasing, setErasing] = useState(false);
|
|
48
|
+
const formRef = useRef(null);
|
|
43
49
|
// An empty `mail:domains` list (the default) means this server enforces no domain restriction —
|
|
44
50
|
// the address field stays free text, exactly as before. Once an admin configures at least one
|
|
45
51
|
// domain, every mailbox this server creates must be on one of them (enforced server-side too, in
|
|
@@ -61,10 +67,15 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
61
67
|
.catch(() => undefined);
|
|
62
68
|
}, []);
|
|
63
69
|
const constrained = domains.length > 0;
|
|
70
|
+
const address = constrained ? `${localPart.trim()}@${domain}` : primarySmtpAddress.trim();
|
|
71
|
+
// What was said about one address is not said about another: typing a different one takes the explanation away.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setLeftover((current) => (current && current.address !== address ? null : current));
|
|
74
|
+
}, [address]);
|
|
64
75
|
async function handleSubmit(e) {
|
|
65
76
|
e.preventDefault();
|
|
66
77
|
setError(null);
|
|
67
|
-
|
|
78
|
+
setLeftover(null);
|
|
68
79
|
if (constrained ? !localPart.trim() : !primarySmtpAddress.trim()) {
|
|
69
80
|
setError("A primary SMTP address is required.");
|
|
70
81
|
return;
|
|
@@ -106,13 +117,24 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
106
117
|
onCreated(mailbox);
|
|
107
118
|
}
|
|
108
119
|
catch (err) {
|
|
109
|
-
|
|
120
|
+
const conflict = leftoverConflictOf(err);
|
|
121
|
+
if (conflict) {
|
|
122
|
+
// Not a plain failure: the address is taken by what a deleted mailbox left, which can be erased right here.
|
|
123
|
+
setLeftover({ address, conflict });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
|
|
127
|
+
}
|
|
110
128
|
}
|
|
111
129
|
finally {
|
|
112
130
|
setSaving(false);
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
|
-
return (_jsxs(_Fragment, { children: [error && _jsx(Alert, { children: error }),
|
|
133
|
+
return (_jsxs(_Fragment, { children: [error && _jsx(Alert, { children: error }), leftover && (_jsxs("div", { role: "alert", className: "py-3 px-3.5 rounded-sm text-sm mb-5 bg-danger-bg text-danger", children: [_jsx("p", { className: "font-semibold mb-1", children: leftover.conflict.reason === "mailbox-data-erasing"
|
|
134
|
+
? "The data left at this address is being erased."
|
|
135
|
+
: "This address still has data from a deleted mailbox." }), _jsx("p", { className: "mb-3", children: leftover.conflict.reason === "mailbox-data-erasing"
|
|
136
|
+
? "A new mailbox can be created there once the erasure has finished."
|
|
137
|
+
: "Deleting a mailbox keeps everything in it, and a new mailbox can't take the address until that data is erased." }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setErasing(true), children: leftover.conflict.reason === "mailbox-data-erasing" ? "Show the erasure" : "Erase the leftover data" })] })), erasing && leftover && (_jsx(EraseLeftoverDataDialog, { address: leftover.address, resume: leftover.conflict.reason === "mailbox-data-erasing" ? leftover.conflict.erasure : undefined, doneLabel: "Create mailbox", onDone: () => formRef.current?.requestSubmit(), onClose: () => setErasing(false) }, leftover.address)), _jsxs("form", { ref: formRef, onSubmit: handleSubmit, className: "bg-surface border border-border rounded-md p-6", children: [constrained ? (_jsx(FormField, { label: "Local part", htmlFor: "localPart", children: _jsxs("div", { className: "flex gap-2 items-center", children: [_jsx("input", { id: "localPart", type: "text", className: INPUT_CLASS, value: localPart, onChange: (e) => setLocalPart(e.target.value), placeholder: "support" }), _jsx("span", { className: "text-text-muted shrink-0", children: "@" }), _jsx("select", { "aria-label": "Domain", className: SELECT_CLASS, value: domain, onChange: (e) => setDomain(e.target.value), children: domains.map((d) => (_jsx("option", { value: d, children: d }, d))) })] }) })) : (_jsx(FormField, { label: "Primary SMTP address", htmlFor: "primarySmtpAddress", children: _jsx("input", { id: "primarySmtpAddress", type: "email", className: INPUT_CLASS, value: primarySmtpAddress, onChange: (e) => setPrimarySmtpAddress(e.target.value), placeholder: "support@example.com" }) })), _jsx(FormField, { label: "Display name", htmlFor: "displayName", children: _jsx("input", { id: "displayName", type: "text", className: INPUT_CLASS, value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Support" }) }), _jsxs("div", { className: "mb-4", children: [_jsx("span", { className: "block text-sm font-semibold mb-1.5 text-text", children: "Owner" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "radio", name: "ownerMode", checked: ownerMode === "shared", onChange: () => {
|
|
116
138
|
setOwnerMode("shared");
|
|
117
139
|
setResolvedOwner(null);
|
|
118
140
|
} }), "Shared mailbox (no single owner)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "radio", name: "ownerMode", checked: ownerMode === "owned", onChange: () => setOwnerMode("owned") }), "Owned by a specific person"] }), ownerMode === "owned" &&
|
|
@@ -4,6 +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 { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import { HiCheck } from "react-icons/hi2";
|
|
8
9
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
10
|
import { getRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
@@ -49,6 +50,7 @@ function errorMessage(err, fallback) {
|
|
|
49
50
|
* except the domain can be passed over and configured later from the admin console.
|
|
50
51
|
*/
|
|
51
52
|
export default function SetupWizard({ userUid }) {
|
|
53
|
+
const { navigate } = useRouter();
|
|
52
54
|
const [step, setStep] = useState(null);
|
|
53
55
|
const [status, setStatus] = useState();
|
|
54
56
|
const [domains, setDomains] = useState([]);
|
|
@@ -141,7 +143,7 @@ export default function SetupWizard({ userUid }) {
|
|
|
141
143
|
// Let a step still being recorded land first, so it can't arrive after setup is marked finished.
|
|
142
144
|
await progress.current.running;
|
|
143
145
|
await completeSetup();
|
|
144
|
-
|
|
146
|
+
void navigate("/admin");
|
|
145
147
|
}
|
|
146
148
|
catch (err) {
|
|
147
149
|
setError(errorMessage(err, "Could not finish setup."));
|
|
@@ -131,6 +131,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
131
131
|
timezone: occurrence?.timezone ?? deviceZone,
|
|
132
132
|
attendees: occurrence?.attendees ?? [],
|
|
133
133
|
guestDraft: "",
|
|
134
|
+
guestInvalid: [],
|
|
134
135
|
recurrenceRule: occurrence?.recurrenceRule ?? null,
|
|
135
136
|
reminderMinutes: occurrence?.reminderMinutesBeforeStart?.toString() ?? "",
|
|
136
137
|
busyStatus: occurrence?.busyStatus ?? "busy",
|
|
@@ -195,12 +196,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
195
196
|
}, [occurrence?.videoMeetingUid]);
|
|
196
197
|
// "This event only" detaches a standalone, non-repeating copy - the series' rule doesn't apply to it.
|
|
197
198
|
const editingSingleOccurrence = !!occurrence?.isRecurringOccurrence && editScope === "occurrence";
|
|
198
|
-
dirtyRef.current = !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || hasDescriptionText(values.descriptionHtml));
|
|
199
|
-
function addGuests(text) {
|
|
200
|
-
const merged = mergeGuests(values.attendees, text);
|
|
201
|
-
update({ attendees: merged.attendees, guestDraft: merged.invalid.join(" ") });
|
|
202
|
-
return merged.invalid;
|
|
203
|
-
}
|
|
199
|
+
dirtyRef.current = !!(values.title.trim() || values.location.trim() || values.attendees.length > 0 || values.guestDraft.trim() || values.guestInvalid.length > 0 || hasDescriptionText(values.descriptionHtml));
|
|
204
200
|
function updateAttendee(index, patch) {
|
|
205
201
|
setValues((prev) => ({ ...prev, attendees: prev.attendees.map((a, i) => (i === index ? { ...a, ...patch } : a)) }));
|
|
206
202
|
}
|
|
@@ -382,8 +378,8 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
382
378
|
setError("A title is required.");
|
|
383
379
|
return;
|
|
384
380
|
}
|
|
385
|
-
// An address typed into the guests field but not added yet is added now - or, not being one, stops the save.
|
|
386
|
-
const merged = mergeGuests(values.attendees, values.guestDraft);
|
|
381
|
+
// An address typed into the guests field but not added yet is added now - or, not being one (nor what was already flagged there), stops the save.
|
|
382
|
+
const merged = mergeGuests(values.attendees, [...values.guestInvalid, values.guestDraft].join(", "), [effectiveOrganizerAddress]);
|
|
387
383
|
if (merged.invalid.length > 0) {
|
|
388
384
|
setError(`“${merged.invalid[0]}” isn’t a valid email address.`);
|
|
389
385
|
return;
|
|
@@ -394,7 +390,7 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
394
390
|
return;
|
|
395
391
|
}
|
|
396
392
|
if (values.guestDraft) {
|
|
397
|
-
update({ attendees: merged.attendees, guestDraft: "" });
|
|
393
|
+
update({ attendees: merged.attendees, guestDraft: "", guestInvalid: [] });
|
|
398
394
|
}
|
|
399
395
|
let startDate;
|
|
400
396
|
let endDate;
|
|
@@ -521,7 +517,6 @@ export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyn
|
|
|
521
517
|
deviceZone,
|
|
522
518
|
startWeekday: startWeekdayCode(values.allDay ? values.start : Number.isNaN(startMs) ? "" : new Date(startMs).toISOString(), values.allDay, values.timezone),
|
|
523
519
|
editingSingleOccurrence,
|
|
524
|
-
addGuests,
|
|
525
520
|
updateAttendee,
|
|
526
521
|
removeAttendee,
|
|
527
522
|
addResource,
|
|
@@ -17,8 +17,9 @@ export declare function VideoConferencingRow({ c }: {
|
|
|
17
17
|
c: EventFormController;
|
|
18
18
|
}): React.JSX.Element;
|
|
19
19
|
/**
|
|
20
|
-
* The guests field:
|
|
21
|
-
*
|
|
20
|
+
* The guests field: compose's recipient field (`GuestInput`), which looks a name up among the contacts and the server's mailboxes and lists and
|
|
21
|
+
* turns what is picked, typed or pasted into chips - drawn in the field itself in the quick popover. In the full form the guests are rows with a
|
|
22
|
+
* role, the answer so far and a remove button instead (the field then draws only what is not an address), and rooms and equipment can be added.
|
|
22
23
|
*/
|
|
23
24
|
export declare function GuestsField({ c, compact }: {
|
|
24
25
|
c: EventFormController;
|
|
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { useRef, useState } from "react";
|
|
7
7
|
import { HiOutlineBars3BottomLeft, HiOutlineBell, HiOutlineMapPin, HiOutlineUserGroup, HiOutlineVideoCamera, HiOutlineXMark } from "react-icons/hi2";
|
|
8
8
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
9
|
+
import GuestInput from "./GuestInput.js";
|
|
9
10
|
import LazyDescriptionEditor from "./LazyDescriptionEditor.js";
|
|
10
11
|
import RecurrenceEditor from "./RecurrenceEditor.js";
|
|
11
12
|
import ResourcePicker from "./ResourcePicker.js";
|
|
@@ -31,27 +32,15 @@ export function VideoConferencingRow({ c }) {
|
|
|
31
32
|
return (_jsxs(IconRow, { icon: _jsx(HiOutlineVideoCamera, { size: 20 }), children: [_jsxs("label", { className: "flex items-center gap-2 text-sm py-1.5", children: [_jsx("input", { type: "checkbox", checked: values.videoEnabled, onChange: (e) => c.update({ videoEnabled: e.target.checked }) }), "Add video conferencing"] }), values.videoEnabled && (_jsx("p", { className: "text-xs text-text-muted mb-1", children: "Changing attendees after enabling video conferencing won\u2019t update meeting invitees \u2014 turn this off and back on to reissue links to the current attendee list." })), c.videoError && (_jsx("p", { role: "alert", className: "text-xs text-danger mb-1", children: c.videoError })), c.videoMeetingUid && (_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: !c.joinUrl, onClick: () => window.open(c.joinUrl, "_blank", "noopener,noreferrer"), children: "Join video call" }), !c.joinUrl && (_jsx("span", { className: "text-xs text-text-muted", children: c.organizerJoinUrl === undefined ? "Loading the join link…" : "This meeting’s join link isn’t available." }))] }))] }));
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
* The guests field:
|
|
35
|
-
*
|
|
35
|
+
* The guests field: compose's recipient field (`GuestInput`), which looks a name up among the contacts and the server's mailboxes and lists and
|
|
36
|
+
* turns what is picked, typed or pasted into chips - drawn in the field itself in the quick popover. In the full form the guests are rows with a
|
|
37
|
+
* role, the answer so far and a remove button instead (the field then draws only what is not an address), and rooms and equipment can be added.
|
|
36
38
|
*/
|
|
37
39
|
export function GuestsField({ c, compact }) {
|
|
38
40
|
const { values } = c;
|
|
39
|
-
const [hint, setHint] = useState(null);
|
|
40
41
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
41
42
|
const pickerAnchor = useRef(null);
|
|
42
|
-
|
|
43
|
-
const invalid = c.addGuests(values.guestDraft);
|
|
44
|
-
setHint(invalid.length > 0 ? `“${invalid[0]}” isn’t a valid email address.` : null);
|
|
45
|
-
}
|
|
46
|
-
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("input", { type: "text", "aria-label": "Add guests", placeholder: "Add guests", className: compact ? QUIET_INPUT_CLASS : INPUT_CLASS, value: values.guestDraft, onChange: (e) => {
|
|
47
|
-
setHint(null);
|
|
48
|
-
c.update({ guestDraft: e.target.value });
|
|
49
|
-
}, onKeyDown: (e) => {
|
|
50
|
-
if (e.key === "Enter" || e.key === "," || e.key === ";") {
|
|
51
|
-
e.preventDefault();
|
|
52
|
-
commit();
|
|
53
|
-
}
|
|
54
|
-
} }), hint && (_jsx("p", { role: "alert", className: "text-xs text-danger", children: hint })), compact ? (values.attendees.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-1.5", children: values.attendees.map((attendee, i) => (_jsxs("li", { className: "flex items-center gap-1 text-xs rounded-full bg-surface-alt pl-2.5 pr-1 py-0.5", children: [_jsx("span", { className: "truncate max-w-[16rem]", children: attendee.address }), _jsx("button", { type: "button", onClick: () => c.removeAttendee(i), className: "w-4 h-4 flex items-center justify-center rounded-full text-text-muted hover:text-text", "aria-label": `Remove attendee ${i + 1}`, children: _jsx(HiOutlineXMark, { size: 12, "aria-hidden": "true" }) })] }, i))) }))) : (_jsxs(_Fragment, { children: [values.attendees.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: values.attendees.map((attendee, i) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "w-7 h-7 shrink-0 rounded-full bg-primary/15 text-primary-dark text-xs font-semibold flex items-center justify-center uppercase", children: (attendee.displayName || attendee.address).charAt(0) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm truncate", children: attendee.displayName || attendee.address }), _jsxs("div", { className: "text-xs text-text-muted truncate", children: [attendee.displayName ? `${attendee.address} · ` : "", RESPONSE_STATUS_LABEL[attendee.responseStatus]] })] }), _jsx("select", { className: SELECT_CLASS, value: attendee.role, onChange: (e) => c.updateAttendee(i, { role: e.target.value }), "aria-label": `Attendee role ${i + 1}`, children: ATTENDEE_ROLES.map((role) => (_jsx("option", { value: role, children: role }, role))) }), _jsx("button", { type: "button", onClick: () => c.removeAttendee(i), className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": `Remove attendee ${i + 1}`, children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }, i))) })), _jsx("button", { ref: pickerAnchor, type: "button", onClick: () => setPickerOpen(true), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add room/equipment" }), pickerOpen && (_jsx(ResourcePicker, { anchorRef: pickerAnchor, onClose: () => setPickerOpen(false), onSelect: (mailbox) => {
|
|
43
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(GuestInput, { id: "event-guests", label: "Add guests", placeholder: "Add guests", variant: compact ? "quiet" : "box", listedElsewhere: !compact, guests: values.attendees, invalid: values.guestInvalid, draft: values.guestDraft, skipAddresses: [c.organizerAddress], mailboxUid: values.targetMailboxUid, onChange: (next) => c.update({ attendees: next.guests, guestInvalid: next.invalid, guestDraft: next.draft }) }), compact ? null : (_jsxs(_Fragment, { children: [values.attendees.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: values.attendees.map((attendee, i) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "w-7 h-7 shrink-0 rounded-full bg-primary/15 text-primary-dark text-xs font-semibold flex items-center justify-center uppercase", children: (attendee.displayName || attendee.address).charAt(0) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm truncate", children: attendee.displayName || attendee.address }), _jsxs("div", { className: "text-xs text-text-muted truncate", children: [attendee.displayName ? `${attendee.address} · ` : "", RESPONSE_STATUS_LABEL[attendee.responseStatus]] })] }), _jsx("select", { className: SELECT_CLASS, value: attendee.role, onChange: (e) => c.updateAttendee(i, { role: e.target.value }), "aria-label": `Attendee role ${i + 1}`, children: ATTENDEE_ROLES.map((role) => (_jsx("option", { value: role, children: role }, role))) }), _jsx("button", { type: "button", onClick: () => c.removeAttendee(i), className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": `Remove attendee ${i + 1}`, children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }, i))) })), _jsx("button", { ref: pickerAnchor, type: "button", onClick: () => setPickerOpen(true), className: "self-start text-xs font-medium text-primary-dark hover:underline", children: "+ Add room/equipment" }), pickerOpen && (_jsx(ResourcePicker, { anchorRef: pickerAnchor, onClose: () => setPickerOpen(false), onSelect: (mailbox) => {
|
|
55
44
|
c.addResource(mailbox);
|
|
56
45
|
setPickerOpen(false);
|
|
57
46
|
}, excludeAddresses: c.guestAddresses }))] }))] }));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Attendee } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
3
|
+
import type { ContactSuggestionOptions, RecipientSuggestion } from "@rapidmx/react-shared/mail/directoryApi.js";
|
|
4
|
+
declare const VARIANT_CLASS: {
|
|
5
|
+
/** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
|
|
6
|
+
quiet: string;
|
|
7
|
+
/** A bordered box, as the full form's fields are. */
|
|
8
|
+
box: string;
|
|
9
|
+
};
|
|
10
|
+
/** What a guests field holds: the guests added, what was committed but is not an address (kept in the field, flagged, until it is fixed or removed),
|
|
11
|
+
* and what is still being typed. */
|
|
12
|
+
export interface GuestEntries {
|
|
13
|
+
guests: Attendee[];
|
|
14
|
+
invalid: string[];
|
|
15
|
+
draft: string;
|
|
16
|
+
}
|
|
17
|
+
export interface GuestInputProps extends GuestEntries {
|
|
18
|
+
/** The input's id, and the prefix of its suggestions' ids. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** The input's accessible name. */
|
|
21
|
+
label: string;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
variant: keyof typeof VARIANT_CLASS;
|
|
24
|
+
/** The guests are listed elsewhere (rows with a role and an answer): the field draws chips only for what is not an address. */
|
|
25
|
+
listedElsewhere?: boolean;
|
|
26
|
+
/** Addresses that are never added - the organizer's, or the people already invited. */
|
|
27
|
+
skipAddresses?: string[];
|
|
28
|
+
/** The mailbox the event is on, whose contacts are suggested too. */
|
|
29
|
+
mailboxUid?: string;
|
|
30
|
+
onChange: (next: GuestEntries) => void;
|
|
31
|
+
/** Loads suggestions; defaults to the lookup compose's recipient fields use. */
|
|
32
|
+
fetchSuggestions?: (query: string, options: ContactSuggestionOptions) => Promise<RecipientSuggestion[]>;
|
|
33
|
+
debounceMs?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
|
|
37
|
+
* caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
|
|
38
|
+
* Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
|
|
39
|
+
* a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
|
|
40
|
+
* (as compose sends to one), which the mail server delivers to its members.
|
|
41
|
+
*
|
|
42
|
+
* The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
|
|
43
|
+
*/
|
|
44
|
+
export default function GuestInput({ id, label, placeholder, variant, listedElsewhere, skipAddresses, mailboxUid, guests, invalid, draft, onChange, fetchSuggestions, debounceMs, }: GuestInputProps): React.JSX.Element;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import RecipientInput from "../mail/compose/RecipientInput.js";
|
|
3
|
+
import { applyGuestChips, guestChip } from "./eventFormat.js";
|
|
4
|
+
/** Above the event dialog's own backdrop (`z-[1000]`), which the suggestions would otherwise sit under. */
|
|
5
|
+
const DIALOG_SUGGESTIONS_Z_INDEX = 1100;
|
|
6
|
+
const VARIANT_CLASS = {
|
|
7
|
+
/** Reads as plain text until it is hovered or focused, as the rows of the quick-create popover do. */
|
|
8
|
+
quiet: "w-full text-sm py-1 border-b border-transparent hover:border-border focus-within:border-primary",
|
|
9
|
+
/** A bordered box, as the full form's fields are. */
|
|
10
|
+
box: "w-full text-sm py-1.5 px-3 border border-border rounded-md bg-surface text-text focus-within:border-primary",
|
|
11
|
+
};
|
|
12
|
+
/** What `RecipientInput` calls `onChange` with; the entries come through `onEdit`. */
|
|
13
|
+
const IGNORED = () => undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The guests field of an event: compose's recipient field (`RecipientInput`) reading its chips as guests. Typing looks the name up among the
|
|
16
|
+
* caller's contacts and the server's mailboxes and distribution lists; a suggestion (or an address, `Name <address>`, a comma, a semicolon,
|
|
17
|
+
* Enter or leaving the field) becomes a chip, a paste of several addresses becomes several, and something that is not an address stays as
|
|
18
|
+
* a flagged chip once it is committed - nothing is said while it is being typed. A distribution list is added as its own address, one guest
|
|
19
|
+
* (as compose sends to one), which the mail server delivers to its members.
|
|
20
|
+
*
|
|
21
|
+
* The field is controlled: `guests` are the attendees, and every edit hands back the whole of the entries (see `applyGuestChips()`).
|
|
22
|
+
*/
|
|
23
|
+
export default function GuestInput({ id, label, placeholder, variant, listedElsewhere, skipAddresses, mailboxUid, guests, invalid, draft, onChange, fetchSuggestions, debounceMs, }) {
|
|
24
|
+
const value = [...guests.map(guestChip), ...invalid, draft].filter((part) => part.length > 0).join(", ");
|
|
25
|
+
const listed = new Set(listedElsewhere ? guests.map(guestChip) : []);
|
|
26
|
+
return (_jsx(RecipientInput, { id: id, label: label, ariaLabel: label, listLabel: "Guests", placeholder: placeholder, className: VARIANT_CLASS[variant], dropdownZIndex: DIALOG_SUGGESTIONS_Z_INDEX, value: value, initialPending: draft, mailboxUid: mailboxUid, hideChip: (chip) => listed.has(chip), fetchSuggestions: fetchSuggestions, debounceMs: debounceMs, onChange: IGNORED, onEdit: (chips, pending) => {
|
|
27
|
+
const merged = applyGuestChips(guests, chips, skipAddresses);
|
|
28
|
+
onChange({ guests: merged.attendees, invalid: merged.invalid, draft: pending });
|
|
29
|
+
} }));
|
|
30
|
+
}
|
|
@@ -13,6 +13,7 @@ import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
|
13
13
|
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
14
14
|
import { notify } from "../../notifications/store.js";
|
|
15
15
|
import { INPUT_CLASS } from "./EventFormParts.js";
|
|
16
|
+
import GuestInput from "./GuestInput.js";
|
|
16
17
|
import LazyDescriptionEditor from "./LazyDescriptionEditor.js";
|
|
17
18
|
import { descriptionProblem, initialDescriptionHtml } from "./eventDialogFields.js";
|
|
18
19
|
import { mergeGuests } from "./eventFormat.js";
|
|
@@ -35,41 +36,29 @@ export default function RequestChangeForm({ subject, canChange, canInvite, onSen
|
|
|
35
36
|
const [start, setStart] = useState(subject.startDate ? toDatetimeLocal(subject.startDate) : "");
|
|
36
37
|
const [end, setEnd] = useState(subject.endDate ? toDatetimeLocal(subject.endDate) : "");
|
|
37
38
|
const [descriptionHtml, setDescriptionHtml] = useState(() => initialDescriptionHtml(subject));
|
|
38
|
-
|
|
39
|
-
const [
|
|
39
|
+
// The guests to add (not the ones already invited: `subject.guestAddresses` are skipped), what is flagged in the field, and what is being typed.
|
|
40
|
+
const [entries, setEntries] = useState({ guests: [], invalid: [], draft: "" });
|
|
40
41
|
const [error, setError] = useState(null);
|
|
41
42
|
const [sending, setSending] = useState(false);
|
|
42
43
|
const heading = canChange ? "Request a change" : "Add guests";
|
|
43
|
-
/** Adds what was typed into the guests box, returning the guests as they now stand;
|
|
44
|
+
/** Adds what was typed into the guests box, returning the guests to add as they now stand; something that is not an address is said. */
|
|
44
45
|
function addTypedGuests() {
|
|
45
|
-
const
|
|
46
|
-
const merged = mergeGuests(invited.map((guest) => ({ ...guest, role: "required", responseStatus: "needsAction", isOrganizer: false })), guestDraft);
|
|
46
|
+
const merged = mergeGuests(entries.guests, [...entries.invalid, entries.draft].join(", "), subject.guestAddresses);
|
|
47
47
|
if (merged.invalid.length > 0) {
|
|
48
48
|
setError(`“${merged.invalid[0]}” isn’t a valid email address.`);
|
|
49
|
-
setGuestDraft(merged.invalid.join(" "));
|
|
50
49
|
return undefined;
|
|
51
50
|
}
|
|
52
|
-
|
|
53
|
-
return merged.attendees
|
|
54
|
-
}
|
|
55
|
-
function handleGuestKey(event) {
|
|
56
|
-
if (event.key === "Enter" || event.key === "," || event.key === ";") {
|
|
57
|
-
event.preventDefault();
|
|
58
|
-
setError(null);
|
|
59
|
-
const more = addTypedGuests();
|
|
60
|
-
if (more) {
|
|
61
|
-
setAdded((current) => [...current, ...more]);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
51
|
+
setEntries({ guests: merged.attendees, invalid: [], draft: "" });
|
|
52
|
+
return merged.attendees;
|
|
64
53
|
}
|
|
65
54
|
async function handleSubmit(event) {
|
|
66
55
|
event.preventDefault();
|
|
67
56
|
setError(null);
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
57
|
+
const added = canInvite ? addTypedGuests() : [];
|
|
58
|
+
if (!added) {
|
|
70
59
|
return;
|
|
71
60
|
}
|
|
72
|
-
const guests =
|
|
61
|
+
const guests = added.map((guest) => (guest.displayName ? { address: guest.address, displayName: guest.displayName } : { address: guest.address }));
|
|
73
62
|
const request = {};
|
|
74
63
|
if (canChange) {
|
|
75
64
|
if (title.trim() !== subject.title.trim()) {
|
|
@@ -134,5 +123,5 @@ export default function RequestChangeForm({ subject, canChange, canInvite, onSen
|
|
|
134
123
|
setSending(false);
|
|
135
124
|
}
|
|
136
125
|
}
|
|
137
|
-
return (_jsxs("form", { onSubmit: (event) => void handleSubmit(event), "aria-label": heading, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold", children: heading }), _jsx("button", { type: "button", "aria-label": "Cancel request", onClick: onCancel, className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Your request goes to the organizer. Nothing on your calendar changes until their update arrives; when the organizer allows it, it is applied for you." }), error && _jsx(Alert, { children: error }), canChange && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Title", _jsx("input", { type: "text", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Location", _jsx("input", { type: "text", className: INPUT_CLASS, value: location, onChange: (e) => setLocation(e.target.value) })] }), canChangeTime && (_jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Starts", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: start, onChange: (e) => setStart(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Ends", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: end, onChange: (e) => setEnd(e.target.value) })] })] })), !canChangeTime && (_jsx("p", { className: "text-xs text-text-muted", children: subject.recurring ? "A change here applies to the whole series; its times can only be changed by the organizer." : "The time of this event can only be changed by the organizer." })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-xs font-medium", children: "Description" }), _jsx(LazyDescriptionEditor, { value: descriptionHtml, onChange: setDescriptionHtml })] })] })), canInvite && (_jsxs("div", { className: "flex flex-col gap-
|
|
126
|
+
return (_jsxs("form", { onSubmit: (event) => void handleSubmit(event), "aria-label": heading, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold", children: heading }), _jsx("button", { type: "button", "aria-label": "Cancel request", onClick: onCancel, className: "w-7 h-7 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: _jsx(HiOutlineXMark, { size: 16, "aria-hidden": "true" }) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Your request goes to the organizer. Nothing on your calendar changes until their update arrives; when the organizer allows it, it is applied for you." }), error && _jsx(Alert, { children: error }), canChange && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Title", _jsx("input", { type: "text", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Location", _jsx("input", { type: "text", className: INPUT_CLASS, value: location, onChange: (e) => setLocation(e.target.value) })] }), canChangeTime && (_jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Starts", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: start, onChange: (e) => setStart(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium", children: ["Ends", _jsx("input", { type: "datetime-local", className: INPUT_CLASS, value: end, onChange: (e) => setEnd(e.target.value) })] })] })), !canChangeTime && (_jsx("p", { className: "text-xs text-text-muted", children: subject.recurring ? "A change here applies to the whole series; its times can only be changed by the organizer." : "The time of this event can only be changed by the organizer." })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-xs font-medium", children: "Description" }), _jsx(LazyDescriptionEditor, { value: descriptionHtml, onChange: setDescriptionHtml })] })] })), canInvite && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: "request-guests", className: "text-xs font-medium", children: "Guests to add" }), _jsx(GuestInput, { id: "request-guests", label: "Guests to add", placeholder: "Name or name@example.com", variant: "box", skipAddresses: subject.guestAddresses, ...entries, onChange: setEntries })] })), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "submit", loading: sending, disabled: sending, className: "!w-auto", children: "Send request" }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto", disabled: sending, onClick: onCancel, children: "Cancel" })] })] }));
|
|
138
127
|
}
|
|
@@ -22,6 +22,8 @@ export interface EventFormValues {
|
|
|
22
22
|
attendees: Attendee[];
|
|
23
23
|
/** What has been typed into the guests field but not added yet. */
|
|
24
24
|
guestDraft: string;
|
|
25
|
+
/** What was committed to the guests field (Enter, a comma, leaving it) but is not an address: kept there, flagged, until it is fixed or removed. */
|
|
26
|
+
guestInvalid: string[];
|
|
25
27
|
recurrenceRule: RecurrenceRule | null;
|
|
26
28
|
/** Minutes before the start, as typed; blank means no reminder. */
|
|
27
29
|
reminderMinutes: string;
|
|
@@ -57,8 +59,6 @@ export interface EventFormController {
|
|
|
57
59
|
/** The address the event is organized from (the chosen mailbox's, for a new event). */
|
|
58
60
|
organizerAddress: string;
|
|
59
61
|
deviceZone: string;
|
|
60
|
-
/** Adds the addresses in `text`; returns the ones that are not valid addresses. */
|
|
61
|
-
addGuests: (text: string) => string[];
|
|
62
62
|
updateAttendee: (index: number, patch: Partial<Attendee>) => void;
|
|
63
63
|
removeAttendee: (index: number) => void;
|
|
64
64
|
addResource: (mailbox: Mailbox) => void;
|
|
@@ -33,12 +33,24 @@ export type ReminderUnit = (typeof REMINDER_UNITS)[number]["unit"];
|
|
|
33
33
|
export declare function bestReminderUnit(minutes: number): (typeof REMINDER_UNITS)[number];
|
|
34
34
|
/** "Notify 30 minutes before" / "Notify 1 hour before" / "Notify at the start" for a stored reminder, or "No notification". */
|
|
35
35
|
export declare function describeReminder(minutes: number | undefined): string;
|
|
36
|
-
/** Splits what was typed into the guests field into its addresses: commas, semicolons and white space separate them. */
|
|
37
|
-
export declare function splitGuestInput(text: string): string[];
|
|
38
|
-
/** The loosest address check that is still worth making before an invitation is sent: something, an @, something. */
|
|
39
|
-
export declare function isValidGuestAddress(address: string): boolean;
|
|
40
36
|
/** An attendee as the guests field adds one. */
|
|
41
|
-
export declare function newGuest(address: string): Attendee;
|
|
37
|
+
export declare function newGuest(address: string, displayName?: string): Attendee;
|
|
38
|
+
/** A guest as the guests field shows it: `Name <address>`, or the bare address for one without a name (the form compose's recipient fields use). */
|
|
39
|
+
export declare function guestChip(guest: {
|
|
40
|
+
address: string;
|
|
41
|
+
displayName?: string;
|
|
42
|
+
}): string;
|
|
43
|
+
/**
|
|
44
|
+
* The guests the guests field's chips name, given the guests `current`ly on the list: a chip for someone already there keeps that guest as
|
|
45
|
+
* they are (their role and answer, whatever the case of the address), a chip for a new address adds a required guest with the chip's name, and
|
|
46
|
+
* an address twice (or one of `skipAddresses` - the organizer's, or the people already invited) is added once. The chips are the field's whole
|
|
47
|
+
* list, so a guest without a chip is removed. What is not an address comes back as `invalid`, to stay in the field flagged; several addresses
|
|
48
|
+
* separated by white space in one chip ("a@x.com b@x.com", as a paste can be) are taken as several.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyGuestChips(current: Attendee[], chips: string[], skipAddresses?: string[]): {
|
|
51
|
+
attendees: Attendee[];
|
|
52
|
+
invalid: string[];
|
|
53
|
+
};
|
|
42
54
|
/** The wall-clock string a form input holds ("YYYY-MM-DDTHH:mm") read as `zone`'s clock, as an instant. `deviceZone` is the zone the browser
|
|
43
55
|
* itself parses such a string in, which needs no conversion. */
|
|
44
56
|
export declare function wallStringToMs(value: string, zone: string, deviceZone: string): number;
|
|
@@ -55,9 +67,10 @@ export declare function localDateFromKey(key: string): Date;
|
|
|
55
67
|
export declare function formatWhen(start: Date, end: Date, allDay: boolean, now?: Date): string;
|
|
56
68
|
/** `formatWhen()` for a stored event: instants for a timed one, the date-only keys (with the exclusive end) for an all-day one. */
|
|
57
69
|
export declare function formatStoredWhen(startDate: string, endDate: string, allDay: boolean, now?: Date): string;
|
|
58
|
-
/** Adds the
|
|
59
|
-
* address
|
|
60
|
-
|
|
70
|
+
/** Adds the guests typed or pasted into the guests field to `current` - recipients separated by commas or semicolons, each an address or
|
|
71
|
+
* `Name <address>` (see `applyGuestChips()`, which keeps who is already there): an address already on the list (in any case) or in
|
|
72
|
+
* `skipAddresses` is skipped, and what is not an address comes back as `invalid` for the field to keep. */
|
|
73
|
+
export declare function mergeGuests(current: Attendee[], text: string, skipAddresses?: string[]): {
|
|
61
74
|
attendees: Attendee[];
|
|
62
75
|
invalid: string[];
|
|
63
76
|
};
|