feat: Add plugin slots for progress page components (#1496)

This commit is contained in:
Kshitij Sobti
2025-01-16 23:26:07 +05:30
committed by GitHub
parent 826f1382dd
commit d5a6a59d07
39 changed files with 394 additions and 104 deletions

View File

@@ -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';

View File

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

View File

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

View File

@@ -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: {

View File

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

View File

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

View File

@@ -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: {

View File

@@ -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);

View File

@@ -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: {

View File

@@ -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: {

View File

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

View File

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

View File

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

View File

@@ -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);

View File

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

View File

@@ -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);

View File

@@ -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: {

View File

@@ -1,18 +1,16 @@
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, OverlayTrigger, Tooltip } 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 = ({ allOfSomeAssignmentTypeIsLocked }) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
const courseId = useContextId();
const {
gradesFeatureIsFullyLocked,
} = useModel('progress', courseId);

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { getLocale, isRtl, useIntl } 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';
@@ -13,9 +13,7 @@ import messages from '../messages';
const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
const intl = useIntl();
const {
courseId,
} = useSelector(state => state.courseHome);
const courseId = useContextId();
const {
gradingPolicy: {

View File

@@ -1,5 +1,4 @@
import { useContext } from 'react';
import { useSelector } from 'react-redux';
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
import {
@@ -11,6 +10,7 @@ import {
Tooltip,
} from '@openedx/paragon';
import { InfoOutline } from '@openedx/paragon/icons';
import { useContextId } from '../../../../data/hooks';
import { useModel } from '../../../../generic/model-store';
import messages from '../messages';
@@ -29,9 +29,7 @@ const GradeSummaryTableFooter = () => {
0,
).toFixed(2);
const {
courseId,
} = useSelector(state => state.courseHome);
const courseId = useContextId();
const {
courseGrade: {

View File

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

5
src/data/hooks.ts Normal file
View File

@@ -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);

View File

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

View File

@@ -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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

@@ -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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

@@ -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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

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

View File

@@ -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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

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

View File

@@ -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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

View File

@@ -29,3 +29,7 @@ export default function initializeStore() {
}),
});
}
export const store = initializeStore();
export type RootState = ReturnType<typeof store.getState>;