-
Notifications
You must be signed in to change notification settings - Fork 214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add plugin slots for progress page components #1496
base: master
Are you sure you want to change the base?
feat: Add plugin slots for progress page components #1496
Conversation
Thanks for the pull request, @xitij2000! What's next?Please work through the following steps to get your changes 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. 🔘 Let us know that your PR is ready for review:Who will review my changes?This repository is currently maintained by Where 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. |
This PR currently doesn't include any screenshots. I will add them before the PR is final depending on the direction of the review. |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #1496 +/- ##
==========================================
+ Coverage 89.82% 89.88% +0.05%
==========================================
Files 326 332 +6
Lines 5601 5633 +32
Branches 1396 1396
==========================================
+ Hits 5031 5063 +32
Misses 554 554
Partials 16 16 ☔ View full report in Codecov by Sentry. |
@xitij2000 I marked this PR as ready for review assuming that it doesn't change any existing user-facing behavior. Let me know if that's wrong, please (it would need to go through product review in that case). CC @openedx/committers-frontend-app-learning |
@xitij2000 This makes sense to me at a high level. Would you mind adding some screenshots, as you suggested? @arbrandes Do we have any specific guidelines around where we'll accept slots in the UI, or just use my best judgement? |
Additionally, if we do want component-level slots (which I think we do since there is a slot to add contents to a unit title), then perhaps we can structure them in some way in he plugin-slots folder? Otherwise, as the number of slots grows it will become unwieldy. |
@xitij2000 @bradenmacdonald I'm a little unclear on the current status of this PR. Are there any blockers to starting engineering review? |
@itsjeyd I don't think so. I just hoping to hear from @arbrandes. |
ace68ba
to
0c462fa
Compare
@bradenmacdonald OK, got it. |
@bradenmacdonald Do you want to explicitly request a review from him, maybe? That might help with getting this PR unstuck. CC @xitij2000 |
@brian-smith-tcril Could I get your thoughts on this PR? I think it sounds like a good idea, and Adolfo suggested we might want to backport it to Sumac, which I agree with. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for this PR!
Overall I think these are great extension points to have, and I'm super happy to see these documented with screenshots and everything!
I left a few comments with questions and suggestions. Most are quite small, but there's one larger comment that might spark conversation.
pluginProps={{ | ||
courseId, | ||
}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not opposed to having courseId
in pluginProps
, but my general feeling is we should only add pluginProps
when we know that there is a use case that requires them. Adding something to pluginProps
makes it part of the plugin API, meaning removing it would require going through a DEPR process. Do you have an example use case in mind where courseId
would be required in this slot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I will remove them from here since the courseId should be unambiguous in these slots and can probably be fetched from the store.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will eventually want to refactor every page like this in the Learning MFE to not use redux and to use React Context + React Query instead. So if you do need course ID, I would prefer plugins don't fetch it from the store, because that's implicitly making the Redux store part of the API contract here. Our redux stores tend to be messy and buggy because they don't even have a contract within each MFE (they aren't typed using TypeScript), and I really don't want them to become part of the plugin contract, whether officially or unofficially.
In general, my personal view is that plugin slots should pass any props that come from the URL, so that they know exactly which page they're on (in this case it would be the course ID), and any other data that they need should be loaded via small usages of React Query (which should be de-duplicated if any other places on the page need the same data).
In this case, the default content of the plugin slot
does need the course ID (e.g. "Dates" links to /learning/course/:courseId/dates
), so I think it makes sense to make that course ID available to any other plugins filling the slot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think perhaps it makes sense to standardise some kind of common context across all MFEs then and plugin can fetch data from there without needing props. I think courseId / learningContextId probably belongs in the context as it is by definition the context in which each component is rendered. At the bare minimum I think the shared common context should have the contextId
, username
etc. If the username is missing then a user isn't logged in and if the contextId is missing then it doesn't apply (for instance in the profile page or learner dashboard).
Or it could be as simple as having hooks like useContextId
which can then get it from the store right now and from the shared context or whatever mechanism we have later. It seems like too common a usecase here.
The removal of the courseId is in an isolated commit so I can revert easily, but, how about I instead add a useContextId
hook that is designed for any component or plugin to use? If it looks good we can standardise something like that as a common API>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think perhaps it makes sense to standardise some kind of common context across all MFEs then and plugin can fetch data from there without needing props. I think courseId / learningContextId probably belongs in the context as it is by definition the context in which each component is rendered.
Totally agree with this.
Or it could be as simple as having hooks like useContextId which can then get it from the store right now and from the shared context or whatever mechanism we have later. It seems like too common a usecase here.
Great idea. Maybe just call it "useLearningContextId" since "Context" is ambiguous to me.
</div> | ||
|
||
{/* Side panel */} | ||
<div className="col-12 col-md-4 p-0 px-md-4"> | ||
{wideScreen && <CertificateStatus />} | ||
<RelatedLinks /> | ||
{wideScreen && <ProgressTabCertificateStatusSlot courseId={courseId} />} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to hear others thoughts on having the ProgressTabCertificateStatusSlot
in 2 places here. The way this is written now would require plugin authors utilizing the ProgressTabCertificateStatusSlot
to account for the slot being in the side panel or main body depending on the width of the viewport.
I see a few options here:
-
A: Keep the slots as written in this PR
- Requires plugin authors to account for slots being in different places based on viewport width
-
B: Split
ProgressTabCertificateStatusSlot
intoProgressTabCertificateStatusMainBodySlot
andProgressTabCertificateStatusSidePanelSlot
- Requires site operators to put a plugin in 2 slots instead of 1 (even if it's the same plugin)
-
C: Switch to just having a
MainBody
slot and aSidePanel
slot instead of granular slots for each component in the body/panel- This would remove quite a bit of flexibility in customization
-
D: Some combination of (A or B) and C
- This would mean keeping the granular slots, but also wrapping those slots in a bigger slot - similar to how
frontend-component-header
has aDesktopHeaderSlot
that wraps the entire desktop header, while the desktop header component contains more granular slots such asDesktopUserMenuSlot
I think I lean towards B, with a possibility of adding bigger slots later if needed. I am, however, very open to other opinions on the matter, and would love to hear other options I haven't thought of that others have.
- This would mean keeping the granular slots, but also wrapping those slots in a bigger slot - similar to how
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
B seems reasonable to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps there is another option, which is to pass the slot position to component?
i.e. one of the props can be the "placement" of the content, which could be "mainbody" or "sidebar".
I imagine anyone creating a plugin for this pair of slots would either use the same component or have a single component dynamically adjust to the placement. By explicitly providing this value we can ensure that the plugin author will follow the same logic as the app even if that changes over time.
Otherwise, I agree that the two slot approach looks good, and I will adapt the code accordingly. I'll push the approach using the "placement" prop since it's a small change, but will switch to the two component approach if that seems better.
70b6f6e
to
08ef4f7
Compare
@brian-smith-tcril I've updated the slots to remove the courseId. I've updated the examples to pull the course ID from the state so that I wouldn't have to update the images though. I hope that's okay! |
); | ||
|
||
ProgressTabCertificateStatusSlot.propTypes = { | ||
placement: PropTypes.oneOf(['MAIN_BODY', 'SIDEBAR']), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about having placement
as a pluginProp
.
I think 2 slots provides more flexibility. With 2 slots, site operators can use different PLUGIN_OPERATIONS
for each, set keepDefault
differently for each, or set a different priority
for each.
With placement
as a pluginProp
that flexibility is lost.
That being said, I don't know if that flexibility is something we want. My current feeling is, "yes, we should provide the flexibility that comes with using 2 slots" - but I'm open to other opinions on the matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, I did not consider that someone might want to apply different plugin operations depending on placement.
On further thought I do have another issue with "placement" which is that currently it's not standardised. I do think there could be certain slots that would apply to multiple places in an app. In that case, it would be useful to have a standardised terminology for what it means for the placement to be in the "main body" or "sidebar" etc.
I'm wondering then if the logic of which version should be hidden should also be moved to the contents inside the plugin? For instance someone might want to always show it in the sidebar and others always in the main body?
I've updated the PR to use two components and moved the logic of hiding or showing the component to the slots so that a plugin can choose to use different logic. |
17027a6
to
0206392
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I really like how this has come together! I'd still like to have a bigger conversation about best practices for getting data into plugins (context vs props etc.), but I don't think that should block merging this.
It looks like CI is failing commitlint and some tests, once those are passing I'll give this a ✔️!
src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/README.md
Outdated
Show resolved
Hide resolved
src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/README.md
Outdated
Show resolved
Hide resolved
1b24b8b
to
5721f77
Compare
@brian-smith-tcril I've fixed the test issues, and the commitlint issues. Thanks for your thorough review! I feel starting with fewer props and adding more later is less disruptive so keeping courseId out of props seems good for now, and if things change, it will be easy to add it back without hurting existing plugins. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel starting with fewer props and adding more later is less disruptive
I 100% agree!
Thanks for all the work on this! It turned out great!
Are there screenshots and/or a description of this PR that I can share with a product manager or instructional designer? We have a backlog of pedagogical issues with the progress page, and I'm wondering if we can leverage the pluginslots to get the desired behavior. |
This doesn't change the UI at all, so it's hard to screenshot as-is, but I'm adding a screenshot that highlights all the elements that can now be altered with slots. |
@bradenmacdonald Did you want to give this another look before merging? If not, it looks like the changes will be ready to go after another rebase. |
@itsjeyd Nope, this is good to merge without further review from me. Thanks. |
OK, thanks @bradenmacdonald. @xitij2000 Over to you for a final rebase :) |
Adds a slot for different components in the progress tab to allow them to be overridden with custom components. # Conflicts: # src/course-home/progress-tab/certificate-status/CertificateStatus.jsx diff --git a/src/course-home/progress-tab/ProgressHeader.jsx b/src/course-home/progress-tab/ProgressHeader.jsx index 4648fd20..1d0fd56f 100644 --- a/src/course-home/progress-tab/ProgressHeader.jsx +++ b/src/course-home/progress-tab/ProgressHeader.jsx @@ -1,9 +1,9 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; +import { useSelector } from 'react-redux'; import { useModel } from '../../generic/model-store'; diff --git a/src/course-home/progress-tab/ProgressTab.jsx b/src/course-home/progress-tab/ProgressTab.jsx index 1b829037..a0d86a28 100644 --- a/src/course-home/progress-tab/ProgressTab.jsx +++ b/src/course-home/progress-tab/ProgressTab.jsx @@ -1,27 +1,20 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { breakpoints, useWindowSize } from '@openedx/paragon'; +import { useWindowSize } from '@openedx/paragon'; +import { useContextId } from '../../data/hooks'; +import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot'; -import CertificateStatus from './certificate-status/CertificateStatus'; import CourseCompletion from './course-completion/CourseCompletion'; -import CourseGrade from './grades/course-grade/CourseGrade'; -import DetailedGrades from './grades/detailed-grades/DetailedGrades'; -import GradeSummary from './grades/grade-summary/GradeSummary'; import ProgressHeader from './ProgressHeader'; -import RelatedLinks from './related-links/RelatedLinks'; +import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/ProgressTabCertificateStatusMainBodySlot'; +import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot'; +import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot'; +import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot'; import { useModel } from '../../generic/model-store'; const ProgressTab = () => { - const { - courseId, - } = useSelector(state => state.courseHome); - - const { - gradesFeatureIsFullyLocked, disableProgressGraph, - } = useModel('progress', courseId); - - const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; + const courseId = useContextId(); + const { disableProgressGraph } = useModel('progress', courseId); const windowWidth = useWindowSize().width; if (windowWidth === undefined) { @@ -31,7 +24,6 @@ const ProgressTab = () => { return null; } - const wideScreen = windowWidth >= breakpoints.large.minWidth; return ( <> <ProgressHeader /> @@ -39,18 +31,15 @@ const ProgressTab = () => { {/* Main body */} <div className="col-12 col-md-8 p-0"> {!disableProgressGraph && <CourseCompletion />} - {!wideScreen && <CertificateStatus />} - <CourseGrade /> - <div className={`grades my-4 p-4 rounded raised-card ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsFullyLocked}> - <GradeSummary /> - <DetailedGrades /> - </div> + <ProgressTabCertificateStatusMainBodySlot /> + <ProgressTabCourseGradeSlot /> + <ProgressTabGradeBreakdownSlot /> </div> {/* Side panel */} <div className="col-12 col-md-4 p-0 px-md-4"> - {wideScreen && <CertificateStatus />} - <RelatedLinks /> + <ProgressTabCertificateStatusSidePanelSlot /> + <ProgressTabRelatedLinksSlot /> </div> </div> </> diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx index 0d157184..a4ac7da7 100644 --- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx +++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx @@ -1,11 +1,12 @@ import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Button, Card } from '@openedx/paragon'; import { getConfig } from '@edx/frontend-platform'; +import { useContextId } from '../../../data/hooks'; import { useModel } from '../../../generic/model-store'; import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils'; import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links'; @@ -15,9 +16,7 @@ import ProgressCertificateStatusSlot from '../../../plugin-slots/ProgressCertifi const CertificateStatus = () => { const intl = useIntl(); - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { entranceExamData, diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx index 54b6caa9..8c008f0c 100644 --- a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx +++ b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; +import { useContextId } from '../../../data/hooks'; import { useModel } from '../../../generic/model-store'; import CompleteDonutSegment from './CompleteDonutSegment'; @@ -11,9 +11,7 @@ import LockedDonutSegment from './LockedDonutSegment'; import messages from './messages'; const CompletionDonutChart = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { completionSummary: { diff --git a/src/course-home/progress-tab/credit-information/CreditInformation.jsx b/src/course-home/progress-tab/credit-information/CreditInformation.jsx index f1bbcf6a..27843f9b 100644 --- a/src/course-home/progress-tab/credit-information/CreditInformation.jsx +++ b/src/course-home/progress-tab/credit-information/CreditInformation.jsx @@ -1,9 +1,9 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons'; import { Hyperlink, Icon } from '@openedx/paragon'; +import { useContextId } from '../../../data/hooks'; import { useModel } from '../../../generic/model-store'; import { DashboardLink } from '../../../shared/links'; @@ -11,9 +11,7 @@ import { DashboardLink } from '../../../shared/links'; import messages from './messages'; const CreditInformation = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { creditCourseRequirements, diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx index 6aabdc08..c8dfb7e6 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGrade.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; @@ -12,9 +12,7 @@ import CreditInformation from '../../credit-information/CreditInformation'; import messages from '../messages'; const CourseGrade = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { creditCourseRequirements, diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx index e662b137..650e3283 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx @@ -1,19 +1,17 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { CheckCircle, WarningFilled } from '@openedx/paragon/icons'; import { breakpoints, Icon, useWindowSize } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import GradeRangeTooltip from './GradeRangeTooltip'; import messages from '../messages'; const CourseGradeFooter = ({ intl, passingGrade }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { courseGrade: { diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx index 4c4cfc7a..6349240e 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGradeHeader.jsx @@ -1,19 +1,17 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Locked } from '@openedx/paragon/icons'; import { Button, Icon } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; const CourseGradeHeader = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { org, } = useModel('courseHomeMeta', courseId); diff --git a/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx b/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx index b8699370..3ea95785 100644 --- a/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx @@ -1,20 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; import { OverlayTrigger, Popover } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; const CurrentGradeTooltip = ({ intl, tooltipClassName }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { courseGrade: { diff --git a/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx b/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx index 3cbbe5b1..98ed604e 100644 --- a/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx +++ b/src/course-home/progress-tab/grades/course-grade/GradeBar.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import CurrentGradeTooltip from './CurrentGradeTooltip'; import PassingGradeTooltip from './PassingGradeTooltip'; @@ -12,9 +12,7 @@ import PassingGradeTooltip from './PassingGradeTooltip'; import messages from '../messages'; const GradeBar = ({ intl, passingGrade }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { courseGrade: { diff --git a/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx b/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx index 7489e73a..c049cde7 100644 --- a/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx +++ b/src/course-home/progress-tab/grades/course-grade/GradeRangeTooltip.jsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -7,14 +6,13 @@ import { InfoOutline } from '@openedx/paragon/icons'; import { Icon, IconButton, OverlayTrigger, Popover, } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; const GradeRangeTooltip = ({ intl, iconButtonClassName, passingGrade }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { gradesFeatureIsFullyLocked, diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx index 529859c5..deb9dde2 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx @@ -1,11 +1,11 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Blocked } from '@openedx/paragon/icons'; import { Icon, Hyperlink } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import { showUngradedAssignments } from '../../utils'; @@ -15,9 +15,7 @@ import messages from '../messages'; const DetailedGrades = ({ intl }) => { const { administrator } = getAuthenticatedUser(); - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { org, tabs, diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx index f20bae32..4b55e824 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGradesTable.jsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; import { DataTable } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; @@ -12,9 +12,7 @@ import SubsectionTitleCell from './SubsectionTitleCell'; import { showUngradedAssignments } from '../../utils'; const DetailedGradesTable = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { sectionScores, diff --git a/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx b/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx index c3b3cb8b..a1776456 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/SubsectionTitleCell.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; @@ -9,15 +8,14 @@ import { Collapsible, Icon, Row } from '@openedx/paragon'; import { ArrowDropDown, ArrowDropUp, Blocked, Info, } from '@openedx/paragon/icons'; +import { useContextId } from '../../../../data/hooks'; import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; import ProblemScoreDrawer from './ProblemScoreDrawer'; const SubsectionTitleCell = ({ intl, subsection }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { org, } = useModel('courseHomeMeta', courseId); diff --git a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx index 8de9fced..d0602af9 100644 --- a/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/AssignmentTypeCell.jsx @@ -1,18 +1,16 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Blocked } from '@openedx/paragon/icons'; import { Icon } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; const AssignmentTypeCell = ({ intl, assignmentType, footnoteMarker, footnoteId, locked, }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { gradesFeatureIsFullyLocked, diff --git a/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx b/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx index 14f6b2c3..92b78ebe 100644 --- a/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/DroppableAssignmentFootnote.jsx @@ -1,16 +1,15 @@ import React from 'react'; -import { useSelector } from 'react-redux'; + import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useContextId } from '../../../../data/hooks'; import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; const DroppableAssignmentFootnote = ({ footnotes, intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { gradesFeatureIsFullyLocked, } = useModel('progress', courseId); diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx index e6c6b9ad..ffc5e2c8 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx @@ -1,14 +1,13 @@ import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; + +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import GradeSummaryHeader from './GradeSummaryHeader'; import GradeSummaryTable from './GradeSummaryTable'; const GradeSummary = () => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { gradingPolicy: { diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx index fc860c10..6a91061f 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; + import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -7,14 +7,13 @@ import { Icon, IconButton, OverlayTrigger, Popover, } from '@openedx/paragon'; import { Blocked, InfoOutline } from '@openedx/paragon/icons'; +import { useContextId } from '../../../../data/hooks'; import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { gradesFeatureIsFullyLocked, } = useModel('progress', courseId); @@ -28,7 +27,7 @@ const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => { placement="top" show={showTooltip} overlay={( - <Popover> + <Popover id="grade-summary-tooltip"> <Popover.Content className="small text-dark-700"> {intl.formatMessage(messages.gradeSummaryTooltipBody)} </Popover.Content> diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx index 628a65e2..54e0388e 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; import { DataTable } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import AssignmentTypeCell from './AssignmentTypeCell'; @@ -15,9 +15,7 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter'; import messages from '../messages'; const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { gradingPolicy: { diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx index 2c3235be..18ad54d8 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx @@ -1,18 +1,16 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; import { DataTable } from '@openedx/paragon'; +import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; const GradeSummaryTableFooter = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { courseGrade: { diff --git a/src/course-home/progress-tab/related-links/RelatedLinks.jsx b/src/course-home/progress-tab/related-links/RelatedLinks.jsx index e7a6adf3..0030f421 100644 --- a/src/course-home/progress-tab/related-links/RelatedLinks.jsx +++ b/src/course-home/progress-tab/related-links/RelatedLinks.jsx @@ -1,18 +1,16 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Hyperlink } from '@openedx/paragon'; +import { useContextId } from '../../../data/hooks'; import messages from './messages'; import { useModel } from '../../../generic/model-store'; const RelatedLinks = ({ intl }) => { - const { - courseId, - } = useSelector(state => state.courseHome); + const courseId = useContextId(); const { org, tabs, diff --git a/src/data/hooks.ts b/src/data/hooks.ts new file mode 100644 index 00000000..f8ad29be --- /dev/null +++ b/src/data/hooks.ts @@ -0,0 +1,5 @@ +import { useSelector } from 'react-redux'; +import { RootState } from '../store'; + +// eslint-disable-next-line import/prefer-default-export +export const useContextId = () => useSelector<RootState>(state => state.courseHome.courseId); diff --git a/src/index.jsx b/src/index.jsx index 6da653de..972d3c1e 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -26,7 +26,7 @@ import { TabContainer } from './tab-page'; import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data'; import { fetchCourse } from './courseware/data'; -import initializeStore from './store'; +import { store } from './store'; import NoticesProvider from './generic/notices'; import PathFixesProvider from './generic/path-fixes'; import LiveTab from './course-home/live-tab/LiveTab'; @@ -38,7 +38,7 @@ import PageNotFound from './generic/PageNotFound'; subscribe(APP_READY, () => { ReactDOM.render( - <AppProvider store={initializeStore()}> + <AppProvider store={store}> <Helmet> <link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" /> </Helmet> diff --git a/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/README.md b/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/README.md new file mode 100644 index 00000000..f2fe797e --- /dev/null +++ b/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/README.md @@ -0,0 +1,47 @@ +# Progress Tab Certificate Status Slot + +### Slot ID: `progress_tab_certificate_status_main_body_slot` +### Props: + +## Description + +This slot is used to replace or modify the Certificate Status component in the +main body of the Progress Tab. + +## Example + +The following `env.config.jsx` will render the `course_id` of the course as a `<p>` element in a `<div>`. + +![Screenshot of Content added after the Certificate Status Container](./images/progress_tab_certificate_status_slot.png) + +```js +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { useContextId } from './src/data/hooks'; + +const config = { + pluginSlots: { + progress_tab_certificate_status_main_body_slot: { + plugins: [ + { + // Insert custom content after certificate status + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_certificate_status_content', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const courseId = useContextId(); + return ( + <div> + <p>📚: {courseId}</p> + </div> + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/images/progress_tab_certificate_status_slot.png b/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/images/progress_tab_certificate_status_slot.png new file mode 100644 index 00000000..4f5858d4 Binary files /dev/null and b/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/images/progress_tab_certificate_status_slot.png differ diff --git a/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/index.jsx b/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/index.jsx new file mode 100644 index 00000000..563217fb --- /dev/null +++ b/src/plugin-slots/ProgressTabCertificateStatusMainBodySlot/index.jsx @@ -0,0 +1,19 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { breakpoints, useWindowSize } from '@openedx/paragon'; +import CertificateStatus from '../../course-home/progress-tab/certificate-status/CertificateStatus'; + +const ProgressTabCertificateStatusMainBodySlot = () => { + const windowWidth = useWindowSize().width; + const wideScreen = windowWidth >= breakpoints.large.minWidth; + return ( + <PluginSlot + id="progress_tab_certificate_status_main_body_slot" + > + {windowWidth && !wideScreen && <CertificateStatus />} + </PluginSlot> + ); +}; + +ProgressTabCertificateStatusMainBodySlot.propTypes = {}; + +export default ProgressTabCertificateStatusMainBodySlot; diff --git a/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/README.md b/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/README.md new file mode 100644 index 00000000..83f73643 --- /dev/null +++ b/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/README.md @@ -0,0 +1,47 @@ +# Progress Tab Certificate Status Slot + +### Slot ID: `progress_tab_certificate_status_side_panel_slot` +### Props: + +## Description + +This slot is used to replace or modify the Certificate Status component in the +side panel of the Progress Tab. + +## Example + +The following `env.config.jsx` will render the `course_id` of the course as a `<p>` element in a `<div>`. + +![Screenshot of Content added after the Certificate Status Container](./images/progress_tab_certificate_status_slot.png) + +```js +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { useContextId } from './src/data/hooks'; + +const config = { + pluginSlots: { + progress_tab_certificate_status_side_panel_slot: { + plugins: [ + { + // Insert custom content after certificate status + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_certificate_status_content', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const courseId = useContextId(); + return ( + <div> + <p>📚: {courseId}</p> + </div> + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/images/progress_tab_certificate_status_slot.png b/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/images/progress_tab_certificate_status_slot.png new file mode 100644 index 00000000..4f5858d4 Binary files /dev/null and b/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/images/progress_tab_certificate_status_slot.png differ diff --git a/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/index.jsx b/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/index.jsx new file mode 100644 index 00000000..e8354c9f --- /dev/null +++ b/src/plugin-slots/ProgressTabCertificateStatusSidePanelSlot/index.jsx @@ -0,0 +1,19 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { breakpoints, useWindowSize } from '@openedx/paragon'; +import CertificateStatus from '../../course-home/progress-tab/certificate-status/CertificateStatus'; + +const ProgressTabCertificateStatusSidePanelSlot = () => { + const windowWidth = useWindowSize().width; + const wideScreen = windowWidth >= breakpoints.large.minWidth; + return ( + <PluginSlot + id="progress_tab_certificate_status_side_panel_slot" + > + {windowWidth && wideScreen && <CertificateStatus />} + </PluginSlot> + ); +}; + +ProgressTabCertificateStatusSidePanelSlot.propTypes = {}; + +export default ProgressTabCertificateStatusSidePanelSlot; diff --git a/src/plugin-slots/ProgressTabCourseGradeSlot/README.md b/src/plugin-slots/ProgressTabCourseGradeSlot/README.md new file mode 100644 index 00000000..8c0d7381 --- /dev/null +++ b/src/plugin-slots/ProgressTabCourseGradeSlot/README.md @@ -0,0 +1,46 @@ +# Progress Tab Course Grade Slot + +### Slot ID: `progress_tab_course_grade_slot` +### Props: + +## Description + +This slot is used to replace or modify the Course Grades view in the Progress Tab. + +## Example + +The following `env.config.jsx` will render the `course_id` of the course as a `<p>` element in a `<div>`. + +![Screenshot of Content added after the Grades Container](./images/progress_tab_course_grade_slot.png) + +```js +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { useContextId } from './src/data/hooks'; + +const config = { + pluginSlots: { + progress_tab_course_grade_slot: { + plugins: [ + { + // Insert custom content after course grade widget + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_grade_content', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const courseId = useContextId(); + return ( + <div> + <p>📚: {courseId}</p> + </div> + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/ProgressTabCourseGradeSlot/images/progress_tab_course_grade_slot.png b/src/plugin-slots/ProgressTabCourseGradeSlot/images/progress_tab_course_grade_slot.png new file mode 100644 index 00000000..82a15f26 Binary files /dev/null and b/src/plugin-slots/ProgressTabCourseGradeSlot/images/progress_tab_course_grade_slot.png differ diff --git a/src/plugin-slots/ProgressTabCourseGradeSlot/index.jsx b/src/plugin-slots/ProgressTabCourseGradeSlot/index.jsx new file mode 100644 index 00000000..fa4bf956 --- /dev/null +++ b/src/plugin-slots/ProgressTabCourseGradeSlot/index.jsx @@ -0,0 +1,14 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import CourseGrade from '../../course-home/progress-tab/grades/course-grade/CourseGrade'; + +const ProgressTabCourseGradeSlot = () => ( + <PluginSlot + id="progress_tab_course_grade_slot" + > + <CourseGrade /> + </PluginSlot> +); + +ProgressTabCourseGradeSlot.propTypes = {}; + +export default ProgressTabCourseGradeSlot; diff --git a/src/plugin-slots/ProgressTabGradeBreakdownSlot/README.md b/src/plugin-slots/ProgressTabGradeBreakdownSlot/README.md new file mode 100644 index 00000000..85465e69 --- /dev/null +++ b/src/plugin-slots/ProgressTabGradeBreakdownSlot/README.md @@ -0,0 +1,46 @@ +# Progress Tab Grade Breakdown Slot + +### Slot ID: `progress_tab_grade_breakdown_slot` +### Props: + +## Description + +This slot is used to replace or modify the Grade Summary and Details Breakdown view in the Progress Tab. + +## Example + +The following `env.config.jsx` will render the `course_id` of the course as a `<p>` element in a `<div>`. + +![Screenshot of Content added after the Grade Summary and Details Container](./images/progress_tab_grade_breakdown_slot.png) + +```js +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { useContextId } from './src/data/hooks'; + +const config = { + pluginSlots: { + progress_tab_grade_breakdown_slot: { + plugins: [ + { + // Insert custom content after grade summary widget + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_grade_summary_content', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const courseId = useContextId(); + return ( + <div> + <p>📚: {courseId}</p> + </div> + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/ProgressTabGradeBreakdownSlot/images/progress_tab_grade_breakdown_slot.png b/src/plugin-slots/ProgressTabGradeBreakdownSlot/images/progress_tab_grade_breakdown_slot.png new file mode 100644 index 00000000..03df7a4a Binary files /dev/null and b/src/plugin-slots/ProgressTabGradeBreakdownSlot/images/progress_tab_grade_breakdown_slot.png differ diff --git a/src/plugin-slots/ProgressTabGradeBreakdownSlot/index.jsx b/src/plugin-slots/ProgressTabGradeBreakdownSlot/index.jsx new file mode 100644 index 00000000..f54f1f7c --- /dev/null +++ b/src/plugin-slots/ProgressTabGradeBreakdownSlot/index.jsx @@ -0,0 +1,29 @@ +import { useModel } from '@src/generic/model-store'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import React from 'react'; +import DetailedGrades from '../../course-home/progress-tab/grades/detailed-grades/DetailedGrades'; +import GradeSummary from '../../course-home/progress-tab/grades/grade-summary/GradeSummary'; +import { useContextId } from '../../data/hooks'; + +const ProgressTabGradeBreakdownSlot = () => { + const courseId = useContextId(); + const { gradesFeatureIsFullyLocked } = useModel('progress', courseId); + const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : ''; + return ( + <PluginSlot + id="progress_tab_grade_breakdown_slot" + > + <div + className={`grades my-4 p-4 rounded raised-card ${applyLockedOverlay}`} + aria-hidden={gradesFeatureIsFullyLocked} + > + <GradeSummary /> + <DetailedGrades /> + </div> + </PluginSlot> + ); +}; + +ProgressTabGradeBreakdownSlot.propTypes = {}; + +export default ProgressTabGradeBreakdownSlot; diff --git a/src/plugin-slots/ProgressTabRelatedLinksSlot/README.md b/src/plugin-slots/ProgressTabRelatedLinksSlot/README.md new file mode 100644 index 00000000..32ea7610 --- /dev/null +++ b/src/plugin-slots/ProgressTabRelatedLinksSlot/README.md @@ -0,0 +1,46 @@ +# Progress Tab Related Links Slot + +### Slot ID: `progress_tab_related_links_slot` +### Props: + +## Description + +This slot is used to replace or modify the related links view in the Progress Tab. + +## Example + +The following `env.config.jsx` will render the `course_id` of the course as a `<p>` element in a `<div>`. + +![Screenshot of Content added after the Related Links Container](./images/progress_tab_related_links_slot.png) + +```js +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { useContextId } from './src/data/hooks'; + +const config = { + pluginSlots: { + progress_tab_related_links_slot: { + plugins: [ + { + // Insert custom content after related links widget + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_related_links_content', + type: DIRECT_PLUGIN, + RenderWidget: () => { + const courseId = useContextId(); + return ( + <div> + <p>📚: {courseId}</p> + </div> + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/ProgressTabRelatedLinksSlot/images/progress_tab_related_links_slot.png b/src/plugin-slots/ProgressTabRelatedLinksSlot/images/progress_tab_related_links_slot.png new file mode 100644 index 00000000..5ad62f91 Binary files /dev/null and b/src/plugin-slots/ProgressTabRelatedLinksSlot/images/progress_tab_related_links_slot.png differ diff --git a/src/plugin-slots/ProgressTabRelatedLinksSlot/index.jsx b/src/plugin-slots/ProgressTabRelatedLinksSlot/index.jsx new file mode 100644 index 00000000..c91dec1a --- /dev/null +++ b/src/plugin-slots/ProgressTabRelatedLinksSlot/index.jsx @@ -0,0 +1,14 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import RelatedLinks from '../../course-home/progress-tab/related-links/RelatedLinks'; + +const ProgressTabRelatedLinksSlot = () => ( + <PluginSlot + id="progress_tab_related_links_slot" + > + <RelatedLinks /> + </PluginSlot> +); + +ProgressTabRelatedLinksSlot.propTypes = {}; + +export default ProgressTabRelatedLinksSlot; diff --git a/src/store.js b/src/store.ts similarity index 92% rename from src/store.js rename to src/store.ts index 9343b0d2..32a77cda 100644 --- a/src/store.js +++ b/src/store.ts @@ -29,3 +29,7 @@ export default function initializeStore() { }), }); } + +export const store = initializeStore(); + +export type RootState = ReturnType<typeof store.getState>;
5721f77
to
1f04104
Compare
Adds a slot for different components in the progress tab to allow them to be overridden with custom components.
The main aim here is to allow a client to support enabling/disabling individual components on a per-course basis. If this is a feature that will be valuable to the broader community we can change the implementation here to directly support that as well.
This change currently allows overriding individual components. If this is too granular we can also look into creating a slot for the entire page, however that will require more complexity and drift for the overriding component if it just wants to make minor changes.