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 #1946 — Redux → React Query migration (Stage 1). Part of the #1976 courseware decomposition (plan) — Target 1 (courseware player). Stacked on the metadata conversion (#2023).
Goal: convert the sequence fetch to React Query.
Tasks
Convert fetchSequence → query hook feeding the sequences + units models via the extended bridge.
Preserve the 422 sequenceMightBeUnit path (CoursewareContainer relies on the failed request to detect a unit).
Keep the courseware slice sequenceId/sequenceStatus written transitionally.
Verify: sequence + units render from the query; unit-vs-sequence (422) detection still works; useModel('units')/useModel('sequences') readers unaffected.
Full implementation plan (drafted by Claude 🤖)
Context
fetchSequence is the last big courseware fetch thunk still on Redux: it loads one sequence's metadata + its units, mirrors them into the sequences/units model store, and drives the courseware slice's sequenceId/sequenceStatus/sequenceMightBeUnit fields.
The conversion is a faithful continuation of the pattern #2023 established for metadata/outline:
an RQ query hook whose meta.models mirrors the result into the model store via the bridgeToModelStore QueryCache onSuccess (src/data/modelStoreBridge.ts), and
a small transitional status-bridge hook (src/courseware/data/statusBridge.ts) that writes the still-Redux slice status fields, so the many not-yet-converted readers (the container's redirect helpers, Sequence.jsx, breadcrumbs, sequence-navigation, sequence-alerts, the outline sidebar) keep working unchanged.
The one subtlety unique to sequences: CoursewareContainer relies on the sequence fetch failing (a backend 422) to detect that a URL segment is actually a unit, not a sequence (sequenceMightBeUnit) — and on a fast failure, because the redirect that resolves the unit's parent sequence only fires once sequenceStatus === 'failed'.
Key files
src/courseware/data/thunks.js — fetchSequence: the thunk being converted.
src/courseware/data/apiHooks.ts — where useCoursewareMetadata/useCoursewareOutline live; add useSequenceMetadata here.
sequence: (sequenceId, isPreview) => [...coursewareQueryKeys.all, 'sequence', sequenceId, isPreview] as const
(preview flips the preview=1/0 request param, so it must be part of the key).
exportconstuseSequenceMetadata=(sequenceId: string|undefined,isPreview: boolean)=>useQuery({queryKey: coursewareQueryKeys.sequence(sequenceId!,isPreview),queryFn: async()=>{const{ sequence, units }=awaitgetSequenceMetadata(sequenceId,{preview: isPreview ? '1' : '0'});if(sequence.blockType!=='sequential'){// Non-sequential (e.g. 'chapter') block: fail so the bridge doesn't mirror it and// sequenceStatus goes 'failed' (matches the old fetchSequenceFailure branch).thrownewError(`Requested sequence '${sequenceId}' has block type '${sequence.blockType}'; expected block type 'sequential'.`,);}return{ sequence, units };},enabled: !!sequenceId,retry: false,meta: {logStatusAs: {422: 'info'},models: [{modelType: 'sequences',strategy: 'updateModel',source: 'sequence'},{modelType: 'units',strategy: 'updateModels',source: 'units'},],},});
retry: false — the old thunk never retried, and this keeps the expected 422 (unit detection) failing fast instead of RQ's default 3× exponential backoff (~7s) stalling the redirect. The only behavior that would visibly regress without it.
logStatusAs: { 422: 'info' } — the 422 is the expected sequenceMightBeUnit signal; log it at info level like the outline's expected 403. (Behavior note: the old thunk logged nothing on 422; the onError bridge only offers error/info, and a truly silent level would be new machinery — info matches the established 403 treatment.)
meta.models — exactly the two dispatches the thunk did (updateModel sequence + updateModels units), both merges, keyed by id from the normalized payload.
Throwing on non-sequential (instead of logError + failure dispatch) lets the single onError path log it once; message content is preserved.
exportconstuseSequenceStatusBridge=(sequenceId: string|undefined,isPreview: boolean)=>{constdispatch=useDispatch();constquery=useSequenceMetadata(sequenceId,isPreview);useEffect(()=>{if(!sequenceId){return;}// matches the old `if (id)` guardif(query.isPending){dispatch(fetchSequenceRequest({ sequenceId }));return;}if(query.isSuccess){dispatch(fetchSequenceSuccess({ sequenceId }));return;}constsequenceMightBeUnit=query.error?.response?.status===422;dispatch(fetchSequenceFailure({ sequenceId, sequenceMightBeUnit }));},[sequenceId,query,dispatch]);};
The if (!sequenceId) return; guard is essential: a disabled query reports isPending === true, so without it the course-root case (no sequenceId) would wrongly dispatch fetchSequenceRequest. Uses getResponseStatus (src/data/http-error.ts) for the status read, consistent with the rest of the RQ code.
4. CoursewareContainer.tsx
Remove fetchSequence from the ./data import and delete the checkFetchSequence guard and its call.
Add useSequenceStatusBridge(routeSequenceId, isPreview) next to useCourseStatusBridge(routeCourseId).
Everything else is untouched: the redirect helpers and selectors still read sequenceStatus/sequenceMightBeUnit/sequences/units from the slice/model store, which the bridge keeps writing. checkSaveSequencePosition still dispatches the saveSequencePosition thunk (that's Convert saveIntegritySignature + saveSequencePosition to React Query mutations #2015, out of scope).
Cleanup (full)
5. Delete the fetchSequence thunk
Remove fetchSequence from thunks.js and from index.js.
Keep the fetchSequenceRequest/Success/Failure slice actions — now dispatched by the bridge; Sequence.test.jsx also dispatches fetchSequenceFailure directly.
6. setupTest.js — migrate the sequence seed
Extend seedCoursewareModels(store, courseId) to also seed sequences/units directly (fetch each sequence block's metadata via getSequenceMetadata against the existing axios mocks and dispatch updateModel/updateModels, the same shape the bridge produces), plus set the slice status if any existing test depends on sequenceStatus === 'loaded' without rendering the container.
Remove the executeThunk(fetchSequence(block.id)) seed loop and the now-unused fetchSequence import. Preserve the excludeFetchSequence option's effect.
7. redux.test.js
Remove the Test fetchSequence describe block (networkError / non-sequential / normalize+mirror).
Reseat the "Thunks that require fetched sequences" beforeEach (which currently executeThunk(fetchSequence(...))) onto the new direct seed so checkBlockCompletion / saveSequencePosition tests still have populated sequence/unit models.
Tests (new coverage for the converted path)
Add a test that renders useSequenceMetadata + useSequenceStatusBridge through createTestQueryClient(store) (the bridged client) — extending src/courseware/data/apiHooks.test.tsx or a sibling statusBridge-oriented test — migrating the three old thunk cases into hook/bridge terms:
success: sequence mirrored into models.sequences (merge) and units into models.units; slice → sequenceStatus: 'loaded', sequenceId set.
422: query errors, no models mirrored; slice → sequenceStatus: 'failed', sequenceMightBeUnit: true; and the query fn is called once (asserts retry: false).
non-sequential blockType: query errors (thrown), slice → sequenceStatus: 'failed', sequenceMightBeUnit: false, and logError called once.
CoursewareContainer.test.jsx (70 tests, incl. the 422 unit-detection case and the marker/section/unit redirect matrix) should stay green untouched — it drives the container by URL + axios mocks through the bridged query client, so the behavior it asserts is preserved. Adjust only if the de-thunking forces a mechanical change.
Verification
npm run test -- src/courseware/CoursewareContainer.test.jsx src/courseware/data — container matrix + new hook/bridge tests + reseated redux.test green.
npm run types and npm run lint.
Manual smoke (tutor local, DemoX): unit ↔ sequence nav, the bare-unit and section+unit redirect chains (422 unit detection resolves promptly, no ~7s stall), first/last markers, saved unit position, and no console error for the expected 422 (should be an info page action).
Part of #1946 — Redux → React Query migration (Stage 1). Part of the #1976 courseware decomposition (plan) — Target 1 (courseware player). Stacked on the metadata conversion (#2023).
Goal: convert the sequence fetch to React Query.
Tasks
fetchSequence→ query hook feeding thesequences+unitsmodels via the extended bridge.sequenceMightBeUnitpath (CoursewareContainer relies on the failed request to detect a unit).coursewareslicesequenceId/sequenceStatuswritten transitionally.Verify: sequence + units render from the query; unit-vs-sequence (422) detection still works;
useModel('units')/useModel('sequences')readers unaffected.Full implementation plan (drafted by Claude 🤖)
Context
fetchSequenceis the last big courseware fetch thunk still on Redux: it loads one sequence's metadata + its units, mirrors them into thesequences/unitsmodel store, and drives thecoursewareslice'ssequenceId/sequenceStatus/sequenceMightBeUnitfields.The conversion is a faithful continuation of the pattern #2023 established for metadata/outline:
meta.modelsmirrors the result into the model store via thebridgeToModelStoreQueryCacheonSuccess(src/data/modelStoreBridge.ts), andsrc/courseware/data/statusBridge.ts) that writes the still-Redux slice status fields, so the many not-yet-converted readers (the container's redirect helpers,Sequence.jsx, breadcrumbs, sequence-navigation, sequence-alerts, the outline sidebar) keep working unchanged.The one subtlety unique to sequences:
CoursewareContainerrelies on the sequence fetch failing (a backend 422) to detect that a URL segment is actually a unit, not a sequence (sequenceMightBeUnit) — and on a fast failure, because the redirect that resolves the unit's parent sequence only fires oncesequenceStatus === 'failed'.Key files
src/courseware/data/thunks.js—fetchSequence: the thunk being converted.src/courseware/data/apiHooks.ts— whereuseCoursewareMetadata/useCoursewareOutlinelive; adduseSequenceMetadatahere.src/courseware/data/statusBridge.ts—useCourseStatusBridgepattern; adduseSequenceStatusBridgehere.src/courseware/data/queryKeys.ts—coursewareQueryKeys; add asequencekey.src/courseware/data/api.js—getSequenceMetadata(sequenceId, params)(returns{ sequence, units }vianormalizeSequenceMetadata).src/courseware/data/slice.js—fetchSequenceRequest/Success/Failurereducers (kept; now bridge-driven).src/courseware/CoursewareContainer.tsx— the sole production caller (thecheckFetchSequenceguard).src/data/modelStoreBridge.ts— supportsupdateModel+updateModelsstrategies withsource.src/queryClient.ts—onErrormapsmeta.logStatusAs[status]→logInfo/logError.src/setupTest.js—seedCoursewareModels(extend) and theexecuteThunk(fetchSequence(...))seed loop (remove).src/courseware/data/redux.test.js— thefetchSequencedescribe block + the "Thunks that require fetched sequences"beforeEachto reseat.src/courseware/data/index.js—fetchSequencere-export (remove).The conversion
1.
queryKeys.ts— add a sequence keysequence: (sequenceId, isPreview) => [...coursewareQueryKeys.all, 'sequence', sequenceId, isPreview] as const(preview flips the
preview=1/0request param, so it must be part of the key).2.
apiHooks.ts—useSequenceMetadata(sequenceId, isPreview)retry: false— the old thunk never retried, and this keeps the expected 422 (unit detection) failing fast instead of RQ's default 3× exponential backoff (~7s) stalling the redirect. The only behavior that would visibly regress without it.logStatusAs: { 422: 'info' }— the 422 is the expectedsequenceMightBeUnitsignal; log it at info level like the outline's expected 403. (Behavior note: the old thunk logged nothing on 422; theonErrorbridge only offerserror/info, and a truly silent level would be new machinery — info matches the established 403 treatment.)meta.models— exactly the two dispatches the thunk did (updateModelsequence +updateModelsunits), both merges, keyed byidfrom the normalized payload.logError+ failure dispatch) lets the singleonErrorpath log it once; message content is preserved.3.
statusBridge.ts—useSequenceStatusBridge(sequenceId, isPreview)Mirror of
useCourseStatusBridge:The
if (!sequenceId) return;guard is essential: a disabled query reportsisPending === true, so without it the course-root case (no sequenceId) would wrongly dispatchfetchSequenceRequest. UsesgetResponseStatus(src/data/http-error.ts) for the status read, consistent with the rest of the RQ code.4.
CoursewareContainer.tsxfetchSequencefrom the./dataimport and delete thecheckFetchSequenceguard and its call.useSequenceStatusBridge(routeSequenceId, isPreview)next touseCourseStatusBridge(routeCourseId).sequenceStatus/sequenceMightBeUnit/sequences/unitsfrom the slice/model store, which the bridge keeps writing.checkSaveSequencePositionstill dispatches thesaveSequencePositionthunk (that's Convert saveIntegritySignature + saveSequencePosition to React Query mutations #2015, out of scope).Cleanup (full)
5. Delete the
fetchSequencethunkfetchSequencefromthunks.jsand fromindex.js.fetchSequenceRequest/Success/Failureslice actions — now dispatched by the bridge;Sequence.test.jsxalso dispatchesfetchSequenceFailuredirectly.6.
setupTest.js— migrate the sequence seedseedCoursewareModels(store, courseId)to also seed sequences/units directly (fetch each sequence block's metadata viagetSequenceMetadataagainst the existing axios mocks and dispatchupdateModel/updateModels, the same shape the bridge produces), plus set the slice status if any existing test depends onsequenceStatus === 'loaded'without rendering the container.executeThunk(fetchSequence(block.id))seed loop and the now-unusedfetchSequenceimport. Preserve theexcludeFetchSequenceoption's effect.7.
redux.test.jsTest fetchSequencedescribe block (networkError / non-sequential / normalize+mirror).beforeEach(which currentlyexecuteThunk(fetchSequence(...))) onto the new direct seed socheckBlockCompletion/saveSequencePositiontests still have populated sequence/unit models.Tests (new coverage for the converted path)
Add a test that renders
useSequenceMetadata+useSequenceStatusBridgethroughcreateTestQueryClient(store)(the bridged client) — extendingsrc/courseware/data/apiHooks.test.tsxor a siblingstatusBridge-oriented test — migrating the three old thunk cases into hook/bridge terms:models.sequences(merge) and units intomodels.units; slice →sequenceStatus: 'loaded',sequenceIdset.sequenceStatus: 'failed',sequenceMightBeUnit: true; and the query fn is called once (assertsretry: false).sequenceStatus: 'failed',sequenceMightBeUnit: false, andlogErrorcalled once.sequenceStatus: 'failed',logErrorcalled.CoursewareContainer.test.jsx(70 tests, incl. the 422 unit-detection case and the marker/section/unit redirect matrix) should stay green untouched — it drives the container by URL + axios mocks through the bridged query client, so the behavior it asserts is preserved. Adjust only if the de-thunking forces a mechanical change.Verification
npm run test -- src/courseware/CoursewareContainer.test.jsx src/courseware/data— container matrix + new hook/bridge tests + reseated redux.test green.npm run typesandnpm run lint.first/lastmarkers, saved unit position, and no console error for the expected 422 (should be an info page action).