feat: use navigation sequence metadata to disable navigation components (#1273)
Use navigation_disabled sequence metadata based on Hide From TOC block field, so the student cannot navigate to another sequences in the course outline. https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3853975595/Feature+Enhancement+Proposal+Hide+Sections+from+course+outline
This commit is contained in:
@@ -518,6 +518,7 @@ Object {
|
||||
"hideFromTOC": undefined,
|
||||
"icon": null,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
"navigationDisabled": undefined,
|
||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"showLink": true,
|
||||
"title": "Title of Sequence",
|
||||
|
||||
@@ -154,6 +154,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
||||
showLink: !!block.lms_web_url,
|
||||
title: block.display_name,
|
||||
hideFromTOC: block.hide_from_toc,
|
||||
navigationDisabled: block.navigation_disabled,
|
||||
};
|
||||
break;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const Course = ({
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
|
||||
const navigationDisabled = sequence?.navigationDisabled ?? false;
|
||||
|
||||
const pageTitleBreadCrumbs = [
|
||||
sequence,
|
||||
@@ -76,13 +77,17 @@ const Course = ({
|
||||
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="position-relative d-flex align-items-center mb-4 mt-1">
|
||||
<CourseBreadcrumbs
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
{navigationDisabled || (
|
||||
<>
|
||||
<CourseBreadcrumbs
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shouldDisplayChat && (
|
||||
<>
|
||||
<Chat
|
||||
|
||||
@@ -78,6 +78,27 @@ describe('Course', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('removes breadcrumbs when navigation is disabled', async () => {
|
||||
const sequenceBlocks = [Factory.build(
|
||||
'block',
|
||||
{ type: 'sequential', children: [] },
|
||||
{ courseId: mockData.courseId },
|
||||
)];
|
||||
const sequenceMetadata = [Factory.build(
|
||||
'sequenceMetadata',
|
||||
{ navigation_disabled: true },
|
||||
{ courseId: mockData.courseId, sequenceBlock: sequenceBlocks[0] },
|
||||
)];
|
||||
const testStore = await initializeTestStore({ sequenceBlocks, sequenceMetadata }, false);
|
||||
const testData = {
|
||||
...mockData,
|
||||
sequenceId: sequenceBlocks[0].id,
|
||||
onNavigate: jest.fn(),
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays first section celebration modal', async () => {
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { firstSection: true } });
|
||||
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
|
||||
|
||||
@@ -32,7 +32,12 @@ const SequenceNavigation = ({
|
||||
}) => {
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const {
|
||||
isFirstUnit, isLastUnit, nextLink, previousLink,
|
||||
isFirstUnit,
|
||||
isLastUnit,
|
||||
nextLink,
|
||||
previousLink,
|
||||
navigationDisabledPrevSequence,
|
||||
navigationDisabledNextSequence,
|
||||
} = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||
const {
|
||||
courseId,
|
||||
@@ -68,8 +73,7 @@ const SequenceNavigation = ({
|
||||
const renderPreviousButton = () => {
|
||||
const disabled = isFirstUnit;
|
||||
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
|
||||
|
||||
return (
|
||||
return navigationDisabledPrevSequence || (
|
||||
<Button
|
||||
variant="link"
|
||||
className="previous-btn"
|
||||
@@ -90,7 +94,7 @@ const SequenceNavigation = ({
|
||||
const disabled = isLastUnit && !exitActive;
|
||||
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;
|
||||
|
||||
return (
|
||||
return navigationDisabledNextSequence || (
|
||||
<Button
|
||||
variant="link"
|
||||
className="next-btn"
|
||||
|
||||
@@ -161,4 +161,52 @@ describe('Sequence Navigation', () => {
|
||||
fireEvent.click(screen.getByRole('link', { name: /next/i }));
|
||||
expect(nextHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('removes "Previous" for first unit in sequence when navigation is disabled', async () => {
|
||||
const sequenceBlocks = [Factory.build(
|
||||
'block',
|
||||
{ type: 'sequential', children: unitBlocks.map(block => block.id) },
|
||||
{ courseId: courseMetadata.id },
|
||||
)];
|
||||
const sequenceMetadata = [Factory.build(
|
||||
'sequenceMetadata',
|
||||
{ navigation_disabled: true },
|
||||
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
|
||||
)];
|
||||
const testStore = await initializeTestStore({ unitBlocks, sequenceBlocks, sequenceMetadata }, false);
|
||||
const testData = {
|
||||
...mockData,
|
||||
sequenceId: sequenceBlocks[0].id,
|
||||
onNavigate: jest.fn(),
|
||||
};
|
||||
render(<SequenceNavigation {...testData} unitId={unitBlocks[0].id} />, { store: testStore, wrapWithRouter: true });
|
||||
expect(screen.queryByRole('link', { name: /previous/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes "Next" for last unit in sequence when navigation is disabled', async () => {
|
||||
const sequenceBlocks = [Factory.build(
|
||||
'block',
|
||||
{ type: 'sequential', children: unitBlocks.map(block => block.id) },
|
||||
{ courseId: courseMetadata.id },
|
||||
)];
|
||||
const sequenceMetadata = [Factory.build(
|
||||
'sequenceMetadata',
|
||||
{ navigation_disabled: true },
|
||||
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
|
||||
)];
|
||||
const testStore = await initializeTestStore({ unitBlocks, sequenceBlocks, sequenceMetadata }, false);
|
||||
const testData = {
|
||||
...mockData,
|
||||
sequenceId: sequenceBlocks[0].id,
|
||||
onNavigate: jest.fn(),
|
||||
};
|
||||
render(
|
||||
<SequenceNavigation
|
||||
{...testData}
|
||||
unitId={unitBlocks[unitBlocks.length - 1].id}
|
||||
/>,
|
||||
{ store: testStore, wrapWithRouter: true },
|
||||
);
|
||||
expect(screen.queryByRole('link', { name: /next/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,12 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId)
|
||||
|
||||
// If we don't know the sequence and unit yet, then assume no.
|
||||
if (courseStatus !== 'loaded' || sequenceStatus !== 'loaded' || !currentSequenceId || !currentUnitId) {
|
||||
return { isFirstUnit: false, isLastUnit: false };
|
||||
return {
|
||||
isFirstUnit: false,
|
||||
isLastUnit: false,
|
||||
navigationDisabledNextSequence: false,
|
||||
navigationDisabledPrevSequence: false,
|
||||
};
|
||||
}
|
||||
|
||||
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
|
||||
@@ -25,6 +30,9 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId)
|
||||
const isLastSequence = sequenceIndex === sequenceIds.length - 1;
|
||||
const isLastUnitInSequence = unitIndex === sequence.unitIds.length - 1;
|
||||
const isLastUnit = isLastSequence && isLastUnitInSequence;
|
||||
const sequenceNavigationDisabled = sequence.navigationDisabled;
|
||||
const navigationDisabledPrevSequence = sequenceNavigationDisabled && isFirstUnitInSequence;
|
||||
const navigationDisabledNextSequence = sequenceNavigationDisabled && isLastUnitInSequence;
|
||||
|
||||
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
||||
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
|
||||
@@ -52,6 +60,11 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId)
|
||||
}
|
||||
|
||||
return {
|
||||
isFirstUnit, isLastUnit, nextLink, previousLink,
|
||||
isFirstUnit,
|
||||
isLastUnit,
|
||||
nextLink,
|
||||
previousLink,
|
||||
navigationDisabledNextSequence,
|
||||
navigationDisabledPrevSequence,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ function normalizeSequenceMetadata(sequence) {
|
||||
saveUnitPosition: sequence.save_position,
|
||||
showCompletion: sequence.show_completion,
|
||||
allowProctoringOptOut: sequence.allow_proctoring_opt_out,
|
||||
navigationDisabled: sequence.navigation_disabled,
|
||||
},
|
||||
units: sequence.items.map(unit => ({
|
||||
id: unit.id,
|
||||
|
||||
Reference in New Issue
Block a user