@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,192 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useRef, useState } from "react";
|
|
7
|
+
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
|
8
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
9
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
10
|
+
import { BsEraser, BsLink45Deg, BsListOl, BsListUl, BsTypeBold, BsTypeItalic, BsTypeUnderline } from "react-icons/bs";
|
|
11
|
+
import { safeDescriptionHref } from "@rapidmx/react-shared/calendar/eventDescription.js";
|
|
12
|
+
/**
|
|
13
|
+
* The plain text on a clipboard as paragraphs, one for each line - what `handlePaste` inserts instead of the pasted formatting. Empty lines are kept as
|
|
14
|
+
* empty paragraphs, so a pasted block keeps its shape.
|
|
15
|
+
*/
|
|
16
|
+
function paragraphsOf(text) {
|
|
17
|
+
return text.split(/\r\n|\r|\n/).map((line) => ({ type: "paragraph", content: line ? [{ type: "text", text: line }] : [] }));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 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
|
|
21
|
+
* `mailto:` in front of a plain email address. `undefined` for anything else (`javascript:`, `data:`, ...).
|
|
22
|
+
*/
|
|
23
|
+
export function linkFromInput(input) {
|
|
24
|
+
const text = input.trim();
|
|
25
|
+
if (/\s/.test(text)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(text)) {
|
|
29
|
+
return safeDescriptionHref(text);
|
|
30
|
+
}
|
|
31
|
+
return safeDescriptionHref(/^[^\s@/]+@[^\s@/]+$/.test(text) ? `mailto:${text}` : `https://${text}`);
|
|
32
|
+
}
|
|
33
|
+
const NO_MARKS = { bold: false, italic: false, underline: false, bulletList: false, orderedList: false, link: false };
|
|
34
|
+
/**
|
|
35
|
+
* 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
|
|
36
|
+
* formatting) over the text. Built on TipTap with only the schema an event description may hold - paragraphs, line breaks, bold, italic, underline, lists and
|
|
37
|
+
* `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
|
|
38
|
+
* paste keeps the text and drops the formatting.
|
|
39
|
+
*
|
|
40
|
+
* 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
|
|
41
|
+
* buttons do not take focus from the text, so a selection survives pressing one.
|
|
42
|
+
*/
|
|
43
|
+
export default function DescriptionEditor({ value, onChange, autoFocus, label = "Description" }) {
|
|
44
|
+
const onChangeRef = useRef(onChange);
|
|
45
|
+
onChangeRef.current = onChange;
|
|
46
|
+
const openLinkRef = useRef(null);
|
|
47
|
+
// The paste handler is created with the editor, before the editor exists.
|
|
48
|
+
const editorRef = useRef(null);
|
|
49
|
+
const editor = useEditor({
|
|
50
|
+
extensions: [
|
|
51
|
+
StarterKit.configure({
|
|
52
|
+
heading: false,
|
|
53
|
+
blockquote: false,
|
|
54
|
+
code: false,
|
|
55
|
+
codeBlock: false,
|
|
56
|
+
horizontalRule: false,
|
|
57
|
+
strike: false,
|
|
58
|
+
dropcursor: false,
|
|
59
|
+
gapcursor: false,
|
|
60
|
+
trailingNode: false,
|
|
61
|
+
link: {
|
|
62
|
+
openOnClick: false,
|
|
63
|
+
autolink: false,
|
|
64
|
+
linkOnPaste: false,
|
|
65
|
+
protocols: ["http", "https", "mailto"],
|
|
66
|
+
HTMLAttributes: { rel: "noopener noreferrer", target: "_blank", class: "text-primary-dark underline" },
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
Placeholder.configure({ placeholder: "Add description" }),
|
|
70
|
+
],
|
|
71
|
+
content: value,
|
|
72
|
+
autofocus: autoFocus ? "end" : false,
|
|
73
|
+
immediatelyRender: false,
|
|
74
|
+
editorProps: {
|
|
75
|
+
attributes: { role: "textbox", "aria-multiline": "true", "aria-label": label },
|
|
76
|
+
handlePaste: (_view, event) => {
|
|
77
|
+
// A paste that came without a clipboard (ProseMirror then reads it back through a hidden field) has no text to give.
|
|
78
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
79
|
+
if (!text) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
editorRef.current.chain().focus().insertContent(paragraphsOf(text)).run();
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
handleKeyDown: (_view, event) => {
|
|
87
|
+
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === "k") {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
openLinkRef.current();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
onUpdate: ({ editor: updated }) => onChangeRef.current(updated.getHTML()),
|
|
96
|
+
});
|
|
97
|
+
editorRef.current = editor;
|
|
98
|
+
// Which formats the caret is in (re-read on every transaction). There is no editor until it has mounted, and then nothing is on.
|
|
99
|
+
const marks = useEditorState({
|
|
100
|
+
editor,
|
|
101
|
+
selector: ({ editor: current }) => current
|
|
102
|
+
? {
|
|
103
|
+
bold: current.isActive("bold"),
|
|
104
|
+
italic: current.isActive("italic"),
|
|
105
|
+
underline: current.isActive("underline"),
|
|
106
|
+
bulletList: current.isActive("bulletList"),
|
|
107
|
+
orderedList: current.isActive("orderedList"),
|
|
108
|
+
link: current.isActive("link"),
|
|
109
|
+
}
|
|
110
|
+
: NO_MARKS,
|
|
111
|
+
});
|
|
112
|
+
const [linkOpen, setLinkOpen] = useState(false);
|
|
113
|
+
const [linkText, setLinkText] = useState("");
|
|
114
|
+
const [linkError, setLinkError] = useState(null);
|
|
115
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
116
|
+
const buttons = useRef([]);
|
|
117
|
+
function openLink() {
|
|
118
|
+
setLinkText(editor.getAttributes("link").href ?? "");
|
|
119
|
+
setLinkError(null);
|
|
120
|
+
setLinkOpen(true);
|
|
121
|
+
}
|
|
122
|
+
openLinkRef.current = openLink;
|
|
123
|
+
function closeLink() {
|
|
124
|
+
setLinkOpen(false);
|
|
125
|
+
editor.commands.focus();
|
|
126
|
+
}
|
|
127
|
+
function submitLink(event) {
|
|
128
|
+
// The description sits inside the event's own form: applying a link must not submit that.
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
event.stopPropagation();
|
|
131
|
+
const chain = editor.chain().focus();
|
|
132
|
+
if (linkText.trim() === "") {
|
|
133
|
+
chain.extendMarkRange("link").unsetLink().run();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const href = linkFromInput(linkText);
|
|
137
|
+
if (!href) {
|
|
138
|
+
setLinkError("Enter a web address (http or https) or an email address.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (editor.state.selection.empty && !editor.isActive("link")) {
|
|
142
|
+
chain.insertContent({ type: "text", text: linkText.trim(), marks: [{ type: "link", attrs: { href } }] }).run();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
chain.extendMarkRange("link").setLink({ href }).run();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
setLinkOpen(false);
|
|
149
|
+
}
|
|
150
|
+
const items = [
|
|
151
|
+
{ label: "Bold", icon: BsTypeBold, pressed: marks.bold, onClick: () => editor.chain().focus().toggleBold().run() },
|
|
152
|
+
{ label: "Italic", icon: BsTypeItalic, pressed: marks.italic, onClick: () => editor.chain().focus().toggleItalic().run() },
|
|
153
|
+
{ label: "Underline", icon: BsTypeUnderline, pressed: marks.underline, onClick: () => editor.chain().focus().toggleUnderline().run() },
|
|
154
|
+
{ label: "Numbered list", icon: BsListOl, pressed: marks.orderedList, onClick: () => editor.chain().focus().toggleOrderedList().run() },
|
|
155
|
+
{ label: "Bulleted list", icon: BsListUl, pressed: marks.bulletList, onClick: () => editor.chain().focus().toggleBulletList().run() },
|
|
156
|
+
{ label: "Link", icon: BsLink45Deg, pressed: marks.link, onClick: openLink },
|
|
157
|
+
{ label: "Remove formatting", icon: BsEraser, onClick: () => editor.chain().focus().unsetAllMarks().run() },
|
|
158
|
+
];
|
|
159
|
+
function handleToolbarKeyDown(event) {
|
|
160
|
+
const last = items.length - 1;
|
|
161
|
+
const next = event.key === "ArrowRight" ? (focusIndex === last ? 0 : focusIndex + 1)
|
|
162
|
+
: event.key === "ArrowLeft" ? (focusIndex === 0 ? last : focusIndex - 1)
|
|
163
|
+
: event.key === "Home" ? 0
|
|
164
|
+
: event.key === "End" ? last
|
|
165
|
+
: undefined;
|
|
166
|
+
if (next !== undefined) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
setFocusIndex(next);
|
|
169
|
+
buttons.current[next].focus();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return (_jsxs("div", { className: "border border-border rounded-md bg-surface focus-within:border-primary", children: [_jsx("div", { role: "toolbar", "aria-label": "Description formatting", onKeyDown: handleToolbarKeyDown, className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1 border-b border-border", children: items.map((item, index) => (_jsx("button", { ref: (element) => {
|
|
173
|
+
buttons.current[index] = element;
|
|
174
|
+
}, type: "button", "aria-label": item.label, title: item.label, "aria-pressed": item.pressed, disabled: !editor, tabIndex: index === focusIndex ? 0 : -1, onFocus: () => setFocusIndex(index),
|
|
175
|
+
// Pressing a button must not move focus (and the selection) out of the text.
|
|
176
|
+
onMouseDown: (event) => event.preventDefault(), onClick: item.onClick, className: [
|
|
177
|
+
"w-7 h-7 flex items-center justify-center rounded-sm disabled:opacity-40",
|
|
178
|
+
item.pressed ? "bg-primary/10 text-primary-dark" : "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
179
|
+
].join(" "), children: _jsx(item.icon, { size: 15, "aria-hidden": "true" }) }, item.label))) }), linkOpen && (_jsxs("div", { className: "flex flex-col gap-1 px-2 py-1.5 border-b border-border", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "Link address", placeholder: "https://example.com", value: linkText, onChange: (event) => {
|
|
180
|
+
setLinkText(event.target.value);
|
|
181
|
+
setLinkError(null);
|
|
182
|
+
}, onKeyDown: (event) => {
|
|
183
|
+
if (event.key === "Enter") {
|
|
184
|
+
submitLink(event);
|
|
185
|
+
}
|
|
186
|
+
else if (event.key === "Escape") {
|
|
187
|
+
// Closes the prompt, not the dialog around it.
|
|
188
|
+
event.stopPropagation();
|
|
189
|
+
closeLink();
|
|
190
|
+
}
|
|
191
|
+
}, className: "flex-1 min-w-0 text-sm py-1 px-2 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "button", onClick: submitLink, className: "text-xs font-semibold text-primary-dark hover:underline", children: "Apply" }), _jsx("button", { type: "button", onClick: closeLink, className: "text-xs font-medium text-text-muted hover:underline", children: "Cancel" })] }), linkError && (_jsx("p", { role: "alert", className: "text-xs text-danger", children: linkError }))] })), _jsx(EditorContent, { editor: editor, className: "px-3 py-2 text-sm max-h-64 overflow-y-auto [&_.tiptap]:outline-none [&_.tiptap]:min-h-[4.5rem] [&_.tiptap_ul]:list-disc [&_.tiptap_ul]:pl-5 [&_.tiptap_ol]:list-decimal [&_.tiptap_ol]:pl-5 [&_.tiptap_p.is-editor-empty:first-child]:before:content-[attr(data-placeholder)] [&_.tiptap_p.is-editor-empty:first-child]:before:text-text-muted [&_.tiptap_p.is-editor-empty:first-child]:before:float-left [&_.tiptap_p.is-editor-empty:first-child]:before:h-0 [&_.tiptap_p.is-editor-empty:first-child]:before:pointer-events-none" })] }));
|
|
192
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface EventDescriptionViewProps {
|
|
3
|
+
/** The description as HTML, from the server or an invitation - untrusted, so it is read into allowed elements only (`parseEventDescription()`) and drawn as
|
|
4
|
+
* React elements, never as markup. */
|
|
5
|
+
html?: string | null;
|
|
6
|
+
/** The plain-text description, shown (line breaks kept) when there is no HTML, or when the HTML holds nothing this can draw. */
|
|
7
|
+
text?: string | null;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An event's description, read-only: the rich text with its bold, italics, underline, lists and links (which open in a new tab, `noopener noreferrer`),
|
|
12
|
+
* else the plain text. Nothing is drawn for an event with no description.
|
|
13
|
+
*/
|
|
14
|
+
export default function EventDescriptionView({ html, text, className }: EventDescriptionViewProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { htmlToPlainText, parseEventDescription } from "@rapidmx/react-shared/calendar/eventDescription.js";
|
|
3
|
+
function renderNodes(nodes) {
|
|
4
|
+
return nodes.map((node, index) => {
|
|
5
|
+
if ("text" in node) {
|
|
6
|
+
return node.text;
|
|
7
|
+
}
|
|
8
|
+
switch (node.tag) {
|
|
9
|
+
case "br":
|
|
10
|
+
return _jsx("br", {}, index);
|
|
11
|
+
case "a":
|
|
12
|
+
return (_jsx("a", { href: node.href, target: "_blank", rel: "noopener noreferrer", className: "text-primary-dark underline break-words", children: renderNodes(node.children) }, index));
|
|
13
|
+
case "ul":
|
|
14
|
+
return (_jsx("ul", { className: "list-disc pl-5 my-0.5", children: renderNodes(node.children) }, index));
|
|
15
|
+
case "ol":
|
|
16
|
+
return (_jsx("ol", { className: "list-decimal pl-5 my-0.5", children: renderNodes(node.children) }, index));
|
|
17
|
+
case "p":
|
|
18
|
+
return (_jsx("p", { className: "mb-1 last:mb-0 min-h-[1lh]", children: renderNodes(node.children) }, index));
|
|
19
|
+
case "li":
|
|
20
|
+
return _jsx("li", { children: renderNodes(node.children) }, index);
|
|
21
|
+
case "strong":
|
|
22
|
+
return _jsx("strong", { children: renderNodes(node.children) }, index);
|
|
23
|
+
case "em":
|
|
24
|
+
return _jsx("em", { children: renderNodes(node.children) }, index);
|
|
25
|
+
case "u":
|
|
26
|
+
return _jsx("u", { children: renderNodes(node.children) }, index);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An event's description, read-only: the rich text with its bold, italics, underline, lists and links (which open in a new tab, `noopener noreferrer`),
|
|
32
|
+
* else the plain text. Nothing is drawn for an event with no description.
|
|
33
|
+
*/
|
|
34
|
+
export default function EventDescriptionView({ html, text, className }) {
|
|
35
|
+
if (htmlToPlainText(html) !== "") {
|
|
36
|
+
return _jsx("div", { className: ["break-words", className].filter(Boolean).join(" "), children: renderNodes(parseEventDescription(html)) });
|
|
37
|
+
}
|
|
38
|
+
return text?.trim() ? _jsx("div", { className: ["break-words whitespace-pre-wrap", className].filter(Boolean).join(" "), children: text }) : null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AttendeeResponseInput } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
3
|
+
import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
4
|
+
export interface EventDetailsProps {
|
|
5
|
+
occurrence: CalendarOccurrence;
|
|
6
|
+
/** Whether this mailbox is invited to the event rather than organizing it: only the organizer can change an event, so there is no
|
|
7
|
+
* Modify, and the mailbox may answer instead. */
|
|
8
|
+
isInvited: boolean;
|
|
9
|
+
/** Set only when this mailbox is an invited attendee (one of the attendees, and not the organizer): the answer it has given so far, which
|
|
10
|
+
* also switches on the Accept / Tentative / Decline block. */
|
|
11
|
+
myResponse?: AttendeeResponseInput | "needsAction";
|
|
12
|
+
/** Whether an address is one of this mailbox's own (its primary address or an alias): tells the reader's own entry from the other guests when the organizer
|
|
13
|
+
* has hidden the guest list. */
|
|
14
|
+
isOwnAddress?: (address: string) => boolean;
|
|
15
|
+
calendarName?: string;
|
|
16
|
+
calendarColor?: string;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
/** Modify: switch to the edit form. */
|
|
19
|
+
onModify: () => void;
|
|
20
|
+
onSaved: () => void;
|
|
21
|
+
onDeleted: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What clicking an existing event shows first: everything the event says, read-only. The reader who organized it (or whose event has no
|
|
25
|
+
* other organizer) can Modify or Delete it; one who was invited can answer (Accept, Tentative, Decline) or remove it from their own
|
|
26
|
+
* calendar, but not change it - the organizer's next update would overwrite that anyway. When the organizer allows guests to ask for changes (or for
|
|
27
|
+
* guests to be added) the invited reader can "Request a change" / "Add guests": the request goes to the organizer (`RequestChangeForm`) and nothing here
|
|
28
|
+
* changes until their update arrives. When the organizer hides the guest list the reader sees only themselves and is told so.
|
|
29
|
+
*
|
|
30
|
+
* An event this reader may only see as a busy block (`occurrence.redacted`: a private or confidential event on a shared calendar) is drawn as just that -
|
|
31
|
+
* its time and that it is busy - with nothing to change, delete or answer.
|
|
32
|
+
*/
|
|
33
|
+
export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }: EventDetailsProps): React.JSX.Element;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { HiOutlineBars3BottomLeft, HiOutlineBell, HiOutlineBriefcase, HiOutlineCheck, HiOutlineClock, HiOutlineMapPin, HiOutlineUser, HiOutlineUserGroup, HiOutlineVideoCamera, HiOutlineXMark, } from "react-icons/hi2";
|
|
8
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
+
import { guestPermissionsOf, respondToEvent, visibilityOf } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
10
|
+
import { htmlToPlainText } from "@rapidmx/react-shared/calendar/eventDescription.js";
|
|
11
|
+
import { deleteEventOccurrence, deleteEventSeries } from "@rapidmx/react-shared/calendar/calendarMutations.js";
|
|
12
|
+
import { describeRecurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
13
|
+
import { getVideoMeeting } from "@rapidmx/react-shared/videoconf/videoMeetingsApi.js";
|
|
14
|
+
import { deviceTimeZone } from "@rapidmx/react-shared/util/timeZone.js";
|
|
15
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
16
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
17
|
+
import { joinMeetingUrl } from "../../calendar/calendarReminders.js";
|
|
18
|
+
import EventDescriptionView from "./EventDescriptionView.js";
|
|
19
|
+
import RequestChangeForm from "./RequestChangeForm.js";
|
|
20
|
+
import { BUSY_STATUS_LABEL, RESPONSE_STATUS_LABEL, VISIBILITY_LABEL, describeGuestPermissions, describeReminder, formatStoredWhen } from "./eventFormat.js";
|
|
21
|
+
const ROLE_LABEL = { required: "", optional: "Optional", resource: "Room/equipment" };
|
|
22
|
+
function DetailRow({ icon, children }) {
|
|
23
|
+
return (_jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted mt-0.5", children: icon }), _jsx("div", { className: "flex-1 min-w-0 break-words", children: children })] }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What clicking an existing event shows first: everything the event says, read-only. The reader who organized it (or whose event has no
|
|
27
|
+
* other organizer) can Modify or Delete it; one who was invited can answer (Accept, Tentative, Decline) or remove it from their own
|
|
28
|
+
* calendar, but not change it - the organizer's next update would overwrite that anyway. When the organizer allows guests to ask for changes (or for
|
|
29
|
+
* guests to be added) the invited reader can "Request a change" / "Add guests": the request goes to the organizer (`RequestChangeForm`) and nothing here
|
|
30
|
+
* changes until their update arrives. When the organizer hides the guest list the reader sees only themselves and is told so.
|
|
31
|
+
*
|
|
32
|
+
* An event this reader may only see as a busy block (`occurrence.redacted`: a private or confidential event on a shared calendar) is drawn as just that -
|
|
33
|
+
* its time and that it is busy - with nothing to change, delete or answer.
|
|
34
|
+
*/
|
|
35
|
+
export default function EventDetails({ occurrence, isInvited, myResponse, isOwnAddress, calendarName, calendarColor, onClose, onModify, onSaved, onDeleted, }) {
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
39
|
+
const [requesting, setRequesting] = useState(false);
|
|
40
|
+
// The organizer's own join link for the event's video meeting: `undefined` while it is fetched, `null` once known to be none.
|
|
41
|
+
const [organizerJoinUrl, setOrganizerJoinUrl] = useState(undefined);
|
|
42
|
+
const videoMeetingUid = occurrence.videoMeetingUid;
|
|
43
|
+
// The link lives on the meeting, not on the event, so it is fetched once here. A failure and a meeting with no link at all are the
|
|
44
|
+
// same thing to this view - there is nothing to open either way - so both settle on `null`.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!videoMeetingUid) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let cancelled = false;
|
|
50
|
+
void getVideoMeeting(videoMeetingUid)
|
|
51
|
+
.then((meeting) => meeting.organizerJoinUrl ?? null)
|
|
52
|
+
.catch(() => null)
|
|
53
|
+
.then((url) => {
|
|
54
|
+
if (!cancelled) {
|
|
55
|
+
setOrganizerJoinUrl(url);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
cancelled = true;
|
|
60
|
+
};
|
|
61
|
+
}, [videoMeetingUid]);
|
|
62
|
+
// The same scheme allow-list a reminder's own location gets: a hostile or malformed join link never reaches `window.open()`.
|
|
63
|
+
const joinUrl = joinMeetingUrl(organizerJoinUrl);
|
|
64
|
+
async function handleDelete(scope) {
|
|
65
|
+
setError(null);
|
|
66
|
+
setBusy(true);
|
|
67
|
+
try {
|
|
68
|
+
if (occurrence.isRecurringOccurrence && scope === "occurrence") {
|
|
69
|
+
await deleteEventOccurrence(occurrence);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await deleteEventSeries(occurrence);
|
|
73
|
+
}
|
|
74
|
+
onDeleted();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not delete this event.");
|
|
78
|
+
setBusy(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// `@rapidmx/restapi`'s `respond()` route has no occurrence-vs-series scope of its own (see `calendarApi.ts`'s `respondToEvent` doc
|
|
82
|
+
// comment) - it always acts on `occurrence.uid` as a single event document.
|
|
83
|
+
async function handleRespond(responseStatus) {
|
|
84
|
+
setError(null);
|
|
85
|
+
setBusy(true);
|
|
86
|
+
try {
|
|
87
|
+
await respondToEvent(occurrence.uid, responseStatus);
|
|
88
|
+
// A decline soft-deletes the mailbox's own copy server-side - treat it the same as a
|
|
89
|
+
// delete rather than a save so the calendar view drops it immediately.
|
|
90
|
+
if (responseStatus === "declined") {
|
|
91
|
+
onDeleted();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
onSaved();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not send your response.");
|
|
99
|
+
setBusy(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (occurrence.redacted) {
|
|
103
|
+
return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-start gap-3 px-5 pt-4", children: [_jsx("span", { "aria-hidden": "true", className: "w-3.5 h-3.5 rounded-full shrink-0 mt-2", style: { backgroundColor: calendarColor } }), _jsx("h2", { className: "flex-1 min-w-0 text-xl font-semibold break-words", children: "Busy" }), _jsx("button", { type: "button", "aria-label": "Close details", onClick: onClose, className: "w-8 h-8 shrink-0 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: _jsx(HiOutlineXMark, { size: 20, "aria-hidden": "true" }) })] }), _jsxs("div", { className: "flex flex-col gap-3 px-5 pt-3 pb-4 text-sm", children: [_jsxs(DetailRow, { icon: _jsx(HiOutlineClock, { size: 18 }), children: [_jsx("div", { children: formatStoredWhen(occurrence.startDate, occurrence.endDate, occurrence.allDay) }), occurrence.recurrenceRule && _jsxs("div", { className: "text-text-muted", children: ["Repeats ", describeRecurrence(occurrence.recurrenceRule)] })] }), _jsx("p", { className: "text-xs text-text-muted", children: "This time is busy. The event\u2019s details are private, so only its time is shown." })] }), _jsx("div", { className: "flex px-5 pb-4", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto ml-auto", onClick: onClose, children: "Close" }) })] }));
|
|
104
|
+
}
|
|
105
|
+
const permissions = guestPermissionsOf(occurrence);
|
|
106
|
+
// The organizer hid the guest list: this mailbox's own copy lists only itself (the server sends nothing more), and that is all that is shown.
|
|
107
|
+
const hiddenList = isInvited && !permissions.guestsCanSeeGuestList;
|
|
108
|
+
const allGuests = occurrence.attendees.filter((a) => !a.isOrganizer);
|
|
109
|
+
const guests = hiddenList ? allGuests.filter((a) => !!isOwnAddress?.(a.address)) : allGuests;
|
|
110
|
+
const hasDescription = htmlToPlainText(occurrence.descriptionHtml) !== "" || !!occurrence.description?.trim();
|
|
111
|
+
// An invited reader may ask the organizer for what the organizer allows.
|
|
112
|
+
const askable = isInvited && myResponse !== undefined;
|
|
113
|
+
const canChange = askable && permissions.guestsCanModify;
|
|
114
|
+
const canInvite = askable && permissions.guestsCanInviteOthers;
|
|
115
|
+
const organizerName = occurrence.organizer.displayName;
|
|
116
|
+
const eventZone = occurrence.timezone;
|
|
117
|
+
const answers = [
|
|
118
|
+
{ response: "accepted", label: "Accept" },
|
|
119
|
+
{ response: "tentative", label: "Tentative" },
|
|
120
|
+
{ response: "declined", label: "Decline" },
|
|
121
|
+
];
|
|
122
|
+
return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-start gap-3 px-5 pt-4", children: [_jsx("span", { "aria-hidden": "true", className: "w-3.5 h-3.5 rounded-full shrink-0 mt-2", style: { backgroundColor: calendarColor } }), _jsx("h2", { className: "flex-1 min-w-0 text-xl font-semibold break-words", children: occurrence.title || "(no title)" }), _jsx("button", { type: "button", "aria-label": "Close details", onClick: onClose, className: "w-8 h-8 shrink-0 flex items-center justify-center rounded-full text-text-muted hover:bg-surface-alt hover:text-text", children: _jsx(HiOutlineXMark, { size: 20, "aria-hidden": "true" }) })] }), _jsxs("div", { className: "flex flex-col gap-3 px-5 pt-3 pb-4 text-sm", children: [error && _jsx(Alert, { children: error }), _jsxs(DetailRow, { icon: _jsx(HiOutlineClock, { size: 18 }), children: [_jsx("div", { children: formatStoredWhen(occurrence.startDate, occurrence.endDate, occurrence.allDay) }), occurrence.recurrenceRule && _jsxs("div", { className: "text-text-muted", children: ["Repeats ", describeRecurrence(occurrence.recurrenceRule)] }), !occurrence.allDay && eventZone && eventZone !== deviceTimeZone() && (_jsxs("div", { className: "text-text-muted", children: ["Event time zone: ", eventZone.replace(/_/g, " ")] }))] }), occurrence.location && (_jsx(DetailRow, { icon: _jsx(HiOutlineMapPin, { size: 18 }), children: _jsx("div", { children: occurrence.location }) })), hasDescription && (_jsx(DetailRow, { icon: _jsx(HiOutlineBars3BottomLeft, { size: 18 }), children: _jsx(EventDescriptionView, { html: occurrence.descriptionHtml, text: occurrence.description }) })), videoMeetingUid && (_jsx(DetailRow, { icon: _jsx(HiOutlineVideoCamera, { size: 18 }), children: _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: !joinUrl, onClick: () => window.open(joinUrl, "_blank", "noopener,noreferrer"), children: "Join video call" }), !joinUrl && (_jsx("span", { className: "text-xs text-text-muted", children: organizerJoinUrl === undefined ? "Loading the join link…" : "This meeting’s join link isn’t available." }))] }) })), _jsxs(DetailRow, { icon: _jsx(HiOutlineUser, { size: 18 }), children: [_jsx("div", { children: organizerName || occurrence.organizer.address }), _jsxs("div", { className: "text-text-muted", children: [organizerName ? `${occurrence.organizer.address} · ` : "", "Organizer"] })] }), (guests.length > 0 || hiddenList) && (_jsxs(DetailRow, { icon: _jsx(HiOutlineUserGroup, { size: 18 }), children: [_jsx("div", { className: "font-medium mb-1", children: hiddenList ? "The organizer has hidden the guest list" : guests.length === 1 ? "1 guest" : `${guests.length} guests` }), _jsx("ul", { className: "flex flex-col gap-1", children: guests.map((guest, i) => (_jsxs("li", { className: "flex items-baseline justify-between gap-3", children: [_jsxs("span", { className: "min-w-0 truncate", children: [guest.displayName || guest.address, ROLE_LABEL[guest.role] && _jsxs("span", { className: "text-text-muted", children: [" (", ROLE_LABEL[guest.role], ")"] })] }), _jsx("span", { className: "shrink-0 text-xs text-text-muted", children: RESPONSE_STATUS_LABEL[guest.responseStatus] })] }, i))) }), !isInvited && _jsx("div", { className: "text-xs text-text-muted mt-1", children: describeGuestPermissions(permissions) })] })), _jsxs(DetailRow, { icon: _jsx(HiOutlineBriefcase, { size: 18 }), children: [_jsx("div", { children: BUSY_STATUS_LABEL[occurrence.busyStatus] }), _jsx("div", { className: "text-text-muted", children: VISIBILITY_LABEL[visibilityOf(occurrence)] }), occurrence.autoReplyEnabled && (_jsx("div", { className: "text-text-muted", children: "Sends an automatic reply while this event is happening" }))] }), _jsx(DetailRow, { icon: _jsx(HiOutlineBell, { size: 18 }), children: _jsx("div", { children: describeReminder(occurrence.reminderMinutesBeforeStart) }) }), calendarName && (_jsxs("div", { className: "flex items-center gap-3 text-text-muted", children: [_jsx("span", { "aria-hidden": "true", className: "w-3 h-3 ml-[3px] rounded-full shrink-0", style: { backgroundColor: calendarColor } }), _jsx("span", { className: "truncate", children: calendarName })] })), isInvited && (_jsxs("p", { className: "text-xs text-text-muted", children: ["You were invited to this event. Only the organizer can change its details", canChange || canInvite ? ", but you can ask them to." : "."] }))] }), requesting && (_jsx("div", { className: "mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: _jsx(RequestChangeForm, { subject: {
|
|
123
|
+
uid: occurrence.uid,
|
|
124
|
+
title: occurrence.title,
|
|
125
|
+
location: occurrence.location,
|
|
126
|
+
startDate: occurrence.startDate,
|
|
127
|
+
endDate: occurrence.endDate,
|
|
128
|
+
allDay: occurrence.allDay,
|
|
129
|
+
recurring: occurrence.isRecurringOccurrence,
|
|
130
|
+
description: occurrence.description,
|
|
131
|
+
descriptionHtml: occurrence.descriptionHtml,
|
|
132
|
+
guestAddresses: occurrence.attendees.map((a) => a.address),
|
|
133
|
+
}, canChange: canChange, canInvite: canInvite, onSent: () => setRequesting(false), onCancel: () => setRequesting(false) }) })), myResponse !== undefined && (_jsxs("div", { className: "flex flex-col gap-2 mx-5 mb-3 p-3 rounded-md bg-surface-alt", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsx("span", { className: "text-sm font-medium", children: "Your response" }), _jsx("div", { className: "flex gap-2", children: answers.map(({ response, label }) => {
|
|
134
|
+
const current = myResponse === response;
|
|
135
|
+
return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", response === "declined" ? "text-danger" : "", current ? "!bg-primary/10 !border-primary" : ""].join(" "), "aria-pressed": current, disabled: busy, onClick: () => void handleRespond(response), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
|
|
136
|
+
}) })] }), _jsx("p", { className: "text-xs text-text-muted", children: "Other attendees\u2019 responses may take a few minutes to update." })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-3 px-5 pb-4", children: [!isInvited && (_jsx(Button, { type: "button", className: "!w-auto", disabled: busy, onClick: onModify, children: "Modify" })), (canChange || canInvite) && !requesting && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setRequesting(true), children: canChange ? "Request a change" : "Add guests" })), !occurrence.isRecurringOccurrence ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Delete" })) : !confirmingDelete ? (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => setConfirmingDelete(true), children: "Delete" })) : (_jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("occurrence"), children: "Delete this event" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto text-danger", disabled: busy, onClick: () => void handleDelete("series"), children: "Delete series" })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto ml-auto", onClick: onClose, children: "Close" })] })] }));
|
|
137
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { MutableRefObject } from "react";
|
|
2
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
|
+
import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
4
|
+
import { QuickCreateConfig } from "./QuickCreateFaces.js";
|
|
5
|
+
/**
|
|
6
|
+
* The Location text this modal writes when it mints a video meeting for an event, and the one value it will
|
|
7
|
+
* clear again when video conferencing is turned back off (see `applyVideoConferencing()`).
|
|
8
|
+
*
|
|
9
|
+
* Deliberately generic and identical for everyone: each attendee's own personal join link is substituted into
|
|
10
|
+
* their own copy of the invitation server-side (`@rapidmx/restapi`'s `MeetingSchedulingJob`), so no link -
|
|
11
|
+
* personal or otherwise - may be stored on the shared event itself, where every attendee would read the same
|
|
12
|
+
* one.
|
|
13
|
+
*/
|
|
14
|
+
export declare const VIDEO_LOCATION_PLACEHOLDER = "Video call \u2014 link in this invitation";
|
|
15
|
+
export interface EventEditorProps {
|
|
16
|
+
/** The quick-create popover's face of the form, or the full form's (only a new event, which has `quickCreate`, has the quick one). Both show the same values. */
|
|
17
|
+
layout: "quick" | "expanded";
|
|
18
|
+
/** "More options": the quick face asks to become the full one. */
|
|
19
|
+
onExpand: () => void;
|
|
20
|
+
/** Leave without saving (nothing was stored). */
|
|
21
|
+
onCancel: () => void;
|
|
22
|
+
/** Something was stored: the dialog closes and the calendar reloads. */
|
|
23
|
+
onSaved: () => void;
|
|
24
|
+
/** A series save could not move every occurrence edited on its own with it (`detachedOccurrenceSyncFailed`). */
|
|
25
|
+
onSyncWarning: () => void;
|
|
26
|
+
/** Kept up to date with whether the form holds anything a stray click outside it would throw away. */
|
|
27
|
+
dirtyRef: MutableRefObject<boolean>;
|
|
28
|
+
mailboxUid: string;
|
|
29
|
+
folderUid: string;
|
|
30
|
+
calendars?: {
|
|
31
|
+
uid: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}[];
|
|
34
|
+
mailboxOptions?: {
|
|
35
|
+
mailbox: Mailbox;
|
|
36
|
+
calendars: {
|
|
37
|
+
uid: string;
|
|
38
|
+
name: string;
|
|
39
|
+
}[];
|
|
40
|
+
}[];
|
|
41
|
+
/** Each calendar's colour, keyed by folder uid. */
|
|
42
|
+
folderColors?: Record<string, string>;
|
|
43
|
+
organizerAddress: string;
|
|
44
|
+
/** `null` when creating a new event. */
|
|
45
|
+
occurrence: CalendarOccurrence | null;
|
|
46
|
+
initialStart?: Date;
|
|
47
|
+
initialEnd?: Date;
|
|
48
|
+
/** A new event starting as an all-day one (a day cell of the month view was clicked). */
|
|
49
|
+
initialAllDay?: boolean;
|
|
50
|
+
/** A new event's tabs (Event, Task, Appointment schedule): with it the form is drawn by `QuickCreateFaces`; without it, as the full card. */
|
|
51
|
+
quickCreate?: QuickCreateConfig;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The event form and everything it does: create, and edit (one occurrence, or the entire series, of a recurring event - the choice
|
|
55
|
+
* Outlook's own convention offers for the same ambiguity, see `calendarMutations.ts`), plus video conferencing. It owns the form's
|
|
56
|
+
* one set of values and draws them as either the quick-create popover or the full card (`layout`), so growing the one into the other
|
|
57
|
+
* keeps what was typed.
|
|
58
|
+
*/
|
|
59
|
+
export default function EventEditor({ layout, onExpand, onCancel, onSaved, onSyncWarning, dirtyRef, mailboxUid, folderUid, calendars, mailboxOptions, folderColors, organizerAddress, occurrence, initialStart, initialEnd, initialAllDay, quickCreate, }: EventEditorProps): React.JSX.Element;
|