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, +};