Skip to content

Commit d47433e

Browse files
authored
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
1 parent 6f11596 commit d47433e

17 files changed

+1194
-171
lines changed

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const DECODE_ROUTES = {
1313
'/course/:courseId/:sequenceId/:unitId',
1414
'/course/:courseId/:sequenceId',
1515
'/course/:courseId',
16+
'/preview/course/:courseId/:sequenceId/:unitId',
17+
'/preview/course/:courseId/:sequenceId',
1618
],
1719
REDIRECT_HOME: 'home/:courseId',
1820
REDIRECT_SURVEY: 'survey/:courseId',

src/courseware/CoursewareContainer.jsx

Lines changed: 143 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -19,104 +19,131 @@ import { handleNextSectionCelebration } from './course/celebration';
1919
import withParamsAndNavigation from './utils';
2020

2121
// Look at where this is called in componentDidUpdate for more info about its usage
22-
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId, navigate) => {
23-
if (courseStatus === 'loaded' && !sequenceId) {
24-
// Note that getResumeBlock is just an API call, not a redux thunk.
25-
getResumeBlock(courseId).then((data) => {
26-
// This is a replace because we don't want this change saved in the browser's history.
27-
if (data.sectionId && data.unitId) {
28-
navigate(`/course/${courseId}/${data.sectionId}/${data.unitId}`, { replace: true });
29-
} else if (firstSequenceId) {
30-
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
31-
}
32-
});
33-
}
34-
});
22+
export const checkResumeRedirect = memoize(
23+
(courseStatus, courseId, sequenceId, firstSequenceId, navigate, isPreview) => {
24+
if (courseStatus === 'loaded' && !sequenceId) {
25+
// Note that getResumeBlock is just an API call, not a redux thunk.
26+
getResumeBlock(courseId).then((data) => {
27+
// This is a replace because we don't want this change saved in the browser's history.
28+
if (data.sectionId && data.unitId) {
29+
const baseUrl = `/course/${courseId}/${data.sectionId}`;
30+
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
31+
navigate(`${sequenceUrl}/${data.unitId}`, { replace: true });
32+
} else if (firstSequenceId) {
33+
navigate(`/course/${courseId}/${firstSequenceId}`, { replace: true });
34+
}
35+
}, () => {});
36+
}
37+
},
38+
);
3539

3640
// Look at where this is called in componentDidUpdate for more info about its usage
37-
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
41+
export const checkSectionUnitToUnitRedirect = memoize((
42+
courseStatus,
43+
courseId,
44+
sequenceStatus,
45+
section,
46+
unitId,
47+
navigate,
48+
isPreview,
49+
) => {
3850
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
39-
navigate(`/course/${courseId}/${unitId}`, { replace: true });
51+
const baseUrl = `/course/${courseId}`;
52+
const courseUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
53+
navigate(`${courseUrl}/${unitId}`, { replace: true });
4054
}
4155
});
4256

