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

Merged
merged 9 commits into from
May 8, 2025
Merged
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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ECOMMERCE_BASE_URL=''
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
ENTERPRISE_LEARNER_PORTAL_URL=''
EXAMS_BASE_URL=''
FAVICON_URL=''
IGNORED_ERROR_REGEX=''
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
EXAMS_BASE_URL=''
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
Expand Down
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=''
Expand Down Expand Up @@ -48,3 +49,4 @@ TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
4 changes: 2 additions & 2 deletions src/courseware/course/course-exit/CourseCelebration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,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 Down
17 changes: 3 additions & 14 deletions src/courseware/course/course-exit/CourseExit.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';

import { getConfig } from '@edx/frontend-platform';
import { useIntl } 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 = () => {
const intl = useIntl();
const { courseId } = useSelector(state => state.courseware);
const {
certificateData,
Expand Down Expand Up @@ -64,14 +60,7 @@ const CourseExit = () => {

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}
</>
);
Expand Down
36 changes: 4 additions & 32 deletions src/courseware/course/course-exit/DashboardFootnote.jsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';

import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedMessage, useIntl } 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 = ({ variant }) => {
const intl = useIntl();
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 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 })}
/>
);
};
Expand Down
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
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ initialize({
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
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: `org.openedx.frontend.learning.course_exit_view_courses.v1`
### Props:
* `content: { 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: {
'org.openedx.frontend.learning.course_exit_view_courses.v1: {
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,37 @@
import { Button } from '@openedx/paragon';
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';

interface Props {
href: string
}

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

export const CourseExitViewCoursesPluginSlot: React.FC = () => {
const href = `${getConfig().LMS_BASE_URL}/dashboard`;
return (
<PluginSlot
id="org.openedx.frontend.learning.course_exit_view_courses.v1"
slotOptions={{
mergeProps: true,
}}
>
<ViewCoursesLink href={href} />
</PluginSlot>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import CourseRecommendations from '../../../courseware/course/course-exit/CourseRecommendations';

interface Props {
variant: string;
}

export const CourseRecommendationsSlot: React.FC<Props> = ({ variant }: Props) => (
<PluginSlot
id="org.openedx.frontend.learning.course_recommendations.v1"
idAliases={['course_recommendations_slot']}
>
<CourseRecommendations variant={variant} />
</PluginSlot>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Course Exit Dashboard Footnote Link Plugin Slot

### Slot ID: `org.openedx.frontend.learning.course_exit_dashboard_footnote_link.v1`
### 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: {
'org.openedx.frontend.learning.course_exit_dashboard_footnote_link.v1': {
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,49 @@
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 { 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';
import { useContextId } from '../../../data/hooks';

interface LinkProps {
variant: string;
destination: string;
}

const DashboardFootnoteLink: React.FC<LinkProps> = ({ variant, destination }: LinkProps) => {
const intl = useIntl();
const courseId = useContextId();
const { org } = useModel('courseHomeMeta', courseId);
const { administrator } = getAuthenticatedUser();
return (
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={destination}
className="text-reset"
onClick={() => logClick(org, courseId, administrator, 'dashboard_footnote', { variant })}

Check warning on line 26 in src/plugin-slots/CourseExitPluginSlots/DashboardFootnoteLinkPluginSlot/index.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugin-slots/CourseExitPluginSlots/DashboardFootnoteLinkPluginSlot/index.tsx#L26

Added line #L26 was not covered by tests
>
{intl.formatMessage(messages.dashboardLink)}
</Hyperlink>
);
};

interface PluginProps {
variant: string
}

export const DashboardFootnoteLinkPluginSlot: React.FC = ({ variant }: PluginProps) => {
const destination = `${getConfig().LMS_BASE_URL}/dashboard`;
return (
<PluginSlot
id="org.openedx.frontend.learning.course_exit_dashboard_footnote_link.v1"
slotOptions={{
mergeProps: true,
}}
>
<DashboardFootnoteLink variant={variant} destination={destination} />
</PluginSlot>
);
};
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,
};
18 changes: 0 additions & 18 deletions src/plugin-slots/CourseRecommendationsSlot/index.jsx

This file was deleted.