@rapidmx/web-client 0.28.0 → 0.29.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 +3 -0
- package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +22 -10
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +60 -0
- package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +108 -0
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +25 -1
- package/apps/shared/components/calendar/CalendarSearchBox.tsx +83 -0
- package/apps/shared/components/calendar/EventMatchNav.tsx +72 -0
- package/apps/shared/components/calendar/EventModal.tsx +33 -12
- package/apps/shared/components/calendar/ListView.tsx +207 -0
- package/apps/shared/components/calendar/MonthView.tsx +14 -2
- package/apps/shared/components/calendar/SlideIn.tsx +47 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +5 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +23 -12
- package/apps/shared/components/calendar/activeOccurrence.ts +28 -0
- package/apps/shared/components/calendar/allDay.ts +16 -1
- package/apps/shared/components/calendar/calendarView.ts +41 -0
- package/apps/shared/components/layout/AppShell.tsx +13 -6
- package/apps/shared/components/mail/ConversationList.tsx +29 -3
- package/apps/shared/components/mail/ConversationThreadPane.tsx +15 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +117 -55
- package/apps/shared/components/mail/MessageDetailPane.tsx +3 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +193 -35
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +62 -11
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +21 -10
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +2 -1
- package/apps/shared/components/mail/compose/quotedBody.ts +45 -2
- package/apps/shared/components/mail/labelMenu.tsx +4 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +9 -2
- package/apps/shared/components/mail/reading/MessageBody.tsx +1 -1
- package/apps/shared/components/mail/reading/bodyContent.ts +15 -13
- package/apps/shared/components/mail/reading/bodyHtml.ts +12 -7
- package/apps/shared/gestures/useEnterSlide.ts +1 -1
- package/apps/shared/keyboard/keymap.ts +2 -0
- package/apps/shared/mail/rangeSelection.ts +45 -0
- package/apps/shared/mail/rowExit.ts +39 -0
- package/apps/www/calendar/index.tsx +251 -53
- package/apps/www/index.tsx +522 -343
- package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +3 -3
- package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +15 -9
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +15 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +16 -0
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +20 -0
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +31 -0
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +20 -1
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +7 -0
- package/dist/apps/shared/components/calendar/CalendarSearchBox.d.ts +22 -0
- package/dist/apps/shared/components/calendar/CalendarSearchBox.js +24 -0
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +17 -0
- package/dist/apps/shared/components/calendar/EventMatchNav.js +37 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +17 -1
- package/dist/apps/shared/components/calendar/EventModal.js +13 -11
- package/dist/apps/shared/components/calendar/ListView.d.ts +30 -0
- package/dist/apps/shared/components/calendar/ListView.js +111 -0
- package/dist/apps/shared/components/calendar/MonthView.js +12 -3
- package/dist/apps/shared/components/calendar/SlideIn.d.ts +16 -0
- package/dist/apps/shared/components/calendar/SlideIn.js +32 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +5 -1
- package/dist/apps/shared/components/calendar/TimeGridView.js +11 -3
- package/dist/apps/shared/components/calendar/activeOccurrence.d.ts +15 -0
- package/dist/apps/shared/components/calendar/activeOccurrence.js +21 -0
- package/dist/apps/shared/components/calendar/allDay.d.ts +4 -0
- package/dist/apps/shared/components/calendar/allDay.js +14 -1
- package/dist/apps/shared/components/calendar/calendarView.d.ts +16 -0
- package/dist/apps/shared/components/calendar/calendarView.js +36 -0
- package/dist/apps/shared/components/layout/AppShell.js +8 -4
- package/dist/apps/shared/components/mail/ConversationList.d.ts +9 -1
- package/dist/apps/shared/components/mail/ConversationList.js +10 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +9 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
- package/dist/apps/shared/components/mail/MessageDetailPane.js +3 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +31 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +123 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +6 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +37 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +5 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +10 -6
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +2 -1
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +43 -1
- package/dist/apps/shared/components/mail/labelMenu.d.ts +3 -1
- package/dist/apps/shared/components/mail/labelMenu.js +2 -2
- package/dist/apps/shared/components/mail/layout/MailShell.js +7 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +1 -1
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyContent.js +11 -11
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +3 -2
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +8 -4
- package/dist/apps/shared/gestures/useEnterSlide.d.ts +2 -0
- package/dist/apps/shared/gestures/useEnterSlide.js +1 -1
- package/dist/apps/shared/keyboard/keymap.d.ts +12 -0
- package/dist/apps/shared/keyboard/keymap.js +2 -0
- package/dist/apps/shared/mail/rangeSelection.d.ts +20 -0
- package/dist/apps/shared/mail/rangeSelection.js +34 -0
- package/dist/apps/shared/mail/rowExit.d.ts +12 -0
- package/dist/apps/shared/mail/rowExit.js +33 -0
- package/dist/apps/www/calendar/index.js +177 -18
- package/dist/apps/www/index.js +220 -65
- package/dist/lib/calendar/eventSearch.d.ts +42 -0
- package/dist/lib/calendar/eventSearch.js +152 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +15 -3
- package/dist/lib/components/navigation/BottomTabBar.js +52 -15
- package/dist/lib/components/navigation/NavBadge.d.ts +13 -0
- package/dist/lib/components/navigation/NavBadge.js +18 -0
- package/dist/lib/mail/compose/quotedImages.d.ts +21 -0
- package/dist/lib/mail/compose/quotedImages.js +41 -0
- package/dist/lib/mail/inlineImages.d.ts +38 -0
- package/dist/lib/mail/inlineImages.js +90 -0
- package/dist/lib/mail/mailApi.d.ts +8 -1
- package/dist/lib/mail/mailApi.js +57 -5
- package/lib/calendar/eventSearch.ts +172 -0
- package/lib/components/navigation/BottomTabBar.tsx +107 -22
- package/lib/components/navigation/NavBadge.tsx +33 -0
- package/lib/mail/compose/quotedImages.ts +49 -0
- package/lib/mail/inlineImages.ts +104 -0
- package/lib/mail/mailApi.ts +64 -5
- package/package.json +1 -1
- package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +0 -36
- package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +0 -10
- package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +0 -11
|
@@ -17,19 +17,25 @@ import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calen
|
|
|
17
17
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
18
18
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
19
19
|
import { expandAllOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
20
|
+
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
20
21
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
21
22
|
import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
22
23
|
import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
|
|
24
|
+
import CalendarSearchBox from "../../shared/components/calendar/CalendarSearchBox.js";
|
|
25
|
+
import { ActiveOccurrenceContext } from "../../shared/components/calendar/activeOccurrence.js";
|
|
26
|
+
import { isUpcoming, occurrenceDay } from "../../shared/components/calendar/allDay.js";
|
|
23
27
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
24
28
|
import EventModal from "../../shared/components/calendar/EventModal.js";
|
|
25
29
|
import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
|
|
26
30
|
import { anchorOf } from "../../shared/components/calendar/EventShell.js";
|
|
27
31
|
import MiniDatePicker from "../../../lib/components/pickers/MiniDatePicker.js";
|
|
32
|
+
import ListView from "../../shared/components/calendar/ListView.js";
|
|
28
33
|
import MonthView from "../../shared/components/calendar/MonthView.js";
|
|
29
34
|
import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
|
|
30
35
|
import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
|
|
31
36
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
32
37
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
38
|
+
import { CALENDAR_VIEWS, getStoredCalendarView, isCalendarView, storeCalendarView } from "../../shared/components/calendar/calendarView.js";
|
|
33
39
|
import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
|
|
34
40
|
import { nextHalfHour } from "../../shared/components/calendar/timePicker.js";
|
|
35
41
|
import { useSwipeSlide } from "../../shared/gestures/useSwipeSlide.js";
|
|
@@ -41,13 +47,15 @@ import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
|
41
47
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
42
48
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
43
49
|
import { redactedEventUidOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
50
|
+
/** The furthest instants a `Date` can hold: the List's "range" reaches both. */
|
|
51
|
+
const MAX_DATE_MS = 8.64e15;
|
|
44
52
|
function CalendarPage(props) {
|
|
45
53
|
return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid, bookingHref: bookingSettingsHref(props.pluginNav) }) }));
|
|
46
54
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4); the split view has none. */
|
|
55
|
+
const VIEW_LABELS = { list: "List", month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
|
|
56
|
+
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
|
|
50
57
|
const VIEW_SHORTCUTS = {
|
|
58
|
+
list: SHORTCUTS.calendar.list,
|
|
51
59
|
day: SHORTCUTS.calendar.day,
|
|
52
60
|
workWeek: SHORTCUTS.calendar.workWeek,
|
|
53
61
|
week: SHORTCUTS.calendar.week,
|
|
@@ -75,7 +83,20 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
75
83
|
// dnd-kit's own documented pattern for this exact conflict.
|
|
76
84
|
const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }));
|
|
77
85
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
78
|
-
|
|
86
|
+
// Month, until the effects below know better: a remembered view, or the List on a phone (never read while rendering, so the server's markup and the
|
|
87
|
+
// first client render agree). A desktop always opens on the Month unless the link or a remembered Week, Work Week, Day, Split or Month says otherwise.
|
|
88
|
+
const [view, setViewState] = useState("month");
|
|
89
|
+
// Whether the view was decided by the URL, a remembered choice or the reader (and so is not the device's default to follow).
|
|
90
|
+
const viewChosenRef = useRef(false);
|
|
91
|
+
/** Switches the view; a reader's own choice (`remember`) is also what the Calendar opens on next time - except the List on a desktop, which
|
|
92
|
+
* only a phone opens on, so it is never written there (and so can never turn up as the next desktop visit's view). */
|
|
93
|
+
function setView(next, remember = true) {
|
|
94
|
+
viewChosenRef.current = true;
|
|
95
|
+
setViewState(next);
|
|
96
|
+
if (remember && (next !== "list" || isMobile)) {
|
|
97
|
+
storeCalendarView(next);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
79
100
|
const [viewDate, setViewDate] = useState(() => startOfDay(new Date()));
|
|
80
101
|
const [events, setEvents] = useState([]);
|
|
81
102
|
const [loading, setLoading] = useState(true);
|
|
@@ -90,8 +111,14 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
90
111
|
useEffect(() => {
|
|
91
112
|
const params = new URLSearchParams(window.location.search);
|
|
92
113
|
const requestedView = params.get("view");
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
const storedView = getStoredCalendarView();
|
|
115
|
+
if (isCalendarView(requestedView)) {
|
|
116
|
+
// Asked for by the link: shown, but not remembered - it is not a choice of the reader's.
|
|
117
|
+
setView(requestedView, false);
|
|
118
|
+
}
|
|
119
|
+
else if (storedView && storedView !== "list") {
|
|
120
|
+
// A remembered List is not restored: it is the phone's default (below) and never the desktop's, whatever an older visit left in storage.
|
|
121
|
+
setView(storedView, false);
|
|
95
122
|
}
|
|
96
123
|
const requestedDate = params.get("date");
|
|
97
124
|
// `parseISO()`, not `new Date()` - `?date=2026-06-16` is a local calendar day, but `new Date()` reads a
|
|
@@ -101,6 +128,13 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
101
128
|
setViewDate(startOfDay(parsedDate));
|
|
102
129
|
}
|
|
103
130
|
}, []);
|
|
131
|
+
// With nothing remembered the device decides: the List on a phone, the Month elsewhere (`useIsMobile()` only knows after mounting).
|
|
132
|
+
const isMobile = useIsMobile();
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!viewChosenRef.current) {
|
|
135
|
+
setViewState(isMobile ? "list" : "month");
|
|
136
|
+
}
|
|
137
|
+
}, [isMobile]);
|
|
104
138
|
const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
|
|
105
139
|
const folderColors = useMemo(() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])), [calendarFolders, colorFor]);
|
|
106
140
|
function toggleCalendar(toggledUid) {
|
|
@@ -121,6 +155,10 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
121
155
|
reloadFolders();
|
|
122
156
|
}
|
|
123
157
|
const { rangeStart, rangeEnd, days } = useMemo(() => {
|
|
158
|
+
if (view === "list") {
|
|
159
|
+
// The list is not a range at all: every event, whenever it falls (a series bounded as the search bounds it - see `allOccurrences()`).
|
|
160
|
+
return { rangeStart: new Date(-MAX_DATE_MS), rangeEnd: new Date(MAX_DATE_MS), days: [] };
|
|
161
|
+
}
|
|
124
162
|
if (view === "month") {
|
|
125
163
|
const start = startOfWeek(startOfMonth(viewDate), { weekStartsOn: 1 });
|
|
126
164
|
const end = endOfWeek(endOfMonth(viewDate), { weekStartsOn: 1 });
|
|
@@ -221,11 +259,103 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
221
259
|
}
|
|
222
260
|
});
|
|
223
261
|
}), [client]);
|
|
224
|
-
|
|
262
|
+
// Search: `searchText` is the box, `query` what it held once typing paused (or Enter was pressed) and is what is searched for. A search covers every
|
|
263
|
+
// event of the checked calendars whatever the view shows (see `searchOccurrences()`); while one runs the views show only its matches, and
|
|
264
|
+
// `activeKey` is the match being pointed at (Previous/Next match step through `matches`).
|
|
265
|
+
const [searchText, setSearchText] = useState("");
|
|
266
|
+
const [query, setQuery] = useState("");
|
|
267
|
+
const [activeKey, setActiveKey] = useState(null);
|
|
268
|
+
const searchInputRef = useRef(null);
|
|
269
|
+
const searching = searchTerms(query).length > 0;
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const timer = setTimeout(() => setQuery(searchText), 300);
|
|
272
|
+
return () => clearTimeout(timer);
|
|
273
|
+
}, [searchText]);
|
|
274
|
+
// The List shows only what is from today on, so that is all a search steps through there; the other views reach the past ones.
|
|
275
|
+
const matches = useMemo(() => {
|
|
276
|
+
const found = searching ? searchOccurrences(events, query) : [];
|
|
277
|
+
const today = new Date();
|
|
278
|
+
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
279
|
+
}, [events, query, searching, view]);
|
|
280
|
+
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
281
|
+
// What an open event card steps through, in order: the matches while a search runs, else every event of the checked calendars (bounded as a
|
|
282
|
+
// search is). The List holds only what is from today on, so that is all its card steps through. Worked out only when something needs it.
|
|
283
|
+
const everything = useMemo(() => (view === "list" || modal?.occurrence ? allOccurrences(events) : []), [events, view, !!modal?.occurrence]);
|
|
284
|
+
const sequence = useMemo(() => {
|
|
285
|
+
if (searching) {
|
|
286
|
+
return matches;
|
|
287
|
+
}
|
|
288
|
+
if (view === "list") {
|
|
289
|
+
const today = new Date();
|
|
290
|
+
return everything.filter((occurrence) => isUpcoming(occurrence, today));
|
|
291
|
+
}
|
|
292
|
+
return everything;
|
|
293
|
+
}, [searching, matches, view, everything]);
|
|
294
|
+
const occurrences = useMemo(() => {
|
|
295
|
+
if (view === "list") {
|
|
296
|
+
return sequence;
|
|
297
|
+
}
|
|
298
|
+
return searching ? occurrencesInRange(matches, rangeStart, rangeEnd) : expandAllOccurrences(events, rangeStart, rangeEnd);
|
|
299
|
+
}, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
|
|
300
|
+
/** Points at `match`, moving the view to the day it is on unless the view already shows it. */
|
|
301
|
+
function showMatch(match) {
|
|
302
|
+
setActiveKey(match.occurrenceKey);
|
|
303
|
+
// An event's card that is open moves on to it, so the card and the view stay together.
|
|
304
|
+
setModal((current) => (current?.occurrence ? { occurrence: match } : current));
|
|
305
|
+
if (occurrencesInRange([match], rangeStart, rangeEnd).length === 0) {
|
|
306
|
+
setViewDate(occurrenceDay(match));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// The step that moved an open event card on, for the card to slide in from the side of the button pressed (also on a wrap-around).
|
|
310
|
+
const [slide, setSlide] = useState(undefined);
|
|
311
|
+
// The card open is where the sequence stands; with none, the match the search is on. A search wraps round at either end, the events of the
|
|
312
|
+
// calendar do not.
|
|
313
|
+
const stepFrom = modal?.occurrence;
|
|
314
|
+
const stepIndex = (direction) => stepSequenceIndex(sequence, stepFrom?.occurrenceKey ?? activeKey, stepFrom ? new Date(stepFrom.startDate) : undefined, direction, searching);
|
|
315
|
+
function stepMatch(direction) {
|
|
316
|
+
const next = sequence[stepIndex(direction)];
|
|
317
|
+
if (next) {
|
|
318
|
+
setSlide({ from: direction === 1 ? "right" : "left", occurrenceKey: next.occurrenceKey });
|
|
319
|
+
showMatch(next);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function clearSearch() {
|
|
323
|
+
setSearchText("");
|
|
324
|
+
setQuery("");
|
|
325
|
+
}
|
|
326
|
+
function submitSearch(backwards) {
|
|
327
|
+
if (searchText !== query) {
|
|
328
|
+
setQuery(searchText);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
stepMatch(backwards ? -1 : 1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// A new search goes to its first match: the first one in the view if there is one, else the first from the day being viewed on (the nearest
|
|
335
|
+
// before it when there is none after). Matches that change under the same query (a saved edit, a live update) leave the view where it is.
|
|
336
|
+
const searchedQueryRef = useRef("");
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (!searching || matches.length === 0) {
|
|
339
|
+
searchedQueryRef.current = "";
|
|
340
|
+
setActiveKey(null);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (searchedQueryRef.current !== query) {
|
|
344
|
+
searchedQueryRef.current = query;
|
|
345
|
+
// The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
|
|
346
|
+
const from = startOfDay(viewDate);
|
|
347
|
+
showMatch(matches[view === "list" ? initialMatchIndex(matches, from, rangeEnd, from) : initialMatchIndex(matches, rangeStart, rangeEnd, from)]);
|
|
348
|
+
}
|
|
349
|
+
}, [matches, query, searching]);
|
|
225
350
|
const splitColumns = useMemo(() => calendarFolders
|
|
226
351
|
.filter((f) => checkedFolderUids.has(f.uid))
|
|
227
352
|
.map((f) => ({ folderUid: f.uid, name: f.name, color: colorFor(f) })), [calendarFolders, checkedFolderUidsKey, colorFor]);
|
|
228
353
|
function shiftView(direction) {
|
|
354
|
+
if (view === "list") {
|
|
355
|
+
// Previous and Next move the list a month, from wherever it has been scrolled to (the list does the scrolling).
|
|
356
|
+
setListStep((step) => ({ direction, nonce: (step?.nonce ?? 0) + 1 }));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
229
359
|
setViewDate((d) => view === "month"
|
|
230
360
|
? addMonths(d, direction)
|
|
231
361
|
: view === "week" || view === "workWeek"
|
|
@@ -236,9 +366,8 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
236
366
|
// the Previous/Next buttons and shortcuts do, and it is off while an event is being dragged or the editor is open. The views
|
|
237
367
|
// never scroll sideways (their columns share the width), so there is no horizontal scroller for the gesture to fight with.
|
|
238
368
|
// The view follows the finger and, once the swipe commits, slides out while the next period slides in (see `useSwipeSlide()`).
|
|
239
|
-
const isMobile = useIsMobile();
|
|
240
369
|
const swipe = useSwipeSlide({
|
|
241
|
-
enabled: isMobile && !dragging && !modal,
|
|
370
|
+
enabled: isMobile && !dragging && !modal && view !== "list",
|
|
242
371
|
onShift: (direction) => shiftView(SWIPE_PERIOD_SHIFT[direction]),
|
|
243
372
|
});
|
|
244
373
|
// The desktop's mouse wheel steps through the months without end; in the views that scroll through the hours of a day it scrolls
|
|
@@ -249,7 +378,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
249
378
|
const arrivalEdge = useRef(null);
|
|
250
379
|
const scrollerOf = () => swipe.ref.current?.querySelector("[data-calendar-scroller]") ?? null;
|
|
251
380
|
useWheelPaging(swipe.ref, {
|
|
252
|
-
enabled: !isMobile && !dragging && !modal,
|
|
381
|
+
enabled: !isMobile && !dragging && !modal && view !== "list",
|
|
253
382
|
getScroller: () => (view === "month" ? null : scrollerOf()),
|
|
254
383
|
onStep: (direction, edge) => {
|
|
255
384
|
arrivalEdge.current = edge;
|
|
@@ -265,11 +394,20 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
265
394
|
scroller.scrollTop = edge === "top" ? 0 : scroller.scrollHeight;
|
|
266
395
|
}
|
|
267
396
|
}, [viewDate]);
|
|
397
|
+
// Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
const marked = Array.from(swipe.ref.current.querySelectorAll("[data-occurrence-key]"));
|
|
400
|
+
marked.find((el) => el.dataset.occurrenceKey === activeKey)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
401
|
+
}, [activeKey, view === "list" ? null : viewDate, view, loading]);
|
|
402
|
+
// The List scrolls to `viewDate` when this changes (Today, a day picked in the mini calendar), even when it is the date it already has.
|
|
403
|
+
const [jumpNonce, setJumpNonce] = useState(0);
|
|
404
|
+
const [listStep, setListStep] = useState(null);
|
|
268
405
|
function goToday() {
|
|
269
406
|
setViewDate(startOfDay(new Date()));
|
|
407
|
+
setJumpNonce((n) => n + 1);
|
|
270
408
|
}
|
|
271
409
|
function handleSelectDay(date) {
|
|
272
|
-
setView("day");
|
|
410
|
+
setView("day", false);
|
|
273
411
|
setViewDate(startOfDay(date));
|
|
274
412
|
}
|
|
275
413
|
function openNewEvent(start, end, targetFolderUid, anchor, initialAllDay) {
|
|
@@ -288,6 +426,8 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
288
426
|
});
|
|
289
427
|
}
|
|
290
428
|
function openEvent(occurrence) {
|
|
429
|
+
setSlide(undefined);
|
|
430
|
+
setActiveKey(occurrence.occurrenceKey);
|
|
291
431
|
setModal({ occurrence });
|
|
292
432
|
}
|
|
293
433
|
function closeModal() {
|
|
@@ -298,10 +438,15 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
298
438
|
useShortcut(SHORTCUTS.calendar.today, goToday);
|
|
299
439
|
useShortcut(SHORTCUTS.calendar.previous, () => shiftView(-1));
|
|
300
440
|
useShortcut(SHORTCUTS.calendar.next, () => shiftView(1));
|
|
441
|
+
useShortcut(SHORTCUTS.calendar.list, () => setView("list"));
|
|
301
442
|
useShortcut(SHORTCUTS.calendar.day, () => setView("day"));
|
|
302
443
|
useShortcut(SHORTCUTS.calendar.workWeek, () => setView("workWeek"));
|
|
303
444
|
useShortcut(SHORTCUTS.calendar.week, () => setView("week"));
|
|
304
445
|
useShortcut(SHORTCUTS.calendar.month, () => setView("month"));
|
|
446
|
+
useShortcut(SHORTCUTS.calendar.search, () => {
|
|
447
|
+
searchInputRef.current?.focus();
|
|
448
|
+
searchInputRef.current?.select();
|
|
449
|
+
});
|
|
305
450
|
const newEventHint = useShortcutProps("New event", SHORTCUTS.calendar.create, !!mailboxUid && !!folderUid);
|
|
306
451
|
const previousHint = useShortcutProps("Previous", SHORTCUTS.calendar.previous);
|
|
307
452
|
const todayHint = useShortcutProps("Today", SHORTCUTS.calendar.today);
|
|
@@ -334,11 +479,13 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
334
479
|
notifyApiError(err, action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event");
|
|
335
480
|
}
|
|
336
481
|
}
|
|
337
|
-
const title = view === "
|
|
338
|
-
?
|
|
339
|
-
: view === "
|
|
340
|
-
?
|
|
341
|
-
:
|
|
482
|
+
const title = view === "list"
|
|
483
|
+
? "Upcoming events"
|
|
484
|
+
: view === "month"
|
|
485
|
+
? format(viewDate, "MMMM yyyy")
|
|
486
|
+
: view === "week" || view === "workWeek"
|
|
487
|
+
? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
|
|
488
|
+
: format(viewDate, "EEEE, MMMM d, yyyy");
|
|
342
489
|
const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
|
|
343
490
|
const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
|
|
344
491
|
const modalMailbox = mailboxes.find((mb) => mb.uid === modalMailboxUid);
|
|
@@ -348,8 +495,20 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
348
495
|
const mailboxOptions = useMemo(() => mailboxCalendars
|
|
349
496
|
.filter((mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid))
|
|
350
497
|
.map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })), [mailboxCalendars, writableMailboxes]);
|
|
351
|
-
const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) =>
|
|
352
|
-
|
|
498
|
+
const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => {
|
|
499
|
+
setViewDate(startOfDay(date));
|
|
500
|
+
setJumpNonce((n) => n + 1);
|
|
501
|
+
} }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
|
|
502
|
+
return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx(CalendarSearchBox, { value: searchText, onChange: setSearchText, onSubmit: submitSearch, onClear: clearSearch, onPrevious: () => stepMatch(-1), onNext: () => stepMatch(1), active: searching, count: matches.length, position: activeIndex, inputRef: searchInputRef }), _jsx("div", { className: "flex flex-wrap gap-1", children: CALENDAR_VIEWS.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(ActiveOccurrenceContext.Provider, { value: searching ? activeKey : null, children: _jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "list" ? (_jsx(ListView, { occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, focusDate: viewDate, jumpNonce: jumpNonce, step: listStep })) : view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) }) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, initialAllDay: modal.initialAllDay, anchor: modal.anchor, bookingHref: bookingHref, matchNav: modal.occurrence && sequence.length > 0
|
|
503
|
+
? {
|
|
504
|
+
onPrevious: () => stepMatch(-1),
|
|
505
|
+
onNext: () => stepMatch(1),
|
|
506
|
+
canPrevious: stepIndex(-1) >= 0,
|
|
507
|
+
canNext: stepIndex(1) >= 0,
|
|
508
|
+
noun: searching ? "match" : "event",
|
|
509
|
+
slide,
|
|
510
|
+
}
|
|
511
|
+
: undefined, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && _jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() })] })] }));
|
|
353
512
|
}
|
|
354
513
|
export default CalendarPage;
|
|
355
514
|
/** The tab's title: `Brand: Calendar` (see `pageTitle()`). */
|