Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

xitij2000
Copy link
Contributor

@xitij2000 xitij2000 commented Oct 8, 2024

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.

Screenshot 2024-12-12 at 13-07-19 Progress Demonstration Course Nightly

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 8, 2024
@openedx-webhooks
Copy link

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.

🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads

🔘 Get a green build

If 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 @openedx/committers-frontend-app-learning. Tag them in a comment and let them know that your changes are ready for review.

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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@xitij2000
Copy link
Contributor Author

This PR currently doesn't include any screenshots. I will add them before the PR is final depending on the direction of the review.

Copy link

codecov bot commented Oct 8, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 89.88%. Comparing base (4a80532) to head (1f04104).

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.
📢 Have feedback on the report? Share it here.

@itsjeyd itsjeyd added the core contributor PR author is a Core Contributor (who may or may not have write access to this repo). label Oct 11, 2024
@itsjeyd itsjeyd requested a review from a team October 11, 2024 10:10
@itsjeyd itsjeyd added the waiting for eng review PR is ready for review. Review and merge it, or suggest changes. label Oct 11, 2024
@itsjeyd
Copy link

itsjeyd commented Oct 11, 2024

@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

@bradenmacdonald
Copy link
Contributor

@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?

@xitij2000
Copy link
Contributor Author

@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.

@itsjeyd
Copy link

itsjeyd commented Oct 24, 2024

@xitij2000 @bradenmacdonald I'm a little unclear on the current status of this PR.

Are there any blockers to starting engineering review?

@itsjeyd itsjeyd removed the waiting for eng review PR is ready for review. Review and merge it, or suggest changes. label Oct 24, 2024
@bradenmacdonald
Copy link
Contributor

@itsjeyd I don't think so. I just hoping to hear from @arbrandes.

@xitij2000 xitij2000 force-pushed the kshitij/slots-for-progress-page branch from ace68ba to 0c462fa Compare October 30, 2024 05:33
@itsjeyd itsjeyd added the waiting for eng review PR is ready for review. Review and merge it, or suggest changes. label Oct 31, 2024
@itsjeyd
Copy link

itsjeyd commented Oct 31, 2024

@bradenmacdonald OK, got it.

@itsjeyd
Copy link

itsjeyd commented Nov 14, 2024

@bradenmacdonald Do you want to explicitly request a review from him, maybe? That might help with getting this PR unstuck.

CC @xitij2000

@bradenmacdonald
Copy link
Contributor

@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.

Copy link
Contributor

@brian-smith-tcril brian-smith-tcril left a 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.

src/plugin-slots/ProgressTabRelatedLinksSlot/README.md Outdated Show resolved Hide resolved
Comment on lines 8 to 10
pluginProps={{
courseId,
}}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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

Screenshot 2024-11-25 at 10 07 38 AM

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.

Copy link
Contributor Author

@xitij2000 xitij2000 Nov 26, 2024

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>

Copy link
Contributor

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.

src/plugin-slots/ProgressTabGradeBreakdownSlot/index.jsx Outdated Show resolved Hide resolved
src/plugin-slots/ProgressTabCourseGradeSlot/index.jsx Outdated Show resolved Hide resolved
src/plugin-slots/ProgressTabGradeBreakdownSlot/README.md Outdated Show resolved Hide resolved
src/plugin-slots/ProgressTabCourseGradeSlot/README.md Outdated Show resolved Hide resolved
</div>

{/* Side panel */}
<div className="col-12 col-md-4 p-0 px-md-4">
{wideScreen && <CertificateStatus />}
<RelatedLinks />
{wideScreen && <ProgressTabCertificateStatusSlot courseId={courseId} />}
Copy link
Contributor

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 into ProgressTabCertificateStatusMainBodySlot and ProgressTabCertificateStatusSidePanelSlot

    • 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 a SidePanel 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 a DesktopHeaderSlot that wraps the entire desktop header, while the desktop header component contains more granular slots such as DesktopUserMenuSlot

    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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@xitij2000 xitij2000 force-pushed the kshitij/slots-for-progress-page branch from 70b6f6e to 08ef4f7 Compare November 25, 2024 10:53
@xitij2000
Copy link
Contributor Author

@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']),
Copy link
Contributor

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.

Copy link
Contributor Author

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?

@itsjeyd itsjeyd removed the waiting for eng review PR is ready for review. Review and merge it, or suggest changes. label Nov 28, 2024
@xitij2000
Copy link
Contributor Author

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.

@xitij2000 xitij2000 force-pushed the kshitij/slots-for-progress-page branch from 17027a6 to 0206392 Compare December 3, 2024 12:20
@itsjeyd itsjeyd added the waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. label Dec 5, 2024
Copy link
Contributor

@brian-smith-tcril brian-smith-tcril left a 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 ✔️!

@xitij2000 xitij2000 force-pushed the kshitij/slots-for-progress-page branch 2 times, most recently from 1b24b8b to 5721f77 Compare December 10, 2024 07:54
@xitij2000
Copy link
Contributor Author

@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.

Copy link
Contributor

@brian-smith-tcril brian-smith-tcril left a 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!

@pdpinch
Copy link

pdpinch commented Dec 11, 2024

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.

@xitij2000
Copy link
Contributor Author

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.

@itsjeyd
Copy link

itsjeyd commented Dec 12, 2024

@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.

@bradenmacdonald
Copy link
Contributor

@itsjeyd Nope, this is good to merge without further review from me. Thanks.

@itsjeyd
Copy link

itsjeyd commented Dec 19, 2024

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>;
@xitij2000 xitij2000 force-pushed the kshitij/slots-for-progress-page branch from 5721f77 to 1f04104 Compare December 19, 2024 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc.
Projects
Status: In Eng Review
Development

Successfully merging this pull request may close these issues.

6 participants