| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## master #1897 +/- ##
=======================================
Coverage 91.27% 91.28%
=======================================
Files 343 343
Lines 5766 5770 +4
Branches 1387 1388 +1
=======================================
+ Hits 5263 5267 +4
Misses 484 484
Partials 19 19 ☔ View full report in Codecov by Sentry.
|
Sorry, something went wrong.
There was a problem hiding this comment.
This PR moves Discussions data loading (getCourseDiscussionTopics) out of the DiscussionsTrigger component and into a new optional prefetch hook on sidebar widget configs, executed by SidebarContextProvider when the provider mounts and when course metadata updates.
Changes:
Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.
Show a summary per file| File | Description |
|---|---|
| src/courseware/course/sidebar/SidebarContextProvider.jsx | Runs prefetch for enabled widgets via a new effect. |
| src/widgets/discussions/widgetConfig.js | Adds discussionsPrefetch and wires it into the widget config. |
| src/widgets/discussions/DiscussionsTrigger.jsx | Removes fetching logic; now reads the model and renders conditionally. |
| src/widgets/discussions/DiscussionsTrigger.test.jsx | Pre-populates the store by executing the thunk in beforeEach. |
| src/courseware/course/sidebar/SidebarContextProvider.test.jsx | Mocks useDispatch/useModel and adjusts UC7 toggle expectations. |
| src/courseware/course/sidebar/Sidebar.test.jsx | Adds unit tests for Sidebar rendering/switching behavior. |
| src/courseware/course/sidebar/README.md | Documents the new prefetch widget config field. |
| src/courseware/course/sidebar/ARCHITECTURE.md | Documents the provider’s prefetch responsibility and lifecycle. |
| src/courseware/course/sidebar/USE_CASE_VERIFICATION.md | Updates the architecture diagram to include a prefetch effect. |
| src/widgets/discussions/README.md | Documents discussions-specific prefetch behavior and exports. |
src/widgets/discussions/DiscussionsTrigger.jsx:13
import { ensureConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
import { QuestionAnswer } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useModel } from '@src/generic/model-store';
import { WIDGETS } from '@src/constants';
import SidebarTriggerBase from '@src/courseware/course/sidebar/common/TriggerBase';
import SidebarContext from '@src/courseware/course/sidebar/SidebarContext';
import messages from './messages';
ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sorry, something went wrong.
There was a problem hiding this comment.
At first glance this makes a lot of sense: centralizing data prefetch in the provider so that isAvailable checks can rely on pre-populated Redux state. But there's an issue:
The prefetch effect at SidebarContextProvider.jsx#L89-L96 includes coursewareMeta and courseHomeMeta in its dependency array. The two models settle in separate React batches during initial load, which causes the effect to fire twice after the guard in discussionsPrefetch starts passing.
I confirmed this produces duplicate requests to both GET /api/discussion/v1/courses/{courseId} and GET /api/discussion/v2/course_topics/{courseId} on every page load - two requests to each endpoint returning identical data (thunks.js#L268-L286).
The old code in DiscussionsTrigger avoided this because its effect depended on [courseId, baseUrl, edxProvider, dispatch], all of which were stable after the initial render.
A simple fix might be to remove coursewareMeta and courseHomeMeta from the dependency array. The effect only needs to run once per courseId - the course object is only used inside discussionsPrefetch to check tabs, and that check can happen with the latest value via a ref or by passing the models lazily.
Sorry, something went wrong.
There was a problem hiding this comment.
Ok, the ref workaround solves it, but it's a bit hacky. The real solution is going to be moving to React Query, later on.
Let's worry about that later, though! 👍🏼
Sorry, something went wrong.
…onfig lifecycle (openedx#1897)" This reverts commit 59113b0.
Reverts the following two commits: - 59113b0 feat: move discussion topic prefetch from trigger to widget config lifecycle (openedx#1897) - 0664dc3 feat: decouple notifications panel using widget registry mechanism (openedx#1885) Co-Authored-By: Claude <noreply@anthropic.com>
…t it to TypeScript The courseware sidebar provider becomes TypeScript, and the two patterns in it that predate React Query go with the conversion instead of being typed around. The context was created with a default object, so a consumer rendered outside `SidebarProvider` got empty values instead of an error; it is now `createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()` hook that throws outside the provider. The provider ran every widget's `prefetch` from an effect that read the merged course metadata through a ref, PR #1897's fix for the effect firing twice as the two metadata models settled in separate batches, approved then as a workaround pending React Query. The one `prefetch` user, the discussions widget, now loads its topics as a query observer in a `Provider` component, mounted by the framework for every enabled widget inside `SidebarContext`, with `enabled` set only when the discussions MFE is configured and the course has a discussion tab. An observer fetches on mount and on key change, not when the metadata it reads re-renders, so one sidebar mount is one topics request and a metadata write is none. The query definition is a `queryOptions` object, `discussionTopicsQuery`, shared with the tests and with #2087's reader hook; its bridge `meta` stays until that layer converts the three `useModel` readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch` field of the widget contract are gone. The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`, `SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`, `unit: Partial<DiscussionTopic>`, the line #2087 changes) and `SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared beside their queries. Both built-in widget configs are TypeScript and declared against `SidebarWidget`; the upgrade widget's context module converts with them. The sidebar README's contract sections point at the declarations instead of restating them, and the prefetch section describes the `Provider` observer. Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as requests and pins that a course-metadata write does not refetch the topics; `SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`, `SequenceNavigation` and `SequenceNavigationTabs` render their context consumer under a provider, which the default value had let them skip. BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no longer called. A widget that loaded data through it loads it in its `Provider` component with `useQuery` and `enabled`; see "Loading widget data" in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)` returns `null` outside `SidebarProvider`; read the context with `useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t it to TypeScript The courseware sidebar provider becomes TypeScript, and the two patterns in it that predate React Query go with the conversion instead of being typed around. The context was created with a default object, so a consumer rendered outside `SidebarProvider` got empty values instead of an error; it is now `createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()` hook that throws outside the provider. The provider ran every widget's `prefetch` from an effect that read the merged course metadata through a ref, PR #1897's fix for the effect firing twice as the two metadata models settled in separate batches, approved then as a workaround pending React Query. The one `prefetch` user, the discussions widget, now loads its topics as a query observer in a `Provider` component, mounted by the framework for every enabled widget inside `SidebarContext`, with `enabled` set only when the discussions MFE is configured and the course has a discussion tab. An observer fetches on mount and on key change, not when the metadata it reads re-renders, so one sidebar mount is one topics request and a metadata write is none. The query definition is a `queryOptions` object, `discussionTopicsQuery`, shared with the tests and with #2087's reader hook; its bridge `meta` stays until that layer converts the three `useModel` readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch` field of the widget contract are gone. The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`, `SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`, `unit: Partial<DiscussionTopic>`, the line #2087 changes) and `SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared beside their queries. Both built-in widget configs are TypeScript and declared against `SidebarWidget`; the upgrade widget's context module converts with them. The sidebar README's contract sections point at the declarations instead of restating them, and the prefetch section describes the `Provider` observer. Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as requests and pins that a course-metadata write does not refetch the topics; `SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`, `SequenceNavigation` and `SequenceNavigationTabs` render their context consumer under a provider, which the default value had let them skip. BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no longer called. A widget that loaded data through it loads it in its `Provider` component with `useQuery` and `enabled`; see "Loading widget data" in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)` returns `null` outside `SidebarProvider`; read the context with `useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t it to TypeScript The courseware sidebar provider becomes TypeScript, and the two patterns in it that predate React Query go with the conversion instead of being typed around. The context was created with a default object, so a consumer rendered outside `SidebarProvider` got empty values instead of an error; it is now `createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()` hook that throws outside the provider. The provider ran every widget's `prefetch` from an effect that read the merged course metadata through a ref, PR #1897's fix for the effect firing twice as the two metadata models settled in separate batches, approved then as a workaround pending React Query. The one `prefetch` user, the discussions widget, now loads its topics as a query observer in a `Provider` component, mounted by the framework for every enabled widget inside `SidebarContext`, with `enabled` set only when the discussions MFE is configured and the course has a discussion tab. An observer fetches on mount and on key change, not when the metadata it reads re-renders, so one sidebar mount is one topics request and a metadata write is none. The query definition is a `queryOptions` object, `discussionTopicsQuery`, shared with the tests and with #2087's reader hook; its bridge `meta` stays until that layer converts the three `useModel` readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch` field of the widget contract are gone. The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`, `SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`, `unit: Partial<DiscussionTopic>`, the line #2087 changes) and `SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared beside their queries. Both built-in widget configs are TypeScript and declared against `SidebarWidget`; the upgrade widget's context module converts with them. The sidebar README's contract sections point at the declarations instead of restating them, and the prefetch section describes the `Provider` observer. Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as requests and pins that a course-metadata write does not refetch the topics; `SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`, `SequenceNavigation` and `SequenceNavigationTabs` render their context consumer under a provider, which the default value had let them skip. The provider's first render now reads `useWindowSize().width ?? window.innerWidth` instead of an undefined width. That fixes a bug present since #1885: on a narrow viewport the first render seeded the sidebar closed, and the mobile branches of the sidebar hooks never corrected it, so a panel a learner had opened did not restore after a refresh. Desktop reaches the same state as before, without a transient outline render and two storage writes per load. BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no longer called. A widget that loaded data through it loads it in its `Provider` component with `useQuery` and `enabled`; see "Loading widget data" in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)` returns `null` outside `SidebarProvider`; read the context with `useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t it to TypeScript (#2113) The courseware sidebar provider becomes TypeScript, and the two patterns in it that predate React Query go with the conversion instead of being typed around. The context was created with a default object, so a consumer rendered outside `SidebarProvider` got empty values instead of an error; it is now `createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()` hook that throws outside the provider. The provider ran every widget's `prefetch` from an effect that read the merged course metadata through a ref, PR #1897's fix for the effect firing twice as the two metadata models settled in separate batches, approved then as a workaround pending React Query. The one `prefetch` user, the discussions widget, now loads its topics as a query observer in a `Provider` component, mounted by the framework for every enabled widget inside `SidebarContext`, with `enabled` set only when the discussions MFE is configured and the course has a discussion tab. An observer fetches on mount and on key change, not when the metadata it reads re-renders, so one sidebar mount is one topics request and a metadata write is none. The query definition is a `queryOptions` object, `discussionTopicsQuery`, shared with the tests and with #2087's reader hook; its bridge `meta` stays until that layer converts the three `useModel` readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch` field of the widget contract are gone. The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`, `SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`, `unit: Partial<DiscussionTopic>`, the line #2087 changes) and `SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared beside their queries. Both built-in widget configs are TypeScript and declared against `SidebarWidget`; the upgrade widget's context module converts with them. The sidebar README's contract sections point at the declarations instead of restating them, and the prefetch section describes the `Provider` observer. Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as requests and pins that a course-metadata write does not refetch the topics; `SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`, `SequenceNavigation` and `SequenceNavigationTabs` render their context consumer under a provider, which the default value had let them skip. The provider's first render now reads `useWindowSize().width ?? window.innerWidth` instead of an undefined width. That fixes a bug present since #1885: on a narrow viewport the first render seeded the sidebar closed, and the mobile branches of the sidebar hooks never corrected it, so a panel a learner had opened did not restore after a refresh. Desktop reaches the same state as before, without a transient outline render and two storage writes per load. BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no longer called. A widget that loaded data through it loads it in its `Provider` component with `useQuery` and `enabled`; see "Loading widget data" in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)` returns `null` outside `SidebarProvider`; read the context with `useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
| Back | FazBrowse Home | New Git URL |
Summary
Moves data-fetching logic (getCourseDiscussionTopics dispatch) out of DiscussionsTrigger and into a new prefetch field on the widget config. SidebarContextProvider now runs widget.prefetch() for all enabled widgets on mount and when course metadata changes, ensuring Redux store data is populated before isAvailable checks and component rendering.
Motivation
Previously, DiscussionsTrigger was responsible for both rendering and fetching discussion topics. This created a coupling between the trigger component and the data lifecycle. The trigger had to mount before data was available, causing timing issues with widget availability checks. Moving prefetch to the framework level ensures data is ready before any widget logic runs.
Changes
Code (5 files)
Documentation (4 files)
Widget Config API Addition
The prefetch field is a new optional field on the widget config: