diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx
index 3fddf35a..5caeeefa 100644
--- a/src/course-home/outline-tab/OutlineTab.jsx
+++ b/src/course-home/outline-tab/OutlineTab.jsx
@@ -15,7 +15,6 @@ import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
import CourseTools from './widgets/CourseTools';
import { fetchOutlineTab } from '../data';
import messages from './messages';
-import Section from './Section';
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
@@ -28,6 +27,7 @@ import { useModel } from '../../generic/model-store';
import WelcomeMessage from './widgets/WelcomeMessage';
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
+import CourseHomeSectionOutlineSlot from '../../plugin-slots/CourseHomeSectionOutlineSlot';
const OutlineTab = () => {
const intl = useIntl();
@@ -165,24 +165,18 @@ const OutlineTab = () => {
{rootCourseId && (
<>
-
+
-
- {courses[rootCourseId].sectionIds.map((sectionId) => (
-
- ))}
-
+
>
)}
diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx
index 5db8688a..458bc644 100644
--- a/src/course-home/outline-tab/OutlineTab.test.jsx
+++ b/src/course-home/outline-tab/OutlineTab.test.jsx
@@ -168,7 +168,7 @@ describe('Outline Tab', () => {
course_blocks: { blocks: courseBlocks.blocks },
});
await fetchAndRender();
- expect(screen.getByTitle('Completed section')).toBeInTheDocument();
+ expect(screen.getByLabelText('Completed section')).toBeInTheDocument();
});
it('displays correct icon for incomplete assignment', async () => {
@@ -177,7 +177,7 @@ describe('Outline Tab', () => {
course_blocks: { blocks: courseBlocks.blocks },
});
await fetchAndRender();
- expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
+ expect(screen.getByLabelText('Incomplete section')).toBeInTheDocument();
});
it('SequenceLink displays link', async () => {
@@ -293,7 +293,7 @@ describe('Outline Tab', () => {
expect(showMoreButton).toBeInTheDocument();
});
- fit('dismisses message', async () => {
+ it('dismisses message', async () => {
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
const dismissButton = screen.queryByRole('button', { name: 'Dismiss' });
const expandButton = screen.queryByRole('button', { name: 'Expand all' });
diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx
deleted file mode 100644
index 4e013c73..00000000
--- a/src/course-home/outline-tab/Section.jsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import PropTypes from 'prop-types';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Collapsible, IconButton, Icon } from '@openedx/paragon';
-import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
-import { DisabledVisible } from '@openedx/paragon/icons';
-import SequenceLink from './SequenceLink';
-import { useModel } from '../../generic/model-store';
-
-import genericMessages from '../../generic/messages';
-import messages from './messages';
-
-const Section = ({
- courseId,
- defaultOpen,
- expand,
- intl,
- section,
-}) => {
- const {
- complete,
- sequenceIds,
- title,
- hideFromTOC,
- } = section;
- const {
- courseBlocks: {
- sequences,
- },
- } = useModel('outline', courseId);
-
- const [open, setOpen] = useState(defaultOpen);
-
- useEffect(() => {
- setOpen(expand);
- }, [expand]);
-
- useEffect(() => {
- setOpen(defaultOpen);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const sectionTitle = (
-
-
- {complete ? (
-
- ) : (
-
- )}
-
-
- {title}
-
- , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
-
-
- {hideFromTOC && (
-
- {hideFromTOC && (
-
-
-
- {intl.formatMessage(messages.hiddenSection)}
-
-
- )}
-
- )}
-
- );
-
- return (
-
- { setOpen(!open); }}
- iconWhenClosed={(
- { setOpen(true); }}
- size="sm"
- />
- )}
- iconWhenOpen={(
- { setOpen(false); }}
- size="sm"
- />
- )}
- >
-
- {sequenceIds.map((sequenceId, index) => (
-
- ))}
-
-
-
- );
-};
-
-Section.propTypes = {
- courseId: PropTypes.string.isRequired,
- defaultOpen: PropTypes.bool.isRequired,
- expand: PropTypes.bool.isRequired,
- intl: intlShape.isRequired,
- section: PropTypes.shape().isRequired,
-};
-
-export default injectIntl(Section);
diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx
deleted file mode 100644
index 41af780c..00000000
--- a/src/course-home/outline-tab/SequenceLink.jsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { Link } from 'react-router-dom';
-import {
- FormattedMessage,
- FormattedTime,
- injectIntl,
- intlShape,
-} from '@edx/frontend-platform/i18n';
-import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-icons';
-import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
-import { Icon } from '@openedx/paragon';
-import { Block } from '@openedx/paragon/icons';
-import EffortEstimate from '../../shared/effort-estimate';
-import { useModel } from '../../generic/model-store';
-import messages from './messages';
-
-const SequenceLink = ({
- id,
- intl,
- courseId,
- first,
- sequence,
-}) => {
- const {
- complete,
- description,
- due,
- showLink,
- title,
- hideFromTOC,
- } = sequence;
- const {
- userTimezone,
- } = useModel('outline', courseId);
-
- const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
-
- const coursewareUrl = {title};
- const displayTitle = showLink ? coursewareUrl : title;
-
- const dueDateMessage = (
-
- ),
- description: description || '',
- }}
- />
- );
-
- const noDueDateMessage = (
-
- ),
- description: description || '',
- }}
- />
- );
-
- return (
-
-
-
-
- {complete ? (
-
- ) : (
-
- )}
-
-
- {displayTitle}
-
- , {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
-
-
-
-
- {hideFromTOC && (
-
-
-
-
- {intl.formatMessage(messages.hiddenSequenceLink)}
-
-
-
- )}
-
-
- {due ? dueDateMessage : noDueDateMessage}
-
-
-
-
- );
-};
-
-SequenceLink.propTypes = {
- id: PropTypes.string.isRequired,
- intl: intlShape.isRequired,
- courseId: PropTypes.string.isRequired,
- first: PropTypes.bool.isRequired,
- sequence: PropTypes.shape().isRequired,
-};
-
-export default injectIntl(SequenceLink);
diff --git a/src/course-home/outline-tab/messages.ts b/src/course-home/outline-tab/messages.ts
index 46ac3bd2..4c660fee 100644
--- a/src/course-home/outline-tab/messages.ts
+++ b/src/course-home/outline-tab/messages.ts
@@ -341,6 +341,16 @@ const messages = defineMessages({
defaultMessage: 'Onboarding Past Due',
description: 'Text that show when the deadline of proctortrack onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
},
+ sequenceDueDate: {
+ id: 'learning.outline.sequence-due-date-set',
+ defaultMessage: '{description} due {assignmentDue}',
+ description: 'Used below an assignment title',
+ },
+ sequenceNoDueDate: {
+ id: 'learning.outline.sequence-due-date-not-set',
+ defaultMessage: '{description}',
+ description: 'Used below an assignment title',
+ },
});
export default messages;
diff --git a/src/course-home/outline-tab/section-outline/HiddenSequenceLink.tsx b/src/course-home/outline-tab/section-outline/HiddenSequenceLink.tsx
new file mode 100644
index 00000000..b8253801
--- /dev/null
+++ b/src/course-home/outline-tab/section-outline/HiddenSequenceLink.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Icon } from '@openedx/paragon';
+import { Block } from '@openedx/paragon/icons';
+
+import messages from '../messages';
+
+interface Props {}
+
+const HiddenSequenceLink: React.FC = () => {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+ {intl.formatMessage(messages.hiddenSequenceLink)}
+
+
+
+ );
+};
+
+export default HiddenSequenceLink;
diff --git a/src/course-home/outline-tab/section-outline/Section.tsx b/src/course-home/outline-tab/section-outline/Section.tsx
new file mode 100644
index 00000000..f9052940
--- /dev/null
+++ b/src/course-home/outline-tab/section-outline/Section.tsx
@@ -0,0 +1,94 @@
+import React, { useEffect, useState } from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Collapsible, IconButton } from '@openedx/paragon';
+import { Minus, Plus } from '@openedx/paragon/icons';
+
+import { useModel } from '../../../generic/model-store';
+import genericMessages from '../../../generic/messages';
+import { useContextId } from '../../../data/hooks';
+import messages from '../messages';
+import SectionTitle from './SectionTitle';
+import SequenceLink from './SequenceLink';
+
+interface Props {
+ defaultOpen: boolean;
+ expand: boolean;
+ section: {
+ complete: boolean;
+ sequenceIds: string[];
+ title: string;
+ hideFromTOC: boolean;
+ };
+}
+
+const Section: React.FC = ({
+ defaultOpen,
+ expand,
+ section,
+}) => {
+ const intl = useIntl();
+ const courseId = useContextId();
+ const {
+ complete,
+ sequenceIds,
+ title,
+ hideFromTOC,
+ } = section;
+ const {
+ courseBlocks: {
+ sequences,
+ },
+ } = useModel('outline', courseId);
+
+ const [open, setOpen] = useState(defaultOpen);
+
+ useEffect(() => {
+ setOpen(expand);
+ }, [expand]);
+
+ useEffect(() => {
+ setOpen(defaultOpen);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+ }
+ open={open}
+ onToggle={() => { setOpen(!open); }}
+ iconWhenClosed={(
+ { setOpen(true); }}
+ size="sm"
+ />
+ )}
+ iconWhenOpen={(
+ { setOpen(false); }}
+ size="sm"
+ />
+ )}
+ >
+
+ {sequenceIds.map((sequenceId, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Section;
diff --git a/src/course-home/outline-tab/section-outline/SectionTitle.tsx b/src/course-home/outline-tab/section-outline/SectionTitle.tsx
new file mode 100644
index 00000000..69c4ddfd
--- /dev/null
+++ b/src/course-home/outline-tab/section-outline/SectionTitle.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Icon } from '@openedx/paragon';
+import { CheckCircle, CheckCircleOutline, DisabledVisible } from '@openedx/paragon/icons';
+
+import messages from '../messages';
+
+interface Props {
+ complete: boolean;
+ hideFromTOC: boolean;
+ title: string;
+}
+
+const SectionTitle: React.FC = ({ complete, hideFromTOC, title }) => {
+ const intl = useIntl();
+ return (
+
+
+ {complete ? (
+
+ ) : (
+
+ )}
+
+
+ {title}
+
+ , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
+
+
+ {hideFromTOC && (
+
+ {hideFromTOC && (
+
+
+
+ {intl.formatMessage(messages.hiddenSection)}
+
+
+ )}
+
+ )}
+
+ );
+};
+
+export default SectionTitle;
diff --git a/src/course-home/outline-tab/section-outline/SequenceDueDate.tsx b/src/course-home/outline-tab/section-outline/SequenceDueDate.tsx
new file mode 100644
index 00000000..9c253792
--- /dev/null
+++ b/src/course-home/outline-tab/section-outline/SequenceDueDate.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { FormattedTime, useIntl } from '@edx/frontend-platform/i18n';
+import { useModel } from '../../../generic/model-store';
+
+import { useContextId } from '../../../data/hooks';
+import messages from '../messages';
+
+interface Props {
+ due: string;
+ id: string;
+ description: string;
+}
+
+const SequenceDueDate: React.FC = ({
+ due,
+ id,
+ description,
+}) => {
+ const intl = useIntl();
+ const courseId = useContextId();
+ let dueDateMessage: string | React.ReactNode = intl.formatMessage(
+ messages.sequenceNoDueDate,
+ { description: description || '' },
+ );
+ const {
+ userTimezone,
+ } = useModel('outline', courseId);
+
+ if (due) {
+ const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
+
+ dueDateMessage = intl.formatMessage(
+ messages.sequenceDueDate,
+ {
+ assignmentDue: (
+
+ ),
+ description: description || '',
+ },
+ );
+ }
+
+ return (
+
+
+ {dueDateMessage}
+
+
+ );
+};
+
+export default SequenceDueDate;
diff --git a/src/course-home/outline-tab/section-outline/SequenceLink.tsx b/src/course-home/outline-tab/section-outline/SequenceLink.tsx
new file mode 100644
index 00000000..a0eda629
--- /dev/null
+++ b/src/course-home/outline-tab/section-outline/SequenceLink.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import SequenceDueDate from './SequenceDueDate';
+import HiddenSequenceLink from './HiddenSequenceLink';
+import SequenceTitle from './SequenceTitle';
+
+interface Props {
+ id: string;
+ first: boolean;
+ sequence: {
+ complete: boolean;
+ description: string;
+ due: string;
+ showLink: boolean;
+ title: string;
+ hideFromTOC: boolean;
+ }
+}
+
+const SequenceLink: React.FC = ({
+ id,
+ first,
+ sequence,
+}) => {
+ const {
+ complete,
+ description,
+ due,
+ showLink,
+ title,
+ hideFromTOC,
+ } = sequence;
+
+ return (
+
+
+
+ {hideFromTOC && (
+
+ )}
+
+
+
+ );
+};
+
+export default SequenceLink;
diff --git a/src/course-home/outline-tab/section-outline/SequenceTitle.tsx b/src/course-home/outline-tab/section-outline/SequenceTitle.tsx
new file mode 100644
index 00000000..ec035dfa
--- /dev/null
+++ b/src/course-home/outline-tab/section-outline/SequenceTitle.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Link } from 'react-router-dom';
+import { Icon } from '@openedx/paragon';
+import { CheckCircleOutline, CheckCircle } from '@openedx/paragon/icons';
+
+import EffortEstimate from '../../../shared/effort-estimate';
+import messages from '../messages';
+import { useContextId } from '../../../data/hooks';
+
+interface Props {
+ complete: boolean;
+ showLink: boolean;
+ title: string;
+ sequence: object;
+ id: string;
+}
+
+const SequenceTitle: React.FC = ({
+ complete,
+ showLink,
+ title,
+ sequence,
+ id,
+}) => {
+ const intl = useIntl();
+ const courseId = useContextId();
+ const coursewareUrl = {title};
+ const displayTitle = showLink ? coursewareUrl : title;
+
+ return (
+
+
+ {complete ? (
+
+ ) : (
+
+ )}
+
+
+ {displayTitle}
+
+ , {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
+
+
+
+
+ );
+};
+
+export default SequenceTitle;
diff --git a/src/plugin-slots/CourseHomeSectionOutlineSlot/README.md b/src/plugin-slots/CourseHomeSectionOutlineSlot/README.md
new file mode 100644
index 00000000..322d4bfd
--- /dev/null
+++ b/src/plugin-slots/CourseHomeSectionOutlineSlot/README.md
@@ -0,0 +1,56 @@
+# Course Home Section Outline Slot
+
+### Slot ID: `course_home_section_outline_slot`
+
+## Description
+
+This slot is used to replace/modify/hide the course home section outline.
+
+## Example
+
+### Default content
+
+
+### Added with custom component
+
+
+The following `env.config.jsx` will replace the course home section outline entirely.
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+import Section from '@src/course-home/outline-tab/section-outline/Section';
+
+const config = {
+ pluginSlots: {
+ course_home_section_outline_slot: {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'custom_section_outline_component',
+ type: DIRECT_PLUGIN,
+ RenderWidget: (props) => (
+ <>
+ 📌
+
+ {props.sectionIds.map((sectionId) => (
+
+ ))}
+
+ >
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/CourseHomeSectionOutlineSlot/index.tsx b/src/plugin-slots/CourseHomeSectionOutlineSlot/index.tsx
new file mode 100644
index 00000000..5d7ae6e0
--- /dev/null
+++ b/src/plugin-slots/CourseHomeSectionOutlineSlot/index.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import Section from '@src/course-home/outline-tab/section-outline/Section';
+
+interface Props {
+ expandAll: boolean;
+ sections: object;
+ sectionIds: string[];
+}
+
+const CourseHomeSectionOutlineSlot: React.FC = ({
+ expandAll, sections, sectionIds,
+}) => (
+
+
+ {sectionIds.map((sectionId) => (
+
+ ))}
+
+
+);
+
+export default CourseHomeSectionOutlineSlot;
diff --git a/src/plugin-slots/CourseHomeSectionOutlineSlot/screenshot_custom.png b/src/plugin-slots/CourseHomeSectionOutlineSlot/screenshot_custom.png
new file mode 100644
index 00000000..a335f0bc
Binary files /dev/null and b/src/plugin-slots/CourseHomeSectionOutlineSlot/screenshot_custom.png differ
diff --git a/src/plugin-slots/CourseHomeSectionOutlineSlot/screenshot_default.png b/src/plugin-slots/CourseHomeSectionOutlineSlot/screenshot_default.png
new file mode 100644
index 00000000..370b8518
Binary files /dev/null and b/src/plugin-slots/CourseHomeSectionOutlineSlot/screenshot_default.png differ