feat: add course end dashboard plugin slots (#1658)

* feat: add additional course end plugin slots

* fix: bring plugin slot names in line with new naming scheme

* refactor: change plugin files to tsx,remove propTypes

* fixup! refactor: change plugin files to tsx,remove propTypes

* fixup! fixup! refactor: change plugin files to tsx,remove propTypes

* fixup! fixup! fixup! refactor: change plugin files to tsx,remove propTypes

* fix: accidentally committed test code

* fix: plugin-slot fixes

* chore: add ENTERPRISE_LEARNER_PORTAL_URL env var
This commit is contained in:
Jansen Kantor
2025-05-08 14:23:41 -04:00
committed by GitHub
parent 73406fbb31
commit b1ee8a3713
17 changed files with 206 additions and 66 deletions

1
.env
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
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';
@@ -10,13 +7,12 @@ 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,
@@ -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}
</>
);

View File

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

View File

@@ -76,6 +76,11 @@ const messages = defineMessages({
defaultMessage: 'Dashboard',
description: 'Link to users 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.',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
import { DashboardFootnoteLinkPluginSlot } from './DashboardFootnoteLinkPluginSlot';
import { CourseRecommendationsSlot } from './CourseRecommendationsSlot';
import { CourseExitViewCoursesPluginSlot } from './CourseExitViewCoursesPluginSlot';
export {
DashboardFootnoteLinkPluginSlot,
CourseRecommendationsSlot,
CourseExitViewCoursesPluginSlot,
};

View File

@@ -1,18 +0,0 @@
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import CourseRecommendations from '../../courseware/course/course-exit/CourseRecommendations';
const CourseRecommendationsSlot = ({ variant }) => (
<PluginSlot
id="org.openedx.frontend.learning.course_recommendations.v1"
idAliases={['course_recommendations_slot']}
>
<CourseRecommendations variant={variant} />
</PluginSlot>
);
CourseRecommendationsSlot.propTypes = {
variant: PropTypes.string.isRequired,
};
export default CourseRecommendationsSlot;