Skip to content

Commit 26c3235

Browse files
committed
feat: [FC-0070] rendering library content in unit page
1 parent bc8d59b commit 26c3235

File tree

21 files changed

+259
-120
lines changed

21 files changed

+259
-120
lines changed

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const COURSE_BLOCK_NAMES = ({
5858
chapter: { id: 'chapter', name: 'Section' },
5959
sequential: { id: 'sequential', name: 'Subsection' },
6060
vertical: { id: 'vertical', name: 'Unit' },
61+
libraryContent: { id: 'library_content', name: 'Library content' },
6162
component: { id: 'component', name: 'Component' },
6263
});
6364

src/course-unit/CourseUnit.jsx

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import ProcessingNotification from '../generic/processing-notification';
2222
import { SavingErrorAlert } from '../generic/saving-error-alert';
2323
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
2424
import Loading from '../generic/Loading';
25+
import { COURSE_BLOCK_NAMES } from '../constants';
2526
import AddComponent from './add-component/AddComponent';
2627
import HeaderTitle from './header-title/HeaderTitle';
2728
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
@@ -44,6 +45,7 @@ const CourseUnit = ({ courseId }) => {
4445
isLoading,
4546
sequenceId,
4647
unitTitle,
48+
unitCategory,
4749
errorMessage,
4850
sequenceStatus,
4951
savingStatus,
@@ -70,6 +72,19 @@ const CourseUnit = ({ courseId }) => {
7072
handleNavigateToTargetUnit,
7173
} = useCourseUnit({ courseId, blockId });
7274

75+
const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
76+
const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
77+
78+
const unitLayout = [{ span: 12 }, { span: 0 }];
79+
const defaultLayout = {
80+
lg: [{ span: 8 }, { span: 4 }],
81+
md: [{ span: 8 }, { span: 4 }],
82+
sm: [{ span: 8 }, { span: 3 }],
83+
xs: [{ span: 9 }, { span: 3 }],
84+
xl: [{ span: 9 }, { span: 3 }],
85+
};
86+
const layoutGrid = isUnitLibraryType ? { lg: unitLayout } : defaultLayout;
87+
7388
useEffect(() => {
7489
document.title = getPageHeadTitle('', unitTitle);
7590
}, [unitTitle]);
@@ -141,30 +156,30 @@ const CourseUnit = ({ courseId }) => {
141156
/>
142157
)}
143158
breadcrumbs={(
144-
<Breadcrumbs />
159+
<Breadcrumbs
160+
courseId={courseId}
161+
sequenceId={sequenceId}
162+
/>
145163
)}
146164
headerActions={(
147165
<HeaderNavigations
166+
unitCategory={unitCategory}
148167
headerNavigationsActions={headerNavigationsActions}
149168
/>
150169
)}
151170
/>
152-
<Sequence
153-
courseId={courseId}
154-
sequenceId={sequenceId}
155-
unitId={blockId}
156-
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
157-
showPasteUnit={showPasteUnit}
158-
/>
159-
<Layout
160-
lg={[{ span: 8 }, { span: 4 }]}
161-
md={[{ span: 8 }, { span: 4 }]}
162-
sm={[{ span: 8 }, { span: 3 }]}
163-
xs={[{ span: 9 }, { span: 3 }]}
164-
xl={[{ span: 9 }, { span: 3 }]}
165-
>
171+
{isUnitVerticalType && (
172+
<Sequence
173+
courseId={courseId}
174+
sequenceId={sequenceId}
175+
unitId={blockId}
176+
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
177+
showPasteUnit={showPasteUnit}
178+
/>
179+
)}
180+
<Layout {...layoutGrid}>
166181
<Layout.Element>
167-
{currentlyVisibleToStudents && (
182+
{!currentlyVisibleToStudents && (
168183
<AlertMessage
169184
className="course-unit__alert"
170185
title={intl.formatMessage(messages.alertUnpublishedVersion)}
@@ -183,11 +198,13 @@ const CourseUnit = ({ courseId }) => {
183198
unitXBlockActions={unitXBlockActions}
184199
courseVerticalChildren={courseVerticalChildren.children}
185200
/>
186-
<AddComponent
187-
blockId={blockId}
188-
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
189-
/>
190-
{showPasteXBlock && canPasteComponent && (
201+
{isUnitVerticalType && (
202+
<AddComponent
203+
blockId={blockId}
204+
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
205+
/>
206+
)}
207+
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
191208
<PasteComponent
192209
clipboardData={sharedClipboardData}
193210
onClick={handleCreateNewCourseXBlock}
@@ -203,18 +220,21 @@ const CourseUnit = ({ courseId }) => {
203220
</Layout.Element>
204221
<Layout.Element>
205222
<Stack gap={3}>
206-
<Sidebar data-testid="course-unit-sidebar">
207-
<PublishControls blockId={blockId} />
208-
</Sidebar>
209-
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
210-
&& (
211-
<Sidebar className="tags-sidebar">
212-
<TagsSidebarControls />
213-
</Sidebar>
223+
{isUnitVerticalType && (
224+
<>
225+
<Sidebar data-testid="course-unit-sidebar">
226+
<PublishControls blockId={blockId} />
227+
</Sidebar>
228+
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
229+
<Sidebar className="tags-sidebar">
230+
<TagsSidebarControls />
231+
</Sidebar>
232+
)}
233+
<Sidebar data-testid="course-unit-location-sidebar">
234+
<LocationInfo />
235+
</Sidebar>
236+
</>
214237
)}
215-
<Sidebar data-testid="course-unit-location-sidebar">
216-
<LocationInfo />
217-
</Sidebar>
218238
</Stack>
219239
</Layout.Element>
220240
</Layout>

src/course-unit/CourseUnit.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
@import "./header-title/HeaderTitle";
66
@import "./move-modal";
77

8+
.course-unit {
9+
min-width: 900px;
10+
}
11+
812
.course-unit__alert {
913
margin-bottom: 1.75rem;
1014
}

src/course-unit/CourseUnit.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import MockAdapter from 'axios-mock-adapter';
22
import {
3-
act, render, waitFor, within, screen,
3+
act, render, waitFor, within,
44
} from '@testing-library/react';
55
import userEvent from '@testing-library/user-event';
66
import { IntlProvider } from '@edx/frontend-platform/i18n';

src/course-unit/add-component/AddComponent.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
1717
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
1818
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
1919
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
20-
const { componentTemplates } = useSelector(getCourseSectionVertical);
20+
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
2121
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
2222

2323
const handleLibraryV2Selection = (selection) => {
Lines changed: 58 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import PropTypes from 'prop-types';
12
import { useSelector } from 'react-redux';
23
import { useIntl } from '@edx/frontend-platform/i18n';
34
import { Dropdown, Icon } from '@openedx/paragon';
@@ -10,77 +11,79 @@ import { getConfig } from '@edx/frontend-platform';
1011

1112
import { getWaffleFlags } from '../../data/selectors';
1213
import { getCourseSectionVertical } from '../data/selectors';
14+
import { adoptCourseSectionUrl } from '../utils';
1315
import messages from './messages';
1416

15-
const Breadcrumbs = () => {
17+
const Breadcrumbs = ({ courseId, sequenceId }) => {
1618
const intl = useIntl();
17-
const { ancestorXblocks } = useSelector(getCourseSectionVertical);
18-
const [section, subsection] = ancestorXblocks ?? [];
19+
const { ancestorXblocks = [] } = useSelector(getCourseSectionVertical);
1920
const waffleFlags = useSelector(getWaffleFlags);
2021

2122
const getPathToCourseOutlinePage = (url) => (waffleFlags.useNewCourseOutlinePage
2223
? url : `${getConfig().STUDIO_BASE_URL}${url}`);
2324

25+
const getPathToCourseUnitPage = (url) => (waffleFlags.useNewUnitPage
26+
? adoptCourseSectionUrl({ url, courseId, sequenceId })
27+
: `${getConfig().STUDIO_BASE_URL}${url}`);
28+
29+
const getPathToCoursePage = (isOutlinePage, url) => (
30+
isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url)
31+
);
32+
2433
return (
2534
<nav className="d-flex align-center mb-2.5">
2635
<ol className="p-0 m-0 d-flex align-center">
27-
<li className="d-flex">
28-
<Dropdown>
29-
<Dropdown.Toggle id="breadcrumbs-dropdown-section" variant="link" className="p-0 text-primary small">
30-
<span className="small text-gray-700">{section.title}</span>
36+
{ancestorXblocks.map(({ children, title, isLast }, index) => (
37+
<li
38+
className="d-flex"
39+
key={title}
40+
>
41+
<Dropdown>
42+
<Dropdown.Toggle
43+
id="breadcrumbs-dropdown-section"
44+
variant="link"
45+
className="p-0 text-primary small"
46+
>
47+
<span className="small text-gray-700">
48+
{title}
49+
</span>
50+
<Icon
51+
src={ArrowDropDownIcon}
52+
className="text-primary ml-1"
53+
/>
54+
</Dropdown.Toggle>
55+
<Dropdown.Menu>
56+
{children.map(({ url, displayName }) => (
57+
<Dropdown.Item
58+
as={Link}
59+
key={url}
60+
to={getPathToCoursePage(index < 2, url)}
61+
className="small"
62+
data-testid="breadcrumbs-section-dropdown-item"
63+
>
64+
{displayName}
65+
</Dropdown.Item>
66+
))}
67+
</Dropdown.Menu>
68+
</Dropdown>
69+
{!isLast && (
3170
<Icon
32-
src={ArrowDropDownIcon}
33-
className="text-primary ml-1"
71+
src={ChevronRightIcon}
72+
size="md"
73+
className="text-primary mx-2"
74+
alt={intl.formatMessage(messages.altIconChevron)}
3475
/>
35-
</Dropdown.Toggle>
36-
<Dropdown.Menu>
37-
{section.children.map(({ url, displayName }) => (
38-
<Dropdown.Item
39-
as={Link}
40-
key={url}
41-
to={getPathToCourseOutlinePage(url)}
42-
className="small"
43-
data-testid="breadcrumbs-section-dropdown-item"
44-
>
45-
{displayName}
46-
</Dropdown.Item>
47-
))}
48-
</Dropdown.Menu>
49-
</Dropdown>
50-
<Icon
51-
src={ChevronRightIcon}
52-
size="md"
53-
className="text-primary mx-2"
54-
alt={intl.formatMessage(messages.altIconChevron)}
55-
/>
56-
</li>
57-
<li className="d-flex">
58-
<Dropdown>
59-
<Dropdown.Toggle id="breadcrumbs-dropdown-subsection" variant="link" className="p-0 text-primary">
60-
<span className="small text-gray-700">{subsection.title}</span>
61-
<Icon
62-
src={ArrowDropDownIcon}
63-
className="text-primary ml-1"
64-
/>
65-
</Dropdown.Toggle>
66-
<Dropdown.Menu>
67-
{subsection.children.map(({ url, displayName }) => (
68-
<Dropdown.Item
69-
as={Link}
70-
key={url}
71-
to={getPathToCourseOutlinePage(url)}
72-
className="small"
73-
data-testid="breadcrumbs-subsection-dropdown-item"
74-
>
75-
{displayName}
76-
</Dropdown.Item>
77-
))}
78-
</Dropdown.Menu>
79-
</Dropdown>
80-
</li>
76+
)}
77+
</li>
78+
))}
8179
</ol>
8280
</nav>
8381
);
8482
};
8583

84+
Breadcrumbs.propTypes = {
85+
courseId: PropTypes.string.isRequired,
86+
sequenceId: PropTypes.string.isRequired,
87+
};
88+
8689
export default Breadcrumbs;

src/course-unit/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const messageTypes = {
5252
videoFullScreen: 'plugin.videoFullScreen',
5353
refreshXBlock: 'refreshXBlock',
5454
showMoveXBlockModal: 'showMoveXBlockModal',
55+
handleViewXBlockContent: 'handleViewXBlockContent',
5556
};
5657

5758
export const IFRAME_FEATURE_POLICY = (

src/course-unit/context/iFrameContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {
1+
import React, {
22
createContext, MutableRefObject, useRef, useCallback, useMemo, ReactNode,
33
} from 'react';
44
import { logError } from '@edx/frontend-platform/logging';

src/course-unit/data/api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export async function createCourseXblock({
9898
* @param {string} type - The action type (e.g., PUBLISH_TYPES.discardChanges).
9999
* @param {boolean} isVisible - The visibility status for students.
100100
* @param {boolean} groupAccess - Access group key set.
101+
* @param {boolean} isDiscussionEnabled - Indicates whether the discussion feature is enabled.
101102
* @returns {Promise<any>} A promise that resolves with the response data.
102103
*/
103104
export async function handleCourseUnitVisibilityAndData(unitId, type, isVisible, groupAccess, isDiscussionEnabled) {

src/course-unit/data/thunk.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function fetchCourseSectionVerticalData(courseId, sequenceId) {
7474
}));
7575
dispatch(updateModels({
7676
modelType: 'units',
77-
models: courseSectionVerticalData.units,
77+
models: courseSectionVerticalData.units || [],
7878
}));
7979
dispatch(fetchStaticFileNoticesSuccess(JSON.parse(localStorage.getItem('staticFileNotices'))));
8080
localStorage.removeItem('staticFileNotices');
@@ -107,7 +107,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
107107
}));
108108
dispatch(updateModels({
109109
modelType: 'units',
110-
models: courseSectionVerticalData.units,
110+
models: courseSectionVerticalData.units || [],
111111
}));
112112
dispatch(fetchSequenceSuccess({ sequenceId }));
113113
dispatch(fetchCourseItemSuccess(courseUnit));

src/course-unit/data/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ export function normalizeCourseSectionVerticalData(metadata) {
1010
sequence: {
1111
id: data.subsectionLocation,
1212
title: data.xblock.displayName,
13-
unitIds: data.xblockInfo.ancestorInfo.ancestors[0].childInfo.children.map((item) => item.id),
13+
unitIds: data.xblockInfo.ancestorInfo?.ancestors[0].childInfo.children.map((item) => item.id),
1414
},
15-
units: data.xblockInfo.ancestorInfo.ancestors[0].childInfo.children.map((unit) => ({
15+
units: data.xblockInfo.ancestorInfo?.ancestors[0].childInfo.children.map((unit) => ({
1616
id: unit.id,
1717
sequenceId: data.subsectionLocation,
1818
bookmarked: unit.bookmarked,

0 commit comments

Comments
 (0)