+ {/**
+ SequenceNavigationSlot renders nothing by default.
+ However, we still pass nextHandler, previousHandler, and onNavigate,
+ because, as per the slot's contract, if this slot is replaced
+ with the default SequenceNavigation component, these props are required.
+ These handlers are excluded from test coverage via istanbul ignore,
+ since they are not used unless the slot is overridden.
+ */}
+
+
{unitHasLoaded && renderUnitNavigation(false)}
diff --git a/src/courseware/course/sequence/Sequence.test.jsx b/src/courseware/course/sequence/Sequence.test.jsx
index 5473e24c30..ae58bb18fc 100644
--- a/src/courseware/course/sequence/Sequence.test.jsx
+++ b/src/courseware/course/sequence/Sequence.test.jsx
@@ -24,7 +24,6 @@ describe('Sequence', () => {
{ type: 'vertical' },
{ courseId: courseMetadata.id },
));
- const enableNavigationSidebar = { enable_navigation_sidebar: false };
beforeAll(async () => {
const store = await initializeTestStore({ courseMetadata, unitBlocks });
@@ -96,7 +95,6 @@ describe('Sequence', () => {
unitBlocks,
sequenceBlocks,
sequenceMetadata,
- enableNavigationSidebar: { enable_navigation_sidebar: true },
}, false);
const { container } = render(
,
@@ -131,7 +129,7 @@ describe('Sequence', () => {
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({
- courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata, enableNavigationSidebar,
+ courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata,
}, false);
render(
,
@@ -190,7 +188,7 @@ describe('Sequence', () => {
beforeAll(async () => {
testStore = await initializeTestStore({
- courseMetadata, unitBlocks, sequenceBlocks, enableNavigationSidebar,
+ courseMetadata, unitBlocks, sequenceBlocks,
}, false);
});
@@ -366,7 +364,6 @@ describe('Sequence', () => {
unitBlocks,
sequenceBlocks: testSequenceBlocks,
sequenceMetadata: testSequenceMetadata,
- enableNavigationSidebar,
}, false);
const testData = {
...mockData,
diff --git a/src/courseware/course/sequence/SequenceContent.jsx b/src/courseware/course/sequence/SequenceContent.jsx
index 905ffbf255..6caa0fce27 100644
--- a/src/courseware/course/sequence/SequenceContent.jsx
+++ b/src/courseware/course/sequence/SequenceContent.jsx
@@ -16,7 +16,6 @@ const SequenceContent = ({
unitId,
unitLoadedHandler,
isOriginalUserStaff,
- isEnabledOutlineSidebar,
renderUnitNavigation,
}) => {
const intl = useIntl();
@@ -63,7 +62,6 @@ const SequenceContent = ({
id={unitId}
onLoaded={unitLoadedHandler}
isOriginalUserStaff={isOriginalUserStaff}
- isEnabledOutlineSidebar={isEnabledOutlineSidebar}
renderUnitNavigation={renderUnitNavigation}
/>
);
@@ -76,7 +74,6 @@ SequenceContent.propTypes = {
unitId: PropTypes.string,
unitLoadedHandler: PropTypes.func.isRequired,
isOriginalUserStaff: PropTypes.bool.isRequired,
- isEnabledOutlineSidebar: PropTypes.bool.isRequired,
renderUnitNavigation: PropTypes.func.isRequired,
};
diff --git a/src/courseware/course/sequence/SequenceContent.test.jsx b/src/courseware/course/sequence/SequenceContent.test.jsx
index a2f14490d3..e9c3a2d785 100644
--- a/src/courseware/course/sequence/SequenceContent.test.jsx
+++ b/src/courseware/course/sequence/SequenceContent.test.jsx
@@ -15,6 +15,7 @@ describe('Sequence Content', () => {
sequenceId: courseware.sequenceId,
unitId: models.sequences[courseware.sequenceId].unitIds[0],
unitLoadedHandler: () => { },
+ renderUnitNavigation: () => { },
};
});
@@ -38,7 +39,7 @@ describe('Sequence Content', () => {
});
it('displays message for no content', () => {
- render(
, { wrapWithRouter: true });
+ render(
, { wrapWithRouter: true });
expect(screen.getByText('There is no content here.')).toBeInTheDocument();
});
});
diff --git a/src/courseware/course/sequence/Unit/index.jsx b/src/courseware/course/sequence/Unit/index.jsx
index 37eb396d88..3a9c71bc94 100644
--- a/src/courseware/course/sequence/Unit/index.jsx
+++ b/src/courseware/course/sequence/Unit/index.jsx
@@ -22,7 +22,6 @@ const Unit = ({
onLoaded,
id,
isOriginalUserStaff,
- isEnabledOutlineSidebar,
renderUnitNavigation,
}) => {
const { formatMessage } = useIntl();
@@ -48,7 +47,7 @@ const Unit = ({
return (
-
+
enabled && 'UnitNaviagtion'),
};
@@ -68,16 +67,8 @@ describe(' ', () => {
expect(screen.getByText('Bookmark this page')).toBeInTheDocument();
});
- it('does not render unit navigation buttons', () => {
- renderComponent(defaultProps);
-
- const nextButton = screen.queryByText('UnitNaviagtion');
-
- expect(nextButton).toBeNull();
- });
-
- it('renders unit navigation buttons when isEnabledOutlineSidebar is true', () => {
- const props = { ...defaultProps, isEnabledOutlineSidebar: true };
+ it('renders unit navigation buttons', () => {
+ const props = { ...defaultProps };
renderComponent(props);
const nextButton = screen.getByText('UnitNaviagtion');
diff --git a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
index 22f61c478a..81cd5a6bc7 100644
--- a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
+++ b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
@@ -100,13 +100,13 @@ const SequenceNavigation = ({
);
};
- return sequenceStatus === LOADED && (
+ return sequenceStatus === LOADED ? (
{renderPreviousButton()}
{renderUnitButtons()}
{renderNextButton()}
- );
+ ) : null;
};
SequenceNavigation.propTypes = {
diff --git a/src/courseware/course/sidebar/SidebarContextProvider.jsx b/src/courseware/course/sidebar/SidebarContextProvider.jsx
index 05472c01dd..9b3b824d53 100644
--- a/src/courseware/course/sidebar/SidebarContextProvider.jsx
+++ b/src/courseware/course/sidebar/SidebarContextProvider.jsx
@@ -1,13 +1,11 @@
import { breakpoints, useWindowSize } from '@openedx/paragon';
import PropTypes from 'prop-types';
-import { useSelector } from 'react-redux';
import {
useEffect, useState, useMemo, useCallback,
} from 'react';
import { useModel } from '@src/generic/model-store';
import { getLocalStorage, setLocalStorage } from '@src/data/localStorage';
-import { getCoursewareOutlineSidebarSettings } from '../../data/selectors';
import * as discussionsSidebar from './sidebars/discussions';
import * as notificationsSidebar from './sidebars/notifications';
@@ -25,11 +23,10 @@ const SidebarProvider = ({
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.extraLarge.minWidth;
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.extraLarge.minWidth;
const query = new URLSearchParams(window.location.search);
- const { alwaysOpenAuxiliarySidebar } = useSelector(getCoursewareOutlineSidebarSettings);
const isInitiallySidebarOpen = shouldDisplaySidebarOpen || query.get('sidebar') === 'true';
let initialSidebar = shouldDisplayFullScreen ? getLocalStorage(`sidebar.${courseId}`) : null;
- if (!shouldDisplayFullScreen && isInitiallySidebarOpen && alwaysOpenAuxiliarySidebar) {
+ if (!shouldDisplayFullScreen && isInitiallySidebarOpen) {
initialSidebar = isUnitHasDiscussionTopics
? SIDEBARS[discussionsSidebar.ID].ID
: verifiedMode && SIDEBARS[notificationsSidebar.ID].ID;
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx
index a2ebfaf0af..d30a49aff0 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx
@@ -24,7 +24,6 @@ const CourseOutlineTray = () => {
const {
courseId,
unitId,
- isEnabledSidebar,
currentSidebar,
handleToggleCollapse,
isActiveEntranceExam,
@@ -78,7 +77,7 @@ const CourseOutlineTray = () => {
);
- if (!isEnabledSidebar || isActiveEntranceExam || currentSidebar !== ID) {
+ if (isActiveEntranceExam || currentSidebar !== ID) {
return null;
}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
index 5fda3616a7..1c75bbf322 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
@@ -67,15 +67,6 @@ describe('
', () => {
expect(screen.queryByRole('button', { name: 'Course outline' })).not.toBeInTheDocument();
});
- it('doesn\'t render when outline sidebar is disabled', async () => {
- await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: false } });
- renderWithProvider();
-
- await expect(screen.queryByText(messages.loading.defaultMessage)).not.toBeInTheDocument();
- expect(screen.queryByRole('button', { name: section.title })).not.toBeInTheDocument();
- expect(screen.queryByRole('button', { name: messages.toggleCourseOutlineTrigger.defaultMessage })).not.toBeInTheDocument();
- });
-
it('renders correctly when course outline is loaded', async () => {
await initTestStore();
renderWithProvider();
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx
index dfc698de3d..abccd14aed 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx
@@ -15,13 +15,12 @@ const CourseOutlineTrigger = ({ isMobileView }) => {
shouldDisplayFullScreen,
handleToggleCollapse,
isActiveEntranceExam,
- isEnabledSidebar,
} = useCourseOutlineSidebar();
const isDisplayForDesktopView = !isMobileView && !shouldDisplayFullScreen && currentSidebar !== ID;
const isDisplayForMobileView = isMobileView && shouldDisplayFullScreen;
- if ((!isDisplayForDesktopView && !isDisplayForMobileView) || !isEnabledSidebar || isActiveEntranceExam) {
+ if ((!isDisplayForDesktopView && !isDisplayForMobileView) || isActiveEntranceExam) {
return null;
}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx
index cca273db71..3b931acdad 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx
@@ -45,7 +45,7 @@ describe('
', () => {
it('renders correctly for desktop when sidebar is enabled', async () => {
const user = userEvent.setup();
const mockToggleSidebar = jest.fn();
- await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: true } });
+ await initTestStore();
renderWithProvider({ toggleSidebar: mockToggleSidebar }, { isMobileView: false });
const toggleButton = await screen.getByRole('button', {
@@ -62,7 +62,7 @@ describe('
', () => {
it('renders correctly for mobile when sidebar is enabled', async () => {
const user = userEvent.setup();
const mockToggleSidebar = jest.fn();
- await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: true } });
+ await initTestStore();
renderWithProvider({
toggleSidebar: mockToggleSidebar,
shouldDisplayFullScreen: true,
@@ -82,7 +82,7 @@ describe('
', () => {
it('changes current sidebar value on click', async () => {
const user = userEvent.setup();
const mockToggleSidebar = jest.fn();
- await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: true } });
+ await initTestStore();
renderWithProvider({
toggleSidebar: mockToggleSidebar,
shouldDisplayFullScreen: true,
@@ -99,14 +99,4 @@ describe('
', () => {
expect(mockToggleSidebar).toHaveBeenCalledTimes(1);
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
});
-
- it('does not render when isEnabled is false', async () => {
- await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: false } });
- renderWithProvider({}, { isMobileView: false });
-
- const toggleButton = await screen.queryByRole('button', {
- name: messages.toggleCourseOutlineTrigger.defaultMessage,
- });
- expect(toggleButton).not.toBeInTheDocument();
- });
});
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx b/src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx
index c685fe59d2..ea56491878 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx
@@ -22,7 +22,9 @@ import { ID } from './constants';
export const useCourseOutlineSidebar = () => {
const dispatch = useDispatch();
const isCollapsedOutlineSidebar = window.sessionStorage.getItem('hideCourseOutlineSidebar');
- const { enableNavigationSidebar: isEnabledSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
+ const {
+ enableCompletionTracking: isEnabledCompletionTracking,
+ } = useSelector(getCoursewareOutlineSidebarSettings);
const courseOutlineShouldUpdate = useSelector(getCourseOutlineShouldUpdate);
const courseOutlineStatus = useSelector(getCourseOutlineStatus);
const sequenceStatus = useSelector(getSequenceStatus);
@@ -42,7 +44,7 @@ export const useCourseOutlineSidebar = () => {
shouldDisplayFullScreen,
} = useContext(SidebarContext);
- const isOpenSidebar = !initialSidebar && isEnabledSidebar && !isCollapsedOutlineSidebar;
+ const isOpenSidebar = !initialSidebar && !isCollapsedOutlineSidebar;
const [isOpen, setIsOpen] = useState(true);
const {
@@ -99,17 +101,17 @@ export const useCourseOutlineSidebar = () => {
}, [initialSidebar, unitId]);
useEffect(() => {
- if ((isEnabledSidebar && courseOutlineStatus !== LOADED) || courseOutlineShouldUpdate) {
+ if (courseOutlineStatus !== LOADED || courseOutlineShouldUpdate) {
dispatch(getCourseOutlineStructure(courseId));
}
- }, [courseId, isEnabledSidebar, courseOutlineShouldUpdate]);
+ }, [courseId, courseOutlineShouldUpdate]);
return {
courseId,
unitId,
currentSidebar,
shouldDisplayFullScreen,
- isEnabledSidebar,
+ isEnabledCompletionTracking,
isOpen,
setIsOpen,
handleToggleCollapse,
diff --git a/src/courseware/data/api.js b/src/courseware/data/api.js
index 199aa37cbc..f7e45fae05 100644
--- a/src/courseware/data/api.js
+++ b/src/courseware/data/api.js
@@ -104,16 +104,15 @@ export async function getCourseOutline(courseId) {
}
/**
- * Get waffle flag value that enable courseware outline sidebar and always open auxiliary sidebar.
+ * Get waffle flag value that enables completion tracking.
* @param {string} courseId - The unique identifier for the course.
- * @returns {Promise<{enable_navigation_sidebar: boolean, enable_navigation_sidebar: boolean}>} - The object
- * of boolean values of enabling of the outline sidebar and is always open auxiliary sidebar.
+ * @returns {Promise<{enable_completion_tracking: boolean}>} - The object
+ * of boolean values of enabling of the completion tracking.
*/
export async function getCoursewareOutlineSidebarToggles(courseId) {
const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-navigation-sidebar/toggles/`);
const { data } = await getAuthenticatedHttpClient().get(url.href);
return {
- enable_navigation_sidebar: data.enable_navigation_sidebar || false,
- always_open_auxiliary_sidebar: data.always_open_auxiliary_sidebar || false,
+ enable_completion_tracking: data.enable_completion_tracking || false,
};
}
diff --git a/src/courseware/data/redux.test.js b/src/courseware/data/redux.test.js
index 2c8f5469a1..5a2c54c8fa 100644
--- a/src/courseware/data/redux.test.js
+++ b/src/courseware/data/redux.test.js
@@ -111,8 +111,7 @@ describe('Data layer integration tests', () => {
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
- enable_navigation_sidebar: true,
- always_open_auxiliary_sidebar: true,
+ enable_completion_tracking: true,
});
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
@@ -124,8 +123,7 @@ describe('Data layer integration tests', () => {
expect(state.courseware.sequenceStatus).toEqual('loading');
expect(state.courseware.sequenceId).toEqual(null);
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
- enableNavigationSidebar: true,
- alwaysOpenAuxiliarySidebar: true,
+ enableCompletionTracking: true,
});
// check that at least one key camel cased, thus course data normalized
@@ -139,8 +137,7 @@ describe('Data layer integration tests', () => {
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, simpleOutline);
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
- enable_navigation_sidebar: false,
- always_open_auxiliary_sidebar: false,
+ enable_completion_tracking: false,
});
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
@@ -152,8 +149,7 @@ describe('Data layer integration tests', () => {
expect(state.courseware.sequenceStatus).toEqual('loading');
expect(state.courseware.sequenceId).toEqual(null);
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
- enableNavigationSidebar: false,
- alwaysOpenAuxiliarySidebar: false,
+ enableCompletionTracking: false,
});
// check that at least one key camel cased, thus course data normalized
diff --git a/src/courseware/data/thunks.js b/src/courseware/data/thunks.js
index 3f84fbf221..2312cf4daa 100644
--- a/src/courseware/data/thunks.js
+++ b/src/courseware/data/thunks.js
@@ -88,10 +88,11 @@ export function fetchCourse(courseId) {
if (fetchedCoursewareOutlineSidebarTogglesResult) {
const {
- enable_navigation_sidebar: enableNavigationSidebar,
- always_open_auxiliary_sidebar: alwaysOpenAuxiliarySidebar,
+ enable_completion_tracking: enableCompletionTracking,
} = coursewareOutlineSidebarTogglesResult.value;
- dispatch(setCoursewareOutlineSidebarToggles({ enableNavigationSidebar, alwaysOpenAuxiliarySidebar }));
+ dispatch(setCoursewareOutlineSidebarToggles(
+ { enableCompletionTracking },
+ ));
}
// Log errors for each request if needed. Outline failures may occur
diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/README.md b/src/plugin-slots/CourseBreadcrumbsSlot/README.md
index 12863d4e08..4538e41412 100644
--- a/src/plugin-slots/CourseBreadcrumbsSlot/README.md
+++ b/src/plugin-slots/CourseBreadcrumbsSlot/README.md
@@ -14,6 +14,44 @@ This slot is used to replace/modify/hide the course breadcrumbs.
### Default content

+### Replace with default breadcrumbs component
+You can also inject the default `CourseBreadcrumbs` component explicitly using the slot system, for example to wrap or style it differently.
+
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+import CourseBreadcrumbs from './src/courseware/course/breadcrumbs';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.learning.course_breadcrumbs.v1': {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'default_breadcrumbs_component',
+ type: DIRECT_PLUGIN,
+ RenderWidget: ({ courseId, sectionId, sequenceId, isStaff, unitId }) => (
+
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
+
### Replaced with custom component

diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx b/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx
index 8a438968a1..3f672476f8 100644
--- a/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx
+++ b/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx
@@ -2,8 +2,6 @@ import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
-import CourseBreadcrumbs from '../../courseware/course/breadcrumbs';
-
interface Props {
courseId: string;
sectionId?: string;
@@ -21,13 +19,12 @@ export const CourseBreadcrumbsSlot : React.FC
= ({
slotOptions={{
mergeProps: true,
}}
- >
-
-
+ pluginProps={{
+ courseId,
+ sectionId,
+ sequenceId,
+ unitId,
+ isStaff,
+ }}
+ />
);
diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png
index c10ca827d5..a2e223c364 100644
Binary files a/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png and b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png differ
diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_with_default_breadcrumbs.png b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_with_default_breadcrumbs.png
new file mode 100644
index 0000000000..c10ca827d5
Binary files /dev/null and b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_with_default_breadcrumbs.png differ
diff --git a/src/plugin-slots/README.md b/src/plugin-slots/README.md
index 6444bbaf8e..be410164d2 100644
--- a/src/plugin-slots/README.md
+++ b/src/plugin-slots/README.md
@@ -23,4 +23,5 @@
* [`org.openedx.frontend.learning.progress_tab_grade_breakdown.v1`](./ProgressTabGradeBreakdownSlot/)
* [`org.openedx.frontend.learning.progress_tab_related_links.v1`](./ProgressTabRelatedLinksSlot/)
* [`org.openedx.frontend.learning.sequence_container.v1`](./SequenceContainerSlot/)
+* [`org.openedx.frontend.learning.sequence_navigation.v1`](./SequenceNavigationSlot/)
* [`org.openedx.frontend.learning.unit_title.v1`](./UnitTitleSlot/)
diff --git a/src/plugin-slots/SequenceNavigationSlot/README.md b/src/plugin-slots/SequenceNavigationSlot/README.md
new file mode 100644
index 0000000000..ba2ffeb9d7
--- /dev/null
+++ b/src/plugin-slots/SequenceNavigationSlot/README.md
@@ -0,0 +1,118 @@
+# Sequence Navigation Slot
+
+### Slot ID: `org.openedx.frontend.learning.sequence_navigation.v1`
+
+### Props:
+* `sequenceId` (string) — Current sequence identifier
+* `unitId` (string) — Current unit identifier
+* `nextHandler` (function) — Handler for next navigation action
+* `onNavigate` (function) — Handler for direct unit navigation
+* `previousHandler` (function) — Handler for previous navigation action
+
+## Description
+
+This slot is used to replace/modify/hide the sequence navigation component that controls navigation between units within a course sequence.
+
+## Example
+
+### Default content
+
+
+### Replace with default sequence navigation component
+You can also inject the default `SequenceNavigation` component explicitly using the slot system, for example to wrap or style it differently.
+
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+import { SequenceNavigation } from './src/courseware/course/sequence/sequence-navigation';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.learning.sequence_navigation.v1': {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'custom_sequence_navigation',
+ type: DIRECT_PLUGIN,
+ RenderWidget: ({ sequenceId, unitId, nextHandler, onNavigate, previousHandler }) => (
+
+ ),
+ },
+ },
+ ],
+ },
+ },
+};
+
+export default config;
+```
+
+### Replaced with a custom component
+
+
+The following `env.config.jsx` will replace the sequence navigation with a custom implementation that uses all available props.
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.learning.sequence_navigation.v1': {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'custom_sequence_navigation',
+ type: DIRECT_PLUGIN,
+ RenderWidget: ({ sequenceId, unitId, nextHandler, onNavigate, previousHandler }) => {
+ // Mock unit data for demonstration
+ const units = ['unit-1', 'unit-2', 'unit-3'];
+
+ return (
+
+
+ ⬅️ Previous
+
+
+ {units.map((unit, index) => (
+ onNavigate(unit)}
+ >
+ {index + 1}
+
+ ))}
+
+
+ Next ➡️
+
+
+ )
+ },
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/SequenceNavigationSlot/index.jsx b/src/plugin-slots/SequenceNavigationSlot/index.jsx
new file mode 100644
index 0000000000..b4ceb2f409
--- /dev/null
+++ b/src/plugin-slots/SequenceNavigationSlot/index.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+const SequenceNavigationSlot = ({
+ sequenceId,
+ unitId,
+ nextHandler,
+ onNavigate,
+ previousHandler,
+}) => (
+
+);
+
+SequenceNavigationSlot.propTypes = {
+ sequenceId: PropTypes.string.isRequired,
+ unitId: PropTypes.string.isRequired,
+ nextHandler: PropTypes.func.isRequired,
+ onNavigate: PropTypes.func.isRequired,
+ previousHandler: PropTypes.func.isRequired,
+};
+
+export default SequenceNavigationSlot;
diff --git a/src/plugin-slots/SequenceNavigationSlot/screenshot_custom.png b/src/plugin-slots/SequenceNavigationSlot/screenshot_custom.png
new file mode 100644
index 0000000000..b2bb10a91b
Binary files /dev/null and b/src/plugin-slots/SequenceNavigationSlot/screenshot_custom.png differ
diff --git a/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png b/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png
new file mode 100644
index 0000000000..93b8c0cb5f
Binary files /dev/null and b/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png differ
diff --git a/src/plugin-slots/SequenceNavigationSlot/screenshot_with_default_nav.png b/src/plugin-slots/SequenceNavigationSlot/screenshot_with_default_nav.png
new file mode 100644
index 0000000000..036cca1c3f
Binary files /dev/null and b/src/plugin-slots/SequenceNavigationSlot/screenshot_with_default_nav.png differ
diff --git a/src/plugin-slots/UnitTitleSlot/README.md b/src/plugin-slots/UnitTitleSlot/README.md
index 4da59b47b3..9308ddfddb 100644
--- a/src/plugin-slots/UnitTitleSlot/README.md
+++ b/src/plugin-slots/UnitTitleSlot/README.md
@@ -8,12 +8,13 @@
### Props:
* `unitId`
* `unit`
-* `isEnabledOutlineSidebar`
* `renderUnitNavigation`
## Description
This slot is used for adding content before or after the Unit title.
+`isEnabledOutlineSidebar` is no longer used in the default implementation,
+but is still passed as a plugin prop with a default value of `true` for backward compatibility.
## Example
@@ -34,9 +35,9 @@ const config = {
widget: {
id: 'custom_unit_title_content',
type: DIRECT_PLUGIN,
- RenderWidget: ({ unitId, unit, isEnabledOutlineSidebar, renderUnitNavigation }) => (
+ RenderWidget: ({ unitId, unit, renderUnitNavigation }) => (
<>
- {isEnabledOutlineSidebar && renderUnitNavigation(true)}
+ {renderUnitNavigation(true)}
📙: {unit.title}
📙: {unitId}
>
diff --git a/src/plugin-slots/UnitTitleSlot/index.jsx b/src/plugin-slots/UnitTitleSlot/index.jsx
index f753efc921..d21ef42eb0 100644
--- a/src/plugin-slots/UnitTitleSlot/index.jsx
+++ b/src/plugin-slots/UnitTitleSlot/index.jsx
@@ -8,7 +8,6 @@ import messages from '@src/courseware/course/sequence/messages';
const UnitTitleSlot = ({
unitId,
unit,
- isEnabledOutlineSidebar,
renderUnitNavigation,
}) => {
const { formatMessage } = useIntl();
@@ -21,7 +20,7 @@ const UnitTitleSlot = ({
pluginProps={{
unitId,
unit,
- isEnabledOutlineSidebar,
+ isEnabledOutlineSidebar: true,
renderUnitNavigation,
}}
>
@@ -29,7 +28,7 @@ const UnitTitleSlot = ({
{unit.title}
- {isEnabledOutlineSidebar && renderUnitNavigation(true)}
+ {renderUnitNavigation(true)}