refactor: convert Unit, UnitSuspense, UnitTitleSlot and BookmarkButton to TypeScript - #2126
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2121
September 25, 2026 11:41
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2126 +/- ##
==========================================
+ Coverage 94.85% 94.88% +0.02%
==========================================
Files 370 370
Lines 5985 6020 +35
Branches 1466 1471 +5
==========================================
+ Hits 5677 5712 +35
- Misses 295 296 +1
+ Partials 13 12 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/unit-components-typescript
branch
from
September 25, 2026 12:12
950b8c7 to
ef8af39
Compare
brian-smith-tcril
marked this pull request as ready for review
September 25, 2026 12:24
16 tasks
brian-smith-tcril
force-pushed
the
bsmith/unit-components-typescript
branch
5 times, most recently
from
September 28, 2026 07:03
9cb5ae3 to
1e72a9f
Compare
…n to TypeScript Peeled out of the units layer of #2088, which threads a `sequenceId` through all four: with `Props` interfaces in place, that layer changes a type in each instead of adding four `propTypes` entries. - `git mv` to `.tsx`; `propTypes` become a `Props` interface with destructuring defaults (`format = null`, `isBookmarked = false`) in place of the two `defaultProps`, the `UnitButton.tsx` shape in the same subtree. JavaScript imports stay untyped at this layer. - `UnitTitleSlot`'s `unit` shape becomes a local interface with `bookmarkedUpdateState` optional: the sequence endpoint never sends it and the bookmark writer sets it, so a fresh unit never had it despite the `isRequired`. - `getIFrameUrl` encodes its `preview` boolean itself. `Unit` passed `shouldDisplayUnitPreview ? '1' : '0'` into a parameter declared `boolean` and sent as `String(preview)`, because the endpoint compares `request.GET.get('preview', '0') == '1'`; the builder's own case named "url param equals 1" asserted `preview=true`. The builder now sends `preview ? '1' : '0'`, `Unit` passes the boolean, and the `urls.test.ts` expectations say `preview=0` / `preview=1`. `Unit` passes `jumpToId` as `undefined` when the param is absent, where `URLSearchParams.get` gives `null`. No request changes. Part of #1946. Closes #2125. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/unit-components-typescript
branch
from
September 28, 2026 09:07
1e72a9f to
5a07cf4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unit,UnitSuspense,UnitTitleSlotandBookmarkButtonbecome.tsx, with aPropsinterface in place ofpropTypes/defaultProps, theUnitButton.tsxshape already in the same subtree. Peeled out of #2088's units layer, which threads asequenceIdthrough all four: with the interfaces in place that layer changes a type in each instead of adding fourpropTypesentries to components the repo is moving offpropTypes. No behaviour change and no request change. The one value-level edit the conversion forced is ingetIFrameUrl: it now encodes itspreviewboolean itself (preview ? '1' : '0'), whereUnitused to pre-encode the string before calling it — see What changed. Part of the Redux → React Query migration (#1946, Stage 1); sixth layer of stack #2121, on top of #2124. Closes #2125.What changed
git mvrenames to.tsx.propTypes→ aPropsinterface; the twodefaultProps(format: null,isBookmarked: false) → destructuring defaults;onLoadedsimply optional. JavaScript imports (useModel,useExamAccess,useShouldDisplayHonorCode,ContentIFrame,HonorCode,PageLoading) stay untyped at this layer, sounitfromuseModelisanyuntil Read units and sequences from the courseware queries, not useModel #2088's units layer types it asSequenceUnit.UnitSuspensekeeps theimport * as hooksits suite mocks through.renderUnitNavigationis(isAtTop: boolean) => React.ReactNode, whatSequence.jsxpasses andUnitTitleSlotcalls.UnitTitleSlot'sunitshape is inlined inProps, withbookmarkedUpdateStateoptional. ThepropTypessaidisRequired, but the sequence endpoint never sends the field and the bookmark writer sets it, so a fresh unit always rendered without it, with a dev-mode propTypes warning. Inline rather than a named interface (decision 3): nothing else references the shape, and the units layer replaces it withSequenceUnit.getIFrameUrlencodespreviewitself.UnitpassedshouldDisplayUnitPreview ? '1' : '0'into a parameterurls.tsdeclaredbooleanand sent asString(preview); the endpoint comparesrequest.GET.get('preview', '0') == '1'(openedx-platform,lms/djangoapps/courseware/views/views.py:1602), so a boolean sent as-is would have disabled preview, and the builder's own case named preview is true and url param equals 1 assertedpreview=true. OnceUnitwas typed the mismatch surfaced. The builder now sendspreview ? '1' : '0',Unitpasses the boolean, and theurls.test.tsexpectations readpreview=0/preview=1, what that case's name always claimed. Production URLs are unchanged.UnitpassesjumpToIdasundefinedwhen the search param is absent, whereURLSearchParams.getgivesnull(decision 4).Testing
npm run typesandnpm run lintclean; full suite 116 suites, 1177 passed, 0 skipped (two cases added after the first push for the branches the conversion introduced, decision 6). Spot check on tutor dev of the one value-level change: the unit iframesrccarriespreview=0on the learner route andpreview=1under/preview/course/…— see the checklist below.Decisions
Full decision log
Decisions — convert Unit, UnitSuspense, UnitTitleSlot and BookmarkButton to TypeScript (#2125)
Peeled out of #2088's layer A during its code review (2026-09-25), the way
#2111 was peeled out of #2087: layer A adds a
sequenceIdto all four, andthe review caught that as four
propTypesadditions to components the repois otherwise moving off
propTypes. The sixth layer of the running stack#2121, on top of #2124 (#2123), branch
bsmith/unit-components-typescript;layer A (
bsmith/units-query-reads, backed up with its docs onbsmith/units-query-reads-wip) is re-added on top once this lands. Entrieslanded with the code.
Four
git mvrenames,propTypes→ aPropsinterface withdestructuring defaults, the
UnitButton.tsxshape in the same subtree.format = nullandisBookmarked = falsereplace the twodefaultProps;onLoadedis simply optional, as itsdefaultProps: undefinedsaid.Every JavaScript import (
useModel,useExamAccess,useShouldDisplayHonorCode,ContentIFrame,HonorCode,PageLoading)stays untyped at this layer;
unitfromuseModelisanyuntil layerA types it as
SequenceUnit.UnitSuspensekeeps itsimport * as hooksnamespace import, which its suite mocks through.
renderUnitNavigation: (isAtTop: boolean) => React.ReactNode. WhatSequence.jsxpasses ((isAtTop) => <UnitNavigation isAtTop={isAtTop} …/>)and what
UnitTitleSlotcalls (renderUnitNavigation(true)).UnitTitleSlot'sunitshape is inlined inProps, withbookmarkedUpdateStateoptional. ThepropTypessaidisRequired, but the field exists only after the bookmark writer's firstwrite (
useSetBookmarkedsets it; the sequence endpoint never sends it),so a fresh unit always rendered without it, with a dev-mode propTypes
warning. The type says what is true, with no comment (the
?carriesit; the provenance is recorded here). Inline rather than a named
interface, settled in review: nothing else references the shape, and
layer A replaces it with
SequenceUnit, which carries the sameoptionality.
getIFrameUrl'spreviewbecomes a boolean, and the builder encodesit.
UnitpassedshouldDisplayUnitPreview ? '1' : '0'into aparameter
urls.tsdeclaredbooleanand sent asString(preview), soproduction URLs carry
preview=1/preview=0while the builder's owncase named preview is true and url param equals 1 asserted
preview=true. Checked againstopenedx-platform(
lms/djangoapps/courseware/views/views.py:1602):is_preview = request.GET.get('preview', '0') == '1', so a boolean sentas-is would have silently disabled preview. The encoding now lives in the
builder —
searchParams.set('preview', preview ? '1' : '0'), no comment—
Unitpasses the boolean, and theurls.test.tsexpectations readpreview=0/preview=1, what the lastcase's name always claimed. No request changes. A first version typed the
parameter as
'1' | '0', the wire value, and was replaced in review: thecaller was doing the builder's job.
jumpToIdkeeps itsstring | undefinedtype;UnitpassessearchParams.get('jumpToId') ?? undefined,since
URLSearchParams.getreturnsnullfor an absent param and thebuilder's
if (jumpToId)treats both the same. A first version widenedthe builder's type to
string | nullinstead, pushing the caller's sourcequirk into the interface.
Commit:
refactor:, no!. No behaviour change, no request change,no plugin-facing change:
UnitTitleSlot'spluginPropskeep their shape,BookmarkButton's props are the same three, and the.tsxfiles resolveat the same import paths (
courseware/course/bookmark/index.js,'./Unit','./UnitSuspense','../../../../plugin-slots/UnitTitleSlot').Two cases in
Unit/index.test.jsxfor the branches the conversionintroduced. Codecov flagged
Unit/index.tsxon the first push: theformat = nulldestructuring default is a branch wheredefaultPropswas not, and
searchParams.get('jumpToId') ?? undefinedis a branch wherethe bare
getwas not; no case omittedformator passed ajumpToId.omits format from the iframe url when the unit has none and passes
jumpToId from the search params into the iframe url cover them, in their
own describe because the
ContentIFramedescribe'sbeforeEachrendersa unit of its own. The two branches still uncovered — the public view for
an anonymous user and the preview route — predate this layer.
Manual testing
Checklist
Manual testing — convert Unit, UnitSuspense, UnitTitleSlot and BookmarkButton to TypeScript (#2125)
In-browser verification against a live backend (tutor dev).
What changed: four
.jsxfiles became.tsxwithPropsinterfaces inplace of
propTypes/defaultProps;getIFrameUrl'spreviewparameteris typed as the
'1' | '0'string the request has always carried. Nobehaviour change and no request change intended.
The bugs this layer could introduce. (1) A default that changed — the
two
defaultProps(format: null,isBookmarked: false) becamedestructuring defaults; a wrong default would show as a missing
formatquery parameter on the unit iframe or a bookmark button that starts as
"Bookmarked". (2) The preview flag — the only value-level touchpoint: the
iframe URL must still carry
preview=1in preview mode andpreview=0otherwise (decision 4).
Setup
A course with a unit; staff access for preview mode. Devtools Elements (or
Network filtered to
xblock/) to read the unit iframe'ssrc.Checks
src(learner route): containspreview=0,format=<the sequence format>when the sequence has one, andview=student_view.src(staff,/preview/course/…): containspreview=1; the unit renders the draft content.UnitTitleSlot,Unit,UnitSuspenseorBookmarkButton(thebookmarkedUpdateStatewarning on a fresh unit is gone with thepropTypes).Results
Run 2026-09-25 on tutor dev, as a spot check of the one value-level change
rather than a full run.
Run
srccarriespreview=0; under/preview/course/…as staff it carriespreview=1andpreview works.
Not run
format=on the iframesrc: no sequence with a format in the testcourse. The
format = nulldestructuring default replaces adefaultPropsof the same value, and format provided, exam access and token available
in
urls.test.tspins the parameter's presence.isBookmarked = falsereplaces adefaultPropsof the same value, andBookmarkButton.test.jsxcovers both start states.🤖 Generated with Claude Code