Skip to content

feat: add course end dashboard plugin slots #1658

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/courseware/course/course-exit/CourseCelebration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import messages from './messages';
import { useModel } from '../../../generic/model-store';
import { requestCert } from '../../../course-home/data/thunks';
import ProgramCompletion from './ProgramCompletion';
import DashboardFootnote from './DashboardFootnote';
import UpgradeFootnote from './UpgradeFootnote';
import SocialIcons from '../../social-share/SocialIcons';
import { logClick, logVisit } from './utils';
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
import CourseRecommendationsSlot from '../../../plugin-slots/CourseRecommendationsSlot';
import DashboardFootnote from './DashboardFootnote';
import { CourseRecommendationsSlot } from '../../../plugin-slots/CourseExitPluginSlots';

const LINKEDIN_BLUE = '#2867B2';

Expand All @@ -54,11 +54,20 @@ const CourseCelebration = ({ intl }) => {

const {
org,
verifiedMode,
// verifiedMode,
canViewCertificate,
userTimezone,
} = useModel('courseHomeMeta', courseId);

const verifiedMode = {
accessExpirationDate: null,
currency: 'USD',
currencySymbol: '$',
price: 99,
sku: '765F6C2',
upgradeUrl: 'https://commerce-coordinator.edx.org/lms/payment_page_redirect/?sku=765F6C2&course_run_key=course-v1%3AIBM%2BPY0101EN%2B2T2023',
};

const {
certStatus,
certWebViewUrl,
Expand All @@ -84,7 +93,7 @@ const CourseCelebration = ({ intl }) => {
let certHeader;
let visitEvent = 'celebration_generic';
// These cases are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
switch (certStatus) {
switch ('audit_passing') {
case 'downloadable':
certHeader = intl.formatMessage(messages.certificateHeaderDownloadable);
message = (
Expand Down
24 changes: 5 additions & 19 deletions src/courseware/course/course-exit/CourseExit.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';

import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';

import CourseCelebration from './CourseCelebration';
import CourseInProgress from './CourseInProgress';
import CourseNonPassing from './CourseNonPassing';
import { COURSE_EXIT_MODES, getCourseExitMode } from './utils';
import messages from './messages';
import { unsubscribeFromGoalReminders } from './data/thunks';
import { CourseExitViewCoursesPluginSlot } from '../../../plugin-slots/CourseExitPluginSlots';

import { useModel } from '../../../generic/model-store';

const CourseExit = ({ intl }) => {
const CourseExit = () => {
const { courseId } = useSelector(state => state.courseware);
const {
certificateData,
Expand Down Expand Up @@ -63,21 +60,10 @@ const CourseExit = ({ intl }) => {

return (
<>
<div className="row w-100 mt-2 mb-4 justify-content-end">
<Button
variant="outline-primary"
href={`${getConfig().LMS_BASE_URL}/dashboard`}
>
{intl.formatMessage(messages.viewCoursesButton)}
</Button>
</div>
<CourseExitViewCoursesPluginSlot />
{body}
</>
);
};

CourseExit.propTypes = {
intl: intlShape.isRequired,
};

export default injectIntl(CourseExit);
export default CourseExit;
47 changes: 10 additions & 37 deletions src/courseware/course/course-exit/DashboardFootnote.jsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';

import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
import { getConfig } from '@edx/frontend-platform';

import { useModel } from '../../../generic/model-store';

import { DashboardFootnoteLinkPluginSlot } from '../../../plugin-slots/CourseExitPluginSlots';
import Footnote from './Footnote';
import messages from './messages';
import { logClick } from './utils';

const DashboardFootnote = ({ intl, variant }) => {
const { courseId } = useSelector(state => state.courseware);
const { org } = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();

const dashboardLink = (
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={`${getConfig().LMS_BASE_URL}/dashboard`}
className="text-reset"
onClick={() => logClick(org, courseId, administrator, 'dashboard_footnote', { variant })}
>
{intl.formatMessage(messages.dashboardLink)}
</Hyperlink>
);
const DashboardFootnote = ({ variant }) => {
const intl = useIntl();
const dashboardLink = (<DashboardFootnoteLinkPluginSlot variant={variant} />);

return (
<Footnote
icon={faCalendarAlt}
text={(
<FormattedMessage
id="courseCelebration.dashboardInfo" // for historical reasons
defaultMessage="You can access this course and its materials on your {dashboardLink}."
description="Text that precedes link to learner's dashboard"
values={{ dashboardLink }}
/>
)}
text={intl.formatMessage(messages.dashboardInfo, { dashboardLink })}
/>
);
};

DashboardFootnote.propTypes = {
intl: intlShape.isRequired,
content: PropTypes.shape({
dashboardFootnoteUrl: PropTypes.string,
}),
variant: PropTypes.string.isRequired,
};

export default injectIntl(DashboardFootnote);
export default DashboardFootnote;
5 changes: 5 additions & 0 deletions src/courseware/course/course-exit/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ const messages = defineMessages({
defaultMessage: 'Dashboard',
description: 'Link to user’s dashboard',
},
dashboardInfo: {
id: 'courseCelebration.dashboardInfo', // for historical reasons
defaultMessage: 'You can access this course and its materials on your {dashboardLink}.',
description: "Text that precedes link to learner's dashboard",
},
endOfCourseDescription: {
id: 'courseExit.endOfCourseDescription',
defaultMessage: 'Unfortunately, you are not currently eligible for a certificate. You need to receive a passing grade to be eligible for a certificate.',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Course Exit "View Courses" Button Plugin Slot

### Slot ID: `course_exit_view_courses_slot`
### Props:
* `href`

## Description

This slot is used for modifying "View Courses" button in the course exit screen

## Example

The following `env.config.jsx` will make the link link to `example.com`

```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';

const config = {
pluginSlots: {
course_exit_view_courses_slot: {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: (widget) => {
widget.content.href = 'http://www.example.com';
return widget;
}
},
]
},
},
}

export default config;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Button } from '@openedx/paragon';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../../../courseware/course/course-exit/messages';

const ViewCoursesLink = ({ content }) => {
const intl = useIntl();
return (
<div className="row w-100 mt-2 mb-4 justify-content-end">
<Button
variant="outline-primary"
href={content.href}
>
{intl.formatMessage(messages.viewCoursesButton)}
</Button>
</div>
);
};

ViewCoursesLink.propTypes = {
content: PropTypes.shape({
href: PropTypes.string.isRequired,
}).isRequired,
};

const CourseExitViewCoursesPluginSlot = () => {
const href = `${getConfig().LMS_BASE_URL}/dashboard`;
return (
<PluginSlot id="course_exit_view_courses_slot">
<ViewCoursesLink content={{ href }} />
</PluginSlot>
);
};

CourseExitViewCoursesPluginSlot.propTypes = {};

export default CourseExitViewCoursesPluginSlot;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to block this PR if you didn't know, but: whenever creating new files in this repo or any MFE repo, please:

  1. Use TypeScript .tsx instead of .jsx
  2. Use TypeScript types instead of propTypes - propTypes have been deprecated for 8 years!

You also don't need to use default exports anymore, but you can if you want to.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import CourseRecommendations from '../../courseware/course/course-exit/CourseRecommendations';
import CourseRecommendations from '../../../courseware/course/course-exit/CourseRecommendations';

const CourseRecommendationsSlot = ({ variant }) => (
<PluginSlot id="course_recommendations_slot">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Course Exit Dashboard Footnote Link Plugin Slot

### Slot ID: `course_exit_dashboard_footnote_link_slot`
### Props:
* `variant`
* `content: { destination }`

## Description

This slot is used for modifying the link to the learner dashboard in the footnote on the course exit page

## Example

The following `env.config.jsx` will change the link to point to `example.com`

![Screenshot of modified course celebration](./screenshot_custom.png)

```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';

const config = {
pluginSlots: {
course_exit_dashboard_footnote_link_slot: {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: (widget) => {
widget.content.destination = 'http://www.example.com';
return widget;
}
},
]
},
},
}

export default config;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import { Hyperlink } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import messages from '../../../courseware/course/course-exit/messages';
import { logClick } from '../../../courseware/course/course-exit/utils';
import { useModel } from '../../../generic/model-store';

const DashboardFootnoteLink = ({ variant, content }) => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const { org } = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
const { destination } = content;
return (
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={destination}
className="text-reset"
onClick={() => logClick(org, courseId, administrator, 'dashboard_footnote', { variant })}
>
{intl.formatMessage(messages.dashboardLink)}
</Hyperlink>
);
};

DashboardFootnoteLink.propTypes = {
variant: PropTypes.string.isRequired,
content: PropTypes.shape({
destination: PropTypes.string.isRequired,
}).isRequired,
};

const DashboardFootnoteLinkPluginSlot = ({ variant }) => {
const destination = `${getConfig().LMS_BASE_URL}/dashboard`;
return (
<PluginSlot id="course_exit_dashboard_footnote_link_slot">
<DashboardFootnoteLink variant={variant} content={{ destination }} />
</PluginSlot>
);
};

DashboardFootnoteLinkPluginSlot.propTypes = {
variant: PropTypes.string.isRequired,
};

export default DashboardFootnoteLinkPluginSlot;
9 changes: 9 additions & 0 deletions src/plugin-slots/CourseExitPluginSlots/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import DashboardFootnoteLinkPluginSlot from './DashboardFootnoteLinkPluginSlot';
import CourseRecommendationsSlot from './CourseRecommendationsSlot';
import CourseExitViewCoursesPluginSlot from './CourseExitViewCoursesPluginSlot';

export {
DashboardFootnoteLinkPluginSlot,
CourseRecommendationsSlot,
CourseExitViewCoursesPluginSlot,
};
Loading