From 650da6c7302063891e047ea06db848abba008b3d Mon Sep 17 00:00:00 2001 From: abccodes Date: Fri, 19 Jun 2026 18:00:33 -0700 Subject: [PATCH] [aidan] feat/schedule-list: lazy-load list view via scroll sentinel --- .../app/pages/Workflows/ScheduleCalendar.tsx | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx b/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx index dc09a23d..29239a88 100644 --- a/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx +++ b/frontend/src/app/pages/Workflows/ScheduleCalendar.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Tooltip from '@mui/material/Tooltip'; @@ -25,6 +25,9 @@ interface Props { // starting hour. The scroll container caps the visible window. const HOURS_24 = Array.from({ length: 24 }, (_, i) => i); +// List view pages in this many occurrences at a time as you scroll. +const LIST_PAGE = 60; + interface CalendarEvent { workflow_id: string; fire_at: string; @@ -160,6 +163,24 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD return { map, start: rangeStart, end: rangeEndExclusive, key: calendarFetchKey }; }, [calendarEvents, calendarFetchKey, calendarRequestKey, workflows, rangeStart, rangeEndExclusive]); + // List view can fan out to ~1300 rows for a dense schedule (every 15 min over + // 14 days), so render a page at a time and grow as a bottom sentinel scrolls + // into view. root:null intersects against the viewport through whichever + // ancestor actually scrolls, so this works in both the popover and the hub. + const [listVisible, setListVisible] = useState(LIST_PAGE); + const listSentinelRef = useRef(null); + useEffect(() => { setListVisible(LIST_PAGE); }, [dayKey, view, eventsByDay.key]); + useEffect(() => { + if (view !== 'List') return; + const el = listSentinelRef.current; + if (!el) return; + const obs = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) setListVisible((n) => n + LIST_PAGE); + }, { rootMargin: '300px' }); + obs.observe(el); + return () => obs.disconnect(); + }, [view, listVisible, eventsByDay.key, dayKey]); + const SLOT_H = compact ? 32 : 44; const ROW_LABEL = compact ? '0.7rem' : '0.74rem'; const DAY_NUM = compact ? '0.95rem' : '1.15rem'; @@ -338,12 +359,23 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD if (arr.length || isToday) upcoming.push({ date: day, events: arr, isToday }); } const accent = c.accent.primary; + // Spend the page budget across day groups in order, truncating the group it + // runs out on; the leftover events appear when the sentinel grows the budget. + const totalEvents = upcoming.reduce((n, u) => n + u.events.length, 0); + let budget = listVisible; + const windowedDays: typeof upcoming = []; + for (const day of upcoming) { + windowedDays.push({ ...day, events: day.events.slice(0, Math.max(0, budget)) }); + budget -= day.events.length; + if (budget <= 0) break; + } + const hasMore = listVisible < totalEvents; return ( {upcoming.length === 0 && ( No scheduled )} - {upcoming.map(({ date, events, isToday }, rowIdx) => ( + {windowedDays.map(({ date, events, isToday }, rowIdx) => ( ))} + {hasMore && } {ctxMenuEl} );