You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #1975 (convert course-home tab data to React Query), Phase 3 of the epic #1946. The top course-home tab layer — stacks on the discussion-tab conversion (#2003), which sits above progress (#2001) — reusing the self-wrapping pattern the dates (#1987), outline (#1997), and progress (#2001) tabs set.
Summary
Convert the live tab off Redux to React Query, following the self-wrapping pattern: LiveTab renders <TabWithTimer> itself and owns its data via useCourseHomeMeta + a new useLiveTabData hook. The iframe HTML moves into a LiveTabContent child so its styling useEffect runs when the content mounts (post-load), not when the outer tab mounts (mid-load). The index.jsx route drops its <TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome"> wrapper for a bare <LiveTab />, and fetchLiveTab is deleted.
No transitional bridge needed
Unlike progress (which keeps a scoped meta bridge for the masquerade banner), useLiveTabData needs no meta tag:
The only reader of state.models.live is LiveTab itself (state.models.live[courseId]?.iframe), and it moves to the query.
The shared masquerade banner (useAccessExpirationMasqueradeBanner(courseId, tab) → useModel(tab, courseId)) is passed tab="lti_live" on this route, but fetchLiveTab writes its data under model live, not lti_live — so that useModel('lti_live', …) already resolves empty and the banner never renders on the live tab today. The conversion preserves that exactly (the TabWithTimer activeTabSlug stays "lti_live"), so nothing needs the bridge.
So this tab retires its Redux with no #1999-style holdout — cleaner than progress.
fetchTab retires here
This is the last course-home tab on the shared fetchTab helper. Discussion (#2003, below) already removed the other consumer, so deleting fetchLiveTab here orphans fetchTab entirely — both go together, and the redux.test.jsTest fetchTab block is removed wholesale. Ordering discussion below live is what makes this a clean deletion instead of retargeting that test block from fetchLiveTab to fetchDiscussionTab and back.
Tasks
queryKeys.ts — add liveTab(courseId).
apiHooks.ts — add useLiveTabData(courseId) (getLiveTabIframe, no meta).
LiveTab.jsx — self-wrap TabWithTimer (activeTabSlug="lti_live", courseStatus={{ metadataQuery, tabDataQuery }}); split the iframe body + its getElementById('lti-tab-embed')useEffect into a LiveTabContent child; courseId from useParams.
index.jsx — live route → <LiveTab />; drop the fetchLiveTab import/wiring.
Tests: LiveTab.test.jsx renders <LiveTab /> directly through the bridged query client (mirroring ProgressTab.test.jsx); add a useLiveTabData block to apiHooks.test.tsx (success/iframe + 404→{}); delete the Test fetchTab block from redux.test.js wholesale (its subject fetchTab is deleted here).
Notes / decisions
The LiveTab/LiveTabContent split is required, not cosmetic. Today LiveTab is TabContainer's child, so it mounts only once the tab data is loaded — its mount useEffect finds the injected #lti-tab-embed iframe and applies the sizing classes. In the self-wrapping shape the data-owning component mounts before load, so the effect must live in the gated LiveTabContent child to keep the same timing.
fetchTab is deleted here, not retargeted. Ordering discussion below live (Convert the discussion tab to React Query #2003) means fetchLiveTab is fetchTab's last consumer; removing it orphans the helper, so both go together and the Test fetchTab block is removed wholesale — no back-and-forth churn in redux.test.js.
Note
The plan below was generated by Claude (Claude Code) and reviewed before posting.
Claude Plan — live tab → React Query
Approach
A self-wrapping conversion mirroring dates/outline/progress: the page owns its data via query hooks and renders <TabWithTimer> directly; TabContainer stops wrapping this route. No model-store bridge — the sole state.models.live reader is the tab itself, and the masquerade banner never resolves to this tab's model (route slug lti_live ≠ data model live), so the conversion is behavior-preserving with the query as the only cache.
constLiveTab=()=>{const{ courseId }=useParams();constmetadataQuery=useCourseHomeMeta(courseId);consttabDataQuery=useLiveTabData(courseId);return(<TabWithTimeractiveTabSlug="lti_live"courseId={courseId}courseStatus={{ metadataQuery, tabDataQuery }}><LiveTabContenthtml={tabDataQuery.data?.iframe}/></TabWithTimer>);};// LiveTabContent: the dangerouslySetInnerHTML div (prop named `html` to avoid shadowing// the effect's `iframe` DOM node) + the existing getElementById('lti-tab-embed') sizing// useEffect (renders only once loaded).
Part of #1975 (convert course-home tab data to React Query), Phase 3 of the epic #1946. The top course-home tab layer — stacks on the discussion-tab conversion (#2003), which sits above progress (#2001) — reusing the self-wrapping pattern the dates (#1987), outline (#1997), and progress (#2001) tabs set.
Summary
Convert the live tab off Redux to React Query, following the self-wrapping pattern:
LiveTabrenders<TabWithTimer>itself and owns its data viauseCourseHomeMeta+ a newuseLiveTabDatahook. The iframe HTML moves into aLiveTabContentchild so its stylinguseEffectruns when the content mounts (post-load), not when the outer tab mounts (mid-load). Theindex.jsxroute drops its<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">wrapper for a bare<LiveTab />, andfetchLiveTabis deleted.No transitional bridge needed
Unlike progress (which keeps a scoped
metabridge for the masquerade banner),useLiveTabDataneeds nometatag:state.models.liveisLiveTabitself (state.models.live[courseId]?.iframe), and it moves to the query.useAccessExpirationMasqueradeBanner(courseId, tab)→useModel(tab, courseId)) is passedtab="lti_live"on this route, butfetchLiveTabwrites its data under modellive, notlti_live— so thatuseModel('lti_live', …)already resolves empty and the banner never renders on the live tab today. The conversion preserves that exactly (theTabWithTimer activeTabSlugstays"lti_live"), so nothing needs the bridge.So this tab retires its Redux with no
#1999-style holdout — cleaner than progress.fetchTab retires here
This is the last course-home tab on the shared
fetchTabhelper. Discussion (#2003, below) already removed the other consumer, so deletingfetchLiveTabhere orphansfetchTabentirely — both go together, and theredux.test.jsTest fetchTabblock is removed wholesale. Ordering discussion below live is what makes this a clean deletion instead of retargeting that test block fromfetchLiveTabtofetchDiscussionTaband back.Tasks
queryKeys.ts— addliveTab(courseId).apiHooks.ts— adduseLiveTabData(courseId)(getLiveTabIframe, nometa).LiveTab.jsx— self-wrapTabWithTimer(activeTabSlug="lti_live",courseStatus={{ metadataQuery, tabDataQuery }}); split the iframe body + itsgetElementById('lti-tab-embed')useEffectinto aLiveTabContentchild;courseIdfromuseParams.index.jsx— live route →<LiveTab />; drop thefetchLiveTabimport/wiring.fetchLiveTaband the now-orphaned sharedfetchTabhelper (discussion, its last other consumer, converted in Convert the discussion tab to React Query #2003).LiveTab.test.jsxrenders<LiveTab />directly through the bridged query client (mirroringProgressTab.test.jsx); add auseLiveTabDatablock toapiHooks.test.tsx(success/iframe + 404→{}); delete theTest fetchTabblock fromredux.test.jswholesale (its subjectfetchTabis deleted here).Notes / decisions
LiveTab/LiveTabContentsplit is required, not cosmetic. TodayLiveTabisTabContainer's child, so it mounts only once the tab data is loaded — its mountuseEffectfinds the injected#lti-tab-embediframe and applies the sizing classes. In the self-wrapping shape the data-owning component mounts before load, so the effect must live in the gatedLiveTabContentchild to keep the same timing.fetchTabis deleted here, not retargeted. Ordering discussion below live (Convert the discussion tab to React Query #2003) meansfetchLiveTabisfetchTab's last consumer; removing it orphans the helper, so both go together and theTest fetchTabblock is removed wholesale — no back-and-forth churn inredux.test.js.Note
The plan below was generated by Claude (Claude Code) and reviewed before posting.
Claude Plan — live tab → React Query
Approach
A self-wrapping conversion mirroring dates/outline/progress: the page owns its data via query hooks and renders
<TabWithTimer>directly;TabContainerstops wrapping this route. No model-store bridge — the solestate.models.livereader is the tab itself, and the masquerade banner never resolves to this tab's model (route sluglti_live≠ data modellive), so the conversion is behavior-preserving with the query as the only cache.Data layer
LiveTab.jsx