@rapidmx/web-client 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +381 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +166 -164
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +350 -348
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +129 -124
- package/apps/admin/mailboxes/[uid].tsx +271 -224
- package/apps/admin/mailboxes/new/index.tsx +30 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +581 -571
- package/apps/www/contacts/[uid].tsx +112 -109
- package/apps/www/contacts/index.tsx +14 -3
- package/apps/www/index.tsx +2962 -2479
- package/apps/www/messages/[uid].tsx +139 -135
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +136 -133
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +1290 -1287
- package/apps/www/settings/filters/[uid].tsx +179 -176
- package/apps/www/settings/filters/index.tsx +105 -102
- package/apps/www/settings/filters/new/index.tsx +165 -140
- package/apps/www/settings/labels/index.tsx +207 -204
- package/apps/www/settings/privacy/index.tsx +495 -492
- package/apps/www/settings/profile/index.tsx +251 -248
- package/apps/www/settings/read-receipts/index.tsx +150 -147
- package/apps/www/settings/sharing/index.tsx +259 -256
- package/apps/www/settings/signatures/[uid].tsx +175 -172
- package/apps/www/settings/signatures/index.tsx +91 -88
- package/apps/www/settings/signatures/new/index.tsx +138 -135
- package/apps/www/tasks/index.tsx +654 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +5 -2
- package/dist/apps/admin/mailboxes/[uid].js +15 -3
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
- package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +7 -4
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +8 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +542 -207
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React from "react";
|
|
6
|
-
import { format } from "date-fns";
|
|
7
|
-
import { RecurrenceFrequency, RecurrenceRule, WeekdayCode } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
8
|
-
import { WEEKDAY_CODES, WEEKDAY_LABELS, describeRecurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
9
|
-
import { recurrenceUntilDateKey, recurrenceUntilInstant } from "./allDay.js";
|
|
10
|
-
|
|
11
|
-
const SELECT_CLASS =
|
|
12
|
-
"text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
13
|
-
const INPUT_CLASS =
|
|
14
|
-
"text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
15
|
-
|
|
16
|
-
const FREQ_LABEL: Record<RecurrenceFrequency, { unit: string; unitPlural: string }> = {
|
|
17
|
-
daily: { unit: "day", unitPlural: "days" },
|
|
18
|
-
weekly: { unit: "week", unitPlural: "weeks" },
|
|
19
|
-
monthly: { unit: "month", unitPlural: "months" },
|
|
20
|
-
yearly: { unit: "year", unitPlural: "years" },
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type EndCondition = "never" | "count" | "until";
|
|
24
|
-
|
|
25
|
-
function endConditionOf(rule: RecurrenceRule): EndCondition {
|
|
26
|
-
if (rule.count) return "count";
|
|
27
|
-
if (rule.until) return "until";
|
|
28
|
-
return "never";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface RecurrenceEditorProps {
|
|
32
|
-
/** `null` means "does not repeat". */
|
|
33
|
-
value: RecurrenceRule | null;
|
|
34
|
-
onChange: (value: RecurrenceRule | null) => void;
|
|
35
|
-
/** Whether the event is all-day - its series expands in UTC, so the inclusive "Ends on" date is stored as
|
|
36
|
-
* the end of that UTC day rather than the local one (see `allDay.ts`'s `recurrenceUntilInstant()`). */
|
|
37
|
-
allDay?: boolean;
|
|
38
|
-
/** The weekday the event starts on (in the frame its series expands in - see `allDay.ts`'s
|
|
39
|
-
* `startWeekdayCode()`): the day a new weekly rule, or one switched to Weekly, repeats on. Defaults to Monday. */
|
|
40
|
-
startWeekday?: WeekdayCode;
|
|
41
|
-
/** Leaves out the "Repeats" checkbox, for a caller that already turns repeating on and off itself (the event form's
|
|
42
|
-
* "Does not repeat" menu): what remains is the rule's details, shown only while there is a rule. */
|
|
43
|
-
hideToggle?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* A recurring-event editor: frequency + interval, a weekday picker (weekly only), and an end
|
|
48
|
-
* condition (never / after N occurrences / on a date) — everything `RecurrenceRule` can express. The
|
|
49
|
-
* live "every ... until/for ..." summary comes from `describeRecurrence()` (built on `rrule`'s own
|
|
50
|
-
* `.toText()`), so it never drifts out of sync with what will actually be submitted.
|
|
51
|
-
*/
|
|
52
|
-
export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO", hideToggle = false }: RecurrenceEditorProps) {
|
|
53
|
-
function handleEnable(enabled: boolean) {
|
|
54
|
-
onChange(enabled ? { freq: "weekly", interval: 1, byDay: [startWeekday], exceptions: [] } : null);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// `update`/`toggleDay` are only ever invoked from handlers rendered inside the `{value && (...)}`
|
|
58
|
-
// block below, so `value` is always non-null here — no defensive null check needed.
|
|
59
|
-
function update(patch: Partial<RecurrenceRule>) {
|
|
60
|
-
onChange({ ...(value as RecurrenceRule), ...patch });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// A weekly rule always keeps at least one weekday: with none left, rrule would repeat every day instead.
|
|
64
|
-
function toggleDay(day: WeekdayCode) {
|
|
65
|
-
const current = (value as RecurrenceRule).byDay ?? [];
|
|
66
|
-
if (current.length === 1 && current[0] === day) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const next = current.includes(day) ? current.filter((d) => d !== day) : [...current, day];
|
|
70
|
-
update({ byDay: next });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// react-shared passes `byDay` to rrule for every frequency, so a weekday list left over from Weekly would
|
|
74
|
-
// turn "every day" into "every Monday". Leaving Weekly drops it; entering Weekly starts on the event's own
|
|
75
|
-
// start weekday (a select only reports an actual change, so this is never Weekly -> Weekly).
|
|
76
|
-
function handleFrequency(freq: RecurrenceFrequency) {
|
|
77
|
-
update({ freq, byDay: freq === "weekly" ? [startWeekday] : undefined });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function handleEndCondition(condition: EndCondition) {
|
|
81
|
-
if (condition === "never") {
|
|
82
|
-
update({ count: undefined, until: undefined });
|
|
83
|
-
} else if (condition === "count") {
|
|
84
|
-
update({ count: 10, until: undefined });
|
|
85
|
-
} else {
|
|
86
|
-
update({ until: recurrenceUntilInstant(format(new Date(), "yyyy-MM-dd"), allDay), count: undefined });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** A positive whole number from a number input, or `null` while it's empty (a user clearing the field
|
|
91
|
-
* to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN. */
|
|
92
|
-
function parsePositive(raw: string): number | null {
|
|
93
|
-
if (raw.trim() === "") return null;
|
|
94
|
-
const n = Math.floor(Number(raw));
|
|
95
|
-
return n >= 1 ? n : 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function handleInterval(raw: string) {
|
|
99
|
-
const interval = parsePositive(raw);
|
|
100
|
-
if (interval !== null) update({ interval });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function handleCount(raw: string) {
|
|
104
|
-
const count = parsePositive(raw);
|
|
105
|
-
if (count !== null) update({ count });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function handleUntil(raw: string) {
|
|
109
|
-
// A cleared (or partially typed) date input reports "" — keep the previous end date.
|
|
110
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) update({ until: recurrenceUntilInstant(raw, allDay) });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<div className="flex flex-col gap-3">
|
|
115
|
-
{!hideToggle && (
|
|
116
|
-
<label className="flex items-center gap-2 text-sm font-medium">
|
|
117
|
-
<input type="checkbox" checked={value !== null} onChange={(e) => handleEnable(e.target.checked)} />
|
|
118
|
-
Repeats
|
|
119
|
-
</label>
|
|
120
|
-
)}
|
|
121
|
-
|
|
122
|
-
{value && (
|
|
123
|
-
<div className="flex flex-col gap-3 pl-6 border-l-2 border-border">
|
|
124
|
-
<div className="flex items-center gap-2 text-sm">
|
|
125
|
-
<span>Every</span>
|
|
126
|
-
<input
|
|
127
|
-
type="number"
|
|
128
|
-
min={1}
|
|
129
|
-
className={`${INPUT_CLASS} w-16`}
|
|
130
|
-
value={value.interval}
|
|
131
|
-
onChange={(e) => handleInterval(e.target.value)}
|
|
132
|
-
aria-label="Recurrence interval"
|
|
133
|
-
/>
|
|
134
|
-
<select
|
|
135
|
-
className={SELECT_CLASS}
|
|
136
|
-
value={value.freq}
|
|
137
|
-
onChange={(e) => handleFrequency(e.target.value as RecurrenceFrequency)}
|
|
138
|
-
aria-label="Recurrence frequency"
|
|
139
|
-
>
|
|
140
|
-
<option value="daily">{value.interval === 1 ? FREQ_LABEL.daily.unit : FREQ_LABEL.daily.unitPlural}</option>
|
|
141
|
-
<option value="weekly">{value.interval === 1 ? FREQ_LABEL.weekly.unit : FREQ_LABEL.weekly.unitPlural}</option>
|
|
142
|
-
<option value="monthly">{value.interval === 1 ? FREQ_LABEL.monthly.unit : FREQ_LABEL.monthly.unitPlural}</option>
|
|
143
|
-
<option value="yearly">{value.interval === 1 ? FREQ_LABEL.yearly.unit : FREQ_LABEL.yearly.unitPlural}</option>
|
|
144
|
-
</select>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
{value.freq === "weekly" && (
|
|
148
|
-
<div className="flex gap-1">
|
|
149
|
-
{WEEKDAY_CODES.map((day) => (
|
|
150
|
-
<button
|
|
151
|
-
key={day}
|
|
152
|
-
type="button"
|
|
153
|
-
onClick={() => toggleDay(day)}
|
|
154
|
-
aria-pressed={(value.byDay ?? []).includes(day)}
|
|
155
|
-
aria-label={WEEKDAY_LABELS[day]}
|
|
156
|
-
className={[
|
|
157
|
-
"w-9 h-9 rounded-full text-xs font-semibold border",
|
|
158
|
-
(value.byDay ?? []).includes(day)
|
|
159
|
-
? "bg-primary text-white border-primary"
|
|
160
|
-
: "bg-surface text-text-muted border-border hover:border-primary",
|
|
161
|
-
].join(" ")}
|
|
162
|
-
>
|
|
163
|
-
<span aria-hidden="true">{WEEKDAY_LABELS[day][0]}</span>
|
|
164
|
-
</button>
|
|
165
|
-
))}
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
<fieldset className="flex flex-col gap-1.5 text-sm">
|
|
170
|
-
<legend className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1">Ends</legend>
|
|
171
|
-
<label className="flex items-center gap-2">
|
|
172
|
-
<input
|
|
173
|
-
type="radio"
|
|
174
|
-
name="recurrence-end"
|
|
175
|
-
checked={endConditionOf(value) === "never"}
|
|
176
|
-
onChange={() => handleEndCondition("never")}
|
|
177
|
-
/>
|
|
178
|
-
Never
|
|
179
|
-
</label>
|
|
180
|
-
<label className="flex items-center gap-2">
|
|
181
|
-
<input
|
|
182
|
-
type="radio"
|
|
183
|
-
name="recurrence-end"
|
|
184
|
-
aria-label="After"
|
|
185
|
-
checked={endConditionOf(value) === "count"}
|
|
186
|
-
onChange={() => handleEndCondition("count")}
|
|
187
|
-
/>
|
|
188
|
-
After
|
|
189
|
-
<input
|
|
190
|
-
type="number"
|
|
191
|
-
min={1}
|
|
192
|
-
className={`${INPUT_CLASS} w-16`}
|
|
193
|
-
value={value.count ?? 10}
|
|
194
|
-
disabled={endConditionOf(value) !== "count"}
|
|
195
|
-
onChange={(e) => handleCount(e.target.value)}
|
|
196
|
-
aria-label="Number of occurrences"
|
|
197
|
-
/>
|
|
198
|
-
occurrences
|
|
199
|
-
</label>
|
|
200
|
-
<label className="flex items-center gap-2">
|
|
201
|
-
<input
|
|
202
|
-
type="radio"
|
|
203
|
-
name="recurrence-end"
|
|
204
|
-
aria-label="On"
|
|
205
|
-
checked={endConditionOf(value) === "until"}
|
|
206
|
-
onChange={() => handleEndCondition("until")}
|
|
207
|
-
/>
|
|
208
|
-
On
|
|
209
|
-
<input
|
|
210
|
-
type="date"
|
|
211
|
-
className={INPUT_CLASS}
|
|
212
|
-
value={value.until ? recurrenceUntilDateKey(value.until, allDay) : ""}
|
|
213
|
-
disabled={endConditionOf(value) !== "until"}
|
|
214
|
-
onChange={(e) => handleUntil(e.target.value)}
|
|
215
|
-
aria-label="End date"
|
|
216
|
-
/>
|
|
217
|
-
</label>
|
|
218
|
-
</fieldset>
|
|
219
|
-
|
|
220
|
-
{(value.freq !== "weekly" || (value.byDay ?? []).length > 0) && (
|
|
221
|
-
<p className="text-xs text-text-muted">Repeats {describeRecurrence(value)}.</p>
|
|
222
|
-
)}
|
|
223
|
-
</div>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { format } from "date-fns";
|
|
7
|
+
import { RecurrenceFrequency, RecurrenceRule, WeekdayCode } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
8
|
+
import { WEEKDAY_CODES, WEEKDAY_LABELS, describeRecurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
9
|
+
import { recurrenceUntilDateKey, recurrenceUntilInstant } from "./allDay.js";
|
|
10
|
+
|
|
11
|
+
const SELECT_CLASS =
|
|
12
|
+
"text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
13
|
+
const INPUT_CLASS =
|
|
14
|
+
"text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
15
|
+
|
|
16
|
+
const FREQ_LABEL: Record<RecurrenceFrequency, { unit: string; unitPlural: string }> = {
|
|
17
|
+
daily: { unit: "day", unitPlural: "days" },
|
|
18
|
+
weekly: { unit: "week", unitPlural: "weeks" },
|
|
19
|
+
monthly: { unit: "month", unitPlural: "months" },
|
|
20
|
+
yearly: { unit: "year", unitPlural: "years" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type EndCondition = "never" | "count" | "until";
|
|
24
|
+
|
|
25
|
+
function endConditionOf(rule: RecurrenceRule): EndCondition {
|
|
26
|
+
if (rule.count) return "count";
|
|
27
|
+
if (rule.until) return "until";
|
|
28
|
+
return "never";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RecurrenceEditorProps {
|
|
32
|
+
/** `null` means "does not repeat". */
|
|
33
|
+
value: RecurrenceRule | null;
|
|
34
|
+
onChange: (value: RecurrenceRule | null) => void;
|
|
35
|
+
/** Whether the event is all-day - its series expands in UTC, so the inclusive "Ends on" date is stored as
|
|
36
|
+
* the end of that UTC day rather than the local one (see `allDay.ts`'s `recurrenceUntilInstant()`). */
|
|
37
|
+
allDay?: boolean;
|
|
38
|
+
/** The weekday the event starts on (in the frame its series expands in - see `allDay.ts`'s
|
|
39
|
+
* `startWeekdayCode()`): the day a new weekly rule, or one switched to Weekly, repeats on. Defaults to Monday. */
|
|
40
|
+
startWeekday?: WeekdayCode;
|
|
41
|
+
/** Leaves out the "Repeats" checkbox, for a caller that already turns repeating on and off itself (the event form's
|
|
42
|
+
* "Does not repeat" menu): what remains is the rule's details, shown only while there is a rule. */
|
|
43
|
+
hideToggle?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A recurring-event editor: frequency + interval, a weekday picker (weekly only), and an end
|
|
48
|
+
* condition (never / after N occurrences / on a date) — everything `RecurrenceRule` can express. The
|
|
49
|
+
* live "every ... until/for ..." summary comes from `describeRecurrence()` (built on `rrule`'s own
|
|
50
|
+
* `.toText()`), so it never drifts out of sync with what will actually be submitted.
|
|
51
|
+
*/
|
|
52
|
+
export default function RecurrenceEditor({ value, onChange, allDay = false, startWeekday = "MO", hideToggle = false }: RecurrenceEditorProps) {
|
|
53
|
+
function handleEnable(enabled: boolean) {
|
|
54
|
+
onChange(enabled ? { freq: "weekly", interval: 1, byDay: [startWeekday], exceptions: [] } : null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `update`/`toggleDay` are only ever invoked from handlers rendered inside the `{value && (...)}`
|
|
58
|
+
// block below, so `value` is always non-null here — no defensive null check needed.
|
|
59
|
+
function update(patch: Partial<RecurrenceRule>) {
|
|
60
|
+
onChange({ ...(value as RecurrenceRule), ...patch });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A weekly rule always keeps at least one weekday: with none left, rrule would repeat every day instead.
|
|
64
|
+
function toggleDay(day: WeekdayCode) {
|
|
65
|
+
const current = (value as RecurrenceRule).byDay ?? [];
|
|
66
|
+
if (current.length === 1 && current[0] === day) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const next = current.includes(day) ? current.filter((d) => d !== day) : [...current, day];
|
|
70
|
+
update({ byDay: next });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// react-shared passes `byDay` to rrule for every frequency, so a weekday list left over from Weekly would
|
|
74
|
+
// turn "every day" into "every Monday". Leaving Weekly drops it; entering Weekly starts on the event's own
|
|
75
|
+
// start weekday (a select only reports an actual change, so this is never Weekly -> Weekly).
|
|
76
|
+
function handleFrequency(freq: RecurrenceFrequency) {
|
|
77
|
+
update({ freq, byDay: freq === "weekly" ? [startWeekday] : undefined });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleEndCondition(condition: EndCondition) {
|
|
81
|
+
if (condition === "never") {
|
|
82
|
+
update({ count: undefined, until: undefined });
|
|
83
|
+
} else if (condition === "count") {
|
|
84
|
+
update({ count: 10, until: undefined });
|
|
85
|
+
} else {
|
|
86
|
+
update({ until: recurrenceUntilInstant(format(new Date(), "yyyy-MM-dd"), allDay), count: undefined });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A positive whole number from a number input, or `null` while it's empty (a user clearing the field
|
|
91
|
+
* to type a new value) — the rule keeps its previous value then, rather than becoming 0/NaN. */
|
|
92
|
+
function parsePositive(raw: string): number | null {
|
|
93
|
+
if (raw.trim() === "") return null;
|
|
94
|
+
const n = Math.floor(Number(raw));
|
|
95
|
+
return n >= 1 ? n : 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleInterval(raw: string) {
|
|
99
|
+
const interval = parsePositive(raw);
|
|
100
|
+
if (interval !== null) update({ interval });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleCount(raw: string) {
|
|
104
|
+
const count = parsePositive(raw);
|
|
105
|
+
if (count !== null) update({ count });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleUntil(raw: string) {
|
|
109
|
+
// A cleared (or partially typed) date input reports "" — keep the previous end date.
|
|
110
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) update({ until: recurrenceUntilInstant(raw, allDay) });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex flex-col gap-3">
|
|
115
|
+
{!hideToggle && (
|
|
116
|
+
<label className="flex items-center gap-2 text-sm font-medium">
|
|
117
|
+
<input type="checkbox" checked={value !== null} onChange={(e) => handleEnable(e.target.checked)} />
|
|
118
|
+
Repeats
|
|
119
|
+
</label>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{value && (
|
|
123
|
+
<div className="flex flex-col gap-3 pl-6 border-l-2 border-border">
|
|
124
|
+
<div className="flex items-center gap-2 text-sm">
|
|
125
|
+
<span>Every</span>
|
|
126
|
+
<input
|
|
127
|
+
type="number"
|
|
128
|
+
min={1}
|
|
129
|
+
className={`${INPUT_CLASS} w-16`}
|
|
130
|
+
value={value.interval}
|
|
131
|
+
onChange={(e) => handleInterval(e.target.value)}
|
|
132
|
+
aria-label="Recurrence interval"
|
|
133
|
+
/>
|
|
134
|
+
<select
|
|
135
|
+
className={SELECT_CLASS}
|
|
136
|
+
value={value.freq}
|
|
137
|
+
onChange={(e) => handleFrequency(e.target.value as RecurrenceFrequency)}
|
|
138
|
+
aria-label="Recurrence frequency"
|
|
139
|
+
>
|
|
140
|
+
<option value="daily">{value.interval === 1 ? FREQ_LABEL.daily.unit : FREQ_LABEL.daily.unitPlural}</option>
|
|
141
|
+
<option value="weekly">{value.interval === 1 ? FREQ_LABEL.weekly.unit : FREQ_LABEL.weekly.unitPlural}</option>
|
|
142
|
+
<option value="monthly">{value.interval === 1 ? FREQ_LABEL.monthly.unit : FREQ_LABEL.monthly.unitPlural}</option>
|
|
143
|
+
<option value="yearly">{value.interval === 1 ? FREQ_LABEL.yearly.unit : FREQ_LABEL.yearly.unitPlural}</option>
|
|
144
|
+
</select>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{value.freq === "weekly" && (
|
|
148
|
+
<div className="flex gap-1">
|
|
149
|
+
{WEEKDAY_CODES.map((day) => (
|
|
150
|
+
<button
|
|
151
|
+
key={day}
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={() => toggleDay(day)}
|
|
154
|
+
aria-pressed={(value.byDay ?? []).includes(day)}
|
|
155
|
+
aria-label={WEEKDAY_LABELS[day]}
|
|
156
|
+
className={[
|
|
157
|
+
"w-9 h-9 rounded-full text-xs font-semibold border",
|
|
158
|
+
(value.byDay ?? []).includes(day)
|
|
159
|
+
? "bg-primary text-white border-primary"
|
|
160
|
+
: "bg-surface text-text-muted border-border hover:border-primary",
|
|
161
|
+
].join(" ")}
|
|
162
|
+
>
|
|
163
|
+
<span aria-hidden="true">{WEEKDAY_LABELS[day][0]}</span>
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<fieldset className="flex flex-col gap-1.5 text-sm">
|
|
170
|
+
<legend className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1">Ends</legend>
|
|
171
|
+
<label className="flex items-center gap-2">
|
|
172
|
+
<input
|
|
173
|
+
type="radio"
|
|
174
|
+
name="recurrence-end"
|
|
175
|
+
checked={endConditionOf(value) === "never"}
|
|
176
|
+
onChange={() => handleEndCondition("never")}
|
|
177
|
+
/>
|
|
178
|
+
Never
|
|
179
|
+
</label>
|
|
180
|
+
<label className="flex items-center gap-2">
|
|
181
|
+
<input
|
|
182
|
+
type="radio"
|
|
183
|
+
name="recurrence-end"
|
|
184
|
+
aria-label="After"
|
|
185
|
+
checked={endConditionOf(value) === "count"}
|
|
186
|
+
onChange={() => handleEndCondition("count")}
|
|
187
|
+
/>
|
|
188
|
+
After
|
|
189
|
+
<input
|
|
190
|
+
type="number"
|
|
191
|
+
min={1}
|
|
192
|
+
className={`${INPUT_CLASS} w-16`}
|
|
193
|
+
value={value.count ?? 10}
|
|
194
|
+
disabled={endConditionOf(value) !== "count"}
|
|
195
|
+
onChange={(e) => handleCount(e.target.value)}
|
|
196
|
+
aria-label="Number of occurrences"
|
|
197
|
+
/>
|
|
198
|
+
occurrences
|
|
199
|
+
</label>
|
|
200
|
+
<label className="flex items-center gap-2">
|
|
201
|
+
<input
|
|
202
|
+
type="radio"
|
|
203
|
+
name="recurrence-end"
|
|
204
|
+
aria-label="On"
|
|
205
|
+
checked={endConditionOf(value) === "until"}
|
|
206
|
+
onChange={() => handleEndCondition("until")}
|
|
207
|
+
/>
|
|
208
|
+
On
|
|
209
|
+
<input
|
|
210
|
+
type="date"
|
|
211
|
+
className={INPUT_CLASS}
|
|
212
|
+
value={value.until ? recurrenceUntilDateKey(value.until, allDay) : ""}
|
|
213
|
+
disabled={endConditionOf(value) !== "until"}
|
|
214
|
+
onChange={(e) => handleUntil(e.target.value)}
|
|
215
|
+
aria-label="End date"
|
|
216
|
+
/>
|
|
217
|
+
</label>
|
|
218
|
+
</fieldset>
|
|
219
|
+
|
|
220
|
+
{(value.freq !== "weekly" || (value.byDay ?? []).length > 0) && (
|
|
221
|
+
<p className="text-xs text-text-muted">Repeats {describeRecurrence(value)}.</p>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useState } from "react";
|
|
6
6
|
import { HiOutlineXMark } from "react-icons/hi2";
|
|
7
|
-
import { EventChangeRequest, requestEventChange } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
7
|
+
import { Attendee, EventChangeRequest, requestEventChange } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
8
8
|
import { htmlToPlainText, sanitizeEventDescriptionHtml } from "@rapidmx/react-shared/calendar/eventDescription.js";
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
10
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
@@ -12,6 +12,7 @@ import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
|
12
12
|
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
13
13
|
import { notify } from "../../notifications/store.js";
|
|
14
14
|
import { INPUT_CLASS } from "./EventFormParts.js";
|
|
15
|
+
import GuestInput, { GuestEntries } from "./GuestInput.js";
|
|
15
16
|
import LazyDescriptionEditor from "./LazyDescriptionEditor.js";
|
|
16
17
|
import { descriptionProblem, initialDescriptionHtml } from "./eventDialogFields.js";
|
|
17
18
|
import { mergeGuests } from "./eventFormat.js";
|
|
@@ -66,47 +67,31 @@ export default function RequestChangeForm({ subject, canChange, canInvite, onSen
|
|
|
66
67
|
const [start, setStart] = useState(subject.startDate ? toDatetimeLocal(subject.startDate) : "");
|
|
67
68
|
const [end, setEnd] = useState(subject.endDate ? toDatetimeLocal(subject.endDate) : "");
|
|
68
69
|
const [descriptionHtml, setDescriptionHtml] = useState(() => initialDescriptionHtml(subject));
|
|
69
|
-
|
|
70
|
-
const [
|
|
70
|
+
// The guests to add (not the ones already invited: `subject.guestAddresses` are skipped), what is flagged in the field, and what is being typed.
|
|
71
|
+
const [entries, setEntries] = useState<GuestEntries>({ guests: [], invalid: [], draft: "" });
|
|
71
72
|
const [error, setError] = useState<string | null>(null);
|
|
72
73
|
const [sending, setSending] = useState(false);
|
|
73
74
|
const heading = canChange ? "Request a change" : "Add guests";
|
|
74
75
|
|
|
75
|
-
/** Adds what was typed into the guests box, returning the guests as they now stand;
|
|
76
|
-
function addTypedGuests():
|
|
77
|
-
const
|
|
78
|
-
const merged = mergeGuests(
|
|
79
|
-
invited.map((guest) => ({ ...guest, role: "required" as const, responseStatus: "needsAction" as const, isOrganizer: false })),
|
|
80
|
-
guestDraft,
|
|
81
|
-
);
|
|
76
|
+
/** Adds what was typed into the guests box, returning the guests to add as they now stand; something that is not an address is said. */
|
|
77
|
+
function addTypedGuests(): Attendee[] | undefined {
|
|
78
|
+
const merged = mergeGuests(entries.guests, [...entries.invalid, entries.draft].join(", "), subject.guestAddresses);
|
|
82
79
|
if (merged.invalid.length > 0) {
|
|
83
80
|
setError(`“${merged.invalid[0]}” isn’t a valid email address.`);
|
|
84
|
-
setGuestDraft(merged.invalid.join(" "));
|
|
85
81
|
return undefined;
|
|
86
82
|
}
|
|
87
|
-
|
|
88
|
-
return merged.attendees
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function handleGuestKey(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
92
|
-
if (event.key === "Enter" || event.key === "," || event.key === ";") {
|
|
93
|
-
event.preventDefault();
|
|
94
|
-
setError(null);
|
|
95
|
-
const more = addTypedGuests();
|
|
96
|
-
if (more) {
|
|
97
|
-
setAdded((current) => [...current, ...more]);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
83
|
+
setEntries({ guests: merged.attendees, invalid: [], draft: "" });
|
|
84
|
+
return merged.attendees;
|
|
100
85
|
}
|
|
101
86
|
|
|
102
87
|
async function handleSubmit(event: FormEvent) {
|
|
103
88
|
event.preventDefault();
|
|
104
89
|
setError(null);
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
90
|
+
const added = canInvite ? addTypedGuests() : [];
|
|
91
|
+
if (!added) {
|
|
107
92
|
return;
|
|
108
93
|
}
|
|
109
|
-
const guests =
|
|
94
|
+
const guests = added.map((guest) => (guest.displayName ? { address: guest.address, displayName: guest.displayName } : { address: guest.address }));
|
|
110
95
|
const request: EventChangeRequest = {};
|
|
111
96
|
|
|
112
97
|
if (canChange) {
|
|
@@ -226,35 +211,19 @@ export default function RequestChangeForm({ subject, canChange, canInvite, onSen
|
|
|
226
211
|
)}
|
|
227
212
|
|
|
228
213
|
{canInvite && (
|
|
229
|
-
<div className="flex flex-col gap-
|
|
230
|
-
<label
|
|
214
|
+
<div className="flex flex-col gap-1">
|
|
215
|
+
<label htmlFor="request-guests" className="text-xs font-medium">
|
|
231
216
|
Guests to add
|
|
232
|
-
<input
|
|
233
|
-
type="text"
|
|
234
|
-
className={INPUT_CLASS}
|
|
235
|
-
placeholder="name@example.com"
|
|
236
|
-
value={guestDraft}
|
|
237
|
-
onChange={(e) => setGuestDraft(e.target.value)}
|
|
238
|
-
onKeyDown={handleGuestKey}
|
|
239
|
-
/>
|
|
240
217
|
</label>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
className="w-4 h-4 flex items-center justify-center rounded-full text-text-muted hover:text-text"
|
|
251
|
-
>
|
|
252
|
-
<HiOutlineXMark size={12} aria-hidden="true" />
|
|
253
|
-
</button>
|
|
254
|
-
</li>
|
|
255
|
-
))}
|
|
256
|
-
</ul>
|
|
257
|
-
)}
|
|
218
|
+
<GuestInput
|
|
219
|
+
id="request-guests"
|
|
220
|
+
label="Guests to add"
|
|
221
|
+
placeholder="Name or name@example.com"
|
|
222
|
+
variant="box"
|
|
223
|
+
skipAddresses={subject.guestAddresses}
|
|
224
|
+
{...entries}
|
|
225
|
+
onChange={setEntries}
|
|
226
|
+
/>
|
|
258
227
|
</div>
|
|
259
228
|
)}
|
|
260
229
|
|