Repository navigation
refactor: type the course-home API module on the functions that produce its results - #2150
Merged
Merged
Conversation
brian-smith-tcril
added this pull request to stack #2141
October 1, 2026 17:59
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2150 +/- ##
==========================================
+ Coverage 95.25% 95.50% +0.24%
==========================================
Files 372 373 +1
Lines 6053 6067 +14
Branches 1499 1496 -3
==========================================
+ Hits 5766 5794 +28
+ Misses 275 262 -13
+ Partials 12 11 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/course-home-api-types
branch
from
October 2, 2026 02:44
068ac50 to
62ff8bd
Compare
brian-smith-tcril
marked this pull request as ready for review
October 2, 2026 03:27
brian-smith-tcril
force-pushed
the
bsmith/course-home-api-types
branch
4 times, most recently
from
October 2, 2026 05:05
75d58eb to
c1d2ce6
Compare
brian-smith-tcril
force-pushed
the
bsmith/course-home-api-types
branch
from
October 2, 2026 05:34
c1d2ce6 to
a3ce7b5
Compare
…ce its results `course-home/data/api.js` becomes TypeScript. The outline's normalizers move to `courseHomeOutline.ts` with complete output types, `CourseHomeOutline` and its blocks, sections and sequences, and input types naming the fields they read. Each passthrough declares its result type beside its function, naming the fields this repo's readers use and leaving the rest reachable as `unknown`: `CourseHomeMeta` moves here from the hooks and gains the fields its JavaScript readers use, `CourseHomeDates`, `CourseHomeProgress`, `ProctoringInfo` (moved in from the outline tab's utils), `CourseLiveIframe`, `ExamAttempt`. The six `catch` blocks read the status through `getResponseStatus`. The hooks import their result types instead of declaring them. No request or result changes. Closes #2130 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/course-home-api-types
branch
from
October 2, 2026 05:47
a3ce7b5 to
4dc6600
Compare
This was referenced Oct 2, 2026
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
course-home/data/api.jsbecomes TypeScript, and every result shape is declared on the function that produces it, socourse-home/data/apiHooks.tsimports its result types instead of declaring them — the course-home half of the sweep #2129 did for the courseware normalizers. The outline's two normalizers move to a newcourseHomeOutline.tswith complete output types (CourseHomeOutlineand its blocks, sections and sequences) and input types naming the fields they read; the fifteen passthroughs and posts each get a result type beside them that names the fields this repo's readers use, JavaScript readers included, and leaves the rest reachable asunknown(CourseHomeMetamoves here and gains seven such fields;CourseHomeDates,CourseHomeProgress,ProctoringInfo,CourseLiveIframe,ExamAttemptare new). The sixcatchblocks read the status throughgetResponseStatus. No request or result changes: both normalizer bodies are byte-identical to the JavaScript apart from annotations. Part of the Redux → React Query migration (#1946, Stage 1); stacked above #2149, which fixed the one outline read the complete type exposed as broken. Closes #2130.What changed
api.ts(decisions 1–3, 6, 8): parameter types on all seventeen functions; result types beside each;getResponseStatusin the sixcatchblocks;getTimeOffsetMillistakes numbers.courseHomeOutline.ts(decisions 1–2, 7, 10):normalizeCourseHomeOutline(courseId, data, timeOffsetMillis)andnormalizeCourseHomeOutlineBlocks, withCourseHomeOutline,CourseHomeOutlineBlocks/Course/Section/Sequenceand the nestedCourseHome…objects readers destructure.getOutlineTabDatareturnsCourseHomeOutline | Record<string, never>for its 403 branch. Two reads the endpoint never answers (navigation_disabled, top-levelhas_scheduled_content) are typed optional with a note and left as they were.apiHooks.ts(decision 9): importsCourseHomeMeta,CallToActionResponseandPostEventDatafrom./api;useOutlineTabDataanduseExamAttemptsDatainfer from the fetchers;useCourseHomeMetakeeps itsRequestErrorgeneric.outline-tab/utils.tsimportsProctoringInfo;SequenceLinkandSequenceTitleimportCourseHomeOutlineSequence;SequenceDueDate'sdueanddescriptionprops becomestring | null, which the serializer sends;SidebarContext,TabPageandCelebrationModal.testimportCourseHomeMetafrom./api;setupTest.jsandCourseExit.test.jsxdrop therootSlugargument Take tab identity out of the course-home metadata query #2084 removed.apiHooks.test.tsx(decision 13): twodata.reads becomedata?.under the typed result.courseHomeOutline.test.tsfor the normalizer's twologInfobranches, and twoapi.test.jscases for the proctoring fetch's LMS URL andsearchCourseContentFromAPI, all uncovered in the JavaScript too but patch lines once the file moves.Testing
npm run typesandnpm run lintclean; full suite 120 suites, 1190 passed, 0 skipped;api.tsandcourseHomeOutline.tsat 100% lines.git grep "^export interface\|^interface" src/course-home/data/apiHooks.tsreturns onlyQueryOptions. The twoOutlineTab.test.jsxcases that reply a bare 404 to the proctoring request now exercise the{}branch instead of aTypeErrorin the query function (decision 6); their assertions are unchanged. Smoke test on tutor local, 2026-10-01: the outline, courseware, progress and dates tabs load; the live tab and the proctoring 404 not run (the latter is pinned by the hook suite and hard to produce on tutor local).Decisions
Full decision log
Decisions — type the course-home API module on the functions that produce its results (#2130)
Sibling of #2129, from its plan's sweep of everything that reshapes an API
response. Layer of the running stack #2141 above #2149 (#2148), branch
bsmith/course-home-api-types. Entries 1–5 were settled with the user inthe plan review (2026-10-01,
plan-2130.md); the rest landed with the code.One new file for the determined normalizer;
api.tsfor the rest.The inline normalization at the end of
getOutlineTabDataandnormalizeOutlineBlocksmove tocourse-home/data/courseHomeOutline.tsas
normalizeCourseHomeOutlineandnormalizeCourseHomeOutlineBlocks,output types first, then the input types, then the functions, with a
header naming the endpoint — Type the courseware normalizers on the functions that produce them (convert courseware/data/utils.js to TypeScript) #2129's shape (
data/<typeName>.tsexportsnormalize<TypeName>), and the splitcourseware/data/api.jshasbetween
getCourseMetadataandnormalizeCoursewareMeta.api.tskeepsthe fetchers and declares each passthrough's result type beside its
function, as
masquerade-widget/data/api.tsdoes. One difference fromnormalizeCoursewareMeta: the outline normalizer takes(courseId, data, timeOffsetMillis)rather than the whole http response,so that
getTimeOffsetMillisstays inapi.tsfor its two importers(
coursewareMeta.ts,api.test.js) withoutcourseHomeOutline.tsimporting a value back from the module that imports it. The one import
it does take from
api.ts,CourseHomeDateBlock, isimport type,erased at compile time. Both normalizer bodies are byte-identical to the
JavaScript apart from the type annotations. The issue body said "
api.ts,types beside functions"; it predates Type the courseware normalizers on the functions that produce them (convert courseware/data/utils.js to TypeScript) #2129's review, and the user took
the new file once it was clear the only code change is the extraction.
CourseHome…throughout. The endpoint, as exposed, iscourse_home/outline(GET api/course_home/v1/outline/{course_key}; "Tab" exists onlyin the platform's class names, route name and docstring), and the repo
already names this API's objects by module:
course_home/course_metadata→CourseHomeMeta. So:CourseHomeOutline,CourseHomeOutlineBlocks,CourseHomeOutlineCourse,CourseHomeOutlineSection,CourseHomeOutlineSequence; the nestedobjects
CourseHomeAccessExpiration,CourseHomeCertData,CourseHomeCourseGoals,CourseHomeCourseTool,CourseHomeDatesBannerInfo,CourseHomeDatesWidget,CourseHomeEnrollAlert,CourseHomeResumeCourse; and, by the same rule,the passthroughs
CourseHomeDates,CourseHomeProgress(withCourseHomeProgressSectionScore,CourseHomeProgressSubsectionScore,CourseHomeProgressAssignmentTypeGradeSummary) and the sharedCourseHomeDateBlock(one entry ofcourse_date_blocks, which both thedates tab and the outline's dates widget carry). The user chose one prefix
for everything: with as many versions of "course", "outline" and
"sequence" as this application has, a
CourseOutlineSequenceunder aCourseHomeOutlineparent is one more thing to confuse, so Type the courseware normalizers on the functions that produce them (convert courseware/data/utils.js to TypeScript) #2129'ssettled
CourseOutlineSequenceis extended by its own rule rather thankept. Not
CourseOutline: the platform's name for the learning-sequencesobject (
CourseOutlineView), whichMinimalCourseOutlinemodels.Outside
course_home:CourseLiveIframe(/api/course_live/iframe/),ExamAttempt(proctored_exam/attempt/exam/attempt),ProctoringInfo(kept; moved in fromoutline-tab/utils.ts, which nowimports it),
CoursewareSearchResponse,UnsubscribeFromCourseGoalResponse,CallToActionResponse(moved from the hooks, beside the two posts thatreturn it) and
PostEventData(the hooks' localPostData, now besideexecutePostFromPostEvent)."Name what we read." A field goes on a passthrough's type because
something in this repo reads it, JavaScript or TypeScript; nothing goes on
because the serializer declares it; the serializer is consulted only for
the nullability of a field we read. The hooks' comment had said
"TypeScript readers", the issue body "this repo's readers"; the user chose
the latter. The depth rule follows: a nested field is named where a reader
destructures or accesses it, and every nested object keeps an index
signature for the rest. Surveyed at implementation:
CourseHomeMetagainscanViewCertificate,courseModes,isEnrolled,isMasquerading(computed here),isSelfPaced,isStaff,username(
userTimezonecame in with The outline tab's sequence due dates ignore the learner's timezone preference #2148).courseId,studioAccessandisNewDiscussionSidebarViewEnabledare declared byCourseHomeMetadataSerializerand read by nothing.CourseHomeDates:courseDateBlocks,datesBannerInfo(four fields),hasEnded;learnerIsFullAccessanduserTimezoneare unread.CourseHomeDateBlock: the nine fieldsTimeline.jsx,Day.jsx,DateSummary.jsxand the alerts read;linkTextandfirstComponentBlockIdare unread.CourseHomeProgress: the nineteen top-level fields the twenty-threeuseProgressData()sites read, plus the two computed flags; nestedcompletionSummary,courseGrade,gradingPolicy.gradeRange,sectionScores[].subsections[](eleven fields),assignmentTypeGradeSummary[](eight),
creditCourseRequirements.requirements[](five),verificationData,certificateData,verifiedMode.upgradeUrl.CourseHomeOutline's nested objects:accessExpiration(four fields,the whole
get_access_expiration_datadict),certData(three),courseGoals,courseTools[],datesBannerInfo,datesWidget(
courseDateBlocks,datesTabLink; itsuserTimezoneis unread sincefeat: add page banner to masquerade (AA-877) #606),
enrollAlert,resumeCourse.offerandverifiedModehave nooutline reader and stay
unknown, as Type the courseware normalizers on the functions that produce them (convert courseware/data/utils.js to TypeScript) #2129 entry 4 left the coursewaremetadata's sub-objects.
ProctoringInfo: the five snake_case fieldsProctoringInfoPanel.jsxand
outline-tab/utils.tsread.CourseLiveIframe:iframe, optionalbecause the view answers 200 with only a
developer_messagewhen thelive tab is disabled.
ExamAttempt:exam.The
userTimezoneread inSequenceDueDate.tsxwas fixed below thislayer (The outline tab's sequence due dates ignore the learner's timezone preference #2148, PR fix: format the outline tab's sequence due dates in the learner's timezone preference #2149), not deleted. The complete outline type turned
it into a compile error, and the history (in The outline tab's sequence due dates ignore the learner's timezone preference #2148) showed feat: add page banner to masquerade (AA-877) #606 broke it
in 2021. With the fix in, this layer drops the
userTimezone?: stringplaceholder refactor: read the dates and outline tab data from their queries #2093 had put on the hook's type without touching the
component.
The stale
rootSlugarguments go.setupTest.jsandCourseExit.test.jsxstill calledgetCourseHomeCourseMetadata(courseId, 'courseware')after Take tab identity out of the course-home metadata query #2084 removed the parameter; both are test files, the only callers
of a function this layer gives a one-parameter signature.
All six
catchblocks read the status throughgetResponseStatus.Three read
error?.response?.statusbefore; three readerror.customAttributes.httpErrorStatus, which the real client'sprocessAxiosErrorinterceptor copies fromresponse.status, so inproduction the two agree.
MockAuthService's client has no interceptors,so a plain
axiosMock.onGet(url).reply(404)reached the second idiom withcustomAttributesundefined and the destructure threw aTypeError;OutlineTab.test.jsx's two proctoring 404 cases did exactly that andpassed because an errored query (
retry: false) renders no panel either.They now exercise the
{}branch; their assertions are unchanged.apiHooks.test.tsx's hand-built errors carryresponse.status, so theykeep working; their
customAttributeslines are left as they are.The empty-object branches. The caveat-form types make every field
optional, as the hooks'
OutlineTabDatacomment already did for the403 branch, so
{}needs nothing more. The completeCourseHomeOutlinecannot, so
getOutlineTabDatareturnsPromise<CourseHomeOutline | Record<string, never>>andcourseBlocksis
CourseHomeOutlineBlocks | Record<string, never>(course_blocksisnullfor an unenrolled learner on a course that is neither public norpublic-outline): both
{}sites are real, andRecord<string, never>lets the TypeScript readers' optional chains (
Section.tsx) type through.getTimeOffsetMillistakes numbers. Both production callers passDate.now();api.test.jspassesDateobjects and stays JavaScript,coercing through
valueOf.The hooks infer from the fetchers.
useOutlineTabDataanduseExamAttemptsDatalose their hand-writtenuseQuery<…>generics(
.exam || {}infersRecord<string, unknown>[]on its own);useCourseHomeMetakeepsuseQuery<CourseHomeMeta, RequestError>forthe error type. The three query functions whose hook takes
courseId: string | undefinedare called withcourseId!, as theirkeys already were;
enabledguards the call.Two fields the normalizer reads that the endpoint never sends.
CourseBlockSerializeremits nonavigation_disabled, andOutlineTabViewemitshas_scheduled_contentonly per block, never atthe top level (the platform's history has neither). So
CourseHomeOutlineSequence.navigationDisabledandCourseHomeOutline.hasScheduledContentare alwaysundefined. Neitherhas a reader of the outline data: the courseware's
navigationDisabledcomes from the sequence metadata, and the scheduled-content alert reads
the per-course
hasScheduledContentfromcourseBlocks.courses. Type the courseware normalizers on the functions that produce them (convert courseware/data/utils.js to TypeScript) #2129entry 5's treatment: optional on the input type with a trailing note,
optional on the output, the reads left as they were (the issue rules out
changing what a function returns). The outline factory's top-level
has_scheduled_content: nullis fixture-only.SequenceDueDate'sdueanddescriptionprops arestring | null.The serializer sends
nullfor both (block.get('due'),block.get('format')), andOutlineSequencehad typed themstringtothe reader; the component already handles the falsy case.
Nullability, from the platform.
DateSummarySerializer(
assignment_typedefaultNone,completeallow_null,extra_infoNoneby default on the block);ProgressTabSerializerandProgressTabView(certificate_dataandcredit_course_requirementsNonefrom their helpers,studio_urlNoneunless the course isedited in Studio,
endandenrollment_modenullable,letter_gradeNonebelow passing,overrideandurlNoneper subsection,has_scheduled_contentfromget_xblock_field);CourseBlockSerializer(
description,due,effort_*,icon,lms_web_url,has_scheduled_content,hide_from_tocvia.get);get_access_expiration_dataandget_cert_data(Nonewhen notapplicable);
CourseHomeMetadataSerializer(user_timezoneNonewithout a preference; the rest non-null).
apiHooks.test.tsxguards two reads.result.current.data.studioUrland
.gradesFeatureIsFullyLockedcompiled asany; under the typedresult
datais possibly undefined, so they becomedata?..Commit:
refactor:, no!. No request or result changes. Nothingoutside the repo names what moved:
OutlineSequencehad two in-repotype importers (
SequenceLink.tsx,SequenceTitle.tsx),OutlineTabDatawas module-local,CourseHomeMeta's four in-repoimporters repoint to
./api, and the READMEs reference the hooks, notthe types' paths.
Four lines no suite reached, pinned (codecov, 2026-10-01). The move
makes every line of the new files patch lines for codecov, which reported
five missing; a full-suite coverage run showed four gaps, all uncovered
in the JavaScript too: the normalizer's two
logInfobranches (anunexpected block type; a section child that is not among the sequences),
the LMS-URL branch of
getProctoringInfoData(every suite configuresEXAMS_BASE_URL), andsearchCourseContentFromAPI, which thecourseware-search hook suite mocks rather than runs.
courseHomeOutline.test.ts(new, two hand-built block maps) pins the first two, asserting the
logged message and that the odd block is skipped or the odd child kept
in its section's
sequenceIdswithout a back-reference;api.test.jsgains a proctoring case asserting the LMS URL with the username, and a
search case asserting the posted form and the camel-cased
data. Bothfiles are at 100% lines after.
Verify:
npm run types,npm run lint, full suite 120 suites, 1190 tests;git grep "^export interface\|^interface" src/course-home/data/apiHooks.tsreturns only
QueryOptions.Manual testing
Manual testing — #2130 (types only)
No behaviour or request change; a smoke pass that the pages whose data this
layer types still render. Tutor local, 2026-10-01, by the user.
branch is hard to produce on tutor local (the LMS proctoring endpoints
there answer 200 with an empty body) and is pinned by the hook suite.
🤖 Generated with Claude Code