4357
// Look at where this is called in componentDidUpdate for more info about its usage
44-
const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
45-
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
46-
// If the section is non-empty, redirect to its first sequence.
47-
if (section.sequenceIds && section.sequenceIds[0]) {
48-
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
49-
// Otherwise, just go to the course root, letting the resume redirect take care of things.
50-
} else {
51-
navigate(`/course/${courseId}`, { replace: true });
52-
}
53-
}
54-
});
55-
56-
// Look at where this is called in componentDidUpdate for more info about its usage
57-
const checkUnitToSequenceUnitRedirect = memoize(
58-
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId, navigate) => {
59-
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
60-
if (sequenceMightBeUnit) {
61-
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
62-
// we need to look up the correct parent sequence for it, and redirect there.
63-
const unitId = sequenceId; // just for clarity during the rest of this method
64-
getSequenceForUnitDeprecated(courseId, unitId).then(
65-
parentId => {
66-
if (parentId) {
67-
navigate(`/course/${courseId}/${parentId}/${unitId}`, { replace: true });
68-
} else {
69-
navigate(`/course/${courseId}`, { replace: true });
70-
}
71-
},
72-
() => { // error case
73-
navigate(`/course/${courseId}`, { replace: true });
74-
},
75-
);
58+
export const checkSectionToSequenceRedirect = memoize(
59+
(courseStatus, courseId, sequenceStatus, section, unitId, navigate) => {
60+
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
61+
// If the section is non-empty, redirect to its first sequence.
62+
if (section.sequenceIds && section.sequenceIds[0]) {
63+
navigate(`/course/${courseId}/${section.sequenceIds[0]}`, { replace: true });
64+
// Otherwise, just go to the course root, letting the resume redirect take care of things.
7665
} else {
77-
// Invalid sequence that isn't a unit either. Redirect up to main course.
7866
navigate(`/course/${courseId}`, { replace: true });
7967
}
8068
}
8169
},
8270
);
8371

8472
// Look at where this is called in componentDidUpdate for more info about its usage
85-
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId, navigate) => {
86-
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
87-
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
88-
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
89-
// This is a replace because we don't want this change saved in the browser's history.
90-
navigate(`/course/${courseId}/${sequence.id}/${nextUnitId}`, { replace: true });
73+
export const checkUnitToSequenceUnitRedirect = memoize((
74+
courseStatus,
75+
courseId,
76+
sequenceStatus,
77+
sequenceMightBeUnit,
78+
sequenceId,
79+
section,
80+
routeUnitId,
81+
navigate,
82+
isPreview,
83+
) => {
84+
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
85+
if (sequenceMightBeUnit) {
86+
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
87+
// we need to look up the correct parent sequence for it, and redirect there.
88+
const unitId = sequenceId; // just for clarity during the rest of this method
89+
getSequenceForUnitDeprecated(courseId, unitId).then(
90+
parentId => {
91+
if (parentId) {
92+
const baseUrl = `/course/${courseId}/${parentId}`;
93+
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
94+
navigate(`${sequenceUrl}/${unitId}`, { replace: true });
95+
} else {
96+
navigate(`/course/${courseId}`, { replace: true });
97+
}
98+
},
99+
() => { // error case
100+
navigate(`/course/${courseId}`, { replace: true });
101+
},
102+
);
103+
} else {
104+
// Invalid sequence that isn't a unit either. Redirect up to main course.
105+
navigate(`/course/${courseId}`, { replace: true });
91106
}
92107
}
93108
});
94109

