diff --git a/src/containers/CoursesPanel/CourseList/index.jsx b/src/containers/CoursesPanel/CourseList/index.jsx
index 2f6111ae..2b178b23 100644
--- a/src/containers/CoursesPanel/CourseList/index.jsx
+++ b/src/containers/CoursesPanel/CourseList/index.jsx
@@ -9,9 +9,10 @@ import CourseCard from 'containers/CourseCard';
import { useIsCollapsed } from './hooks';
-export const CourseList = ({
- filterOptions, setPageNumber, numPages, showFilters, visibleList,
-}) => {
+export const CourseList = ({ courseListData }) => {
+ const {
+ filterOptions, setPageNumber, numPages, showFilters, visibleList,
+ } = courseListData;
const isCollapsed = useIsCollapsed();
return (
<>
@@ -38,14 +39,16 @@ export const CourseList = ({
);
};
-CourseList.propTypes = {
+export const courseListDataShape = PropTypes.shape({
showFilters: PropTypes.bool.isRequired,
- // eslint-disable-next-line react/forbid-prop-types
- visibleList: PropTypes.arrayOf(PropTypes.object).isRequired,
- // eslint-disable-next-line react/forbid-prop-types
- filterOptions: PropTypes.object.isRequired,
+ visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
+ filterOptions: PropTypes.shape().isRequired,
numPages: PropTypes.number.isRequired,
setPageNumber: PropTypes.func.isRequired,
+});
+
+CourseList.propTypes = {
+ courseListData: courseListDataShape,
};
export default CourseList;
diff --git a/src/containers/CoursesPanel/CourseList/index.test.jsx b/src/containers/CoursesPanel/CourseList/index.test.jsx
index a2e09b3f..0584eda5 100644
--- a/src/containers/CoursesPanel/CourseList/index.test.jsx
+++ b/src/containers/CoursesPanel/CourseList/index.test.jsx
@@ -23,7 +23,7 @@ describe('CourseList', () => {
useIsCollapsed.mockReturnValue(false);
const createWrapper = (courseListData = defaultCourseListData) => (
- shallow()
+ shallow()
);
describe('no courses or filters', () => {
diff --git a/src/containers/CoursesPanel/__snapshots__/index.test.jsx.snap b/src/containers/CoursesPanel/__snapshots__/index.test.jsx.snap
index 9ec9d18a..16874328 100644
--- a/src/containers/CoursesPanel/__snapshots__/index.test.jsx.snap
+++ b/src/containers/CoursesPanel/__snapshots__/index.test.jsx.snap
@@ -18,11 +18,7 @@ exports[`CoursesPanel no courses snapshot 1`] = `
-
-
-
+
`;
@@ -44,16 +40,16 @@ exports[`CoursesPanel with courses snapshot 1`] = `
-
-
-
+
`;
diff --git a/src/containers/CoursesPanel/index.jsx b/src/containers/CoursesPanel/index.jsx
index dacb8079..6bf525ab 100644
--- a/src/containers/CoursesPanel/index.jsx
+++ b/src/containers/CoursesPanel/index.jsx
@@ -1,15 +1,13 @@
import React from 'react';
-import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import {
CourseFilterControls,
} from 'containers/CourseFilterControls';
-import NoCoursesView from './NoCoursesView';
-
-import CourseList from './CourseList';
+import CourseListSlot from 'plugin-slots/CourseListSlot';
+import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
import { useCourseListData } from './hooks';
@@ -34,19 +32,7 @@ export const CoursesPanel = () => {
- {hasCourses ? (
-
-
-
- ) : (
-
-
-
- )}
+ {hasCourses ? : }
);
};
diff --git a/src/containers/Dashboard/DashboardLayout.jsx b/src/containers/Dashboard/DashboardLayout.jsx
index 8a5db140..d970998c 100644
--- a/src/containers/Dashboard/DashboardLayout.jsx
+++ b/src/containers/Dashboard/DashboardLayout.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Container, Col, Row } from '@openedx/paragon';
-import WidgetSidebar from '../WidgetContainers/WidgetSidebar';
+import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
import hooks from './hooks';
@@ -42,7 +42,7 @@ export const DashboardLayout = ({ children }) => {
{!isCollapsed && (
)}
-
+
diff --git a/src/containers/Dashboard/DashboardLayout.test.jsx b/src/containers/Dashboard/DashboardLayout.test.jsx
index 247ea79c..4e926375 100644
--- a/src/containers/Dashboard/DashboardLayout.test.jsx
+++ b/src/containers/Dashboard/DashboardLayout.test.jsx
@@ -36,9 +36,9 @@ describe('DashboardLayout', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[0].children).not.toHaveLength(0);
});
- it('displays WidgetSidebar in second column', () => {
+ it('displays WidgetSidebarSlot in second column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
- expect(columns[1].findByType('WidgetSidebar')).toHaveLength(1);
+ expect(columns[1].findByType('WidgetSidebarSlot')).toHaveLength(1);
});
};
const testSidebarLayout = () => {
diff --git a/src/containers/Dashboard/__snapshots__/DashboardLayout.test.jsx.snap b/src/containers/Dashboard/__snapshots__/DashboardLayout.test.jsx.snap
index 161196b2..dc7e7a39 100644
--- a/src/containers/Dashboard/__snapshots__/DashboardLayout.test.jsx.snap
+++ b/src/containers/Dashboard/__snapshots__/DashboardLayout.test.jsx.snap
@@ -38,7 +38,7 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
}
}
>
-
+
@@ -82,7 +82,7 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
}
}
>
-
+
@@ -131,7 +131,7 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
>
-
+
@@ -180,7 +180,7 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
>
-
+
diff --git a/src/containers/WidgetContainers/WidgetSidebar/__snapshots__/index.test.jsx.snap b/src/containers/WidgetContainers/WidgetSidebar/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index c2d1774d..00000000
--- a/src/containers/WidgetContainers/WidgetSidebar/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`WidgetSidebar snapshots 1`] = `
-
-`;
diff --git a/src/containers/WidgetContainers/WidgetSidebar/index.jsx b/src/containers/WidgetContainers/WidgetSidebar/index.jsx
deleted file mode 100644
index 744387ce..00000000
--- a/src/containers/WidgetContainers/WidgetSidebar/index.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import classNames from 'classnames';
-
-import { reduxHooks } from 'hooks';
-import { PluginSlot } from '@openedx/frontend-plugin-framework';
-
-// eslint-disable-next-line arrow-body-style
-export const WidgetSidebar = () => {
- const hasCourses = reduxHooks.useHasCourses();
-
- const widgetSidebarClassNames = classNames('widget-sidebar', { 'px-2': !hasCourses });
- const innerWrapperClassNames = classNames('d-flex', { 'flex-column': hasCourses });
-
- return (
-
- );
-};
-
-export default WidgetSidebar;
diff --git a/src/plugin-slots/CourseListSlot/README.md b/src/plugin-slots/CourseListSlot/README.md
new file mode 100644
index 00000000..6ced154d
--- /dev/null
+++ b/src/plugin-slots/CourseListSlot/README.md
@@ -0,0 +1,60 @@
+# Course List Slot
+
+### Slot ID: `course_list_slot`
+
+## Plugin Props
+
+* courseListData
+
+## Description
+
+This slot is used for replacing or adding content around the `CourseList` component. The `CourseListSlot` is only rendered if the learner has enrolled in at least one course.
+
+## Example
+
+The space will show the `CourseList` component by default. This can be disabled in the configuration with the `keepDefault` boolean.
+
+![Screenshot of the CourseListSlot](./images/course_list_slot.png)
+
+Setting the MFE's `env.config.jsx` to the following will replace the default experience with a list of course titles.
+
+![Screenshot of a custom course list](./images/readme_custom_course_list.png)
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+const config = {
+ pluginSlots: {
+ course_list_slot: {
+ // Hide the default CourseList component
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'custom_course_list',
+ type: DIRECT_PLUGIN,
+ priority: 60,
+ RenderWidget: ({ courseListData }) => {
+ // Extract the "visibleList"
+ const courses = courseListData.visibleList;
+ // Render a list of course names
+ return (
+
+ {courses.map(courseData => (
+
+ {courseData.course.courseName}
+
+ ))}
+
+ )
+ },
+ },
+ },
+ ],
+ },
+ },
+}
+
+export default config;
+```
\ No newline at end of file
diff --git a/src/plugin-slots/CourseListSlot/images/course_list_slot.png b/src/plugin-slots/CourseListSlot/images/course_list_slot.png
new file mode 100644
index 00000000..76136d53
Binary files /dev/null and b/src/plugin-slots/CourseListSlot/images/course_list_slot.png differ
diff --git a/src/plugin-slots/CourseListSlot/images/readme_custom_course_list.png b/src/plugin-slots/CourseListSlot/images/readme_custom_course_list.png
new file mode 100644
index 00000000..f47accc8
Binary files /dev/null and b/src/plugin-slots/CourseListSlot/images/readme_custom_course_list.png differ
diff --git a/src/plugin-slots/CourseListSlot/index.jsx b/src/plugin-slots/CourseListSlot/index.jsx
new file mode 100644
index 00000000..d9533978
--- /dev/null
+++ b/src/plugin-slots/CourseListSlot/index.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import { CourseList, courseListDataShape } from 'containers/CoursesPanel/CourseList';
+
+export const CourseListSlot = ({ courseListData }) => (
+
+
+
+);
+
+CourseListSlot.propTypes = {
+ courseListData: courseListDataShape,
+};
+
+export default CourseListSlot;
diff --git a/src/plugin-slots/NoCoursesViewSlot/README.md b/src/plugin-slots/NoCoursesViewSlot/README.md
new file mode 100644
index 00000000..92389fae
--- /dev/null
+++ b/src/plugin-slots/NoCoursesViewSlot/README.md
@@ -0,0 +1,47 @@
+# No Courses View Slot
+
+### Slot ID: `no_courses_view_slot`
+
+## Description
+
+This slot is used for replacing or adding content around the `NoCoursesView` component. The `NoCoursesViewSlot` only renders if the learner has not yet enrolled in any courses.
+
+## Example
+
+The space will show the `NoCoursesView` by default. This can be disabled in the configuration with the `keepDefault` boolean.
+
+![Screenshot of the no courses view](./images/no_courses_view_slot.png)
+
+Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom call-to-action component.
+
+![Screenshot of a custom no courses view](./images/readme_custom_no_courses_view.png)
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+const config = {
+ pluginSlots: {
+ no_courses_view_slot: {
+ // Hide the default NoCoursesView component
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'custom_no_courses_CTA',
+ type: DIRECT_PLUGIN,
+ priority: 60,
+ RenderWidget: () => (
+
+ Check out our catalog of courses and start learning today!
+
+ ),
+ },
+ },
+ ],
+ },
+ },
+}
+
+export default config;
+```
\ No newline at end of file
diff --git a/src/plugin-slots/NoCoursesViewSlot/images/no_course_view_slot.png b/src/plugin-slots/NoCoursesViewSlot/images/no_course_view_slot.png
new file mode 100644
index 00000000..ecbdbacc
Binary files /dev/null and b/src/plugin-slots/NoCoursesViewSlot/images/no_course_view_slot.png differ
diff --git a/src/plugin-slots/NoCoursesViewSlot/images/readme_custom_no_courses.png b/src/plugin-slots/NoCoursesViewSlot/images/readme_custom_no_courses.png
new file mode 100644
index 00000000..6d33e0c8
Binary files /dev/null and b/src/plugin-slots/NoCoursesViewSlot/images/readme_custom_no_courses.png differ
diff --git a/src/plugin-slots/NoCoursesViewSlot/index.jsx b/src/plugin-slots/NoCoursesViewSlot/index.jsx
new file mode 100644
index 00000000..cbb70e41
--- /dev/null
+++ b/src/plugin-slots/NoCoursesViewSlot/index.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
+
+export const NoCoursesViewSlot = () => (
+
+
+
+);
+
+export default NoCoursesViewSlot;
diff --git a/src/plugin-slots/README.md b/src/plugin-slots/README.md
index 30a6a047..5a134b99 100644
--- a/src/plugin-slots/README.md
+++ b/src/plugin-slots/README.md
@@ -1,3 +1,6 @@
# `frontend-app-learner-dashboard` Plugin Slots
* [`footer_slot`](./FooterSlot/)
+* [`widget_sidebar_slot`](./WidgetSidebarSlot/)
+* [`course_list_slot`](./CourseListSlot/)
+* [`no_courses_view_slot`](./NoCoursesViewSlot/)
\ No newline at end of file
diff --git a/src/plugin-slots/WidgetSidebarSlot/README.md b/src/plugin-slots/WidgetSidebarSlot/README.md
new file mode 100644
index 00000000..39fcbf1a
--- /dev/null
+++ b/src/plugin-slots/WidgetSidebarSlot/README.md
@@ -0,0 +1,58 @@
+# Widget Sidebar Slot
+
+### Slot ID: `widget_sidebar_slot`
+
+## Description
+
+This slot is used for adding content to the right-hand sidebar.
+
+## Example
+
+The space will show the `LookingForChallengeWidget` by default. This can be disabled in the configuration with the `keepDefault` boolean.
+
+![Screenshot of the widget sidebar](./images/widget_sidebar_slot.png)
+
+Setting the MFE's `env.config.jsx` to the following will replace the default experience with a custom sidebar component.
+
+![Screenshot of a custom call-to-action in the sidebar](./images/readme_custom_sidebar.png)
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+const config = {
+ pluginSlots: {
+ widget_sidebar_slot: {
+ // Hide the default LookingForChallenge component
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'custom_sidebar_panel',
+ type: DIRECT_PLUGIN,
+ priority: 60,
+ RenderWidget: () => (
+
+
+ Sidebar Menu
+
+
+ sidebar item #1
+
+
+ sidebar item #2
+
+
+ sidebar item #3
+
+
+ ),
+ },
+ },
+ ],
+ },
+ },
+}
+
+export default config;
+```
\ No newline at end of file
diff --git a/src/plugin-slots/WidgetSidebarSlot/__snapshots__/index.test.jsx.snap b/src/plugin-slots/WidgetSidebarSlot/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..8a7ac4d7
--- /dev/null
+++ b/src/plugin-slots/WidgetSidebarSlot/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WidgetSidebar snapshots 1`] = `
+
+`;
diff --git a/src/plugin-slots/WidgetSidebarSlot/images/readme_custom_sidebar.png b/src/plugin-slots/WidgetSidebarSlot/images/readme_custom_sidebar.png
new file mode 100644
index 00000000..370d69d2
Binary files /dev/null and b/src/plugin-slots/WidgetSidebarSlot/images/readme_custom_sidebar.png differ
diff --git a/src/plugin-slots/WidgetSidebarSlot/images/widget_sidebar_slot.png b/src/plugin-slots/WidgetSidebarSlot/images/widget_sidebar_slot.png
new file mode 100644
index 00000000..b2e7f081
Binary files /dev/null and b/src/plugin-slots/WidgetSidebarSlot/images/widget_sidebar_slot.png differ
diff --git a/src/plugin-slots/WidgetSidebarSlot/index.jsx b/src/plugin-slots/WidgetSidebarSlot/index.jsx
new file mode 100644
index 00000000..d4bebe42
--- /dev/null
+++ b/src/plugin-slots/WidgetSidebarSlot/index.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
+
+// eslint-disable-next-line arrow-body-style
+export const WidgetSidebarSlot = () => (
+
+);
+
+export default WidgetSidebarSlot;
diff --git a/src/containers/WidgetContainers/WidgetSidebar/index.test.jsx b/src/plugin-slots/WidgetSidebarSlot/index.test.jsx
similarity index 81%
rename from src/containers/WidgetContainers/WidgetSidebar/index.test.jsx
rename to src/plugin-slots/WidgetSidebarSlot/index.test.jsx
index a8a6e239..591ddccb 100644
--- a/src/containers/WidgetContainers/WidgetSidebar/index.test.jsx
+++ b/src/plugin-slots/WidgetSidebarSlot/index.test.jsx
@@ -1,6 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
-import WidgetSidebar from '.';
+import WidgetSidebarSlot from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
@@ -12,7 +12,7 @@ describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
test('snapshots', () => {
- const wrapper = shallow();
+ const wrapper = shallow();
expect(wrapper.snapshot).toMatchSnapshot();
});
});
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index a81eabbc..cb7f66cf 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -42,7 +42,7 @@ jest.unmock('react-redux');
jest.unmock('reselect');
jest.unmock('hooks');
-jest.mock('containers/WidgetContainers/WidgetSidebar', () => jest.fn(() => 'widget-sidebar'));
+jest.mock('plugin-slots/WidgetSidebarSlot', () => jest.fn(() => 'widget-sidebar'));
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
jest.mock('@edx/frontend-platform', () => ({
diff --git a/src/widgets/LookingForChallengeWidget/index.jsx b/src/widgets/LookingForChallengeWidget/index.jsx
index abef370a..9410c99a 100644
--- a/src/widgets/LookingForChallengeWidget/index.jsx
+++ b/src/widgets/LookingForChallengeWidget/index.jsx
@@ -8,7 +8,7 @@ import { reduxHooks } from 'hooks';
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
import { baseAppUrl } from 'data/services/lms/urls';
-import track from './track';
+import { findCoursesWidgetClicked } from './track';
import messages from './messages';
import './index.scss';
@@ -17,6 +17,8 @@ export const arrowIcon = ();
export const LookingForChallengeWidget = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
+ const hyperlinkDestination = baseAppUrl(courseSearchUrl) || '';
+
return (
{
{formatMessage(messages.findCoursesButton, { arrow: arrowIcon })}
diff --git a/src/widgets/LookingForChallengeWidget/track.js b/src/widgets/LookingForChallengeWidget/track.js
index 70549d80..d18f1e17 100644
--- a/src/widgets/LookingForChallengeWidget/track.js
+++ b/src/widgets/LookingForChallengeWidget/track.js
@@ -8,3 +8,8 @@ export const linkNames = StrictDict({
export const findCoursesWidgetClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.findCoursesWidget,
});
+
+export default {
+ linkNames,
+ findCoursesWidgetClicked,
+};