diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx index b2f6bc7249..df91440ecc 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx @@ -1,9 +1,10 @@ +import { CourseOutlineSidebarUnitIconSlot } from '@src/plugin-slots/CourseOutlineSidebarUnitIconSlot'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from '../messages'; -import UnitIcon, { UNIT_ICON_TYPES } from './UnitIcon'; +import { UNIT_ICON_TYPES } from './UnitIcon'; import UnitLinkWrapper from './UnitLinkWrapper'; const SidebarUnit = ({ @@ -38,7 +39,7 @@ const SidebarUnit = ({ }} >
- +
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.tsx similarity index 79% rename from src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.tsx index e5f70d1286..4dd02b4447 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.tsx @@ -1,9 +1,10 @@ +import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; -import UnitIcon, { UNIT_ICON_TYPES } from './UnitIcon'; +import UnitIcon, { UNIT_ICON_TYPES, UnitIconType } from './UnitIcon'; describe('', () => { - Object.keys(UNIT_ICON_TYPES).forEach((type) => { + Object.keys(UNIT_ICON_TYPES).forEach((type:UnitIconType) => { it(`renders default ${type} icon correctly`, () => { const { container } = render(); const icon = container.querySelector('svg'); diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.tsx similarity index 56% rename from src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.tsx index 47992f215a..78672b9a63 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Locked as LockedIcon, @@ -10,6 +9,7 @@ import { LmsVideocam as LmsVideocamIcon, LmsVideocamComplete as LmsVideocamCompleteIcon, } from '@openedx/paragon/icons'; +import React, { SVGProps } from 'react'; export const UNIT_ICON_TYPES = { video: 'video', @@ -17,10 +17,30 @@ export const UNIT_ICON_TYPES = { vertical: 'vertical', lock: 'lock', other: 'other', -}; +} as const; + +export type UnitIconType = typeof UNIT_ICON_TYPES[keyof typeof UNIT_ICON_TYPES]; + +export interface UnitIconProps extends SVGProps { + type: UnitIconType; + isCompleted: boolean; +} + +type IconType = React.ComponentType>; + +interface IconPair { + default: IconType; + complete: IconType; +} -const UnitIcon = ({ type, isCompleted, ...props }) => { - const iconMap = { +type IconMapVal = IconType | IconPair; + +function isIconPair(val: IconMapVal): val is IconPair { + return typeof val === 'object' && 'default' in val && 'complete' in val; +} + +const UnitIcon = ({ type, isCompleted, ...props }: UnitIconProps) => { + const iconMap: Record = { [UNIT_ICON_TYPES.video]: { default: LmsVideocamIcon, complete: LmsVideocamCompleteIcon, @@ -37,20 +57,12 @@ const UnitIcon = ({ type, isCompleted, ...props }) => { }, }; - let Icon = iconMap[type || UNIT_ICON_TYPES.other]; - - if (typeof Icon === 'object') { - Icon = iconMap[type || UNIT_ICON_TYPES.other]?.[isCompleted ? 'complete' : 'default']; - } + const iconEntry = iconMap[type || UNIT_ICON_TYPES.other]; + const Icon: IconType = isIconPair(iconEntry) ? iconEntry[isCompleted ? 'complete' : 'default'] : iconEntry; return ( ); }; -UnitIcon.propTypes = { - type: PropTypes.oneOf(Object.keys(UNIT_ICON_TYPES)).isRequired, - isCompleted: PropTypes.bool.isRequired, -}; - export default UnitIcon; diff --git a/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/README.md b/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/README.md new file mode 100644 index 0000000000..45ff57d67e --- /dev/null +++ b/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/README.md @@ -0,0 +1,42 @@ +# Course Outline Sidebar Unit Icon Slot + +### Slot ID: `org.openedx.frontend.learning.course_outline_sidebar_unit_icon.v1` + +### Props: +* `type`: The type of the unit. +* `isCompleted`: Whether the unit is completed. + +## Description + +This slot is used to replace/modify/hide the unit icon in the course outline sidebar. + +## Example + +### Replaced Unit Icon +![Replaced Unit Icon](./course-outline-unit-icon-replaced.png) + +The following `env.config.jsx` will replace the unit icon with a custom icon. + +```jsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.learning.course_outline_sidebar_unit_icon.v1': { + keepDefault: true, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_unit_icon', + type: DIRECT_PLUGIN, + RenderWidget: ({isCompleted}) => (isCompleted ? '🗹' : '☐'), + }, + }, + ], + }, + }, +}; + +export default config; +``` diff --git a/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/course-outline-unit-icon-replaced.png b/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/course-outline-unit-icon-replaced.png new file mode 100644 index 0000000000..afcf03aea2 Binary files /dev/null and b/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/course-outline-unit-icon-replaced.png differ diff --git a/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/index.tsx b/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/index.tsx new file mode 100644 index 0000000000..4ea731b875 --- /dev/null +++ b/src/plugin-slots/CourseOutlineSidebarUnitIconSlot/index.tsx @@ -0,0 +1,28 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import UnitIcon, { + UNIT_ICON_TYPES, + type UnitIconType, + type UnitIconProps, +} from '@src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon'; +import React from 'react'; + +export interface Props extends UnitIconProps { + active: boolean; +} +export { + UNIT_ICON_TYPES, + type UnitIconType, +}; + +export const CourseOutlineSidebarUnitIconSlot = ({ + type, isCompleted, active, ...props +}: Props) => ( + + + +);