95110
// Look at where this is called in componentDidUpdate for more info about its usage
96-
const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
97-
(courseId, sequenceStatus, sequence, unitId, navigate) => {
111+
export const checkSequenceToSequenceUnitRedirect = memoize(
112+
(courseId, sequenceStatus, sequence, unitId, navigate, isPreview) => {
113+
if (sequenceStatus === 'loaded' && sequence.id && !unitId) {
114+
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
115+
const baseUrl = `/course/${courseId}/${sequence.id}`;
116+
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
117+
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
118+
// This is a replace because we don't want this change saved in the browser's history.
119+
navigate(`${sequenceUrl}/${nextUnitId}`, { replace: true });
120+
}
121+
}
122+
},
123+
);
124+
125+
// Look at where this is called in componentDidUpdate for more info about its usage
126+
export const checkSequenceUnitMarkerToSequenceUnitRedirect = memoize(
127+
(courseId, sequenceStatus, sequence, unitId, navigate, isPreview) => {
98128
if (sequenceStatus !== 'loaded' || !sequence.id) {
99129
return;
100130
}
101131

132+
const baseUrl = `/course/${courseId}/${sequence.id}`;
102133
const hasUnits = sequence.unitIds?.length > 0;
103134

104-
if (unitId === 'first') {
105-
if (hasUnits) {
135+
if (hasUnits) {
136+
const sequenceUrl = isPreview ? `/preview${baseUrl}` : baseUrl;
137+
if (unitId === 'first') {
106138
const firstUnitId = sequence.unitIds[0];
107-
navigate(`/course/${courseId}/${sequence.id}/${firstUnitId}`, { replace: true });
108-
} else {
109-
// No units... go to general sequence page
110-
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
111-
}
112-
} else if (unitId === 'last') {
113-
if (hasUnits) {
139+
navigate(`${sequenceUrl}/${firstUnitId}`, { replace: true });
140+
} else if (unitId === 'last') {
114141
const lastUnitId = sequence.unitIds[sequence.unitIds.length - 1];
115-
navigate(`/course/${courseId}/${sequence.id}/${lastUnitId}`, { replace: true });
116-
} else {
117-
// No units... go to general sequence page
118-
navigate(`/course/${courseId}/${sequence.id}`, { replace: true });
142+
navigate(`${sequenceUrl}/${lastUnitId}`, { replace: true });
119143
}
144+
} else {
145+
// No units... go to general sequence page
146+
navigate(baseUrl, { replace: true });
120147
}
121148
},
122149
);
@@ -169,6 +196,7 @@ class CoursewareContainer extends Component {
169196
routeSequenceId,
170197
routeUnitId,
171198
navigate,
199+
isPreview,
172200
} = this.props;
173201

174202
// Load data whenever the course or sequence ID changes.
@@ -197,7 +225,7 @@ class CoursewareContainer extends Component {
197225
// Check resume redirect:
198226
// /course/:courseId -> /course/:courseId/:sequenceId/:unitId
199227
// based on sequence/unit where user was last active.
200-
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate);
228+
checkResumeRedirect(courseStatus, courseId, sequenceId, firstSequenceId, navigate, isPreview);
201229

202230
// Check section-unit to unit redirect:
203231
// /course/:courseId/:sectionId/:unitId -> /course/:courseId/:unitId
@@ -210,33 +238,69 @@ class CoursewareContainer extends Component {
210238
// otherwise, we could get stuck in a redirect loop, since a sequence that failed to load
211239
// would endlessly redirect to itself through `checkSectionUnitToUnitRedirect`
212240
// and `checkUnitToSequenceUnitRedirect`.
213-
checkSectionUnitToUnitRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
241+
checkSectionUnitToUnitRedirect(
242+
courseStatus,
243+
courseId,
244+
sequenceStatus,
245+
sectionViaSequenceId,
246+
routeUnitId,
247+
navigate,
248+
isPreview,
249+
);
214250

215251
// Check section to sequence redirect:
216252
// /course/:courseId/:sectionId -> /course/:courseId/:sequenceId
217253
// by redirecting to the first sequence within the section.
218-
checkSectionToSequenceRedirect(courseStatus, courseId, sequenceStatus, sectionViaSequenceId, routeUnitId, navigate);
254+
checkSectionToSequenceRedirect(
255+
courseStatus,
256+
courseId,
257+
sequenceStatus,
258+
sectionViaSequenceId,
259+
routeUnitId,
260+
navigate,
261+
);
219262

220263
// Check unit to sequence-unit redirect:
221264
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
222265
// by filling in the ID of the parent sequence of :unitId.
223-
checkUnitToSequenceUnitRedirect((
224-
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit,
225-
sequenceId, sectionViaSequenceId, routeUnitId, navigate
226-
));
266+
checkUnitToSequenceUnitRedirect(
267+
courseStatus,
268+
courseId,
269+
sequenceStatus,
270+
sequenceMightBeUnit,
271+
sequenceId,
272+
sectionViaSequenceId,
273+
routeUnitId,
274+
navigate,
275+
isPreview,
276+
);
227277

228278
// Check sequence to sequence-unit redirect:
229279
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
230280
// by filling in the ID the most-recently-active unit in the sequence, OR
231281
// the ID of the first unit the sequence if none is active.
232-
checkSequenceToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
282+
checkSequenceToSequenceUnitRedirect(
283+
courseId,
284+
sequenceStatus,
285+
sequence,
286+
routeUnitId,
287+
navigate,
288+
isPreview,
289+
);
233290

234291
// Check sequence-unit marker to sequence-unit redirect:
235292
// /course/:courseId/:sequenceId/first -> /course/:courseId/:sequenceId/:unitId
236293
// /course/:courseId/:sequenceId/last -> /course/:courseId/:sequenceId/:unitId
237294
// by filling in the ID the first or last unit in the sequence.
238295
// "Sequence unit marker" is an invented term used only in this component.
239-
checkSequenceUnitMarkerToSequenceUnitRedirect(courseId, sequenceStatus, sequence, routeUnitId, navigate);
296+
checkSequenceUnitMarkerToSequenceUnitRedirect(
297+
courseId,
298+
sequenceStatus,
299+
sequence,
300+
routeUnitId,
301+
navigate,
302+
isPreview,
303+
);
240304
}
241305

242306
handleUnitNavigationClick = () => {
@@ -334,6 +398,7 @@ CoursewareContainer.propTypes = {
334398
fetchCourse: PropTypes.func.isRequired,
335399
fetchSequence: PropTypes.func.isRequired,
336400
navigate: PropTypes.func.isRequired,
401+
isPreview: PropTypes.bool.isRequired,
337402
};
338403

339404
CoursewareContainer.defaultProps = {

0 commit comments

Comments
 (0)