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
+
+
+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) => (
+
+);