@rapidmx/web-client 0.2.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/LICENSE +382 -0
- package/apps/admin/_404.tsx +9 -0
- package/apps/admin/_500.tsx +9 -0
- package/apps/admin/_layout.tsx +41 -0
- package/apps/admin/audit-log/index.tsx +187 -0
- package/apps/admin/branding/index.tsx +404 -0
- package/apps/admin/distribution-lists/[uid].tsx +70 -0
- package/apps/admin/distribution-lists/index.tsx +115 -0
- package/apps/admin/distribution-lists/new/index.tsx +114 -0
- package/apps/admin/domains/[uid].tsx +259 -0
- package/apps/admin/domains/index.tsx +125 -0
- package/apps/admin/domains/new/index.tsx +84 -0
- package/apps/admin/index.tsx +82 -0
- package/apps/admin/ingest-queue/index.tsx +116 -0
- package/apps/admin/mailboxes/[uid].tsx +126 -0
- package/apps/admin/mailboxes/new/index.tsx +291 -0
- package/apps/admin/quarantine/index.tsx +171 -0
- package/apps/admin/transport-rules/[uid].tsx +143 -0
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
- package/apps/admin/transport-rules/index.tsx +117 -0
- package/apps/admin/transport-rules/new/index.tsx +105 -0
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
- package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
- package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
- package/apps/shared/components/calendar/EventModal.tsx +513 -0
- package/apps/shared/components/calendar/MonthView.tsx +139 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
- package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
- package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
- package/apps/shared/components/contacts/ContactForm.tsx +314 -0
- package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
- package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
- package/apps/shared/components/layout/AppShell.tsx +165 -0
- package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
- package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
- package/apps/shared/components/layout/UserMenu.tsx +150 -0
- package/apps/shared/components/mail/ConversationList.tsx +61 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
- package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
- package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
- package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
- package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
- package/apps/shared/styles/app.css +265 -0
- package/apps/www/_404.tsx +9 -0
- package/apps/www/_500.tsx +9 -0
- package/apps/www/_layout.tsx +45 -0
- package/apps/www/calendar/index.tsx +407 -0
- package/apps/www/contacts/[uid].tsx +88 -0
- package/apps/www/contacts/index.tsx +436 -0
- package/apps/www/index.tsx +375 -0
- package/apps/www/messages/[uid].tsx +70 -0
- package/apps/www/settings/auto-reply/index.tsx +129 -0
- package/apps/www/settings/booking-types/[uid].tsx +300 -0
- package/apps/www/settings/booking-types/index.tsx +98 -0
- package/apps/www/settings/booking-types/new/index.tsx +212 -0
- package/apps/www/settings/filters/[uid].tsx +155 -0
- package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
- package/apps/www/settings/filters/index.tsx +99 -0
- package/apps/www/settings/filters/new/index.tsx +126 -0
- package/apps/www/settings/focused-inbox/index.tsx +148 -0
- package/apps/www/settings/read-receipts/index.tsx +125 -0
- package/apps/www/settings/signatures/[uid].tsx +167 -0
- package/apps/www/settings/signatures/index.tsx +85 -0
- package/apps/www/settings/signatures/new/index.tsx +129 -0
- package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
- package/apps/www/tasks/index.tsx +530 -0
- package/dist/apps/admin/_404.js +4 -0
- package/dist/apps/admin/_500.js +4 -0
- package/dist/apps/admin/_layout.js +10 -0
- package/dist/apps/admin/audit-log/index.js +66 -0
- package/dist/apps/admin/branding/index.js +126 -0
- package/dist/apps/admin/distribution-lists/[uid].js +34 -0
- package/dist/apps/admin/distribution-lists/index.js +31 -0
- package/dist/apps/admin/distribution-lists/new/index.js +51 -0
- package/dist/apps/admin/domains/[uid].js +102 -0
- package/dist/apps/admin/domains/index.js +31 -0
- package/dist/apps/admin/domains/new/index.js +41 -0
- package/dist/apps/admin/index.js +32 -0
- package/dist/apps/admin/ingest-queue/index.js +48 -0
- package/dist/apps/admin/mailboxes/[uid].js +59 -0
- package/dist/apps/admin/mailboxes/new/index.js +93 -0
- package/dist/apps/admin/quarantine/index.js +66 -0
- package/dist/apps/admin/transport-rules/[uid].js +87 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
- package/dist/apps/admin/transport-rules/index.js +32 -0
- package/dist/apps/admin/transport-rules/new/index.js +50 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
- package/dist/apps/shared/components/calendar/EventModal.js +167 -0
- package/dist/apps/shared/components/calendar/MonthView.js +38 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
- package/dist/apps/shared/components/layout/AppShell.js +68 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
- package/dist/apps/shared/components/layout/UserMenu.js +59 -0
- package/dist/apps/shared/components/mail/ConversationList.js +17 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
- package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
- package/dist/apps/shared/styles/app.css +265 -0
- package/dist/apps/www/_404.js +4 -0
- package/dist/apps/www/_500.js +4 -0
- package/dist/apps/www/_layout.js +8 -0
- package/dist/apps/www/calendar/index.js +228 -0
- package/dist/apps/www/contacts/[uid].js +62 -0
- package/dist/apps/www/contacts/index.js +262 -0
- package/dist/apps/www/index.js +228 -0
- package/dist/apps/www/messages/[uid].js +47 -0
- package/dist/apps/www/settings/auto-reply/index.js +54 -0
- package/dist/apps/www/settings/booking-types/[uid].js +129 -0
- package/dist/apps/www/settings/booking-types/index.js +29 -0
- package/dist/apps/www/settings/booking-types/new/index.js +86 -0
- package/dist/apps/www/settings/filters/[uid].js +94 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
- package/dist/apps/www/settings/filters/index.js +30 -0
- package/dist/apps/www/settings/filters/new/index.js +61 -0
- package/dist/apps/www/settings/focused-inbox/index.js +69 -0
- package/dist/apps/www/settings/read-receipts/index.js +51 -0
- package/dist/apps/www/settings/signatures/[uid].js +96 -0
- package/dist/apps/www/settings/signatures/index.js +34 -0
- package/dist/apps/www/settings/signatures/new/index.js +63 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
- package/dist/apps/www/tasks/index.js +280 -0
- package/package.json +87 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { FormEvent, useRef, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import {
|
|
8
|
+
Attendee,
|
|
9
|
+
AttendeeResponseInput,
|
|
10
|
+
AttendeeResponseStatus,
|
|
11
|
+
AttendeeRole,
|
|
12
|
+
BusyStatus,
|
|
13
|
+
CalendarEventInput,
|
|
14
|
+
RecurrenceRule,
|
|
15
|
+
createCalendarEvent,
|
|
16
|
+
respondToEvent,
|
|
17
|
+
updateCalendarEvent,
|
|
18
|
+
} from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
19
|
+
import { deleteEventOccurrence, deleteEventSeries, detachOccurrence, saveEventSeries } from "@rapidmx/react-shared/calendar/calendarMutations.js";
|
|
20
|
+
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
21
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
22
|
+
import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
23
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
24
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
25
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
26
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
27
|
+
import RecurrenceEditor from "./RecurrenceEditor.js";
|
|
28
|
+
import ResourcePicker from "./ResourcePicker.js";
|
|
29
|
+
|
|
30
|
+
const INPUT_CLASS =
|
|
31
|
+
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
32
|
+
const SELECT_CLASS =
|
|
33
|
+
"text-sm py-2 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
34
|
+
|
|
35
|
+
const ATTENDEE_ROLES: AttendeeRole[] = ["required", "optional", "resource"];
|
|
36
|
+
const BUSY_STATUSES: BusyStatus[] = ["busy", "free", "tentative", "oof"];
|
|
37
|
+
const BUSY_STATUS_LABEL: Record<BusyStatus, string> = { busy: "Busy", free: "Free", tentative: "Tentative", oof: "Out of office" };
|
|
38
|
+
const RESPONSE_STATUS_LABEL: Record<AttendeeResponseStatus, string> = {
|
|
39
|
+
needsAction: "Awaiting response",
|
|
40
|
+
accepted: "Accepted",
|
|
41
|
+
declined: "Declined",
|
|
42
|
+
tentative: "Tentative",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface EventModalProps {
|
|
46
|
+
open: boolean;
|
|
47
|
+
onClose: () => void;
|
|
48
|
+
mailboxUid: string;
|
|
49
|
+
/** The calendar folder a new event is created into (ignored when editing — an existing event keeps
|
|
50
|
+
* its own `folderUid`). When `calendars` names more than one option, a "Calendar" selector lets the
|
|
51
|
+
* user override this default before saving. */
|
|
52
|
+
folderUid: string;
|
|
53
|
+
/** Every calendar the caller could create this event into. Omitted, or a single entry, means "only
|
|
54
|
+
* one calendar exists" — no selector is shown and `folderUid` is used as-is, matching this
|
|
55
|
+
* component's original single-calendar behavior exactly. */
|
|
56
|
+
calendars?: { uid: string; name: string }[];
|
|
57
|
+
organizerAddress: string;
|
|
58
|
+
/** `null` when creating a new event. */
|
|
59
|
+
occurrence: CalendarOccurrence | null;
|
|
60
|
+
/** Prefilled start/end for create mode (e.g. the day/slot the user clicked). */
|
|
61
|
+
initialStart?: Date;
|
|
62
|
+
initialEnd?: Date;
|
|
63
|
+
onSaved: () => void;
|
|
64
|
+
onDeleted: () => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type EditScope = "occurrence" | "series";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create/view/edit/delete for a single calendar event. Editing or deleting a recurring event's
|
|
71
|
+
* occurrence offers a choice between "this event" and "the entire series" (see
|
|
72
|
+
* `calendarMutations.ts`) — Outlook's own convention for the same ambiguity.
|
|
73
|
+
*/
|
|
74
|
+
export default function EventModal({
|
|
75
|
+
open,
|
|
76
|
+
onClose,
|
|
77
|
+
mailboxUid,
|
|
78
|
+
folderUid,
|
|
79
|
+
calendars,
|
|
80
|
+
organizerAddress,
|
|
81
|
+
occurrence,
|
|
82
|
+
initialStart,
|
|
83
|
+
initialEnd,
|
|
84
|
+
onSaved,
|
|
85
|
+
onDeleted,
|
|
86
|
+
}: EventModalProps) {
|
|
87
|
+
const [targetFolderUid, setTargetFolderUid] = useState(folderUid);
|
|
88
|
+
const [title, setTitle] = useState(occurrence?.title ?? "");
|
|
89
|
+
const [location, setLocation] = useState(occurrence?.location ?? "");
|
|
90
|
+
const [start, setStart] = useState(toDatetimeLocal(occurrence?.startDate ?? initialStart?.toISOString() ?? new Date().toISOString()));
|
|
91
|
+
const [end, setEnd] = useState(
|
|
92
|
+
toDatetimeLocal(occurrence?.endDate ?? initialEnd?.toISOString() ?? new Date(Date.now() + 30 * 60_000).toISOString()),
|
|
93
|
+
);
|
|
94
|
+
const [allDay, setAllDay] = useState(occurrence?.allDay ?? false);
|
|
95
|
+
const [timezone] = useState(occurrence?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
96
|
+
const [attendees, setAttendees] = useState<Attendee[]>(occurrence?.attendees ?? []);
|
|
97
|
+
const [recurrenceRule, setRecurrenceRule] = useState<RecurrenceRule | null>(occurrence?.recurrenceRule ?? null);
|
|
98
|
+
const [reminderMinutes, setReminderMinutes] = useState(occurrence?.reminderMinutesBeforeStart?.toString() ?? "");
|
|
99
|
+
const [busyStatus, setBusyStatus] = useState<BusyStatus>(occurrence?.busyStatus ?? "busy");
|
|
100
|
+
const [autoReplyEnabled, setAutoReplyEnabled] = useState(occurrence?.autoReplyEnabled ?? false);
|
|
101
|
+
const [autoReplyMessage, setAutoReplyMessage] = useState(occurrence?.autoReplyMessage ?? "");
|
|
102
|
+
const [editScope, setEditScope] = useState<EditScope>("occurrence");
|
|
103
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
104
|
+
const [error, setError] = useState<string | null>(null);
|
|
105
|
+
const [saving, setSaving] = useState(false);
|
|
106
|
+
const [responding, setResponding] = useState(false);
|
|
107
|
+
const [resourcePickerOpen, setResourcePickerOpen] = useState(false);
|
|
108
|
+
const addResourceButtonRef = useRef<HTMLButtonElement>(null);
|
|
109
|
+
|
|
110
|
+
// The viewing mailbox can respond to an *existing* event it's invited to but doesn't organize —
|
|
111
|
+
// `organizerAddress` is always this mailbox's own primary SMTP address (see `EventModalProps`), so
|
|
112
|
+
// finding it among `attendees` (and not as that attendee's own organizer flag) identifies exactly
|
|
113
|
+
// that case. `@rapidmx/restapi`'s `respond()` route has no occurrence-vs-series scope of its own
|
|
114
|
+
// (see `calendarApi.ts`'s `respondToEvent` doc comment) — it always acts on `occurrence.uid` as a
|
|
115
|
+
// single event document, so this deliberately does not consult `editScope`.
|
|
116
|
+
const myAttendeeIndex = occurrence ? attendees.findIndex((a) => a.address.toLowerCase() === organizerAddress.toLowerCase()) : -1;
|
|
117
|
+
const canRespond = myAttendeeIndex !== -1 && !attendees[myAttendeeIndex].isOrganizer;
|
|
118
|
+
|
|
119
|
+
function updateAttendee(index: number, patch: Partial<Attendee>) {
|
|
120
|
+
setAttendees((prev) => prev.map((a, i) => (i === index ? { ...a, ...patch } : a)));
|
|
121
|
+
}
|
|
122
|
+
function removeAttendee(index: number) {
|
|
123
|
+
setAttendees((prev) => prev.filter((_, i) => i !== index));
|
|
124
|
+
}
|
|
125
|
+
function addResource(mailbox: Mailbox) {
|
|
126
|
+
setAttendees((prev) => [
|
|
127
|
+
...prev,
|
|
128
|
+
{ address: mailbox.primarySmtpAddress, displayName: mailbox.displayName, role: "resource", responseStatus: "needsAction", isOrganizer: false },
|
|
129
|
+
]);
|
|
130
|
+
setResourcePickerOpen(false);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function handleSubmit(e: FormEvent) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
setError(null);
|
|
136
|
+
|
|
137
|
+
if (!title.trim()) {
|
|
138
|
+
setError("A title is required.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (new Date(end) <= new Date(start)) {
|
|
142
|
+
setError("The end time must be after the start time.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const fields: Partial<CalendarEventInput> = {
|
|
147
|
+
title: title.trim(),
|
|
148
|
+
location: location.trim() || undefined,
|
|
149
|
+
startDate: new Date(start).toISOString(),
|
|
150
|
+
endDate: new Date(end).toISOString(),
|
|
151
|
+
allDay,
|
|
152
|
+
timezone,
|
|
153
|
+
organizer: { address: organizerAddress, type: "to" },
|
|
154
|
+
attendees,
|
|
155
|
+
recurrenceRule: recurrenceRule ?? undefined,
|
|
156
|
+
reminderMinutesBeforeStart: reminderMinutes.trim() ? Number(reminderMinutes) : undefined,
|
|
157
|
+
busyStatus,
|
|
158
|
+
autoReplyEnabled,
|
|
159
|
+
autoReplyMessage: autoReplyEnabled ? autoReplyMessage : undefined,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
setSaving(true);
|
|
163
|
+
try {
|
|
164
|
+
if (!occurrence) {
|
|
165
|
+
await createCalendarEvent({ mailboxUid, folderUid: targetFolderUid, ...fields } as CalendarEventInput);
|
|
166
|
+
} else if (occurrence.isRecurringOccurrence && editScope === "occurrence") {
|
|
167
|
+
await detachOccurrence(occurrence, fields);
|
|
168
|
+
} else if (occurrence.isRecurringOccurrence) {
|
|
169
|
+
await saveEventSeries(occurrence, fields);
|
|
170
|
+
} else {
|
|
171
|
+
await updateCalendarEvent({ uid: occurrence.uid, version: occurrence.version, ...fields });
|
|
172
|
+
}
|
|
173
|
+
onSaved();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not save this event.");
|
|
176
|
+
} finally {
|
|
177
|
+
setSaving(false);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Only ever invoked from the Delete button(s) rendered inside the `{occurrence && (...)}` block
|
|
182
|
+
// below, so `occurrence` is always non-null here — no defensive null check needed.
|
|
183
|
+
async function handleDelete(scope: EditScope) {
|
|
184
|
+
setError(null);
|
|
185
|
+
setSaving(true);
|
|
186
|
+
try {
|
|
187
|
+
if (occurrence!.isRecurringOccurrence && scope === "occurrence") {
|
|
188
|
+
await deleteEventOccurrence(occurrence!);
|
|
189
|
+
} else {
|
|
190
|
+
await deleteEventSeries(occurrence!);
|
|
191
|
+
}
|
|
192
|
+
onDeleted();
|
|
193
|
+
} catch (err) {
|
|
194
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not delete this event.");
|
|
195
|
+
setSaving(false);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Only rendered inside the `{canRespond && (...)}` block below, which already requires `occurrence`
|
|
200
|
+
// to be non-null (see `canRespond`'s own definition above) — same established pattern as
|
|
201
|
+
// `handleDelete`'s non-null assertion.
|
|
202
|
+
async function handleRespond(responseStatus: AttendeeResponseInput) {
|
|
203
|
+
setError(null);
|
|
204
|
+
setResponding(true);
|
|
205
|
+
try {
|
|
206
|
+
await respondToEvent(occurrence!.uid, responseStatus);
|
|
207
|
+
// A decline soft-deletes the mailbox's own copy server-side — treat it the same as a
|
|
208
|
+
// delete rather than a save so the calendar view drops it immediately.
|
|
209
|
+
if (responseStatus === "declined") {
|
|
210
|
+
onDeleted();
|
|
211
|
+
} else {
|
|
212
|
+
onSaved();
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not send your response.");
|
|
216
|
+
setResponding(false);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Modal open={open} onClose={onClose} title={occurrence ? "Edit event" : "New event"}>
|
|
222
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-1">
|
|
223
|
+
{error && <Alert>{error}</Alert>}
|
|
224
|
+
|
|
225
|
+
{!occurrence && calendars && calendars.length > 1 && (
|
|
226
|
+
<FormField label="Calendar" htmlFor="event-calendar">
|
|
227
|
+
<select
|
|
228
|
+
id="event-calendar"
|
|
229
|
+
className={INPUT_CLASS}
|
|
230
|
+
value={targetFolderUid}
|
|
231
|
+
onChange={(e) => setTargetFolderUid(e.target.value)}
|
|
232
|
+
>
|
|
233
|
+
{calendars.map((cal) => (
|
|
234
|
+
<option key={cal.uid} value={cal.uid}>
|
|
235
|
+
{cal.name}
|
|
236
|
+
</option>
|
|
237
|
+
))}
|
|
238
|
+
</select>
|
|
239
|
+
</FormField>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<FormField label="Title" htmlFor="event-title">
|
|
243
|
+
<input id="event-title" type="text" className={INPUT_CLASS} value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
244
|
+
</FormField>
|
|
245
|
+
|
|
246
|
+
<FormField label="Location" htmlFor="event-location">
|
|
247
|
+
<input id="event-location" type="text" className={INPUT_CLASS} value={location} onChange={(e) => setLocation(e.target.value)} />
|
|
248
|
+
</FormField>
|
|
249
|
+
|
|
250
|
+
<label className="flex items-center gap-2 text-sm font-medium mb-3">
|
|
251
|
+
<input type="checkbox" checked={allDay} onChange={(e) => setAllDay(e.target.checked)} />
|
|
252
|
+
All day
|
|
253
|
+
</label>
|
|
254
|
+
|
|
255
|
+
<div className="grid grid-cols-2 gap-3">
|
|
256
|
+
<FormField label="Start" htmlFor="event-start">
|
|
257
|
+
<input
|
|
258
|
+
id="event-start"
|
|
259
|
+
type={allDay ? "date" : "datetime-local"}
|
|
260
|
+
className={INPUT_CLASS}
|
|
261
|
+
value={allDay ? start.slice(0, 10) : start}
|
|
262
|
+
onChange={(e) => setStart(allDay ? `${e.target.value}T00:00` : e.target.value)}
|
|
263
|
+
/>
|
|
264
|
+
</FormField>
|
|
265
|
+
<FormField label="End" htmlFor="event-end">
|
|
266
|
+
<input
|
|
267
|
+
id="event-end"
|
|
268
|
+
type={allDay ? "date" : "datetime-local"}
|
|
269
|
+
className={INPUT_CLASS}
|
|
270
|
+
value={allDay ? end.slice(0, 10) : end}
|
|
271
|
+
onChange={(e) => setEnd(allDay ? `${e.target.value}T00:00` : e.target.value)}
|
|
272
|
+
/>
|
|
273
|
+
</FormField>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<FormField label="Attendees" htmlFor="event-attendees">
|
|
277
|
+
<div id="event-attendees" className="flex flex-col gap-2">
|
|
278
|
+
{attendees.map((attendee, i) => (
|
|
279
|
+
<div key={i} className="flex gap-2">
|
|
280
|
+
<input
|
|
281
|
+
type="email"
|
|
282
|
+
className={`${INPUT_CLASS} flex-1`}
|
|
283
|
+
value={attendee.address}
|
|
284
|
+
onChange={(e) => updateAttendee(i, { address: e.target.value })}
|
|
285
|
+
aria-label={`Attendee email ${i + 1}`}
|
|
286
|
+
/>
|
|
287
|
+
<select
|
|
288
|
+
className={SELECT_CLASS}
|
|
289
|
+
value={attendee.role}
|
|
290
|
+
onChange={(e) => updateAttendee(i, { role: e.target.value as AttendeeRole })}
|
|
291
|
+
aria-label={`Attendee role ${i + 1}`}
|
|
292
|
+
>
|
|
293
|
+
{ATTENDEE_ROLES.map((role) => (
|
|
294
|
+
<option key={role} value={role}>
|
|
295
|
+
{role}
|
|
296
|
+
</option>
|
|
297
|
+
))}
|
|
298
|
+
</select>
|
|
299
|
+
<span className="text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt self-center">
|
|
300
|
+
{RESPONSE_STATUS_LABEL[attendee.responseStatus]}
|
|
301
|
+
</span>
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
onClick={() => removeAttendee(i)}
|
|
305
|
+
className="text-sm text-danger px-2"
|
|
306
|
+
aria-label={`Remove attendee ${i + 1}`}
|
|
307
|
+
>
|
|
308
|
+
×
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
<div className="flex gap-3 items-center">
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
onClick={() => setAttendees((prev) => [...prev, { address: "", role: "required", responseStatus: "needsAction", isOrganizer: false }])}
|
|
316
|
+
className="self-start text-xs font-medium text-primary-dark hover:underline"
|
|
317
|
+
>
|
|
318
|
+
+ Add attendee
|
|
319
|
+
</button>
|
|
320
|
+
<button
|
|
321
|
+
ref={addResourceButtonRef}
|
|
322
|
+
type="button"
|
|
323
|
+
onClick={() => setResourcePickerOpen(true)}
|
|
324
|
+
className="self-start text-xs font-medium text-primary-dark hover:underline"
|
|
325
|
+
>
|
|
326
|
+
+ Add room/equipment
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
{resourcePickerOpen && (
|
|
330
|
+
<ResourcePicker
|
|
331
|
+
anchorRef={addResourceButtonRef}
|
|
332
|
+
onClose={() => setResourcePickerOpen(false)}
|
|
333
|
+
onSelect={addResource}
|
|
334
|
+
excludeAddresses={attendees.map((a) => a.address.toLowerCase())}
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</FormField>
|
|
339
|
+
|
|
340
|
+
{canRespond && (
|
|
341
|
+
<div className="flex flex-col gap-2 mb-3 p-3 rounded-sm bg-surface-alt">
|
|
342
|
+
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
343
|
+
<span className="text-sm font-medium">Your response</span>
|
|
344
|
+
<div className="flex gap-2">
|
|
345
|
+
<Button
|
|
346
|
+
type="button"
|
|
347
|
+
variant="secondary"
|
|
348
|
+
className="!w-auto"
|
|
349
|
+
disabled={responding}
|
|
350
|
+
onClick={() => handleRespond("accepted")}
|
|
351
|
+
>
|
|
352
|
+
Accept
|
|
353
|
+
</Button>
|
|
354
|
+
<Button
|
|
355
|
+
type="button"
|
|
356
|
+
variant="secondary"
|
|
357
|
+
className="!w-auto"
|
|
358
|
+
disabled={responding}
|
|
359
|
+
onClick={() => handleRespond("tentative")}
|
|
360
|
+
>
|
|
361
|
+
Tentative
|
|
362
|
+
</Button>
|
|
363
|
+
<Button
|
|
364
|
+
type="button"
|
|
365
|
+
variant="secondary"
|
|
366
|
+
className="!w-auto text-danger"
|
|
367
|
+
disabled={responding}
|
|
368
|
+
onClick={() => handleRespond("declined")}
|
|
369
|
+
>
|
|
370
|
+
Decline
|
|
371
|
+
</Button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<p className="text-xs text-text-muted">Other attendees’ responses may take a few minutes to update.</p>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
<div className="grid grid-cols-2 gap-3">
|
|
379
|
+
<FormField label="Busy status" htmlFor="event-busyStatus">
|
|
380
|
+
<select
|
|
381
|
+
id="event-busyStatus"
|
|
382
|
+
className={INPUT_CLASS}
|
|
383
|
+
value={busyStatus}
|
|
384
|
+
onChange={(e) => setBusyStatus(e.target.value as BusyStatus)}
|
|
385
|
+
>
|
|
386
|
+
{BUSY_STATUSES.map((status) => (
|
|
387
|
+
<option key={status} value={status}>
|
|
388
|
+
{BUSY_STATUS_LABEL[status]}
|
|
389
|
+
</option>
|
|
390
|
+
))}
|
|
391
|
+
</select>
|
|
392
|
+
</FormField>
|
|
393
|
+
<FormField label="Reminder (minutes before)" htmlFor="event-reminder">
|
|
394
|
+
<input
|
|
395
|
+
id="event-reminder"
|
|
396
|
+
type="number"
|
|
397
|
+
min={0}
|
|
398
|
+
className={INPUT_CLASS}
|
|
399
|
+
value={reminderMinutes}
|
|
400
|
+
onChange={(e) => setReminderMinutes(e.target.value)}
|
|
401
|
+
placeholder="None"
|
|
402
|
+
/>
|
|
403
|
+
</FormField>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div className="mb-3">
|
|
407
|
+
<label className="flex items-center gap-2 text-sm font-medium">
|
|
408
|
+
<input
|
|
409
|
+
type="checkbox"
|
|
410
|
+
checked={autoReplyEnabled}
|
|
411
|
+
onChange={(e) => setAutoReplyEnabled(e.target.checked)}
|
|
412
|
+
/>
|
|
413
|
+
Send an automatic reply while this event is happening
|
|
414
|
+
</label>
|
|
415
|
+
<p className="text-xs text-text-muted mt-1">
|
|
416
|
+
Applies in addition to your mailbox’s own Automatic Replies setting (see Settings) —
|
|
417
|
+
this event’s message takes over for its own start/end window.
|
|
418
|
+
</p>
|
|
419
|
+
{autoReplyEnabled && (
|
|
420
|
+
<textarea
|
|
421
|
+
aria-label="Automatic reply message"
|
|
422
|
+
className={`${INPUT_CLASS} mt-2`}
|
|
423
|
+
rows={3}
|
|
424
|
+
value={autoReplyMessage}
|
|
425
|
+
onChange={(e) => setAutoReplyMessage(e.target.value)}
|
|
426
|
+
placeholder="I'm out of office and back on..."
|
|
427
|
+
/>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<FormField label="Recurrence" htmlFor="event-recurrence">
|
|
432
|
+
<RecurrenceEditor value={recurrenceRule} onChange={setRecurrenceRule} />
|
|
433
|
+
</FormField>
|
|
434
|
+
|
|
435
|
+
{occurrence?.isRecurringOccurrence && (
|
|
436
|
+
<fieldset className="flex flex-col gap-1.5 text-sm mb-3">
|
|
437
|
+
<legend className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1">Apply changes to</legend>
|
|
438
|
+
<label className="flex items-center gap-2">
|
|
439
|
+
<input
|
|
440
|
+
type="radio"
|
|
441
|
+
name="event-edit-scope"
|
|
442
|
+
checked={editScope === "occurrence"}
|
|
443
|
+
onChange={() => setEditScope("occurrence")}
|
|
444
|
+
/>
|
|
445
|
+
This event only
|
|
446
|
+
</label>
|
|
447
|
+
<label className="flex items-center gap-2">
|
|
448
|
+
<input
|
|
449
|
+
type="radio"
|
|
450
|
+
name="event-edit-scope"
|
|
451
|
+
checked={editScope === "series"}
|
|
452
|
+
onChange={() => setEditScope("series")}
|
|
453
|
+
/>
|
|
454
|
+
The entire series
|
|
455
|
+
</label>
|
|
456
|
+
</fieldset>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
<div className="flex gap-3 mt-2">
|
|
460
|
+
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
461
|
+
Save
|
|
462
|
+
</Button>
|
|
463
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={onClose}>
|
|
464
|
+
Cancel
|
|
465
|
+
</Button>
|
|
466
|
+
{occurrence &&
|
|
467
|
+
(!occurrence.isRecurringOccurrence ? (
|
|
468
|
+
<Button
|
|
469
|
+
type="button"
|
|
470
|
+
variant="secondary"
|
|
471
|
+
className="!w-auto ml-auto text-danger"
|
|
472
|
+
disabled={saving}
|
|
473
|
+
onClick={() => handleDelete("series")}
|
|
474
|
+
>
|
|
475
|
+
Delete
|
|
476
|
+
</Button>
|
|
477
|
+
) : !confirmingDelete ? (
|
|
478
|
+
<Button
|
|
479
|
+
type="button"
|
|
480
|
+
variant="secondary"
|
|
481
|
+
className="!w-auto ml-auto text-danger"
|
|
482
|
+
disabled={saving}
|
|
483
|
+
onClick={() => setConfirmingDelete(true)}
|
|
484
|
+
>
|
|
485
|
+
Delete
|
|
486
|
+
</Button>
|
|
487
|
+
) : (
|
|
488
|
+
<div className="flex gap-2 ml-auto">
|
|
489
|
+
<Button
|
|
490
|
+
type="button"
|
|
491
|
+
variant="secondary"
|
|
492
|
+
className="!w-auto text-danger"
|
|
493
|
+
disabled={saving}
|
|
494
|
+
onClick={() => handleDelete("occurrence")}
|
|
495
|
+
>
|
|
496
|
+
Delete this event
|
|
497
|
+
</Button>
|
|
498
|
+
<Button
|
|
499
|
+
type="button"
|
|
500
|
+
variant="secondary"
|
|
501
|
+
className="!w-auto text-danger"
|
|
502
|
+
disabled={saving}
|
|
503
|
+
onClick={() => handleDelete("series")}
|
|
504
|
+
>
|
|
505
|
+
Delete series
|
|
506
|
+
</Button>
|
|
507
|
+
</div>
|
|
508
|
+
))}
|
|
509
|
+
</div>
|
|
510
|
+
</form>
|
|
511
|
+
</Modal>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { useDraggable, useDroppable } from "@dnd-kit/core";
|
|
7
|
+
import { eachDayOfInterval, endOfMonth, endOfWeek, format, isSameDay, isSameMonth, isToday, startOfMonth, startOfWeek } from "date-fns";
|
|
8
|
+
import { dayDropId, eventDragId } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
|
|
9
|
+
import { CalendarOccurrence } from "@rapidmx/react-shared/calendar/recurrence.js";
|
|
10
|
+
|
|
11
|
+
const MAX_CHIPS_PER_DAY = 3;
|
|
12
|
+
|
|
13
|
+
export interface MonthViewProps {
|
|
14
|
+
/** Any date within the month to display. */
|
|
15
|
+
viewDate: Date;
|
|
16
|
+
/** Already expanded for the visible grid range — see `recurrence.ts`'s `expandAllOccurrences`. */
|
|
17
|
+
occurrences: CalendarOccurrence[];
|
|
18
|
+
/** Each occurrence's own calendar color, keyed by `folderUid` — see `calendarColors.ts`. */
|
|
19
|
+
folderColors: Record<string, string>;
|
|
20
|
+
onSelectDay: (date: Date) => void;
|
|
21
|
+
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A real 6-week month grid (Mon-start), matching Outlook/Gmail's month view. Multi-day/all-day
|
|
25
|
+
* events are shown only on their start day — a deliberate v1 simplification, not a bug. */
|
|
26
|
+
export default function MonthView({ viewDate, occurrences, folderColors, onSelectDay, onSelectEvent }: MonthViewProps) {
|
|
27
|
+
const gridStart = startOfWeek(startOfMonth(viewDate), { weekStartsOn: 1 });
|
|
28
|
+
const gridEnd = endOfWeek(endOfMonth(viewDate), { weekStartsOn: 1 });
|
|
29
|
+
const days = eachDayOfInterval({ start: gridStart, end: gridEnd });
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex-1 grid grid-cols-7 grid-rows-6 min-h-0" role="grid" aria-label="Month">
|
|
33
|
+
{days.map((day) => (
|
|
34
|
+
<DayCell
|
|
35
|
+
key={day.toISOString()}
|
|
36
|
+
day={day}
|
|
37
|
+
inCurrentMonth={isSameMonth(day, viewDate)}
|
|
38
|
+
occurrences={occurrences.filter((occ) => isSameDay(new Date(occ.startDate), day))}
|
|
39
|
+
folderColors={folderColors}
|
|
40
|
+
onSelectDay={onSelectDay}
|
|
41
|
+
onSelectEvent={onSelectEvent}
|
|
42
|
+
/>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface DayCellProps {
|
|
49
|
+
day: Date;
|
|
50
|
+
inCurrentMonth: boolean;
|
|
51
|
+
occurrences: CalendarOccurrence[];
|
|
52
|
+
folderColors: Record<string, string>;
|
|
53
|
+
onSelectDay: (date: Date) => void;
|
|
54
|
+
onSelectEvent: (occurrence: CalendarOccurrence) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function DayCell({ day, inCurrentMonth, occurrences, folderColors, onSelectDay, onSelectEvent }: DayCellProps) {
|
|
58
|
+
const { setNodeRef, isOver } = useDroppable({ id: dayDropId(day) });
|
|
59
|
+
const visible = occurrences.slice(0, MAX_CHIPS_PER_DAY);
|
|
60
|
+
const overflowCount = occurrences.length - visible.length;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
ref={setNodeRef}
|
|
65
|
+
className={[
|
|
66
|
+
"border-b border-r border-border p-1 flex flex-col gap-0.5 min-h-0 overflow-hidden",
|
|
67
|
+
isOver ? "bg-primary/5" : "",
|
|
68
|
+
inCurrentMonth ? "bg-surface" : "bg-surface-alt",
|
|
69
|
+
].join(" ")}
|
|
70
|
+
>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => onSelectDay(day)}
|
|
74
|
+
className={[
|
|
75
|
+
"self-start text-xs font-semibold w-6 h-6 rounded-full flex items-center justify-center shrink-0",
|
|
76
|
+
isToday(day) ? "bg-primary text-white" : inCurrentMonth ? "text-text" : "text-text-muted",
|
|
77
|
+
].join(" ")}
|
|
78
|
+
>
|
|
79
|
+
{format(day, "d")}
|
|
80
|
+
</button>
|
|
81
|
+
<div className="flex-1 flex flex-col gap-0.5 min-h-0 overflow-hidden">
|
|
82
|
+
{visible.map((occurrence) => (
|
|
83
|
+
<EventChip
|
|
84
|
+
key={occurrence.occurrenceKey}
|
|
85
|
+
occurrence={occurrence}
|
|
86
|
+
color={folderColors[occurrence.folderUid]}
|
|
87
|
+
onSelect={onSelectEvent}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
{overflowCount > 0 && (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => onSelectDay(day)}
|
|
94
|
+
className="text-xs text-text-muted text-left hover:underline shrink-0"
|
|
95
|
+
>
|
|
96
|
+
+{overflowCount} more
|
|
97
|
+
</button>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function EventChip({
|
|
105
|
+
occurrence,
|
|
106
|
+
color,
|
|
107
|
+
onSelect,
|
|
108
|
+
}: {
|
|
109
|
+
occurrence: CalendarOccurrence;
|
|
110
|
+
/** This occurrence's calendar color — see `calendarColors.ts`. Only applied for a "busy" chip; a
|
|
111
|
+
* "free" one keeps Outlook's own muted/outline treatment regardless of which calendar it's on. */
|
|
112
|
+
color: string;
|
|
113
|
+
onSelect: (occurrence: CalendarOccurrence) => void;
|
|
114
|
+
}) {
|
|
115
|
+
const { setNodeRef, listeners, attributes, transform, isDragging } = useDraggable({ id: eventDragId(occurrence) });
|
|
116
|
+
const isFree = occurrence.busyStatus === "free";
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<button
|
|
120
|
+
ref={setNodeRef}
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => onSelect(occurrence)}
|
|
123
|
+
style={{
|
|
124
|
+
...(transform ? { transform: `translate(${transform.x}px, ${transform.y}px)`, zIndex: 10 } : undefined),
|
|
125
|
+
...(isFree ? undefined : { backgroundColor: color, color: "#fff" }),
|
|
126
|
+
}}
|
|
127
|
+
className={[
|
|
128
|
+
"text-xs text-left truncate rounded-sm px-1.5 py-0.5 shrink-0",
|
|
129
|
+
isFree ? "bg-surface-alt text-text-muted" : "",
|
|
130
|
+
isDragging ? "opacity-50" : "",
|
|
131
|
+
].join(" ")}
|
|
132
|
+
{...listeners}
|
|
133
|
+
{...attributes}
|
|
134
|
+
>
|
|
135
|
+
{occurrence.allDay ? "" : `${format(new Date(occurrence.startDate), "h:mma")} `}
|
|
136
|
+
{occurrence.title}
|
|
137
|
+
</button>
|
|
138
|
+
);
|
|
139
|
+
}
|