@rapidmx/web-client 0.14.1 → 0.16.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 +373 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/domains/[uid].tsx +164 -163
- package/apps/admin/escrow-scopes/[uid].tsx +348 -348
- package/apps/admin/index.tsx +127 -124
- package/apps/admin/mailboxes/[uid].tsx +269 -224
- package/apps/admin/mailboxes/new/index.tsx +28 -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/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/appearance/AppearanceForm.tsx +9 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/calendar/bookingPlugin.ts +100 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
- 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 +53 -4
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
- 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 +444 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
- package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
- package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
- package/apps/shared/components/calendar/EventDetails.tsx +401 -0
- package/apps/shared/components/calendar/EventEditor.tsx +635 -0
- package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
- package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
- package/apps/shared/components/calendar/EventModal.tsx +206 -946
- package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
- package/apps/shared/components/calendar/EventShell.tsx +202 -0
- package/apps/shared/components/calendar/FindATime.tsx +379 -0
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -173
- package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
- package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
- package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
- package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
- package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
- package/apps/shared/components/calendar/eventForm.ts +108 -0
- package/apps/shared/components/calendar/eventFormat.ts +210 -0
- package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
- 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 +217 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +480 -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 -372
- package/apps/shared/components/mail/ConversationList.tsx +323 -271
- package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
- package/apps/shared/components/mail/InviteCard.tsx +387 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
- package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
- package/apps/shared/components/mail/SwipeRow.tsx +120 -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 -1673
- 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/invite/InviteRowChip.tsx +104 -0
- package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
- package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
- package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
- package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -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 +255 -253
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
- package/apps/shared/gestures/useEnterSlide.ts +54 -0
- package/apps/shared/gestures/useSwipe.ts +114 -0
- package/apps/shared/gestures/useSwipeSlide.ts +113 -0
- package/apps/shared/gestures/useWheelPaging.ts +107 -0
- 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/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +3 -15
- 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/primaryMailbox.ts +71 -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/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +2 -2
- package/apps/shared/navigation/AppRouter.tsx +300 -300
- package/apps/shared/navigation/routerContext.tsx +83 -83
- package/apps/shared/notifications/preferences.ts +36 -0
- package/apps/shared/notifications/store.ts +9 -0
- 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/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +578 -469
- package/apps/www/contacts/[uid].tsx +109 -109
- package/apps/www/contacts/index.tsx +6 -0
- package/apps/www/index.tsx +2847 -2332
- package/apps/www/messages/[uid].tsx +135 -108
- package/apps/www/settings/auto-reply/index.tsx +133 -133
- package/apps/www/settings/encryption/index.tsx +1287 -1287
- package/apps/www/settings/filters/[uid].tsx +176 -176
- package/apps/www/settings/filters/index.tsx +102 -102
- package/apps/www/settings/filters/new/index.tsx +140 -140
- package/apps/www/settings/labels/index.tsx +204 -204
- package/apps/www/settings/privacy/index.tsx +492 -492
- package/apps/www/settings/profile/index.tsx +248 -0
- package/apps/www/settings/read-receipts/index.tsx +147 -147
- package/apps/www/settings/sharing/index.tsx +256 -256
- package/apps/www/settings/signatures/[uid].tsx +172 -172
- package/apps/www/settings/signatures/index.tsx +88 -88
- package/apps/www/settings/signatures/new/index.tsx +135 -135
- package/apps/www/tasks/index.tsx +649 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/domains/[uid].js +4 -2
- package/dist/apps/admin/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +12 -2
- package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
- package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
- package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -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 +28 -4
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
- package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
- package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
- package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
- package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
- package/dist/apps/shared/components/calendar/EventModal.js +57 -459
- package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
- package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
- package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
- package/dist/apps/shared/components/calendar/EventShell.js +134 -0
- package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
- package/dist/apps/shared/components/calendar/FindATime.js +190 -0
- 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/LazyDescriptionEditor.d.ts +8 -0
- package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
- package/dist/apps/shared/components/calendar/MonthView.js +11 -5
- package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
- package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
- package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
- package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
- package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
- package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
- package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
- package/dist/apps/shared/components/calendar/eventForm.js +1 -0
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
- package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
- package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
- package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
- 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 +4 -3
- 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 +14 -9
- package/dist/apps/shared/components/layout/UserMenu.js +74 -26
- package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
- package/dist/apps/shared/components/mail/ConversationList.js +21 -4
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
- package/dist/apps/shared/components/mail/InviteCard.js +150 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
- package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
- package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
- 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/invite/InviteRowChip.d.ts +23 -0
- package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
- 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 +10 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
- 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/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +7 -5
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
- package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
- package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
- package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
- package/dist/apps/shared/gestures/useSwipe.js +90 -0
- package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
- package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
- package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
- package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
- package/dist/apps/shared/mail/newMailNotifications.js +3 -12
- 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/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -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/useNewMailNotifications.js +3 -2
- package/dist/apps/shared/notifications/preferences.d.ts +13 -0
- package/dist/apps/shared/notifications/preferences.js +36 -0
- package/dist/apps/shared/notifications/store.d.ts +1 -0
- package/dist/apps/shared/notifications/store.js +7 -23
- 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/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +78 -7
- package/dist/apps/www/contacts/index.js +3 -1
- package/dist/apps/www/index.js +548 -210
- package/dist/apps/www/messages/[uid].js +13 -5
- package/dist/apps/www/settings/profile/index.d.ts +4 -0
- package/dist/apps/www/settings/profile/index.js +124 -0
- package/package.json +2 -2
|
@@ -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,11 +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";
|
|
11
|
+
import { deviceTimeZone } from "@rapidmx/react-shared/util/timeZone.js";
|
|
10
12
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
13
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
14
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
13
15
|
import PrincipalResolver, { describePerson } from "../../sharing/PrincipalResolver.js";
|
|
16
|
+
import EraseLeftoverDataDialog from "../mailboxes/EraseLeftoverDataDialog.js";
|
|
14
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";
|
|
15
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";
|
|
16
19
|
/**
|
|
@@ -25,7 +28,8 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
25
28
|
const [displayName, setDisplayName] = useState(defaults?.displayName ?? "");
|
|
26
29
|
const [ownerMode, setOwnerMode] = useState(defaults?.ownerUserUid ? "owned" : "shared");
|
|
27
30
|
const [resolvedOwner, setResolvedOwner] = useState(null);
|
|
28
|
-
|
|
31
|
+
// The admin's own device zone, not "UTC": the server's "UTC" is only a placeholder for a mailbox nobody chose a zone for.
|
|
32
|
+
const [timezone, setTimezone] = useState(deviceTimeZone);
|
|
29
33
|
const [quotaGb, setQuotaGb] = useState(5);
|
|
30
34
|
// Only replaced by the mailbox policy's default if the admin hasn't typed a quota of their own yet.
|
|
31
35
|
const quotaTouched = useRef(false);
|
|
@@ -38,6 +42,10 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
38
42
|
const [maxDurationMinutes, setMaxDurationMinutes] = useState("");
|
|
39
43
|
const [error, setError] = useState(null);
|
|
40
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);
|
|
41
49
|
// An empty `mail:domains` list (the default) means this server enforces no domain restriction —
|
|
42
50
|
// the address field stays free text, exactly as before. Once an admin configures at least one
|
|
43
51
|
// domain, every mailbox this server creates must be on one of them (enforced server-side too, in
|
|
@@ -59,10 +67,15 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
59
67
|
.catch(() => undefined);
|
|
60
68
|
}, []);
|
|
61
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]);
|
|
62
75
|
async function handleSubmit(e) {
|
|
63
76
|
e.preventDefault();
|
|
64
77
|
setError(null);
|
|
65
|
-
|
|
78
|
+
setLeftover(null);
|
|
66
79
|
if (constrained ? !localPart.trim() : !primarySmtpAddress.trim()) {
|
|
67
80
|
setError("A primary SMTP address is required.");
|
|
68
81
|
return;
|
|
@@ -104,13 +117,24 @@ export default function MailboxCreateForm({ onCreated, defaults, submitLabel = "
|
|
|
104
117
|
onCreated(mailbox);
|
|
105
118
|
}
|
|
106
119
|
catch (err) {
|
|
107
|
-
|
|
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
|
+
}
|
|
108
128
|
}
|
|
109
129
|
finally {
|
|
110
130
|
setSaving(false);
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
|
-
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: () => {
|
|
114
138
|
setOwnerMode("shared");
|
|
115
139
|
setResolvedOwner(null);
|
|
116
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" &&
|
|
@@ -197,7 +197,8 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
197
197
|
watchRollout();
|
|
198
198
|
void refreshUpdates();
|
|
199
199
|
}
|
|
200
|
-
/** Runs an action on an installed plugin's
|
|
200
|
+
/** Runs an action on a plugin's row (an installed plugin's, or an uninstalled one's), which is busy meanwhile, and
|
|
201
|
+
* shows why it failed. */
|
|
201
202
|
async function onRow(plugin, action) {
|
|
202
203
|
setBusy(plugin.uid, true);
|
|
203
204
|
setError(null);
|
|
@@ -278,6 +279,20 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
278
279
|
function upgrade(plugin, packageVersion) {
|
|
279
280
|
return onRow(plugin, () => changeVersion(plugin, packageVersion));
|
|
280
281
|
}
|
|
282
|
+
/** Installs an uninstalled plugin again, at its latest version - as adding it by name would. */
|
|
283
|
+
function reinstall(purge) {
|
|
284
|
+
const displayName = purgeName(purge);
|
|
285
|
+
return onRow(purge, async () => {
|
|
286
|
+
let latest;
|
|
287
|
+
try {
|
|
288
|
+
latest = (await planPluginChange(purge.name)).plugin.version;
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return errorMessage(err, `Could not install ${displayName}.`);
|
|
292
|
+
}
|
|
293
|
+
return install(purge.name, latest, displayName);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
281
296
|
/** Runs the steps of a failed data deletion that failed, again. */
|
|
282
297
|
async function retryPurge(purge) {
|
|
283
298
|
setBusy(purge.uid, true);
|
|
@@ -308,7 +323,7 @@ export default function PluginsManager({ embedded = false } = {}) {
|
|
|
308
323
|
.filter((other) => other.uid !== plugin.uid && other.manifest.requires?.[plugin.name] !== undefined)
|
|
309
324
|
.map((other) => other.manifest.displayName);
|
|
310
325
|
return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: plugin.manifest.displayName }), _jsx("div", { className: "text-xs text-text-muted", children: plugin.name }), plugin.manifest.description && (_jsx("div", { className: "text-xs text-text-muted mt-1 max-w-md", children: plugin.manifest.description })), requires.length > 0 && _jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Requires: ", requires.join(", ")] }), requiredBy.length > 0 && (_jsxs("div", { className: "text-xs text-text-muted mt-1", children: ["Required by: ", requiredBy.join(", ")] }))] }), _jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1 whitespace-nowrap`, children: [_jsx("div", { children: plugin.packageVersion }), latest && _jsxs("div", { className: "mt-1 text-xs font-semibold text-primary-dark", children: ["Update available: ", latest] })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-1`, children: _jsx(PluginStatusCell, { plugin: plugin, status: status }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-2 pb-3`, children: _jsxs("div", { className: "flex flex-col items-start gap-1.5 lg:items-end lg:min-w-[11rem]", children: [_jsxs("div", { className: "flex flex-wrap gap-2 lg:justify-end", children: [latest && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Upgrade ${plugin.manifest.displayName} to ${latest}`, disabled: busy, onClick: () => void upgrade(plugin, latest), children: "Upgrade" })), (plugin.manifest.settings ?? []).length > 0 && (_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", onClick: () => setConfiguring(plugin), children: "Settings" })), _jsx(Button, { variant: plugin.enabled ? "secondary" : "primary", type: "button", className: "!w-auto min-w-[5.5rem]", "aria-label": `${plugin.enabled ? "Disable" : "Enable"} ${plugin.manifest.displayName}`, disabled: busy, onClick: () => void toggle(plugin), children: plugin.enabled ? "Disable" : "Enable" })] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 lg:justify-end", children: [_jsx(Button, { variant: "text", type: "button", onClick: () => setUpgrading(plugin), children: "Change version" }), _jsx(Button, { variant: "text", type: "button", onClick: () => setRemoving(plugin), children: "Uninstall" })] })] }) })] }, plugin.uid));
|
|
311
|
-
}), purges.map((purge) => (_jsx(UninstalledPluginRow, { purge: purge, busy: busyUids.has(purge.uid), onRetry: () => void retryPurge(purge) }, purge.uid)))] })] }) })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
|
|
326
|
+
}), purges.map((purge) => (_jsx(UninstalledPluginRow, { purge: purge, busy: busyUids.has(purge.uid), onRetry: () => void retryPurge(purge), onInstall: () => void reinstall(purge) }, purge.uid)))] })] }) })), _jsx(PluginBrowser, { headingLevel: SectionHeading, plugins: plugins, onInstall: (result) => install(result.name, result.version, result.name),
|
|
312
327
|
// The browser only offers an upgrade for a result it matched to one of these same `plugins`.
|
|
313
328
|
onUpgrade: (uid, packageVersion) => upgrade(plugins.find((p) => p.uid === uid), packageVersion) }), _jsx(AddPluginModal, { open: adding, onClose: () => setAdding(false), onAdd: async (name, packageVersion, displayName) => {
|
|
314
329
|
const problem = await install(name, packageVersion, displayName);
|
|
@@ -441,9 +456,9 @@ function PluginServers({ plugin, status }) {
|
|
|
441
456
|
return (_jsxs("div", { className: "text-xs", children: [_jsxs("span", { className: loaded === status.instances.length ? "text-success font-medium" : "text-text-muted", children: ["Loaded on ", loaded, " of ", status.instances.length, " ", status.instances.length === 1 ? "server" : "servers"] }), errors.map((message) => (_jsx("div", { className: "text-danger mt-1 break-words", children: message }, message)))] }));
|
|
442
457
|
}
|
|
443
458
|
/** The row of a plugin that was uninstalled but whose data deletion is still listed: what became of its data. */
|
|
444
|
-
function UninstalledPluginRow({ purge, busy, onRetry }) {
|
|
459
|
+
function UninstalledPluginRow({ purge, busy, onRetry, onInstall }) {
|
|
445
460
|
const failedSteps = purge.steps.filter((step) => !step.ok);
|
|
446
|
-
return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: purgeName(purge) }), _jsx("div", { className: "text-xs text-text-muted", children: purge.name })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} hidden lg:table-cell` }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-3 basis-full lg:basis-auto`, children: _jsxs("div", { className: "flex flex-col items-start gap-1 text-xs max-w-sm", children: [_jsx("span", { className: `${BADGE_CLASS} bg-surface-alt text-text-muted`, children: "Uninstalled" }), purge.state === "pending" && (_jsxs(_Fragment, { children: [_jsx("span", { children: "Uninstalled - data will be deleted after servers restart" }), !!purge.serversRunning && (_jsxs("span", { className: "text-text-muted", children: [purge.serversRunning, " of ", purge.serversTotal, " ", purge.serversTotal === 1 ? "server" : "servers", " still running it"] }))] })), purge.state === "running" && _jsx("span", { children: "Uninstalled - deleting its data now" }), purge.state === "done" && _jsxs("span", { className: "text-success font-medium", children: ["Data deleted", purge.completedAt ? ` ${formatDate(purge.completedAt)}` : ""] }), purge.state === "failed" && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-danger break-words", children: ["Data deletion failed: ", purgeFailure(purge)] }), failedSteps.length > 0 && (_jsx("ul", { className: "list-disc pl-4 text-text-muted break-words", children: failedSteps.map((step) => (_jsxs("li", { children: [step.step, ": ", step.error ?? "failed"] }, step.step))) }))] }))] }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-1 pb-3`, children:
|
|
461
|
+
return (_jsxs("tr", { role: "row", className: INSTALLED_ROW_CLASS, children: [_jsxs("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`, children: [_jsx("div", { className: "font-semibold", children: purgeName(purge) }), _jsx("div", { className: "text-xs text-text-muted", children: purge.name })] }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} hidden lg:table-cell` }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} pt-1 pb-3 basis-full lg:basis-auto`, children: _jsxs("div", { className: "flex flex-col items-start gap-1 text-xs max-w-sm", children: [_jsx("span", { className: `${BADGE_CLASS} bg-surface-alt text-text-muted`, children: "Uninstalled" }), purge.state === "pending" && (_jsxs(_Fragment, { children: [_jsx("span", { children: "Uninstalled - data will be deleted after servers restart" }), !!purge.serversRunning && (_jsxs("span", { className: "text-text-muted", children: [purge.serversRunning, " of ", purge.serversTotal, " ", purge.serversTotal === 1 ? "server" : "servers", " still running it"] }))] })), purge.state === "running" && _jsx("span", { children: "Uninstalled - deleting its data now" }), purge.state === "done" && _jsxs("span", { className: "text-success font-medium", children: ["Data deleted", purge.completedAt ? ` ${formatDate(purge.completedAt)}` : ""] }), purge.state === "failed" && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-danger break-words", children: ["Data deletion failed: ", purgeFailure(purge)] }), failedSteps.length > 0 && (_jsx("ul", { className: "list-disc pl-4 text-text-muted break-words", children: failedSteps.map((step) => (_jsxs("li", { children: [step.step, ": ", step.error ?? "failed"] }, step.step))) }))] }))] }) }), _jsx("td", { role: "cell", className: `${INSTALLED_TD_CLASS} basis-full pt-1 pb-3`, children: _jsxs("div", { className: "flex gap-2 lg:justify-end", children: [purge.state === "failed" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Retry deleting the data of ${purgeName(purge)}`, disabled: busy, loading: busy, onClick: onRetry, children: "Retry" })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": `Install ${purgeName(purge)}`, disabled: busy || purge.state === "running", onClick: onInstall, children: "Install" })] }) })] }));
|
|
447
462
|
}
|
|
448
463
|
function AddPluginModal({ open, onClose, onAdd, }) {
|
|
449
464
|
const [name, setName] = useState("");
|
|
@@ -587,12 +602,30 @@ function needsSelection(definition, saved) {
|
|
|
587
602
|
definition.default === undefined &&
|
|
588
603
|
(definition.options ?? []).length > 0);
|
|
589
604
|
}
|
|
605
|
+
/** What a plugin's setting default writes for the host this console was reached at, like `https://<host>/meet`. */
|
|
606
|
+
const HOST_PLACEHOLDER = "<host>";
|
|
607
|
+
/**
|
|
608
|
+
* The value offered for a text setting whose default names the host (`https://<host>/meet`) and that has no real value
|
|
609
|
+
* saved yet - nothing, an empty value, or the placeholder itself, which an older server stores when it installs the
|
|
610
|
+
* plugin. It's the default with this console's host filled in, and is only stored once the form is saved.
|
|
611
|
+
*/
|
|
612
|
+
function suggestedHost(definition, saved) {
|
|
613
|
+
if (typeof definition.default !== "string" || !definition.default.includes(HOST_PLACEHOLDER) || typeof window === "undefined") {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
const unset = saved === undefined || saved === "" || (typeof saved === "string" && saved.includes(HOST_PLACEHOLDER));
|
|
617
|
+
return unset ? definition.default.split(HOST_PLACEHOLDER).join(window.location.host) : undefined;
|
|
618
|
+
}
|
|
590
619
|
/** A setting's current form value: its saved value, else its default - or, for a required select with neither, its
|
|
591
620
|
* first option. Kept as a string for text inputs. */
|
|
592
621
|
function initialValue(definition, saved) {
|
|
593
622
|
if (needsSelection(definition, saved)) {
|
|
594
623
|
return definition.options[0].value;
|
|
595
624
|
}
|
|
625
|
+
const suggestion = suggestedHost(definition, saved);
|
|
626
|
+
if (suggestion !== undefined) {
|
|
627
|
+
return suggestion;
|
|
628
|
+
}
|
|
596
629
|
const value = saved ?? definition.default;
|
|
597
630
|
if (definition.type === "boolean") {
|
|
598
631
|
return value === true;
|
|
@@ -610,13 +643,14 @@ function SettingsModal({ plugin, onClose, onSaved }) {
|
|
|
610
643
|
e.preventDefault();
|
|
611
644
|
// The server replaces the whole settings object, so every setting is sent. One left alone is sent as it's saved
|
|
612
645
|
// - `null` when nothing is, so it keeps following the plugin's default rather than pinning the default shown.
|
|
613
|
-
// A required select with nothing to fall back on
|
|
646
|
+
// A required select with nothing to fall back on, and a setting offering this console's host, are sent as the
|
|
647
|
+
// form shows them.
|
|
614
648
|
const settings = {};
|
|
615
649
|
let changed = false;
|
|
616
650
|
for (const definition of definitions) {
|
|
617
651
|
const key = definition.key;
|
|
618
652
|
const value = values[key];
|
|
619
|
-
if (value === initial.current[key] && !needsSelection(definition, plugin.settings[key])) {
|
|
653
|
+
if (value === initial.current[key] && !needsSelection(definition, plugin.settings[key]) && suggestedHost(definition, plugin.settings[key]) === undefined) {
|
|
620
654
|
settings[key] = plugin.settings[key] ?? null;
|
|
621
655
|
continue;
|
|
622
656
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { EventFormController } from "./eventForm.js";
|
|
3
|
+
/** What the Appointment schedule tab holds besides the title (which is the Event tab's): kept by `QuickCreateFaces`, so it survives a trip to another tab. */
|
|
4
|
+
export interface AppointmentDraft {
|
|
5
|
+
/** One of `DURATIONS`, or `"custom"` to use `customMinutes`. */
|
|
6
|
+
duration: string;
|
|
7
|
+
customMinutes: string;
|
|
8
|
+
/** Whether each weekday (0 = Sunday) is bookable. */
|
|
9
|
+
days: boolean[];
|
|
10
|
+
/** `HH:mm`, the hours every bookable day is open. */
|
|
11
|
+
startTime: string;
|
|
12
|
+
endTime: string;
|
|
13
|
+
/** Minutes kept free after each appointment. */
|
|
14
|
+
buffer: string;
|
|
15
|
+
video: boolean;
|
|
16
|
+
phone: boolean;
|
|
17
|
+
other: boolean;
|
|
18
|
+
/** Set once the schedule is saved: the face then shows its link. */
|
|
19
|
+
created: {
|
|
20
|
+
name: string;
|
|
21
|
+
url: string;
|
|
22
|
+
uid: string;
|
|
23
|
+
mailboxUid: string;
|
|
24
|
+
} | null;
|
|
25
|
+
}
|
|
26
|
+
/** A new schedule: 30 minutes, Monday to Friday 9:00 to 17:00, a video call, no buffer. */
|
|
27
|
+
export declare function initialAppointmentDraft(): AppointmentDraft;
|
|
28
|
+
export interface AppointmentQuickFormProps {
|
|
29
|
+
c: EventFormController;
|
|
30
|
+
draft: AppointmentDraft;
|
|
31
|
+
onDraftChange: (patch: Partial<AppointmentDraft>) => void;
|
|
32
|
+
tabs: ReactNode;
|
|
33
|
+
/** The booking plugin's Settings path (`/settings/booking-types`), where More options goes. */
|
|
34
|
+
settingsHref: string;
|
|
35
|
+
/** The name booking pages show for the host. */
|
|
36
|
+
hostName: string;
|
|
37
|
+
/** The IANA zone the open hours are in. */
|
|
38
|
+
timeZone: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The Appointment schedule tab of the New event popover: publishes a bookable schedule (a booking type of `@rapidmx/booking-plugin`) with
|
|
42
|
+
* `POST /api/mail/booking-types` - a name (the title), a duration, the weekdays and hours people can book, the buffer after each
|
|
43
|
+
* appointment, how they may meet (video, phone, other) and which calendar takes the bookings - and then shows the public booking link with a
|
|
44
|
+
* Copy button. Everything else about a booking link (approval, notice, a window, per-day hours, date overrides, several meeting types, a
|
|
45
|
+
* description, the page's avatar and banner) is on the plugin's own page, which More options opens.
|
|
46
|
+
*/
|
|
47
|
+
export default function AppointmentQuickForm({ c, draft, onDraftChange, tabs, settingsHref, hostName, timeZone }: AppointmentQuickFormProps): React.JSX.Element;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, 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 { useState } from "react";
|
|
7
|
+
import { HiOutlineArrowTopRightOnSquare, HiOutlineCalendar, HiOutlineCalendarDays, HiOutlineClock, HiOutlineLink, HiOutlineMapPin } from "react-icons/hi2";
|
|
8
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
|
+
import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
11
|
+
import { bookingPublicUrl, createBookingType, newBookingLinkHref, slugFor, } from "../../calendar/bookingPlugin.js";
|
|
12
|
+
import { CalendarField, INPUT_CLASS, IconRow, SELECT_CLASS } from "./EventFormParts.js";
|
|
13
|
+
import QuickFaceFrame from "./QuickFaceFrame.js";
|
|
14
|
+
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
15
|
+
const DURATIONS = ["15", "30", "45", "60"];
|
|
16
|
+
const BUFFERS = ["0", "5", "10", "15", "30"];
|
|
17
|
+
/** How often the plugin lets a slot repeat and how long one may be (`MIN_SLOT_MINUTES` to a day), in minutes. */
|
|
18
|
+
const MIN_DURATION = 5;
|
|
19
|
+
const MAX_DURATION = 1440;
|
|
20
|
+
/** How many times a taken link name is retried with a number after it (`intro`, `intro-2`, ...). */
|
|
21
|
+
const MAX_SLUG_ATTEMPTS = 5;
|
|
22
|
+
const LOCATIONS = [
|
|
23
|
+
{ key: "video", type: "video", label: "Video call" },
|
|
24
|
+
{ key: "phone", type: "phone", label: "Phone call" },
|
|
25
|
+
{ key: "other", type: "other", label: "Other" },
|
|
26
|
+
];
|
|
27
|
+
/** A new schedule: 30 minutes, Monday to Friday 9:00 to 17:00, a video call, no buffer. */
|
|
28
|
+
export function initialAppointmentDraft() {
|
|
29
|
+
return {
|
|
30
|
+
duration: "30",
|
|
31
|
+
customMinutes: "",
|
|
32
|
+
days: [false, true, true, true, true, true, false],
|
|
33
|
+
startTime: "09:00",
|
|
34
|
+
endTime: "17:00",
|
|
35
|
+
buffer: "0",
|
|
36
|
+
video: true,
|
|
37
|
+
phone: false,
|
|
38
|
+
other: false,
|
|
39
|
+
created: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Creates the booking type, trying the next number after `base` (`intro`, `intro-2`, ...) while its link name is taken (a 409). */
|
|
43
|
+
async function createWithFreeSlug(base, input) {
|
|
44
|
+
for (let attempt = 1;; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
return await createBookingType({ ...input, slug: attempt === 1 ? base : `${base}-${attempt}` });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (!(err instanceof ApiRequestError && err.status === 409 && attempt < MAX_SLUG_ATTEMPTS)) {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function minutesOf(time) {
|
|
56
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
57
|
+
return hours * 60 + minutes;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The Appointment schedule tab of the New event popover: publishes a bookable schedule (a booking type of `@rapidmx/booking-plugin`) with
|
|
61
|
+
* `POST /api/mail/booking-types` - a name (the title), a duration, the weekdays and hours people can book, the buffer after each
|
|
62
|
+
* appointment, how they may meet (video, phone, other) and which calendar takes the bookings - and then shows the public booking link with a
|
|
63
|
+
* Copy button. Everything else about a booking link (approval, notice, a window, per-day hours, date overrides, several meeting types, a
|
|
64
|
+
* description, the page's avatar and banner) is on the plugin's own page, which More options opens.
|
|
65
|
+
*/
|
|
66
|
+
export default function AppointmentQuickForm({ c, draft, onDraftChange, tabs, settingsHref, hostName, timeZone }) {
|
|
67
|
+
const { values } = c;
|
|
68
|
+
const [error, setError] = useState(null);
|
|
69
|
+
const [saving, setSaving] = useState(false);
|
|
70
|
+
async function handleSubmit(event) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
if (draft.created) {
|
|
73
|
+
c.onCancel();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setError(null);
|
|
77
|
+
const name = values.title.trim();
|
|
78
|
+
const durationMinutes = Number(draft.duration === "custom" ? draft.customMinutes : draft.duration);
|
|
79
|
+
const startMinute = minutesOf(draft.startTime);
|
|
80
|
+
const endMinute = minutesOf(draft.endTime);
|
|
81
|
+
const locationOptions = LOCATIONS.filter((location) => draft[location.key]).map((location) => ({ type: location.type }));
|
|
82
|
+
let problem;
|
|
83
|
+
if (!name) {
|
|
84
|
+
problem = "A title is required.";
|
|
85
|
+
}
|
|
86
|
+
else if (!Number.isInteger(durationMinutes) || durationMinutes < MIN_DURATION || durationMinutes > MAX_DURATION) {
|
|
87
|
+
problem = `A duration is a whole number of minutes from ${MIN_DURATION} to ${MAX_DURATION}.`;
|
|
88
|
+
}
|
|
89
|
+
else if (!draft.days.some(Boolean)) {
|
|
90
|
+
problem = "Choose at least one day people can book.";
|
|
91
|
+
}
|
|
92
|
+
else if (!(startMinute < endMinute)) {
|
|
93
|
+
problem = "The first bookable time must be before the last.";
|
|
94
|
+
}
|
|
95
|
+
else if (endMinute - startMinute < durationMinutes) {
|
|
96
|
+
problem = "The hours people can book are shorter than one appointment.";
|
|
97
|
+
}
|
|
98
|
+
else if (locationOptions.length === 0) {
|
|
99
|
+
problem = "Choose at least one way to meet.";
|
|
100
|
+
}
|
|
101
|
+
if (problem) {
|
|
102
|
+
setError(problem);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setSaving(true);
|
|
106
|
+
try {
|
|
107
|
+
const base = slugFor(name);
|
|
108
|
+
const availability = draft.days.flatMap((open, dayOfWeek) => (open ? [{ dayOfWeek, startMinute, endMinute }] : []));
|
|
109
|
+
const created = await createWithFreeSlug(base, {
|
|
110
|
+
mailboxUid: values.targetMailboxUid,
|
|
111
|
+
calendarFolderUid: values.targetFolderUid,
|
|
112
|
+
name,
|
|
113
|
+
hostDisplayName: hostName,
|
|
114
|
+
meetingTypes: [{ name, durationMinutes, locationOptions }],
|
|
115
|
+
timezone: timeZone,
|
|
116
|
+
availability,
|
|
117
|
+
bufferAfterMinutes: Number(draft.buffer),
|
|
118
|
+
});
|
|
119
|
+
onDraftChange({
|
|
120
|
+
created: { name: created.name, url: bookingPublicUrl(created.mailboxUid, created.slug), uid: created.uid, mailboxUid: created.mailboxUid },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create this appointment schedule.");
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
setSaving(false);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const { created } = draft;
|
|
131
|
+
return (_jsx(QuickFaceFrame, { c: c, tabs: tabs, tab: "appointment", error: error, hideTitle: !!created, onSubmit: (event) => void handleSubmit(event), footer: created ? (_jsx(Button, { type: "submit", className: "!w-auto", children: "Done" })) : (_jsxs(_Fragment, { children: [_jsxs("a", { href: newBookingLinkHref(settingsHref, values.targetMailboxUid), className: "inline-flex items-center gap-1 text-sm font-semibold text-accent-dark hover:underline px-1 py-1", children: ["More options", _jsx(HiOutlineArrowTopRightOnSquare, { size: 14, "aria-hidden": "true" })] }), _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" })] })), children: created ? (_jsxs(IconRow, { icon: _jsx(HiOutlineLink, { size: 20 }), children: [_jsxs("p", { role: "status", className: "text-sm font-medium mb-1", children: ["\u201C", created.name, "\u201D is ready to book."] }), _jsx("p", { className: "text-xs text-text-muted mb-2", children: "Share this link and anyone can pick an open time - no account needed." }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "text", readOnly: true, "aria-label": "Booking link", className: `${INPUT_CLASS} min-w-0`, value: created.url, onFocus: (e) => e.target.select() }), _jsx(CopyButton, { value: created.url, label: "Copy the booking link" })] }), _jsx("a", { href: `${settingsHref}/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(created.mailboxUid)}`, className: "inline-block text-xs text-primary-dark hover:underline mt-2", children: "Change its settings" })] })) : (_jsxs(_Fragment, { children: [_jsx(IconRow, { icon: _jsx(HiOutlineClock, { size: 20 }), children: _jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsxs("select", { "aria-label": "Duration", className: SELECT_CLASS, value: draft.duration, onChange: (e) => onDraftChange({ duration: e.target.value }), children: [DURATIONS.map((minutes) => (_jsxs("option", { value: minutes, children: [minutes, " minutes"] }, minutes))), _jsx("option", { value: "custom", children: "Custom" })] }), draft.duration === "custom" && (_jsxs(_Fragment, { children: [_jsx("input", { type: "number", min: MIN_DURATION, max: MAX_DURATION, "aria-label": "Custom duration in minutes", className: `${INPUT_CLASS} !w-24`, value: draft.customMinutes, onChange: (e) => onDraftChange({ customMinutes: e.target.value }) }), _jsx("span", { className: "text-text-muted", children: "minutes" })] })), _jsx("span", { className: "text-text-muted", children: "with" }), _jsx("select", { "aria-label": "Buffer after each appointment", className: SELECT_CLASS, value: draft.buffer, onChange: (e) => onDraftChange({ buffer: e.target.value }), children: BUFFERS.map((minutes) => (_jsx("option", { value: minutes, children: minutes === "0" ? "no buffer" : `${minutes} min buffer after` }, minutes))) })] }) }), _jsxs(IconRow, { icon: _jsx(HiOutlineCalendarDays, { size: 20 }), children: [_jsx("div", { role: "group", "aria-label": "Days people can book", className: "flex gap-1 mb-2", children: DAY_NAMES.map((day, index) => (_jsx("button", { type: "button", "aria-label": day, "aria-pressed": draft.days[index], title: day, onClick: () => onDraftChange({ days: draft.days.map((open, i) => (i === index ? !open : open)) }), className: [
|
|
132
|
+
"w-8 h-8 rounded-full text-xs font-medium border",
|
|
133
|
+
draft.days[index] ? "bg-primary text-white border-primary" : "border-border text-text-muted hover:bg-surface-alt",
|
|
134
|
+
].join(" "), children: day[0] }, day))) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsx("input", { type: "time", "aria-label": "Bookable from", className: `${SELECT_CLASS} min-w-0`, value: draft.startTime, onChange: (e) => onDraftChange({ startTime: e.target.value }) }), _jsx("span", { className: "text-text-muted", children: "to" }), _jsx("input", { type: "time", "aria-label": "Bookable until", className: `${SELECT_CLASS} min-w-0`, value: draft.endTime, onChange: (e) => onDraftChange({ endTime: e.target.value }) })] }), _jsxs("p", { className: "text-xs text-text-muted mt-1", children: ["Times are in ", timeZone.replace(/_/g, " "), "."] })] }), _jsx(IconRow, { icon: _jsx(HiOutlineMapPin, { size: 20 }), children: _jsx("div", { role: "group", "aria-label": "How people can meet", className: "flex flex-wrap gap-x-4 gap-y-1 text-sm py-1", children: LOCATIONS.map((location) => (_jsxs("label", { className: "flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: draft[location.key], onChange: (e) => onDraftChange({ [location.key]: e.target.checked }) }), location.label] }, location.key))) }) }), _jsxs(IconRow, { icon: _jsx(HiOutlineCalendar, { size: 20 }), children: [_jsx("p", { className: "text-xs text-text-muted mb-1", children: "Bookings are added to" }), _jsx(CalendarField, { c: c })] })] })) }));
|
|
135
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface DescriptionEditorProps {
|
|
3
|
+
/** The description to start with, as HTML. Read once, when the editor is created: TipTap owns the document from then on. */
|
|
4
|
+
value: string;
|
|
5
|
+
/** Called with the document as HTML (`<p></p>` for an empty one) each time it changes. */
|
|
6
|
+
onChange: (html: string) => void;
|
|
7
|
+
/** Focus the text when the editor appears (the "Add description" row that opened it). */
|
|
8
|
+
autoFocus?: boolean;
|
|
9
|
+
/** The accessible name of the text box. */
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What a typed link address is: an `http`, `https` or `mailto` link, with `https://` put in front of an address that names no scheme ("example.com") and
|
|
14
|
+
* `mailto:` in front of a plain email address. `undefined` for anything else (`javascript:`, `data:`, ...).
|
|
15
|
+
*/
|
|
16
|
+
export declare function linkFromInput(input: string): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The rich-text box of an event's description, as Google Calendar draws it: a toolbar (bold, italic, underline, numbered list, bulleted list, link, remove
|
|
19
|
+
* formatting) over the text. Built on TipTap with only the schema an event description may hold - paragraphs, line breaks, bold, italic, underline, lists and
|
|
20
|
+
* `http`/`https`/`mailto` links, so the HTML it writes has nothing the server would remove. Ctrl/Cmd+B, I and U format, Ctrl/Cmd+K asks for a link, and a
|
|
21
|
+
* paste keeps the text and drops the formatting.
|
|
22
|
+
*
|
|
23
|
+
* The toolbar is a WAI-ARIA toolbar: one tab stop, the arrow keys, Home and End move along it, and each toggle says whether it is on (`aria-pressed`). The
|
|
24
|
+
* buttons do not take focus from the text, so a selection survives pressing one.
|
|
25
|
+
*/
|
|
26
|
+
export default function DescriptionEditor({ value, onChange, autoFocus, label }: DescriptionEditorProps): React.JSX.Element;
|