Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 = ({
Expand Down Expand Up @@ -38,7 +39,7 @@ const SidebarUnit = ({
}}
>
<div className="col-auto p-0">
<UnitIcon type={iconType} isCompleted={completeAndEnabled} />
<CourseOutlineSidebarUnitIconSlot type={iconType} isCompleted={completeAndEnabled} active={isActive} />
</div>
<div className="col-10 p-0 ml-3 text-break">
<span className="align-middle">
Expand Down
Original file line number Diff line number Diff line change
@@ -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('<UnitIcon />', () => {
Object.keys(UNIT_ICON_TYPES).forEach((type) => {
Object.keys(UNIT_ICON_TYPES).forEach((type:UnitIconType) => {
it(`renders default ${type} icon correctly`, () => {
const { container } = render(<UnitIcon type={type} isCompleted={false} />);
const icon = container.querySelector('svg');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
Locked as LockedIcon,
Expand All @@ -10,17 +9,38 @@ import {
LmsVideocam as LmsVideocamIcon,
LmsVideocamComplete as LmsVideocamCompleteIcon,
} from '@openedx/paragon/icons';
import React, { SVGProps } from 'react';

export const UNIT_ICON_TYPES = {
video: 'video',
problem: 'problem',
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<SVGSVGElement> {
type: UnitIconType;
isCompleted: boolean;
}

type IconType = React.ComponentType<SVGProps<SVGSVGElement>>;

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<UnitIconType, IconMapVal> = {
[UNIT_ICON_TYPES.video]: {
default: LmsVideocamIcon,
complete: LmsVideocamCompleteIcon,
Expand All @@ -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 (
<Icon {...props} className={classNames({ 'text-success': isCompleted, 'text-gray-300': !isCompleted })} />
);
};

UnitIcon.propTypes = {
type: PropTypes.oneOf(Object.keys(UNIT_ICON_TYPES)).isRequired,
isCompleted: PropTypes.bool.isRequired,
};

export default UnitIcon;
42 changes: 42 additions & 0 deletions src/plugin-slots/CourseOutlineSidebarUnitIconSlot/README.md
Original file line number Diff line number Diff line change
@@ -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;
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions src/plugin-slots/CourseOutlineSidebarUnitIconSlot/index.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<PluginSlot
id="org.openedx.frontend.learning.course_outline_sidebar_unit_icon.v1"
pluginProps={{
type, isCompleted, active, ...props,
}}
>
<UnitIcon {...props} type={type} isCompleted={isCompleted} />
</PluginSlot>
);
Loading