refactor(course-outline): migrate to React Query and consolidate outline architecture#3073
refactor(course-outline): migrate to React Query and consolidate outline architecture#3073navinkarkera wants to merge 106 commits into
Conversation
|
Thanks for the pull request, @navinkarkera! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
f28d857 to
b13cc19
Compare
…o context - Add intent-level drag handlers (previewSections, cancelReorderPreview, commitSectionReorder, commitSubsectionReorder, commitUnitReorder) - Use preview/commit snapshot pattern with rollback on failure - Await refreshed sections before clearing subsection/unit reorder preview - Add optimistic section preview on drag drop - Remove setSections/restoreSectionList from public API - Remove duplicate useCourseOutlineIndex hook from apiHooks
After successful section/subsection/unit reorder, sync React Query outline index cache so committed tree does not snap back to pre-drop state. CourseOutlineStateContext.tsx: - Replace acceptReorderPreview with acceptReorderAndSyncSections / acceptReorderAndSyncSectionOrder helpers that update query cache on success. - Use effectiveOutlineIndexData (prefer query cache) over outlineIndexData. - Read sections from query-backed data with Redux fallback. apiHooks.ts: - Extract replaceSectionInOutlineIndex, appendSectionToOutlineIndex, insertDuplicatedSectionInOutlineIndex, invalidateParentQueriesAndSync helpers. - Remove remaining Redux dispatch imports (addSection, updateSectionList, duplicateSection); write to query cache instead. CourseOutline.test.tsx: - Update courseId fixture to realistic format.
…mplify boilerplate - CourseOutlineStateContext: removed 13 trivial context pass-through assertions, merged 2 duplicate courseId-change tests into 1 - useDeleteModal: merged 2 similar 'close but no clear selection' tests, removed defensive early-return - useUnlinkModal: removed defensive early-return test - useHighlightsModal: removed 3 trivial tests, kept 2 real filtering edge-case tests - Preserved: selection state-machine, highlights filtering, complex drag-drop logic - Net: 22→16 tests (27% fewer), 742→638 lines (14% reduction)
…ls stub - Production hook calls useEnableCourseHighlightsEmails at initialization - Mock must exist even though we don't test that specific behavior - All 4 Phase 2 test suites now pass (9 tests)
Also makes outline discussions settings optional through the tree prop path.
I'll be creating a small follow up PR to move styles and message files out of these folders. |
|
@ChrisChV @bradenmacdonald This is ready for review. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3073 +/- ##
==========================================
- Coverage 95.61% 95.61% -0.01%
==========================================
Files 1391 1410 +19
Lines 33077 33408 +331
Branches 7444 7841 +397
==========================================
+ Hits 31628 31942 +314
- Misses 1396 1414 +18
+ Partials 53 52 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ChrisChV
left a comment
There was a problem hiding this comment.
@navinkarkera Thanks for this work! It is an amazing refactor! It is a lot of code. I've left some comments, I'm missing some files, and need to test the changes
| useOutlineMutation<{ sectionId: string; subsectionId: string; unitListIds: string[]; }, unknown>(courseId, { | ||
| operation: 'reorderUnits', | ||
| mutationFn: (variables) => setCourseItemOrderList(variables.subsectionId, variables.unitListIds), | ||
| onSettled: () => {}, // suppress default parent invalidation |
There was a problem hiding this comment.
It would be good to explain why parent invalidation is being suppressed from these functions.
| for (const m of mutations) { | ||
| if (m.status !== 'success' && m.status !== 'error') { continue; } | ||
| const t = m.submittedAt ?? 0; | ||
| if (t > 0 && (!latest || t > latest.submittedAt)) { | ||
| latest = { status: m.status as 'success' | 'error', submittedAt: t }; | ||
| } | ||
| } |
There was a problem hiding this comment.
In non-inline blocks/functions, I don't find the use of single-letter named variables very readable. Can you update it to readable names?
| for (const m of mutations) { | ||
| if (m.status !== 'success' && m.status !== 'error') { continue; } | ||
| const t = m.submittedAt ?? 0; | ||
| if (t > 0 && (!latest || (latest.submittedAt ?? 0) < t)) { | ||
| latest = m; | ||
| } | ||
| } |
|
|
||
| const latest = latestMutation(mutations); | ||
| const status = latest?.status; | ||
| if (status === 'error' && latest) { |
There was a problem hiding this comment.
Nit
| if (status === 'error' && latest) { | |
| if (status === 'error') { |
| export { getLastEditableItem, getLastEditableSubsection } from '../utils/editability'; | ||
| export type { EditableSubsection } from '../utils/editability'; | ||
| export { | ||
| computeErrorSignature, | ||
| filterDismissedErrors, | ||
| pruneDismissedErrorSignatures, | ||
| } from '../utils/outlineErrorDismissal'; | ||
| export { useConfigureDialog } from './useConfigureModal'; | ||
| export type { UseConfigureDialogOutput } from './useConfigureModal'; | ||
| export { useCreateBlockSidebar } from './useCreateBlockSidebar'; | ||
| export { useDeleteModal } from './useDeleteModal'; | ||
| export type { UseDeleteModalOutput } from './useDeleteModal'; | ||
| export { useHighlightsModal } from './useHighlightsModal'; | ||
| export type { UseHighlightsModalOutput } from './useHighlightsModal'; | ||
| export { useOutlineReorderState } from './useOutlineReorderState'; | ||
| export type { UseOutlineReorderStateOutput } from './useOutlineReorderState'; | ||
| export { useOutlineStatusState } from './useOutlineStatusState'; | ||
| export type { UseOutlineStatusStateOutput } from './useOutlineStatusState'; | ||
| export { useUnlinkModal } from './useUnlinkModal'; | ||
| export type { UseUnlinkModalOutput } from './useUnlinkModal'; |
There was a problem hiding this comment.
Nit
| export { getLastEditableItem, getLastEditableSubsection } from '../utils/editability'; | |
| export type { EditableSubsection } from '../utils/editability'; | |
| export { | |
| computeErrorSignature, | |
| filterDismissedErrors, | |
| pruneDismissedErrorSignatures, | |
| } from '../utils/outlineErrorDismissal'; | |
| export { useConfigureDialog } from './useConfigureModal'; | |
| export type { UseConfigureDialogOutput } from './useConfigureModal'; | |
| export { useCreateBlockSidebar } from './useCreateBlockSidebar'; | |
| export { useDeleteModal } from './useDeleteModal'; | |
| export type { UseDeleteModalOutput } from './useDeleteModal'; | |
| export { useHighlightsModal } from './useHighlightsModal'; | |
| export type { UseHighlightsModalOutput } from './useHighlightsModal'; | |
| export { useOutlineReorderState } from './useOutlineReorderState'; | |
| export type { UseOutlineReorderStateOutput } from './useOutlineReorderState'; | |
| export { useOutlineStatusState } from './useOutlineStatusState'; | |
| export type { UseOutlineStatusStateOutput } from './useOutlineStatusState'; | |
| export { useUnlinkModal } from './useUnlinkModal'; | |
| export type { UseUnlinkModalOutput } from './useUnlinkModal'; | |
| export { getLastEditableItem, getLastEditableSubsection, type EditableSubsection } from '../utils/editability'; | |
| export { | |
| computeErrorSignature, | |
| filterDismissedErrors, | |
| pruneDismissedErrorSignatures, | |
| } from '../utils/outlineErrorDismissal'; | |
| export { useConfigureDialog, type UseConfigureDialogOutput } from './useConfigureModal'; | |
| export { useCreateBlockSidebar } from './useCreateBlockSidebar'; | |
| export { useDeleteModal, type UseDeleteModalOutput } from './useDeleteModal'; | |
| export { useHighlightsModal, type UseHighlightsModalOutput } from './useHighlightsModal'; | |
| export { useOutlineReorderState, type UseOutlineReorderStateOutput } from './useOutlineReorderState'; | |
| export { useOutlineStatusState, type UseOutlineStatusStateOutput } from './useOutlineStatusState'; | |
| export { useUnlinkModal, type UseUnlinkModalOutput } from './useUnlinkModal'; |
| @@ -0,0 +1,47 @@ | |||
| import { findLast, findLastIndex } from 'lodash'; | |||
|
|
|||
| import { type XBlock, type XBlockBase } from '@src/data/types'; | |||
There was a problem hiding this comment.
| import { type XBlock, type XBlockBase } from '@src/data/types'; | |
| import type { XBlock, XBlockBase } from '@src/data/types'; |
| await waitFor(() => { | ||
| const cachedData = queryClient.getQueryData(courseOutlineQueryKeys.index(courseId)); | ||
| const firstSectionSubsections = cachedData?.courseStructure?.childInfo?.children[0]?.childInfo?.children || []; | ||
| expect(firstSectionSubsections.length).toBe(firstSection.childInfo.children.length + 1); | ||
| expect(firstSectionSubsections[firstSectionSubsections.length - 1]?.id).toBe(subsection.id); | ||
| const subsectionsSecondSection = cachedData?.courseStructure?.childInfo?.children[1]?.childInfo?.children || []; | ||
| expect(subsectionsSecondSection.length).toBe(section.childInfo.children.length - 1); | ||
| }); |
There was a problem hiding this comment.
Do all these expects need to be within waitFor?
| await waitFor(() => { | ||
| const cachedData = queryClient.getQueryData(courseOutlineQueryKeys.index(courseId)); | ||
| const firstSectionSubsections = cachedData?.courseStructure?.childInfo?.children[0]?.childInfo?.children || []; | ||
| expect(firstSectionSubsections.length).toBe(section.childInfo.children.length - 1); | ||
| const subsectionsSecondSection = cachedData?.courseStructure?.childInfo?.children[1]?.childInfo?.children || []; | ||
| expect(subsectionsSecondSection.length).toBe(secondSection.childInfo.children.length + 1); | ||
| expect(subsectionsSecondSection[0]?.id).toBe(subsection.id); | ||
| }); |
| import type { Depth } from './outline-level'; | ||
| import { containsSearchResult } from './outline-level'; |
There was a problem hiding this comment.
| import type { Depth } from './outline-level'; | |
| import { containsSearchResult } from './outline-level'; | |
| import { containsSearchResult, type Depth } from './outline-level'; |
| isHeaderVisible: boolean; | ||
| activeId: UniqueIdentifier | null; | ||
| overId: UniqueIdentifier | null; | ||
| scrollState: ScrollState | null | undefined; |
There was a problem hiding this comment.
| scrollState: ScrollState | null | undefined; | |
| scrollState?: ScrollState | null; |
Description
Large architectural refactor of the course outline module. Migrated from Redux to React Query for outline data and mutation flows, consolidated context management, and unified component rendering.
1. Redux to React Query Migration
apiHooks.tscacheInvalidation.ts2. Context Consolidation
CourseOutlineContext; sidebar/drag contexts remain separateuseOutlineReorderState,useOutlineStatusState,useConfigureModal,useDeleteModal,useHighlightsModal,useUnlinkModal,useCreateBlockSidebar,useModalStateOutlineModalscomponent3. Component Unification
OutlineNodecomponent; legacy style/message modules remain where still importedOutlineTreefor recursive outline rendering with depth-based logic4. Test Infrastructure Modernization
__mocks__/testSetup.tsxand__mocks__/helpers.ts5. Bug fixes
?show=is prevented. It will only scroll to the item once.Replacing redux with react query has improved the performance a lot. Going back and forth from outline to any other page is instant.
Useful information to include:
Supporting information
Private-ref: https://tasks.opencraft.com/browse/FAL-3835Testing instructions
Outline Loading & Display
Drag-and-Drop Reordering
Add Operations
Edit Operations
Delete Operations
Publish Operations
Sidebar & Navigation
Search & URL Parameters
?show=<block_id>URL param → verify correct item expands, scrolls into view, and highlights once without repeated scroll on later selection or sidebar interactionOther information
Best Practices Checklist
We're trying to move away from some deprecated patterns in this codebase. Please
check if your PR meets these recommendations before asking for a review:
.ts,.tsx).propTypesanddefaultPropsin any new or modified code.src/testUtils.tsx(specificallyinitializeMocks)apiHooks.tsin this repo for examples.messages.tsfiles have adescriptionfor translators to use.../in import paths. To import from parent folders, use@src, e.g.import { initializeMocks } from '@src/testUtils';instead offrom '../../../../testUtils'