feat: add functionality to see unit draft preview (#1501)
* feat: add functionality to see unit draft preview * feat: add tests for course link redirects * fix: course redirect unit to sequnce unit redirect * fix: test coverage
This commit is contained in:
@@ -13,6 +13,8 @@ export const DECODE_ROUTES = {
|
|||||||
'/course/:courseId/:sequenceId/:unitId',
|
'/course/:courseId/:sequenceId/:unitId',
|
||||||
'/course/:courseId/:sequenceId',
|
'/course/:courseId/:sequenceId',
|
||||||
'/course/:courseId',
|
'/course/:courseId',
|
||||||
|
'/preview/course/:courseId/:sequenceId/:unitId',
|
||||||
|
'/preview/course/:courseId/:sequenceId',
|
||||||
],
|
],
|
||||||
REDIRECT_HOME: 'home/:courseId',
|
REDIRECT_HOME: 'home/:courseId',
|
||||||
REDIRECT_SURVEY: 'survey/:courseId',
|
REDIRECT_SURVEY: 'survey/:courseId',
|
||||||
|
|||||||
@@ -19,62 +19,50 @@ import { handleNextSectionCelebration } from './course/celebration';
|
|||||||
import withParamsAndNavigation from './utils';
|
import withParamsAndNavigation from './utils';
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId, navigate) => {
|
export const checkResumeRedirect = memoize(
|
||||||
if (courseStatus === 'loaded' && !sequenceId) {
|
(courseStatus, courseId, sequenceId, firstSequenceId, navigate, isPreview) => {
|
||||||
// Note that getResumeBlock is just an API call, not a redux thunk.
|
if (courseStatus === 'loaded' && !sequenceId) {
|
||||||
getResumeBlock(courseId).then((data) => {
|
// Note that getResumeBlock is just an API call, not a redux thunk.
|
||||||
// This is a replace because we don't want this change saved in the browser's history.
|
getResumeBlock(courseId).then((data) => {
|
||||||
if (data.sectionId && data.unitId) {
|
// This is a replace because we don't want this change saved in the browser's history.
|
||||||
navigate(`/course/${courseId}/${data.sectionId}/${data.unitId}`, { replace: true });
|
if (data.sectionId && data.unitId) {
|
||||||
} else if (firstSequenceId) {
|
const baseUrl = `/course/${courseId}/${data.sectionId}`;
|
||||||
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
|
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||||
}
|
navigate(`${sequenceUrl}/${data.unitId}`, { replace: true });
|
||||||
});
|
} else if (firstSequenceId) {
|
||||||
}
|
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
|
||||||
});
|
}
|
||||||
|
}, () => {});
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
|
||||||
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
|
|
||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
|
|
||||||
navigate(`/course/${courseId}/${unitId}`, { replace: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
|
||||||
const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
|
|
||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
|
|
||||||
// If the section is non-empty, redirect to its first sequence.
|
|
||||||
if (section.sequenceIds && section.sequenceIds[0]) {
|
|
||||||
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
|
|
||||||
// Otherwise, just go to the course root, letting the resume redirect take care of things.
|
|
||||||
} else {
|
|
||||||
navigate(`/course/${courseId}`, { replace: true });
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
|
export const checkSectionUnitToUnitRedirect = memoize((
|
||||||
|
courseStatus,
|
||||||
|
courseId,
|
||||||
|
sequenceStatus,
|
||||||
|
section,
|
||||||
|
unitId,
|
||||||
|
navigate,
|
||||||
|
isPreview,
|
||||||
|
) => {
|
||||||
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
|
||||||
|
const baseUrl = `/course/${courseId}`;
|
||||||
|
const courseUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||||
|
navigate(`${courseUrl}/${unitId}`, { replace: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
const checkUnitToSequenceUnitRedirect = memoize(
|
export const checkSectionToSequenceRedirect = memoize(
|
||||||
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId, navigate) => {
|
(courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
|
||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
|
||||||
if (sequenceMightBeUnit) {
|
// If the section is non-empty, redirect to its first sequence.
|
||||||
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
|
if (section.sequenceIds && section.sequenceIds[0]) {
|
||||||
// we need to look up the correct parent sequence for it, and redirect there.
|
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
|
||||||
const unitId = sequenceId; // just for clarity during the rest of this method
|
// Otherwise, just go to the course root, letting the resume redirect take care of things.
|
||||||
getSequenceForUnitDeprecated(courseId, unitId).then(
|
|
||||||
parentId => {
|
|
||||||
if (parentId) {
|
|
||||||
navigate(`/course/${courseId}/${parentId}/${unitId}`, { replace: true });
|
|
||||||
} else {
|
|
||||||
navigate(`/course/${courseId}`, { replace: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() => { // error case
|
|
||||||
navigate(`/course/${courseId}`, { replace: true });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Invalid sequence that isn't a unit either. Redirect up to main course.
|
|
||||||
navigate(`/course/${courseId}`, { replace: true });
|
navigate(`/course/${courseId}`, { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,41 +70,80 @@ const checkUnitToSequenceUnitRedirect = memoize(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId, navigate) => {
|
export const checkUnitToSequenceUnitRedirect = memoize((
|
||||||
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
|
courseStatus,
|
||||||
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
|
courseId,
|
||||||
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
|
sequenceStatus,
|
||||||
// This is a replace because we don't want this change saved in the browser's history.
|
sequenceMightBeUnit,
|
||||||
navigate(`/course/${courseId}/${sequence.id}/${nextUnitId}`, { replace: true });
|
sequenceId,
|
||||||
|
section,
|
||||||
|
routeUnitId,
|
||||||
|
navigate,
|
||||||
|
isPreview,
|
||||||
|
) => {
|
||||||
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
||||||
|
if (sequenceMightBeUnit) {
|
||||||
|
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
|
||||||
|
// we need to look up the correct parent sequence for it, and redirect there.
|
||||||
|
const unitId = sequenceId; // just for clarity during the rest of this method
|
||||||
|
getSequenceForUnitDeprecated(courseId, unitId).then(
|
||||||
|
parentId => {
|
||||||
|
if (parentId) {
|
||||||
|
const baseUrl = `/course/${courseId}/${parentId}`;
|
||||||
|
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||||
|
navigate(`${sequenceUrl}/${unitId}`, { replace: true });
|
||||||
|
} else {
|
||||||
|
navigate(`/course/${courseId}`, { replace: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => { // error case
|
||||||
|
navigate(`/course/${courseId}`, { replace: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Invalid sequence that isn't a unit either. Redirect up to main course.
|
||||||
|
navigate(`/course/${courseId}`, { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Look at where this is called in componentDidUpdate for more info about its usage
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
|
export const checkSequenceToSequenceUnitRedirect = memoize(
|
||||||
(courseId, sequenceStatus, sequence, unitId, navigate) => {
|
(courseId, sequenceStatus, sequence, unitId, navigate, isPreview) => {
|
||||||
|
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
|
||||||
|
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
|
||||||
|
const baseUrl = `/course/${courseId}/${sequence.id}`;
|
||||||
|
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||||
|
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
|
||||||
|
// This is a replace because we don't want this change saved in the browser's history.
|
||||||
|
navigate(`${sequenceUrl}/${nextUnitId}`, { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Look at where this is called in componentDidUpdate for more info about its usage
|
||||||
|
export const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
|
||||||
|
(courseId, sequenceStatus, sequence, unitId, navigate, isPreview) => {
|
||||||
if (sequenceStatus !== 'loaded' || !sequence.id) {
|
if (sequenceStatus !== 'loaded' || !sequence.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseUrl = `/course/${courseId}/${sequence.id}`;
|
||||||
const hasUnits = sequence.unitIds?.length > 0;
|
const hasUnits = sequence.unitIds?.length > 0;
|
||||||
|
|
||||||
if (unitId === 'first') {
|
if (hasUnits) {
|
||||||
if (hasUnits) {
|
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||||
|
if (unitId === 'first') {
|
||||||
const firstUnitId = sequence.unitIds[0];
|
const firstUnitId = sequence.unitIds[0];
|
||||||
navigate(`/course/${courseId}/${sequence.id}/${firstUnitId}`, { replace: true });
|
navigate(`${sequenceUrl}/${firstUnitId}`, { replace: true });
|
||||||
} else {
|
} else if (unitId === 'last') {
|
||||||
// No units... go to general sequence page
|
|
||||||
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
|
|
||||||
}
|
|
||||||
} else if (unitId === 'last') {
|
|
||||||
if (hasUnits) {
|
|
||||||
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
|
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
|
||||||
navigate(`/course/${courseId}/${sequence.id}/${lastUnitId}`, { replace: true });
|
navigate(`${sequenceUrl}/${lastUnitId}`, { replace: true });
|
||||||
} else {
|
|
||||||
// No units... go to general sequence page
|
|
||||||
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No units... go to general sequence page
|
||||||
|
navigate(baseUrl, { replace: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -169,6 +196,7 @@ class CoursewareContainer extends Component {
|
|||||||
routeSequenceId,
|
routeSequenceId,
|
||||||
routeUnitId,
|
routeUnitId,
|
||||||
navigate,
|
navigate,
|
||||||
|
isPreview,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Load data whenever the course or sequence ID changes.
|
// Load data whenever the course or sequence ID changes.
|
||||||
@@ -197,7 +225,7 @@ class CoursewareContainer extends Component {
|
|||||||
// Check resume redirect:
|
// Check resume redirect:
|
||||||
// /course/:courseId -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId -> /course/:courseId/:sequenceId/:unitId
|
||||||
// based on sequence/unit where user was last active.
|
// based on sequence/unit where user was last active.
|
||||||
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate);
|
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate, isPreview);
|
||||||
|
|
||||||
// Check section-unit to unit redirect:
|
// Check section-unit to unit redirect:
|
||||||
// /course/:courseId/:sectionId/:unitId -> /course/:courseId/:unitId
|
// /course/:courseId/:sectionId/:unitId -> /course/:courseId/:unitId
|
||||||
@@ -210,33 +238,69 @@ class CoursewareContainer extends Component {
|
|||||||
// otherwise, we could get stuck in a redirect loop, since a sequence that failed to load
|
// otherwise, we could get stuck in a redirect loop, since a sequence that failed to load
|
||||||
// would endlessly redirect to itself through `checkSectionUnitToUnitRedirect`
|
// would endlessly redirect to itself through `checkSectionUnitToUnitRedirect`
|
||||||
// and `checkUnitToSequenceUnitRedirect`.
|
// and `checkUnitToSequenceUnitRedirect`.
|
||||||
checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
|
checkSectionUnitToUnitRedirect(
|
||||||
|
courseStatus,
|
||||||
|
courseId,
|
||||||
|
sequenceStatus,
|
||||||
|
sectionViaSequenceId,
|
||||||
|
routeUnitId,
|
||||||
|
navigate,
|
||||||
|
isPreview,
|
||||||
|
);
|
||||||
|
|
||||||
// Check section to sequence redirect:
|
// Check section to sequence redirect:
|
||||||
// /course/:courseId/:sectionId -> /course/:courseId/:sequenceId
|
// /course/:courseId/:sectionId -> /course/:courseId/:sequenceId
|
||||||
// by redirecting to the first sequence within the section.
|
// by redirecting to the first sequence within the section.
|
||||||
checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
|
checkSectionToSequenceRedirect(
|
||||||
|
courseStatus,
|
||||||
|
courseId,
|
||||||
|
sequenceStatus,
|
||||||
|
sectionViaSequenceId,
|
||||||
|
routeUnitId,
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
|
||||||
// Check unit to sequence-unit redirect:
|
// Check unit to sequence-unit redirect:
|
||||||
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
|
||||||
// by filling in the ID of the parent sequence of :unitId.
|
// by filling in the ID of the parent sequence of :unitId.
|
||||||
checkUnitToSequenceUnitRedirect((
|
checkUnitToSequenceUnitRedirect(
|
||||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit,
|
courseStatus,
|
||||||
sequenceId, sectionViaSequenceId, routeUnitId, navigate
|
courseId,
|
||||||
));
|
sequenceStatus,
|
||||||
|
sequenceMightBeUnit,
|
||||||
|
sequenceId,
|
||||||
|
sectionViaSequenceId,
|
||||||
|
routeUnitId,
|
||||||
|
navigate,
|
||||||
|
isPreview,
|
||||||
|
);
|
||||||
|
|
||||||
// Check sequence to sequence-unit redirect:
|
// Check sequence to sequence-unit redirect:
|
||||||
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
||||||
// by filling in the ID the most-recently-active unit in the sequence, OR
|
// by filling in the ID the most-recently-active unit in the sequence, OR
|
||||||
// the ID of the first unit the sequence if none is active.
|
// the ID of the first unit the sequence if none is active.
|
||||||
checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
courseId,
|
||||||
|
sequenceStatus,
|
||||||
|
sequence,
|
||||||
|
routeUnitId,
|
||||||
|
navigate,
|
||||||
|
isPreview,
|
||||||
|
);
|
||||||
|
|
||||||
// Check sequence-unit marker to sequence-unit redirect:
|
// Check sequence-unit marker to sequence-unit redirect:
|
||||||
// /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId
|
||||||
// /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId
|
// /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId
|
||||||
// by filling in the ID the first or last unit in the sequence.
|
// by filling in the ID the first or last unit in the sequence.
|
||||||
// "Sequence unit marker" is an invented term used only in this component.
|
// "Sequence unit marker" is an invented term used only in this component.
|
||||||
checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
courseId,
|
||||||
|
sequenceStatus,
|
||||||
|
sequence,
|
||||||
|
routeUnitId,
|
||||||
|
navigate,
|
||||||
|
isPreview,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnitNavigationClick = () => {
|
handleUnitNavigationClick = () => {
|
||||||
@@ -334,6 +398,7 @@ CoursewareContainer.propTypes = {
|
|||||||
fetchCourse: PropTypes.func.isRequired,
|
fetchCourse: PropTypes.func.isRequired,
|
||||||
fetchSequence: PropTypes.func.isRequired,
|
fetchSequence: PropTypes.func.isRequired,
|
||||||
navigate: PropTypes.func.isRequired,
|
navigate: PropTypes.func.isRequired,
|
||||||
|
isPreview: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CoursewareContainer.defaultProps = {
|
CoursewareContainer.defaultProps = {
|
||||||
|
|||||||
@@ -16,11 +16,19 @@ import tabMessages from '../tab-page/messages';
|
|||||||
import { initializeMockApp, waitFor } from '../setupTest';
|
import { initializeMockApp, waitFor } from '../setupTest';
|
||||||
import { DECODE_ROUTES } from '../constants';
|
import { DECODE_ROUTES } from '../constants';
|
||||||
|
|
||||||
import CoursewareContainer from './CoursewareContainer';
|
import CoursewareContainer, {
|
||||||
|
checkResumeRedirect,
|
||||||
|
checkSectionToSequenceRedirect,
|
||||||
|
checkSectionUnitToUnitRedirect,
|
||||||
|
checkSequenceToSequenceUnitRedirect,
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect,
|
||||||
|
checkUnitToSequenceUnitRedirect,
|
||||||
|
} from './CoursewareContainer';
|
||||||
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
|
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
|
||||||
import initializeStore from '../store';
|
import initializeStore from '../store';
|
||||||
import { appendBrowserTimezoneToUrl } from '../utils';
|
import { appendBrowserTimezoneToUrl } from '../utils';
|
||||||
import { buildOutlineFromBlocks } from './data/__factories__/learningSequencesOutline.factory';
|
import { buildOutlineFromBlocks } from './data/__factories__/learningSequencesOutline.factory';
|
||||||
|
import { getSequenceForUnitDeprecatedUrl } from './data/api';
|
||||||
|
|
||||||
// NOTE: Because the unit creates an iframe, we choose to mock it out as its rendering isn't
|
// NOTE: Because the unit creates an iframe, we choose to mock it out as its rendering isn't
|
||||||
// pertinent to this test. Instead, we render a simple div that displays the properties we expect
|
// pertinent to this test. Instead, we render a simple div that displays the properties we expect
|
||||||
@@ -525,3 +533,838 @@ describe('CoursewareContainer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Course redirect functions', () => {
|
||||||
|
let navigate;
|
||||||
|
let axiosMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
navigate = jest.fn();
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isPreview equals true', () => {
|
||||||
|
describe('checkSequenceUnitMarkerToSequenceUnitRedirect', () => {
|
||||||
|
it('return when sequence is not loaded', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loading',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1'] },
|
||||||
|
'first',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return when sequence id is null', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: null, unitIds: ['unit_1'] },
|
||||||
|
'first',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with first unit id', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||||
|
'first',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/preview/course/courseId/sequence_1/unit_1';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with last unit id', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||||
|
'last',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/preview/course/courseId/sequence_1/unit_2';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkSequenceToSequenceUnitRedirect', () => {
|
||||||
|
it('calls navigate with next unit id', () => {
|
||||||
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/preview/course/courseId/sequence_1/unit_1';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequence status is loading', () => {
|
||||||
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loading',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequence id is null', () => {
|
||||||
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loading',
|
||||||
|
{ unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when unit id is defined', () => {
|
||||||
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||||
|
'unit_2',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when unit ids are undefiend', () => {
|
||||||
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', activeUnitIndex: 0 },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkUnitToSequenceUnitRedirect', () => {
|
||||||
|
const { href: apiUrl } = getSequenceForUnitDeprecatedUrl('courseId');
|
||||||
|
|
||||||
|
it('calls navigate with parentId and sequenceId', () => {
|
||||||
|
const getSequenceForUnitDeprecated = jest.fn();
|
||||||
|
axiosMock.onGet(apiUrl).reply(200, {
|
||||||
|
blocks: [{
|
||||||
|
id: 'sequence_1',
|
||||||
|
type: 'sequential',
|
||||||
|
children: ['unit_1'],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate to course page when getSequenceForUnitDeprecated errors', () => {
|
||||||
|
const getSequenceForUnitDeprecated = jest.fn();
|
||||||
|
axiosMock.onGet(apiUrl).reply(404);
|
||||||
|
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate to course page when no parent id is returned', () => {
|
||||||
|
const getSequenceForUnitDeprecated = jest.fn();
|
||||||
|
axiosMock.onGet(apiUrl).reply(200, {
|
||||||
|
blocks: [{
|
||||||
|
id: 'sequence_1',
|
||||||
|
type: 'sequential',
|
||||||
|
children: ['block_1'],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate to course page when sequnce is not unit', () => {
|
||||||
|
const getSequenceForUnitDeprecated = jest.fn();
|
||||||
|
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
false,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getSequenceForUnitDeprecated).not.toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when course status is loading', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loading',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequence status is not failed', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when section is defined', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when routeUnitId is defined', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
'unit_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkSectionUnitToUnitRedirect', () => {
|
||||||
|
it('calls navigate with unitId', () => {
|
||||||
|
checkSectionUnitToUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_2',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/preview/course/courseId/unit_2';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when course status is loading', () => {
|
||||||
|
checkSectionUnitToUnitRedirect(
|
||||||
|
'loading',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_2',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequence status is loading', () => {
|
||||||
|
checkSectionUnitToUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_2',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when section is null', () => {
|
||||||
|
checkSectionUnitToUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
null,
|
||||||
|
'unit_2',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when unitId is null', () => {
|
||||||
|
checkSectionUnitToUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkResumeRedirect', () => {
|
||||||
|
it('calls navigate with unitId', () => {
|
||||||
|
axiosMock.onGet(
|
||||||
|
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||||
|
).reply(200, {
|
||||||
|
section_id: 'section_1',
|
||||||
|
unitId: 'unit_1',
|
||||||
|
});
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/preview/course/courseId/section_1/unit_1';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with firstSequenceId', () => {
|
||||||
|
axiosMock.onGet(
|
||||||
|
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||||
|
).reply(200, {
|
||||||
|
section_id: 'section_1',
|
||||||
|
first_sequence_id: 'sequence_1',
|
||||||
|
});
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns after calling getResumeBlock', () => {
|
||||||
|
const getResumeBlock = jest.fn();
|
||||||
|
axiosMock.onGet(
|
||||||
|
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||||
|
).reply(200, {
|
||||||
|
course_id: 'courseId',
|
||||||
|
});
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getResumeBlock).toHaveBeenCalled();
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when course status is loading', () => {
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loading',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequenceId is defined', () => {
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'sequence_3',
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when getResumeBlock throws error', () => {
|
||||||
|
const getResumeBlock = jest.fn();
|
||||||
|
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`).reply(404);
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getResumeBlock).toHaveBeenCalled();
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isPreview equals false', () => {
|
||||||
|
describe('checkSectionToSequenceRedirect', () => {
|
||||||
|
it('calls navigate with section based sequence id', () => {
|
||||||
|
checkSectionToSequenceRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
{ sequenceIds: ['sequence_1'] },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with course id only', () => {
|
||||||
|
checkSectionToSequenceRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
{ sequenceIds: [] },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when course status is loading', () => {
|
||||||
|
checkSectionToSequenceRedirect(
|
||||||
|
'loading',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
{ sequenceIds: [] },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequence status is not failed', () => {
|
||||||
|
checkSectionToSequenceRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loading',
|
||||||
|
{ sequenceIds: [] },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when section is not defined', () => {
|
||||||
|
checkSectionToSequenceRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when unitId is defined', () => {
|
||||||
|
checkSectionToSequenceRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
null,
|
||||||
|
'unit_1',
|
||||||
|
navigate,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkResumeRedirect', () => {
|
||||||
|
it('calls navigate with unitId', () => {
|
||||||
|
axiosMock.onGet(
|
||||||
|
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||||
|
).reply(200, {
|
||||||
|
section_id: 'section_1',
|
||||||
|
unitId: 'unit_1',
|
||||||
|
});
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/preview/course/courseId/section_1/unit_1';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with firstSequenceId', () => {
|
||||||
|
axiosMock.onGet(
|
||||||
|
`${getConfig().LMS_BASE_URL}/api/courseware/resume/courseId`,
|
||||||
|
).reply(200, {
|
||||||
|
section_id: 'section_1',
|
||||||
|
first_sequence_id: 'sequence_1',
|
||||||
|
});
|
||||||
|
checkResumeRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
null,
|
||||||
|
'sequence_1',
|
||||||
|
navigate,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkSequenceUnitMarkerToSequenceUnitRedirect', () => {
|
||||||
|
it('calls navigate with first unit id', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||||
|
'first',
|
||||||
|
navigate,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1/unit_1';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with base url when no unit id', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: [] },
|
||||||
|
'first',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate with last unit id', () => {
|
||||||
|
checkSequenceUnitMarkerToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'] },
|
||||||
|
'last',
|
||||||
|
navigate,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1/unit_2';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkSequenceToSequenceUnitRedirect', () => {
|
||||||
|
it('calls navigate with next unit id', () => {
|
||||||
|
checkSequenceToSequenceUnitRedirect(
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
{ id: 'sequence_1', unitIds: ['unit_1', 'unit_2'], activeUnitIndex: 0 },
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1/unit_1';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkUnitToSequenceUnitRedirect', () => {
|
||||||
|
const apiUrl = getSequenceForUnitDeprecatedUrl('courseId');
|
||||||
|
|
||||||
|
it('calls navigate with parentId and sequenceId', () => {
|
||||||
|
axiosMock.onGet(apiUrl).reply(200, {
|
||||||
|
parent: { id: 'sequence_1' },
|
||||||
|
});
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/sequence_1';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate to course page when getSequenceForUnitDeprecated errors', () => {
|
||||||
|
const getSequenceForUnitDeprecated = jest.fn();
|
||||||
|
axiosMock.onGet(apiUrl).reply(404);
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls navigate to course page when no parent id is returned', () => {
|
||||||
|
const getSequenceForUnitDeprecated = jest.fn();
|
||||||
|
axiosMock.onGet(apiUrl).reply(200, {
|
||||||
|
parent: { children: ['block_1'] },
|
||||||
|
});
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId';
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(getSequenceForUnitDeprecated).toHaveBeenCalled();
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when course status is loading', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loading',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when sequence status is not failed', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when section is defined', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns when routeUnitId is defined', () => {
|
||||||
|
checkUnitToSequenceUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'loaded',
|
||||||
|
true,
|
||||||
|
'unit_1',
|
||||||
|
false,
|
||||||
|
'unit_1',
|
||||||
|
navigate,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkSectionUnitToUnitRedirect', () => {
|
||||||
|
it('calls navigate with unitId', () => {
|
||||||
|
checkSectionUnitToUnitRedirect(
|
||||||
|
'loaded',
|
||||||
|
'courseId',
|
||||||
|
'failed',
|
||||||
|
true,
|
||||||
|
'unit_2',
|
||||||
|
navigate,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const expectedUrl = '/course/courseId/unit_2';
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith(expectedUrl, { replace: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||||
|
|
||||||
import { AlertList } from '@src/generic/user-messages';
|
import { AlertList } from '@src/generic/user-messages';
|
||||||
@@ -38,6 +39,13 @@ const Course = ({
|
|||||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||||
const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||||
const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false);
|
const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
if (!isStaff && pathname.startsWith('/preview')) {
|
||||||
|
const courseUrl = pathname.replace('/preview', '');
|
||||||
|
navigate(courseUrl, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
const pageTitleBreadCrumbs = [
|
const pageTitleBreadCrumbs = [
|
||||||
sequence,
|
sequence,
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ const Sequence = ({
|
|||||||
sequenceId={sequenceId}
|
sequenceId={sequenceId}
|
||||||
unitId={unitId}
|
unitId={unitId}
|
||||||
unitLoadedHandler={handleUnitLoaded}
|
unitLoadedHandler={handleUnitLoaded}
|
||||||
|
isStaff={isStaff}
|
||||||
/>
|
/>
|
||||||
{unitHasLoaded && renderUnitNavigation(false)}
|
{unitHasLoaded && renderUnitNavigation(false)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Suspense, useEffect } from 'react';
|
import React, { Suspense, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import PageLoading from '../../../generic/PageLoading';
|
import PageLoading from '../../../generic/PageLoading';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
@@ -11,12 +11,13 @@ const ContentLock = React.lazy(() => import('./content-lock'));
|
|||||||
|
|
||||||
const SequenceContent = ({
|
const SequenceContent = ({
|
||||||
gated,
|
gated,
|
||||||
intl,
|
|
||||||
courseId,
|
courseId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
unitId,
|
unitId,
|
||||||
unitLoadedHandler,
|
unitLoadedHandler,
|
||||||
|
isStaff,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const sequence = useModel('sequences', sequenceId);
|
const sequence = useModel('sequences', sequenceId);
|
||||||
|
|
||||||
// Go back to the top of the page whenever the unit or sequence changes.
|
// Go back to the top of the page whenever the unit or sequence changes.
|
||||||
@@ -59,6 +60,7 @@ const SequenceContent = ({
|
|||||||
key={unitId}
|
key={unitId}
|
||||||
id={unitId}
|
id={unitId}
|
||||||
onLoaded={unitLoadedHandler}
|
onLoaded={unitLoadedHandler}
|
||||||
|
isStaff={isStaff}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -69,11 +71,11 @@ SequenceContent.propTypes = {
|
|||||||
sequenceId: PropTypes.string.isRequired,
|
sequenceId: PropTypes.string.isRequired,
|
||||||
unitId: PropTypes.string,
|
unitId: PropTypes.string,
|
||||||
unitLoadedHandler: PropTypes.func.isRequired,
|
unitLoadedHandler: PropTypes.func.isRequired,
|
||||||
intl: intlShape.isRequired,
|
isStaff: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
SequenceContent.defaultProps = {
|
SequenceContent.defaultProps = {
|
||||||
unitId: null,
|
unitId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SequenceContent);
|
export default SequenceContent;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
@@ -22,15 +22,18 @@ const Unit = ({
|
|||||||
format,
|
format,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
id,
|
id,
|
||||||
|
isStaff,
|
||||||
}) => {
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const { pathname } = useLocation();
|
||||||
const { authenticatedUser } = React.useContext(AppContext);
|
const { authenticatedUser } = React.useContext(AppContext);
|
||||||
const examAccess = useExamAccess({ id });
|
const examAccess = useExamAccess({ id });
|
||||||
const shouldDisplayHonorCode = useShouldDisplayHonorCode({ courseId, id });
|
const shouldDisplayHonorCode = useShouldDisplayHonorCode({ courseId, id });
|
||||||
const unit = useModel(modelKeys.units, id);
|
const unit = useModel(modelKeys.units, id);
|
||||||
const isProcessing = unit.bookmarkedUpdateState === 'loading';
|
const isProcessing = unit.bookmarkedUpdateState === 'loading';
|
||||||
const view = authenticatedUser ? views.student : views.public;
|
const view = authenticatedUser ? views.student : views.public;
|
||||||
|
const shouldDisplayUnitPreview = pathname.startsWith('/preview') && isStaff;
|
||||||
|
|
||||||
const getUrl = usePluginsCallback('getIFrameUrl', () => getIFrameUrl({
|
const getUrl = usePluginsCallback('getIFrameUrl', () => getIFrameUrl({
|
||||||
id,
|
id,
|
||||||
@@ -38,6 +41,7 @@ const Unit = ({
|
|||||||
format,
|
format,
|
||||||
examAccess,
|
examAccess,
|
||||||
jumpToId: searchParams.get('jumpToId'),
|
jumpToId: searchParams.get('jumpToId'),
|
||||||
|
preview: shouldDisplayUnitPreview ? '1' : '0',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const iframeUrl = getUrl();
|
const iframeUrl = getUrl();
|
||||||
@@ -74,6 +78,7 @@ Unit.propTypes = {
|
|||||||
format: PropTypes.string,
|
format: PropTypes.string,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
onLoaded: PropTypes.func,
|
onLoaded: PropTypes.func,
|
||||||
|
isStaff: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
Unit.defaultProps = {
|
Unit.defaultProps = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { when } from 'jest-when';
|
import { when } from 'jest-when';
|
||||||
import { formatMessage, shallow } from '@edx/react-unit-test-utils/dist';
|
import { formatMessage, shallow } from '@edx/react-unit-test-utils/dist';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { useModel } from '@src/generic/model-store';
|
import { useModel } from '@src/generic/model-store';
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ const props = {
|
|||||||
format: 'test-format',
|
format: 'test-format',
|
||||||
onLoaded: jest.fn().mockName('props.onLoaded'),
|
onLoaded: jest.fn().mockName('props.onLoaded'),
|
||||||
id: 'test-props-id',
|
id: 'test-props-id',
|
||||||
|
isStaff: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const context = { authenticatedUser: { test: 'user' } };
|
const context = { authenticatedUser: { test: 'user' } };
|
||||||
@@ -89,6 +90,7 @@ describe('Unit component', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useSearchParams.mockImplementation(() => [searchParams, setSearchParams]);
|
useSearchParams.mockImplementation(() => [searchParams, setSearchParams]);
|
||||||
|
useLocation.mockImplementation(() => ({ pathname: `/course/${props.courseId}` }));
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
el = shallow(<Unit {...props} />);
|
el = shallow(<Unit {...props} />);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const getIFrameUrl = ({
|
|||||||
format,
|
format,
|
||||||
examAccess,
|
examAccess,
|
||||||
jumpToId,
|
jumpToId,
|
||||||
|
preview,
|
||||||
}) => {
|
}) => {
|
||||||
const xblockUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
|
const xblockUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}`;
|
||||||
return stringifyUrl({
|
return stringifyUrl({
|
||||||
@@ -20,6 +21,7 @@ export const getIFrameUrl = ({
|
|||||||
query: {
|
query: {
|
||||||
...iframeParams,
|
...iframeParams,
|
||||||
view,
|
view,
|
||||||
|
preview,
|
||||||
...(format && { format }),
|
...(format && { format }),
|
||||||
...(!examAccess.blockAccess && { exam_access: examAccess.accessToken }),
|
...(!examAccess.blockAccess && { exam_access: examAccess.accessToken }),
|
||||||
jumpToId, // Pass jumpToId as query param as fragmentIdentifier is not passed to server.
|
jumpToId, // Pass jumpToId as query param as fragmentIdentifier is not passed to server.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const props = {
|
|||||||
view: 'test-view',
|
view: 'test-view',
|
||||||
format: 'test-format',
|
format: 'test-format',
|
||||||
examAccess: { blockAccess: false, accessToken: 'test-access-token' },
|
examAccess: { blockAccess: false, accessToken: 'test-access-token' },
|
||||||
|
preview: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('urls module getIFrameUrl', () => {
|
describe('urls module getIFrameUrl', () => {
|
||||||
@@ -28,6 +29,7 @@ describe('urls module getIFrameUrl', () => {
|
|||||||
view: props.view,
|
view: props.view,
|
||||||
format: props.format,
|
format: props.format,
|
||||||
exam_access: props.examAccess.accessToken,
|
exam_access: props.examAccess.accessToken,
|
||||||
|
preview: props.preview,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(getIFrameUrl(props)).toEqual(url);
|
expect(getIFrameUrl(props)).toEqual(url);
|
||||||
@@ -35,11 +37,12 @@ describe('urls module getIFrameUrl', () => {
|
|||||||
test('no format provided, exam access blocked', () => {
|
test('no format provided, exam access blocked', () => {
|
||||||
const url = stringifyUrl({
|
const url = stringifyUrl({
|
||||||
url: `${config.LMS_BASE_URL}/xblock/${props.id}`,
|
url: `${config.LMS_BASE_URL}/xblock/${props.id}`,
|
||||||
query: { ...iframeParams, view: props.view },
|
query: { ...iframeParams, view: props.view, preview: props.preview },
|
||||||
});
|
});
|
||||||
expect(getIFrameUrl({
|
expect(getIFrameUrl({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
view: props.view,
|
view: props.view,
|
||||||
|
preview: props.preview,
|
||||||
examAccess: { blockAccess: true },
|
examAccess: { blockAccess: true },
|
||||||
})).toEqual(url);
|
})).toEqual(url);
|
||||||
});
|
});
|
||||||
@@ -50,6 +53,7 @@ describe('urls module getIFrameUrl', () => {
|
|||||||
...iframeParams,
|
...iframeParams,
|
||||||
view: props.view,
|
view: props.view,
|
||||||
format: props.format,
|
format: props.format,
|
||||||
|
preview: props.preview,
|
||||||
exam_access: props.examAccess.accessToken,
|
exam_access: props.examAccess.accessToken,
|
||||||
jumpToId: 'some-xblock-id',
|
jumpToId: 'some-xblock-id',
|
||||||
},
|
},
|
||||||
@@ -60,4 +64,20 @@ describe('urls module getIFrameUrl', () => {
|
|||||||
jumpToId: 'some-xblock-id',
|
jumpToId: 'some-xblock-id',
|
||||||
})).toEqual(url);
|
})).toEqual(url);
|
||||||
});
|
});
|
||||||
|
test('preview is true and url param equals 1', () => {
|
||||||
|
const url = stringifyUrl({
|
||||||
|
url: `${config.LMS_BASE_URL}/xblock/${props.id}`,
|
||||||
|
query: {
|
||||||
|
...iframeParams,
|
||||||
|
view: props.view,
|
||||||
|
format: props.format,
|
||||||
|
preview: true,
|
||||||
|
exam_access: props.examAccess.accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getIFrameUrl({
|
||||||
|
...props,
|
||||||
|
preview: true,
|
||||||
|
})).toEqual(url);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { breakpoints, Button, useWindowSize } from '@openedx/paragon';
|
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||||
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
injectIntl,
|
|
||||||
intlShape,
|
|
||||||
isRtl,
|
|
||||||
getLocale,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
@@ -21,9 +14,10 @@ import { useSequenceNavigationMetadata } from './hooks';
|
|||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import PreviousButton from './generic/PreviousButton';
|
||||||
|
import NextButton from './generic/NextButton';
|
||||||
|
|
||||||
const SequenceNavigation = ({
|
const SequenceNavigation = ({
|
||||||
intl,
|
|
||||||
unitId,
|
unitId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
className,
|
className,
|
||||||
@@ -36,6 +30,7 @@ const SequenceNavigation = ({
|
|||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const sequence = useModel('sequences', sequenceId);
|
const sequence = useModel('sequences', sequenceId);
|
||||||
const {
|
const {
|
||||||
isFirstUnit,
|
isFirstUnit,
|
||||||
@@ -76,29 +71,21 @@ const SequenceNavigation = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPreviousButton = () => {
|
const renderPreviousButton = () => navigationDisabledPrevSequence || (
|
||||||
const disabled = isFirstUnit;
|
<PreviousButton
|
||||||
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
|
variant="link"
|
||||||
return navigationDisabledPrevSequence || (
|
buttonStyle="previous-btn"
|
||||||
<Button
|
onClick={previousHandler}
|
||||||
variant="link"
|
previousLink={previousLink}
|
||||||
className="previous-btn"
|
isFirstUnit={isFirstUnit}
|
||||||
onClick={previousHandler}
|
buttonLabel={shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)}
|
||||||
disabled={disabled}
|
/>
|
||||||
iconBefore={prevArrow}
|
);
|
||||||
as={disabled ? undefined : Link}
|
|
||||||
to={disabled ? undefined : previousLink}
|
|
||||||
>
|
|
||||||
{shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNextButton = () => {
|
const renderNextButton = () => {
|
||||||
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
|
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
|
||||||
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
|
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
|
||||||
const disabled = isLastUnit && !exitActive;
|
const disabled = isLastUnit && !exitActive;
|
||||||
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;
|
|
||||||
|
|
||||||
return navigationDisabledNextSequence || (
|
return navigationDisabledNextSequence || (
|
||||||
<PluginSlot
|
<PluginSlot
|
||||||
@@ -106,10 +93,8 @@ const SequenceNavigation = ({
|
|||||||
pluginProps={{
|
pluginProps={{
|
||||||
courseId,
|
courseId,
|
||||||
disabled,
|
disabled,
|
||||||
buttonText,
|
buttonText: shouldDisplayNotificationTriggerInSequence ? null : buttonText,
|
||||||
nextArrow,
|
|
||||||
nextLink,
|
nextLink,
|
||||||
shouldDisplayNotificationTriggerInSequence,
|
|
||||||
sequenceId,
|
sequenceId,
|
||||||
unitId,
|
unitId,
|
||||||
nextSequenceHandler,
|
nextSequenceHandler,
|
||||||
@@ -117,20 +102,16 @@ const SequenceNavigation = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
linkComponent: Link,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<NextButton
|
||||||
variant="link"
|
variant="link"
|
||||||
className="next-btn"
|
buttonStyle="next-btn"
|
||||||
onClick={nextHandler}
|
onClick={nextHandler}
|
||||||
|
nextLink={nextLink}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
iconAfter={nextArrow}
|
buttonLabel={shouldDisplayNotificationTriggerInSequence ? null : buttonText}
|
||||||
as={disabled ? undefined : Link}
|
/>
|
||||||
to={disabled ? undefined : nextLink}
|
|
||||||
>
|
|
||||||
{shouldDisplayNotificationTriggerInSequence ? null : buttonText}
|
|
||||||
</Button>
|
|
||||||
</PluginSlot>
|
</PluginSlot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -145,7 +126,6 @@ const SequenceNavigation = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
SequenceNavigation.propTypes = {
|
SequenceNavigation.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
sequenceId: PropTypes.string.isRequired,
|
sequenceId: PropTypes.string.isRequired,
|
||||||
unitId: PropTypes.string,
|
unitId: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
@@ -169,4 +149,4 @@ SequenceNavigation.defaultProps = {
|
|||||||
nextSequenceHandler: null,
|
nextSequenceHandler: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SequenceNavigation);
|
export default SequenceNavigation;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -22,6 +22,9 @@ const UnitButton = ({
|
|||||||
showTitle,
|
showTitle,
|
||||||
}) => {
|
}) => {
|
||||||
const { courseId, sequenceId } = useSelector(state => state.courseware);
|
const { courseId, sequenceId } = useSelector(state => state.courseware);
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const basePath = `/course/${courseId}/${sequenceId}/${unitId}`;
|
||||||
|
const unitPath = pathname.startsWith('/preview') ? `/preview${basePath}` : basePath;
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onClick(unitId);
|
onClick(unitId);
|
||||||
@@ -37,7 +40,7 @@ const UnitButton = ({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
title={title}
|
title={title}
|
||||||
as={Link}
|
as={Link}
|
||||||
to={`/course/${courseId}/${sequenceId}/${unitId}`}
|
to={unitPath}
|
||||||
>
|
>
|
||||||
<UnitIcon type={contentType} />
|
<UnitIcon type={contentType} />
|
||||||
{showTitle && <span className="unit-title">{title}</span>}
|
{showTitle && <span className="unit-title">{title}</span>}
|
||||||
|
|||||||
@@ -1,70 +1,53 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button } from '@openedx/paragon';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import {
|
|
||||||
injectIntl, intlShape, isRtl, getLocale,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { GetCourseExitNavigation } from '../../course-exit';
|
import { GetCourseExitNavigation } from '../../course-exit';
|
||||||
|
|
||||||
import UnitNavigationEffortEstimate from './UnitNavigationEffortEstimate';
|
|
||||||
import { useSequenceNavigationMetadata } from './hooks';
|
import { useSequenceNavigationMetadata } from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import PreviousButton from './generic/PreviousButton';
|
||||||
|
import NextButton from './generic/NextButton';
|
||||||
|
|
||||||
const UnitNavigation = ({
|
const UnitNavigation = ({
|
||||||
intl,
|
|
||||||
sequenceId,
|
sequenceId,
|
||||||
unitId,
|
unitId,
|
||||||
onClickPrevious,
|
onClickPrevious,
|
||||||
onClickNext,
|
onClickNext,
|
||||||
isAtTop,
|
isAtTop,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
isFirstUnit, isLastUnit, nextLink, previousLink,
|
isFirstUnit, isLastUnit, nextLink, previousLink,
|
||||||
} = useSequenceNavigationMetadata(sequenceId, unitId);
|
} = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||||
const { courseId } = useSelector(state => state.courseware);
|
const { courseId } = useSelector(state => state.courseware);
|
||||||
|
|
||||||
const renderPreviousButton = () => {
|
const renderPreviousButton = () => (
|
||||||
const disabled = isFirstUnit;
|
<PreviousButton
|
||||||
const prevArrow = isRtl(getLocale()) ? faChevronRight : faChevronLeft;
|
isFirstUnit={isFirstUnit}
|
||||||
return (
|
variant="outline-secondary"
|
||||||
<Button
|
buttonLabel={intl.formatMessage(messages.previousButton)}
|
||||||
variant="outline-secondary"
|
buttonStyle="previous-button justify-content-center"
|
||||||
className="previous-button mr-sm-2 d-flex align-items-center justify-content-center"
|
onClick={onClickPrevious}
|
||||||
disabled={disabled}
|
previousLink={previousLink}
|
||||||
onClick={onClickPrevious}
|
/>
|
||||||
as={disabled ? undefined : Link}
|
);
|
||||||
to={disabled ? undefined : previousLink}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={prevArrow} className="mr-2" size="sm" />
|
|
||||||
{intl.formatMessage(messages.previousButton)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNextButton = () => {
|
const renderNextButton = () => {
|
||||||
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
|
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
|
||||||
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
|
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
|
||||||
const disabled = isLastUnit && !exitActive;
|
const disabled = isLastUnit && !exitActive;
|
||||||
const nextArrow = isRtl(getLocale()) ? faChevronLeft : faChevronRight;
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<NextButton
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
className="next-button d-flex align-items-center justify-content-center"
|
buttonStyle="next-button justify-content-center"
|
||||||
onClick={onClickNext}
|
onClick={onClickNext}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
as={disabled ? undefined : Link}
|
buttonLabel={buttonText}
|
||||||
to={disabled ? undefined : nextLink}
|
nextLink={nextLink}
|
||||||
>
|
hasEffortEstimate
|
||||||
<UnitNavigationEffortEstimate sequenceId={sequenceId} unitId={unitId}>
|
/>
|
||||||
{buttonText}
|
|
||||||
</UnitNavigationEffortEstimate>
|
|
||||||
<FontAwesomeIcon icon={nextArrow} className="ml-2" size="sm" />
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,7 +60,6 @@ const UnitNavigation = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
UnitNavigation.propTypes = {
|
UnitNavigation.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
sequenceId: PropTypes.string.isRequired,
|
sequenceId: PropTypes.string.isRequired,
|
||||||
unitId: PropTypes.string,
|
unitId: PropTypes.string,
|
||||||
onClickPrevious: PropTypes.func.isRequired,
|
onClickPrevious: PropTypes.func.isRequired,
|
||||||
@@ -90,4 +72,4 @@ UnitNavigation.defaultProps = {
|
|||||||
isAtTop: false,
|
isAtTop: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(UnitNavigation);
|
export default UnitNavigation;
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { Button } from '@openedx/paragon';
|
||||||
|
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
|
||||||
|
import { isRtl, getLocale } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import UnitNavigationEffortEstimate from '../UnitNavigationEffortEstimate';
|
||||||
|
|
||||||
|
const NextButton = ({
|
||||||
|
onClick,
|
||||||
|
buttonLabel,
|
||||||
|
nextLink,
|
||||||
|
variant,
|
||||||
|
buttonStyle,
|
||||||
|
disabled,
|
||||||
|
hasEffortEstimate,
|
||||||
|
}) => {
|
||||||
|
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const navLink = pathname.startsWith('/preview') ? `/preview${nextLink}` : nextLink;
|
||||||
|
const buttonContent = hasEffortEstimate ? (
|
||||||
|
<UnitNavigationEffortEstimate>
|
||||||
|
{buttonLabel}
|
||||||
|
</UnitNavigationEffortEstimate>
|
||||||
|
) : buttonLabel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
className={buttonStyle}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
as={disabled ? undefined : Link}
|
||||||
|
to={disabled ? undefined : navLink}
|
||||||
|
iconAfter={nextArrow}
|
||||||
|
>
|
||||||
|
{buttonContent}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NextButton.defaultProps = {
|
||||||
|
hasEffortEstimate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
NextButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
buttonLabel: PropTypes.string.isRequired,
|
||||||
|
nextLink: PropTypes.string.isRequired,
|
||||||
|
variant: PropTypes.string.isRequired,
|
||||||
|
buttonStyle: PropTypes.string.isRequired,
|
||||||
|
disabled: PropTypes.bool.isRequired,
|
||||||
|
hasEffortEstimate: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NextButton;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { Button } from '@openedx/paragon';
|
||||||
|
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
|
||||||
|
import { isRtl, getLocale } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const PreviousButton = ({
|
||||||
|
onClick,
|
||||||
|
buttonLabel,
|
||||||
|
previousLink,
|
||||||
|
variant,
|
||||||
|
buttonStyle,
|
||||||
|
isFirstUnit,
|
||||||
|
}) => {
|
||||||
|
const disabled = isFirstUnit;
|
||||||
|
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const navLink = pathname.startsWith('/preview') ? `/preview${previousLink}` : previousLink;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
className={buttonStyle}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
as={disabled ? undefined : Link}
|
||||||
|
to={disabled ? undefined : navLink}
|
||||||
|
iconBefore={prevArrow}
|
||||||
|
>
|
||||||
|
{buttonLabel}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PreviousButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
buttonLabel: PropTypes.string.isRequired,
|
||||||
|
previousLink: PropTypes.string.isRequired,
|
||||||
|
variant: PropTypes.string.isRequired,
|
||||||
|
buttonStyle: PropTypes.string.isRequired,
|
||||||
|
isFirstUnit: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviousButton;
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
// Do not add further calls to this API - we don't like making use of the modulestore if we can help it
|
// Do not add further calls to this API - we don't like making use of the modulestore if we can help it
|
||||||
export async function getSequenceForUnitDeprecated(courseId, unitId) {
|
export const getSequenceForUnitDeprecatedUrl = (courseId) => {
|
||||||
const authenticatedUser = getAuthenticatedUser();
|
const authenticatedUser = getAuthenticatedUser();
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/`);
|
const url = new URL(`${getConfig().LMS_BASE_URL}/api/courses/v2/blocks/`);
|
||||||
url.searchParams.append('course_id', courseId);
|
url.searchParams.append('course_id', courseId);
|
||||||
@@ -14,6 +14,10 @@ export async function getSequenceForUnitDeprecated(courseId, unitId) {
|
|||||||
url.searchParams.append('depth', 3);
|
url.searchParams.append('depth', 3);
|
||||||
url.searchParams.append('requested_fields', 'children,discussions_url');
|
url.searchParams.append('requested_fields', 'children,discussions_url');
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
export async function getSequenceForUnitDeprecated(courseId, unitId) {
|
||||||
|
const url = getSequenceForUnitDeprecatedUrl(courseId);
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
|
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
|
||||||
const parent = Object.values(data.blocks).find(block => block.type === 'sequential' && block.children.includes(unitId));
|
const parent = Object.values(data.blocks).find(block => block.type === 'sequential' && block.children.includes(unitId));
|
||||||
return parent?.id;
|
return parent?.id;
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
const withParamsAndNavigation = WrappedComponent => {
|
const withParamsAndNavigation = WrappedComponent => {
|
||||||
const WithParamsNavigationComponent = props => {
|
const WithParamsNavigationComponent = props => {
|
||||||
const { courseId, sequenceId, unitId } = useParams();
|
const { courseId, sequenceId, unitId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const isPreview = pathname.startsWith('/preview');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WrappedComponent
|
<WrappedComponent
|
||||||
routeCourseId={courseId}
|
routeCourseId={courseId}
|
||||||
routeSequenceId={sequenceId}
|
routeSequenceId={sequenceId}
|
||||||
routeUnitId={unitId}
|
routeUnitId={unitId}
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
|
isPreview={isPreview}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user