@rapidmx/web-client 0.28.0 → 0.30.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 +38 -5
- 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/SwipeRow.tsx +26 -4
- 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/gestures/useLongPress.ts +138 -0
- 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 +570 -358
- 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 +16 -3
- 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/SwipeRow.d.ts +5 -1
- package/dist/apps/shared/components/mail/SwipeRow.js +17 -3
- 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/gestures/useLongPress.d.ts +41 -0
- package/dist/apps/shared/gestures/useLongPress.js +112 -0
- 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 +257 -73
- 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
|
@@ -29,19 +29,26 @@ import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calen
|
|
|
29
29
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
30
30
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
31
31
|
import { CalendarOccurrence, expandAllOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
32
|
+
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
32
33
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
33
34
|
import CalendarShell, { CalendarShellProps, useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
34
35
|
import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
|
|
36
|
+
import CalendarSearchBox from "../../shared/components/calendar/CalendarSearchBox.js";
|
|
37
|
+
import { ActiveOccurrenceContext } from "../../shared/components/calendar/activeOccurrence.js";
|
|
38
|
+
import { isUpcoming, occurrenceDay } from "../../shared/components/calendar/allDay.js";
|
|
35
39
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
36
40
|
import EventModal from "../../shared/components/calendar/EventModal.js";
|
|
37
41
|
import FloatingActionButton from "../../shared/components/layout/FloatingActionButton.js";
|
|
38
42
|
import { EventAnchor, anchorOf } from "../../shared/components/calendar/EventShell.js";
|
|
39
43
|
import MiniDatePicker from "../../../lib/components/pickers/MiniDatePicker.js";
|
|
44
|
+
import ListView, { ListStep } from "../../shared/components/calendar/ListView.js";
|
|
40
45
|
import MonthView from "../../shared/components/calendar/MonthView.js";
|
|
41
46
|
import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
|
|
42
47
|
import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
|
|
43
48
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
44
49
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
50
|
+
import { CALENDAR_VIEWS, CalendarView, getStoredCalendarView, isCalendarView, storeCalendarView } from "../../shared/components/calendar/calendarView.js";
|
|
51
|
+
import { SlideFrom } from "../../shared/components/calendar/SlideIn.js";
|
|
45
52
|
import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
|
|
46
53
|
import { nextHalfHour } from "../../shared/components/calendar/timePicker.js";
|
|
47
54
|
import { useSwipeSlide } from "../../shared/gestures/useSwipeSlide.js";
|
|
@@ -54,6 +61,9 @@ import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
|
54
61
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
55
62
|
import { redactedEventUidOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
56
63
|
|
|
64
|
+
/** The furthest instants a `Date` can hold: the List's "range" reaches both. */
|
|
65
|
+
const MAX_DATE_MS = 8.64e15;
|
|
66
|
+
|
|
57
67
|
function CalendarPage(props: CalendarShellProps) {
|
|
58
68
|
return (
|
|
59
69
|
<CalendarShell {...props}>
|
|
@@ -62,11 +72,11 @@ function CalendarPage(props: CalendarShellProps) {
|
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
type ViewType =
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4); the split view has none. */
|
|
75
|
+
type ViewType = CalendarView;
|
|
76
|
+
const VIEW_LABELS: Record<ViewType, string> = { list: "List", month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
|
|
77
|
+
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
|
|
69
78
|
const VIEW_SHORTCUTS: Partial<Record<ViewType, ShortcutDef>> = {
|
|
79
|
+
list: SHORTCUTS.calendar.list,
|
|
70
80
|
day: SHORTCUTS.calendar.day,
|
|
71
81
|
workWeek: SHORTCUTS.calendar.workWeek,
|
|
72
82
|
week: SHORTCUTS.calendar.week,
|
|
@@ -122,7 +132,20 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
122
132
|
);
|
|
123
133
|
|
|
124
134
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
125
|
-
|
|
135
|
+
// 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
|
|
136
|
+
// 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.
|
|
137
|
+
const [view, setViewState] = useState<ViewType>("month");
|
|
138
|
+
// Whether the view was decided by the URL, a remembered choice or the reader (and so is not the device's default to follow).
|
|
139
|
+
const viewChosenRef = useRef(false);
|
|
140
|
+
/** 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
|
|
141
|
+
* only a phone opens on, so it is never written there (and so can never turn up as the next desktop visit's view). */
|
|
142
|
+
function setView(next: ViewType, remember = true) {
|
|
143
|
+
viewChosenRef.current = true;
|
|
144
|
+
setViewState(next);
|
|
145
|
+
if (remember && (next !== "list" || isMobile)) {
|
|
146
|
+
storeCalendarView(next);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
126
149
|
const [viewDate, setViewDate] = useState(() => startOfDay(new Date()));
|
|
127
150
|
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
|
128
151
|
const [loading, setLoading] = useState(true);
|
|
@@ -138,8 +161,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
138
161
|
useEffect(() => {
|
|
139
162
|
const params = new URLSearchParams(window.location.search);
|
|
140
163
|
const requestedView = params.get("view");
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
const storedView = getStoredCalendarView();
|
|
165
|
+
if (isCalendarView(requestedView)) {
|
|
166
|
+
// Asked for by the link: shown, but not remembered - it is not a choice of the reader's.
|
|
167
|
+
setView(requestedView, false);
|
|
168
|
+
} else if (storedView && storedView !== "list") {
|
|
169
|
+
// 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.
|
|
170
|
+
setView(storedView, false);
|
|
143
171
|
}
|
|
144
172
|
const requestedDate = params.get("date");
|
|
145
173
|
// `parseISO()`, not `new Date()` - `?date=2026-06-16` is a local calendar day, but `new Date()` reads a
|
|
@@ -150,6 +178,14 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
150
178
|
}
|
|
151
179
|
}, []);
|
|
152
180
|
|
|
181
|
+
// With nothing remembered the device decides: the List on a phone, the Month elsewhere (`useIsMobile()` only knows after mounting).
|
|
182
|
+
const isMobile = useIsMobile();
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!viewChosenRef.current) {
|
|
185
|
+
setViewState(isMobile ? "list" : "month");
|
|
186
|
+
}
|
|
187
|
+
}, [isMobile]);
|
|
188
|
+
|
|
153
189
|
const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
|
|
154
190
|
const folderColors = useMemo(
|
|
155
191
|
() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])),
|
|
@@ -175,6 +211,10 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
175
211
|
}
|
|
176
212
|
|
|
177
213
|
const { rangeStart, rangeEnd, days } = useMemo(() => {
|
|
214
|
+
if (view === "list") {
|
|
215
|
+
// The list is not a range at all: every event, whenever it falls (a series bounded as the search bounds it - see `allOccurrences()`).
|
|
216
|
+
return { rangeStart: new Date(-MAX_DATE_MS), rangeEnd: new Date(MAX_DATE_MS), days: [] as Date[] };
|
|
217
|
+
}
|
|
178
218
|
if (view === "month") {
|
|
179
219
|
const start = startOfWeek(startOfMonth(viewDate), { weekStartsOn: 1 });
|
|
180
220
|
const end = endOfWeek(endOfMonth(viewDate), { weekStartsOn: 1 });
|
|
@@ -287,10 +327,104 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
287
327
|
[client],
|
|
288
328
|
);
|
|
289
329
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
);
|
|
330
|
+
// 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
|
|
331
|
+
// event of the checked calendars whatever the view shows (see `searchOccurrences()`); while one runs the views show only its matches, and
|
|
332
|
+
// `activeKey` is the match being pointed at (Previous/Next match step through `matches`).
|
|
333
|
+
const [searchText, setSearchText] = useState("");
|
|
334
|
+
const [query, setQuery] = useState("");
|
|
335
|
+
const [activeKey, setActiveKey] = useState<string | null>(null);
|
|
336
|
+
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
|
337
|
+
const searching = searchTerms(query).length > 0;
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
const timer = setTimeout(() => setQuery(searchText), 300);
|
|
340
|
+
return () => clearTimeout(timer);
|
|
341
|
+
}, [searchText]);
|
|
342
|
+
// 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.
|
|
343
|
+
const matches = useMemo(() => {
|
|
344
|
+
const found = searching ? searchOccurrences(events, query) : [];
|
|
345
|
+
const today = new Date();
|
|
346
|
+
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
347
|
+
}, [events, query, searching, view]);
|
|
348
|
+
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
349
|
+
|
|
350
|
+
// 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
|
|
351
|
+
// 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.
|
|
352
|
+
const everything = useMemo(() => (view === "list" || modal?.occurrence ? allOccurrences(events) : []), [events, view, !!modal?.occurrence]);
|
|
353
|
+
const sequence = useMemo(() => {
|
|
354
|
+
if (searching) {
|
|
355
|
+
return matches;
|
|
356
|
+
}
|
|
357
|
+
if (view === "list") {
|
|
358
|
+
const today = new Date();
|
|
359
|
+
return everything.filter((occurrence) => isUpcoming(occurrence, today));
|
|
360
|
+
}
|
|
361
|
+
return everything;
|
|
362
|
+
}, [searching, matches, view, everything]);
|
|
363
|
+
|
|
364
|
+
const occurrences = useMemo(() => {
|
|
365
|
+
if (view === "list") {
|
|
366
|
+
return sequence;
|
|
367
|
+
}
|
|
368
|
+
return searching ? occurrencesInRange(matches, rangeStart, rangeEnd) : expandAllOccurrences(events, rangeStart, rangeEnd);
|
|
369
|
+
}, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
|
|
370
|
+
|
|
371
|
+
/** Points at `match`, moving the view to the day it is on unless the view already shows it. */
|
|
372
|
+
function showMatch(match: CalendarOccurrence) {
|
|
373
|
+
setActiveKey(match.occurrenceKey);
|
|
374
|
+
// An event's card that is open moves on to it, so the card and the view stay together.
|
|
375
|
+
setModal((current) => (current?.occurrence ? { occurrence: match } : current));
|
|
376
|
+
if (occurrencesInRange([match], rangeStart, rangeEnd).length === 0) {
|
|
377
|
+
setViewDate(occurrenceDay(match));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 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).
|
|
382
|
+
const [slide, setSlide] = useState<{ from: SlideFrom; occurrenceKey: string } | undefined>(undefined);
|
|
383
|
+
|
|
384
|
+
// 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
|
|
385
|
+
// calendar do not.
|
|
386
|
+
const stepFrom = modal?.occurrence;
|
|
387
|
+
const stepIndex = (direction: 1 | -1) =>
|
|
388
|
+
stepSequenceIndex(sequence, stepFrom?.occurrenceKey ?? activeKey, stepFrom ? new Date(stepFrom.startDate) : undefined, direction, searching);
|
|
389
|
+
|
|
390
|
+
function stepMatch(direction: 1 | -1) {
|
|
391
|
+
const next = sequence[stepIndex(direction)];
|
|
392
|
+
if (next) {
|
|
393
|
+
setSlide({ from: direction === 1 ? "right" : "left", occurrenceKey: next.occurrenceKey });
|
|
394
|
+
showMatch(next);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function clearSearch() {
|
|
399
|
+
setSearchText("");
|
|
400
|
+
setQuery("");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function submitSearch(backwards: boolean) {
|
|
404
|
+
if (searchText !== query) {
|
|
405
|
+
setQuery(searchText);
|
|
406
|
+
} else {
|
|
407
|
+
stepMatch(backwards ? -1 : 1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 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
|
|
412
|
+
// 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.
|
|
413
|
+
const searchedQueryRef = useRef("");
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
if (!searching || matches.length === 0) {
|
|
416
|
+
searchedQueryRef.current = "";
|
|
417
|
+
setActiveKey(null);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (searchedQueryRef.current !== query) {
|
|
421
|
+
searchedQueryRef.current = query;
|
|
422
|
+
// The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
|
|
423
|
+
const from = startOfDay(viewDate);
|
|
424
|
+
showMatch(matches[view === "list" ? initialMatchIndex(matches, from, rangeEnd, from) : initialMatchIndex(matches, rangeStart, rangeEnd, from)]);
|
|
425
|
+
}
|
|
426
|
+
}, [matches, query, searching]);
|
|
427
|
+
|
|
294
428
|
|
|
295
429
|
const splitColumns = useMemo(
|
|
296
430
|
() =>
|
|
@@ -301,6 +435,11 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
301
435
|
);
|
|
302
436
|
|
|
303
437
|
function shiftView(direction: 1 | -1) {
|
|
438
|
+
if (view === "list") {
|
|
439
|
+
// Previous and Next move the list a month, from wherever it has been scrolled to (the list does the scrolling).
|
|
440
|
+
setListStep((step) => ({ direction, nonce: (step?.nonce ?? 0) + 1 }));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
304
443
|
setViewDate((d) =>
|
|
305
444
|
view === "month"
|
|
306
445
|
? addMonths(d, direction)
|
|
@@ -314,9 +453,8 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
314
453
|
// the Previous/Next buttons and shortcuts do, and it is off while an event is being dragged or the editor is open. The views
|
|
315
454
|
// never scroll sideways (their columns share the width), so there is no horizontal scroller for the gesture to fight with.
|
|
316
455
|
// The view follows the finger and, once the swipe commits, slides out while the next period slides in (see `useSwipeSlide()`).
|
|
317
|
-
const isMobile = useIsMobile();
|
|
318
456
|
const swipe = useSwipeSlide({
|
|
319
|
-
enabled: isMobile && !dragging && !modal,
|
|
457
|
+
enabled: isMobile && !dragging && !modal && view !== "list",
|
|
320
458
|
onShift: (direction) => shiftView(SWIPE_PERIOD_SHIFT[direction]),
|
|
321
459
|
});
|
|
322
460
|
|
|
@@ -328,7 +466,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
328
466
|
const arrivalEdge = useRef<WheelEdge | null>(null);
|
|
329
467
|
const scrollerOf = (): HTMLElement | null => swipe.ref.current?.querySelector<HTMLElement>("[data-calendar-scroller]") ?? null;
|
|
330
468
|
useWheelPaging(swipe.ref, {
|
|
331
|
-
enabled: !isMobile && !dragging && !modal,
|
|
469
|
+
enabled: !isMobile && !dragging && !modal && view !== "list",
|
|
332
470
|
getScroller: () => (view === "month" ? null : scrollerOf()),
|
|
333
471
|
onStep: (direction, edge) => {
|
|
334
472
|
arrivalEdge.current = edge;
|
|
@@ -345,12 +483,22 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
345
483
|
}
|
|
346
484
|
}, [viewDate]);
|
|
347
485
|
|
|
486
|
+
// Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
const marked = Array.from(swipe.ref.current!.querySelectorAll<HTMLElement>("[data-occurrence-key]"));
|
|
489
|
+
marked.find((el) => el.dataset.occurrenceKey === activeKey)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
490
|
+
}, [activeKey, view === "list" ? null : viewDate, view, loading]);
|
|
491
|
+
|
|
492
|
+
// 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.
|
|
493
|
+
const [jumpNonce, setJumpNonce] = useState(0);
|
|
494
|
+
const [listStep, setListStep] = useState<ListStep | null>(null);
|
|
348
495
|
function goToday() {
|
|
349
496
|
setViewDate(startOfDay(new Date()));
|
|
497
|
+
setJumpNonce((n) => n + 1);
|
|
350
498
|
}
|
|
351
499
|
|
|
352
500
|
function handleSelectDay(date: Date) {
|
|
353
|
-
setView("day");
|
|
501
|
+
setView("day", false);
|
|
354
502
|
setViewDate(startOfDay(date));
|
|
355
503
|
}
|
|
356
504
|
|
|
@@ -371,6 +519,8 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
371
519
|
}
|
|
372
520
|
|
|
373
521
|
function openEvent(occurrence: CalendarOccurrence) {
|
|
522
|
+
setSlide(undefined);
|
|
523
|
+
setActiveKey(occurrence.occurrenceKey);
|
|
374
524
|
setModal({ occurrence });
|
|
375
525
|
}
|
|
376
526
|
|
|
@@ -383,10 +533,15 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
383
533
|
useShortcut(SHORTCUTS.calendar.today, goToday);
|
|
384
534
|
useShortcut(SHORTCUTS.calendar.previous, () => shiftView(-1));
|
|
385
535
|
useShortcut(SHORTCUTS.calendar.next, () => shiftView(1));
|
|
536
|
+
useShortcut(SHORTCUTS.calendar.list, () => setView("list"));
|
|
386
537
|
useShortcut(SHORTCUTS.calendar.day, () => setView("day"));
|
|
387
538
|
useShortcut(SHORTCUTS.calendar.workWeek, () => setView("workWeek"));
|
|
388
539
|
useShortcut(SHORTCUTS.calendar.week, () => setView("week"));
|
|
389
540
|
useShortcut(SHORTCUTS.calendar.month, () => setView("month"));
|
|
541
|
+
useShortcut(SHORTCUTS.calendar.search, () => {
|
|
542
|
+
searchInputRef.current?.focus();
|
|
543
|
+
searchInputRef.current?.select();
|
|
544
|
+
});
|
|
390
545
|
const newEventHint = useShortcutProps("New event", SHORTCUTS.calendar.create, !!mailboxUid && !!folderUid);
|
|
391
546
|
const previousHint = useShortcutProps("Previous", SHORTCUTS.calendar.previous);
|
|
392
547
|
const todayHint = useShortcutProps("Today", SHORTCUTS.calendar.today);
|
|
@@ -422,8 +577,10 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
422
577
|
}
|
|
423
578
|
|
|
424
579
|
const title =
|
|
425
|
-
view === "
|
|
426
|
-
?
|
|
580
|
+
view === "list"
|
|
581
|
+
? "Upcoming events"
|
|
582
|
+
: view === "month"
|
|
583
|
+
? format(viewDate, "MMMM yyyy")
|
|
427
584
|
: view === "week" || view === "workWeek"
|
|
428
585
|
? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
|
|
429
586
|
: format(viewDate, "EEEE, MMMM d, yyyy");
|
|
@@ -444,7 +601,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
444
601
|
|
|
445
602
|
const sidebarContent = (
|
|
446
603
|
<>
|
|
447
|
-
<MiniDatePicker
|
|
604
|
+
<MiniDatePicker
|
|
605
|
+
selected={viewDate}
|
|
606
|
+
onSelect={(date) => {
|
|
607
|
+
setViewDate(startOfDay(date));
|
|
608
|
+
setJumpNonce((n) => n + 1);
|
|
609
|
+
}}
|
|
610
|
+
/>
|
|
448
611
|
<CalendarListSidebar
|
|
449
612
|
mailboxCalendars={mailboxCalendars}
|
|
450
613
|
checkedFolderUids={checkedFolderUids}
|
|
@@ -490,8 +653,20 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
490
653
|
</button>
|
|
491
654
|
</div>
|
|
492
655
|
<h1 className="hidden md:block text-lg font-bold tracking-tight">{title}</h1>
|
|
493
|
-
<
|
|
494
|
-
{
|
|
656
|
+
<CalendarSearchBox
|
|
657
|
+
value={searchText}
|
|
658
|
+
onChange={setSearchText}
|
|
659
|
+
onSubmit={submitSearch}
|
|
660
|
+
onClear={clearSearch}
|
|
661
|
+
onPrevious={() => stepMatch(-1)}
|
|
662
|
+
onNext={() => stepMatch(1)}
|
|
663
|
+
active={searching}
|
|
664
|
+
count={matches.length}
|
|
665
|
+
position={activeIndex}
|
|
666
|
+
inputRef={searchInputRef}
|
|
667
|
+
/>
|
|
668
|
+
<div className="flex flex-wrap gap-1">
|
|
669
|
+
{CALENDAR_VIEWS.map((v) => (
|
|
495
670
|
<ViewButton key={v} view={v} current={view} onSelect={setView} />
|
|
496
671
|
))}
|
|
497
672
|
</div>
|
|
@@ -509,39 +684,50 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
509
684
|
{loading ? (
|
|
510
685
|
<p className="p-4 text-sm text-text-muted">Loading…</p>
|
|
511
686
|
) : (
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
687
|
+
<ActiveOccurrenceContext.Provider value={searching ? activeKey : null}>
|
|
688
|
+
<DndContext
|
|
689
|
+
sensors={sensors}
|
|
690
|
+
onDragStart={() => setDragging(true)}
|
|
691
|
+
onDragEnd={handleDragEnd}
|
|
692
|
+
onDragCancel={() => setDragging(false)}
|
|
693
|
+
>
|
|
694
|
+
{view === "list" ? (
|
|
695
|
+
<ListView
|
|
696
|
+
occurrences={occurrences}
|
|
697
|
+
folderColors={folderColors}
|
|
698
|
+
onSelectEvent={openEvent}
|
|
699
|
+
focusDate={viewDate}
|
|
700
|
+
jumpNonce={jumpNonce}
|
|
701
|
+
step={listStep}
|
|
702
|
+
/>
|
|
703
|
+
) : view === "month" ? (
|
|
704
|
+
<MonthView
|
|
705
|
+
viewDate={viewDate}
|
|
706
|
+
occurrences={occurrences}
|
|
707
|
+
folderColors={folderColors}
|
|
708
|
+
onSelectDay={handleSelectDay}
|
|
709
|
+
onSelectEvent={openEvent}
|
|
710
|
+
onSelectSlot={(start, end, anchor) => openNewEvent(start, end, undefined, anchor, true)}
|
|
711
|
+
/>
|
|
712
|
+
) : view === "split" ? (
|
|
713
|
+
<SplitDayView
|
|
714
|
+
day={viewDate}
|
|
715
|
+
columns={splitColumns}
|
|
716
|
+
occurrences={occurrences}
|
|
717
|
+
onSelectEvent={openEvent}
|
|
718
|
+
onSelectSlot={(start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor)}
|
|
719
|
+
/>
|
|
720
|
+
) : (
|
|
721
|
+
<TimeGridView
|
|
722
|
+
days={days}
|
|
723
|
+
occurrences={occurrences}
|
|
724
|
+
folderColors={folderColors}
|
|
725
|
+
onSelectEvent={openEvent}
|
|
726
|
+
onSelectSlot={(start, end, anchor) => openNewEvent(start, end, undefined, anchor)}
|
|
727
|
+
/>
|
|
728
|
+
)}
|
|
729
|
+
</DndContext>
|
|
730
|
+
</ActiveOccurrenceContext.Provider>
|
|
545
731
|
)}
|
|
546
732
|
</div>
|
|
547
733
|
</div>
|
|
@@ -568,6 +754,18 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
568
754
|
initialAllDay={modal.initialAllDay}
|
|
569
755
|
anchor={modal.anchor}
|
|
570
756
|
bookingHref={bookingHref}
|
|
757
|
+
matchNav={
|
|
758
|
+
modal.occurrence && sequence.length > 0
|
|
759
|
+
? {
|
|
760
|
+
onPrevious: () => stepMatch(-1),
|
|
761
|
+
onNext: () => stepMatch(1),
|
|
762
|
+
canPrevious: stepIndex(-1) >= 0,
|
|
763
|
+
canNext: stepIndex(1) >= 0,
|
|
764
|
+
noun: searching ? "match" : "event",
|
|
765
|
+
slide,
|
|
766
|
+
}
|
|
767
|
+
: undefined
|
|
768
|
+
}
|
|
571
769
|
onSaved={handleSaved}
|
|
572
770
|
onDeleted={handleDeleted}
|
|
573
771
|
/>
|