Bound continuous calendar event loading
This commit is contained in:
@@ -66,6 +66,7 @@ import {
|
||||
agendaDateLabel,
|
||||
agendaGroupsForDays,
|
||||
calendarEventColorStyle,
|
||||
continuousEventWindow,
|
||||
daysBetweenCount,
|
||||
daysForMode,
|
||||
dayKey,
|
||||
@@ -92,6 +93,7 @@ import {
|
||||
} from "./calendarViewModel";
|
||||
|
||||
type EventDialogState = {kind: "create";} | {kind: "edit";event: CalendarEvent;};
|
||||
const INITIAL_CONTINUOUS_WEEKS = { before: 4, after: 6 };
|
||||
|
||||
const modeOptions: {id: CalendarMode;label: string;}[] = [
|
||||
{ id: "continuous", label: "i18n:govoplan-calendar.continuous.04f2ccda" },
|
||||
@@ -109,9 +111,10 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const eventsRef = useRef<CalendarEvent[]>([]);
|
||||
const eventDeltaRef = useRef<{key: string;watermark: string | null;}>({ key: "", watermark: null });
|
||||
const eventRequestRef = useRef(0);
|
||||
const [mode, setMode] = useState<CalendarMode>(() => loadCalendarMode());
|
||||
const [focusDate, setFocusDate] = useState(() => startOfDay(new Date()));
|
||||
const [continuousWeeks, setContinuousWeeks] = useState({ before: 8, after: 12 });
|
||||
const [continuousWeeks, setContinuousWeeks] = useState(INITIAL_CONTINUOUS_WEEKS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -137,6 +140,10 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
const targetCalendarId = eventCalendarId || calendars[0]?.id || "";
|
||||
const visibleRange = useMemo(() => rangeForMode(mode, focusDate, continuousWeeks), [continuousWeeks, focusDate, mode]);
|
||||
const days = useMemo(() => daysForMode(mode, focusDate, continuousWeeks), [continuousWeeks, focusDate, mode]);
|
||||
const eventWindow = useMemo(
|
||||
() => mode === "continuous" ? continuousEventWindow(days, continuousViewport) : visibleRange,
|
||||
[continuousViewport.height, continuousViewport.scrollTop, days, mode, visibleRange]
|
||||
);
|
||||
const eventsByDay = useMemo(() => groupEventsByDay(visibleEvents), [visibleEvents]);
|
||||
const agendaGroups = useMemo(() => agendaGroupsForDays(days, eventsByDay), [days, eventsByDay]);
|
||||
const canWrite = hasScope(auth, "calendar:event:write");
|
||||
@@ -158,9 +165,10 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
if (calendars.length) {
|
||||
void loadEvents();
|
||||
} else {
|
||||
eventsRef.current = [];
|
||||
setEvents([]);
|
||||
}
|
||||
}, [calendars.length, visibleRange.start.getTime(), visibleRange.end.getTime()]);
|
||||
}, [calendars.length, eventWindow.start.getTime(), eventWindow.end.getTime()]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "continuous") return undefined;
|
||||
@@ -209,10 +217,11 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
const requestId = ++eventRequestRef.current;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const start = visibleRange.start.toISOString();
|
||||
const end = visibleRange.end.toISOString();
|
||||
const start = eventWindow.start.toISOString();
|
||||
const end = eventWindow.end.toISOString();
|
||||
const deltaKey = `${start}|${end}`;
|
||||
try {
|
||||
let since = eventDeltaRef.current.key === deltaKey ? eventDeltaRef.current.watermark : null;
|
||||
@@ -227,16 +236,18 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
nextEvents = mergeCalendarEventDelta(nextEvents, response);
|
||||
since = response.watermark || null;
|
||||
} while (response.has_more && response.watermark);
|
||||
if (requestId !== eventRequestRef.current) return;
|
||||
eventsRef.current = nextEvents;
|
||||
eventDeltaRef.current = { key: deltaKey, watermark: since };
|
||||
setEvents(nextEvents);
|
||||
} catch (err) {
|
||||
if (requestId !== eventRequestRef.current) return;
|
||||
setError(errorText(err));
|
||||
eventsRef.current = [];
|
||||
eventDeltaRef.current = { key: "", watermark: null };
|
||||
setEvents([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (requestId === eventRequestRef.current) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +352,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
if (calendars.length > 1 || payload.event_action === "move") {
|
||||
await loadEvents();
|
||||
} else {
|
||||
eventsRef.current = [];
|
||||
setEvents([]);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -403,6 +415,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
return;
|
||||
}
|
||||
pendingContinuousScrollDayRef.current = today;
|
||||
setContinuousWeeks(INITIAL_CONTINUOUS_WEEKS);
|
||||
setFocusDate(today);
|
||||
window.requestAnimationFrame(() => {
|
||||
if (pendingContinuousScrollDayRef.current) {
|
||||
@@ -704,7 +717,13 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
|
||||
size="equal"
|
||||
ariaLabel="i18n:govoplan-calendar.calendar_views.9e6b9c2b"
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
onChange={(nextMode) => {
|
||||
if (nextMode === "continuous") {
|
||||
setContinuousWeeks(INITIAL_CONTINUOUS_WEEKS);
|
||||
setContinuousViewport({ scrollTop: 0, height: 0 });
|
||||
}
|
||||
setMode(nextMode);
|
||||
}}
|
||||
options={modeOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -123,6 +123,45 @@ export function daysForMode(
|
||||
return daysBetween(range.start, range.end);
|
||||
}
|
||||
|
||||
export function continuousEventWindow(
|
||||
days: Date[],
|
||||
viewport: ContinuousViewport,
|
||||
options: {overscanWeeks?: number;minimumWeeks?: number;stepWeeks?: number;} = {},
|
||||
): Range {
|
||||
if (days.length === 0) {
|
||||
const now = startOfDay(new Date());
|
||||
return { start: now, end: addDays(now, 7) };
|
||||
}
|
||||
const totalWeeks = Math.max(1, Math.ceil(days.length / 7));
|
||||
const overscanWeeks = Math.max(0, Math.floor(options.overscanWeeks ?? 2));
|
||||
const minimumWeeks = Math.max(1, Math.floor(options.minimumWeeks ?? 10));
|
||||
const stepWeeks = Math.max(1, Math.floor(options.stepWeeks ?? 4));
|
||||
const firstVisibleWeek = Math.max(
|
||||
0,
|
||||
Math.min(totalWeeks - 1, Math.floor(viewport.scrollTop / CONTINUOUS_WEEK_ROW_HEIGHT)),
|
||||
);
|
||||
const visibleWeeks = Math.max(
|
||||
1,
|
||||
Math.ceil(Math.max(viewport.height, CONTINUOUS_WEEK_ROW_HEIGHT) / CONTINUOUS_WEEK_ROW_HEIGHT),
|
||||
);
|
||||
const desiredWeeks = Math.min(
|
||||
totalWeeks,
|
||||
Math.max(minimumWeeks, visibleWeeks + overscanWeeks * 2),
|
||||
);
|
||||
let startWeek = Math.max(
|
||||
0,
|
||||
Math.floor(Math.max(0, firstVisibleWeek - overscanWeeks) / stepWeeks) * stepWeeks,
|
||||
);
|
||||
if (startWeek + desiredWeeks > totalWeeks) {
|
||||
startWeek = Math.max(0, totalWeeks - desiredWeeks);
|
||||
}
|
||||
const endWeek = Math.min(totalWeeks, startWeek + desiredWeeks);
|
||||
return {
|
||||
start: days[startWeek * 7],
|
||||
end: addDays(days[Math.min(days.length - 1, endWeek * 7 - 1)], 1),
|
||||
};
|
||||
}
|
||||
|
||||
export function groupEventsByDay(
|
||||
events: CalendarEvent[],
|
||||
): Map<string, CalendarEvent[]> {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
continuousEventWindow,
|
||||
continuousVirtualWindow,
|
||||
daysForMode,
|
||||
groupEventsByDay,
|
||||
layoutTimedEventsForDay,
|
||||
moveEventToDay,
|
||||
@@ -49,6 +51,23 @@ assert(
|
||||
"virtualization should preserve offscreen geometry",
|
||||
);
|
||||
|
||||
const continuousDays = daysForMode("continuous", focus, {
|
||||
before: 50,
|
||||
after: 50,
|
||||
});
|
||||
const eventWindow = continuousEventWindow(
|
||||
continuousDays,
|
||||
{ scrollTop: 40 * 92, height: 6 * 92 },
|
||||
);
|
||||
assert(
|
||||
(eventWindow.end.getTime() - eventWindow.start.getTime()) / 86_400_000 === 10 * 7,
|
||||
"continuous event loading should stay bounded to a ten-week window",
|
||||
);
|
||||
assert(
|
||||
eventWindow.start > continuousDays[0],
|
||||
"continuous event loading should follow the virtualized viewport",
|
||||
);
|
||||
|
||||
const day = new Date(2026, 6, 7);
|
||||
const parallel = [0, 1, 2, 3].map((index) =>
|
||||
event(
|
||||
|
||||
Reference in New Issue
Block a user