Skip to content

Commit

Permalink
feat: [FC-0070] rendering library content in unit page
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor-romaniuk committed Nov 25, 2024
1 parent 55fe87a commit f836c6c
Show file tree
Hide file tree
Showing 31 changed files with 305 additions and 140 deletions.
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const COURSE_BLOCK_NAMES = ({
chapter: { id: 'chapter', name: 'Section' },
sequential: { id: 'sequential', name: 'Subsection' },
vertical: { id: 'vertical', name: 'Unit' },
libraryContent: { id: 'library_content', name: 'Library content' },
component: { id: 'component', name: 'Component' },
});

Expand Down
82 changes: 51 additions & 31 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ProcessingNotification from '../generic/processing-notification';
import { SavingErrorAlert } from '../generic/saving-error-alert';
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
import Loading from '../generic/Loading';
import { COURSE_BLOCK_NAMES } from '../constants';
import AddComponent from './add-component/AddComponent';
import HeaderTitle from './header-title/HeaderTitle';
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
Expand All @@ -45,6 +46,7 @@ const CourseUnit = ({ courseId }) => {
isLoading,
sequenceId,
unitTitle,
unitCategory,
errorMessage,
sequenceStatus,
savingStatus,
Expand All @@ -71,6 +73,19 @@ const CourseUnit = ({ courseId }) => {
handleNavigateToTargetUnit,
} = useCourseUnit({ courseId, blockId });

const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;

const unitLayout = [{ span: 12 }, { span: 0 }];
const defaultLayout = {
lg: [{ span: 8 }, { span: 4 }],
md: [{ span: 8 }, { span: 4 }],
sm: [{ span: 8 }, { span: 3 }],
xs: [{ span: 9 }, { span: 3 }],
xl: [{ span: 9 }, { span: 3 }],
};
const layoutGrid = isUnitLibraryType ? { lg: unitLayout } : defaultLayout;

useEffect(() => {
document.title = getPageHeadTitle('', unitTitle);
}, [unitTitle]);
Expand Down Expand Up @@ -142,28 +157,28 @@ const CourseUnit = ({ courseId }) => {
/>
)}
breadcrumbs={(
<Breadcrumbs />
<Breadcrumbs
courseId={courseId}
sequenceId={sequenceId}
/>
)}
headerActions={(
<HeaderNavigations
unitCategory={unitCategory}
headerNavigationsActions={headerNavigationsActions}
/>
)}
/>
<Sequence
courseId={courseId}
sequenceId={sequenceId}
unitId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
showPasteUnit={showPasteUnit}
/>
<Layout
lg={[{ span: 8 }, { span: 4 }]}
md={[{ span: 8 }, { span: 4 }]}
sm={[{ span: 8 }, { span: 3 }]}
xs={[{ span: 9 }, { span: 3 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
{isUnitVerticalType && (
<Sequence
courseId={courseId}
sequenceId={sequenceId}
unitId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
showPasteUnit={showPasteUnit}
/>
)}
<Layout {...layoutGrid}>
<Layout.Element>
{currentlyVisibleToStudents && (
<AlertMessage
Expand All @@ -184,11 +199,13 @@ const CourseUnit = ({ courseId }) => {
unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children}
/>
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
{showPasteXBlock && canPasteComponent && (
{isUnitVerticalType && (
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
)}
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
<PasteComponent
clipboardData={sharedClipboardData}
onClick={handleCreateNewCourseXBlock}
Expand All @@ -205,18 +222,21 @@ const CourseUnit = ({ courseId }) => {
</Layout.Element>
<Layout.Element>
<Stack gap={3}>
<Sidebar data-testid="course-unit-sidebar">
<PublishControls blockId={blockId} />
</Sidebar>
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true'
&& (
<Sidebar className="tags-sidebar">
<TagsSidebarControls />
</Sidebar>
{isUnitVerticalType && (
<>
<Sidebar data-testid="course-unit-sidebar">
<PublishControls blockId={blockId} />
</Sidebar>
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Sidebar className="tags-sidebar">
<TagsSidebarControls />
</Sidebar>
)}
<Sidebar data-testid="course-unit-location-sidebar">
<LocationInfo />
</Sidebar>
</>
)}
<Sidebar data-testid="course-unit-location-sidebar">
<LocationInfo />
</Sidebar>
</Stack>
</Layout.Element>
</Layout>
Expand Down
4 changes: 4 additions & 0 deletions src/course-unit/CourseUnit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
@import "./move-modal";
@import "./preview-changes";

.course-unit {
min-width: 900px;
}

.course-unit__alert {
margin-bottom: 1.75rem;
}
23 changes: 19 additions & 4 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import sidebarMessages from './sidebar/messages';
import { extractCourseUnitId } from './sidebar/utils';
import CourseUnit from './CourseUnit';

import { getClipboardUrl } from '../generic/data/api';
import configureModalMessages from '../generic/configure-modal/messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
import addComponentMessages from './add-component/messages';
Expand Down Expand Up @@ -138,6 +139,9 @@ describe('<CourseUnit />', () => {
global.localStorage.clear();
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getClipboardUrl())
.reply(200, clipboardUnit);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseUnitIndexMock);
Expand Down Expand Up @@ -226,6 +230,19 @@ describe('<CourseUnit />', () => {
display_name: newDisplayName,
},
});
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
display_name: newDisplayName,
},
xblock: {
...courseSectionVerticalMock.xblock,
display_name: newDisplayName,
},
});

await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
Expand Down Expand Up @@ -914,9 +931,7 @@ describe('<CourseUnit />', () => {
.reply(200, clipboardMockResponse);
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...updatedCourseSectionVerticalData,
});
.reply(200, updatedCourseSectionVerticalData);

global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
Expand Down Expand Up @@ -1190,7 +1205,7 @@ describe('<CourseUnit />', () => {

axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {});
.reply(200, courseUnitIndexMock);

await act(async () => {
await waitFor(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/course-unit/add-component/AddComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates } = useSelector(getCourseSectionVertical);
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { Icon } from '@openedx/paragon';
import { EditNote as EditNoteIcon } from '@openedx/paragon/icons';

import { COMPONENT_TYPES, COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';
import { COMPONENT_TYPE_ICON_MAP } from '../../../generic/block-type-utils/constants';

const AddComponentIcon = ({ type }) => {
const icon = COMPONENT_TYPE_ICON_MAP[type] || EditNoteIcon;
Expand All @@ -11,7 +11,7 @@ const AddComponentIcon = ({ type }) => {
};

AddComponentIcon.propTypes = {
type: PropTypes.oneOf(Object.values(COMPONENT_TYPES)).isRequired,
type: PropTypes.string.isRequired,
};

export default AddComponentIcon;
1 change: 1 addition & 0 deletions src/course-unit/add-component/add-component-btn/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import { Badge, Button } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';

import { COMPONENT_TYPES } from '../../../generic/block-type-utils/constants';

Check failure on line 5 in src/course-unit/add-component/add-component-btn/index.jsx

View workflow job for this annotation

GitHub Actions / tests

'COMPONENT_TYPES' is defined but never used
import messages from '../messages';
import AddComponentIcon from './AddComponentIcon';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ComponentModalView = ({
<OverlayTrigger
placement="right"
overlay={(
<Tooltip>
<Tooltip id={`${componentTemplate.displayName}-support-tooltip`}>
{supportLabels[componentTemplate.supportLevel].tooltip}
</Tooltip>
)}
Expand Down
113 changes: 58 additions & 55 deletions src/course-unit/breadcrumbs/Breadcrumbs.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon } from '@openedx/paragon';
Expand All @@ -10,77 +11,79 @@ import { getConfig } from '@edx/frontend-platform';

import { getWaffleFlags } from '../../data/selectors';
import { getCourseSectionVertical } from '../data/selectors';
import { adoptCourseSectionUrl } from '../utils';
import messages from './messages';

const Breadcrumbs = () => {
const Breadcrumbs = ({ courseId, sequenceId }) => {
const intl = useIntl();
const { ancestorXblocks } = useSelector(getCourseSectionVertical);
const [section, subsection] = ancestorXblocks ?? [];
const { ancestorXblocks = [] } = useSelector(getCourseSectionVertical);
const waffleFlags = useSelector(getWaffleFlags);

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

const getPathToCourseUnitPage = (url) => (waffleFlags.useNewUnitPage
? adoptCourseSectionUrl({ url, courseId, sequenceId })
: `${getConfig().STUDIO_BASE_URL}${url}`);

const getPathToCoursePage = (isOutlinePage, url) => (
isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url)
);

return (
<nav className="d-flex align-center mb-2.5">
<ol className="p-0 m-0 d-flex align-center">
<li className="d-flex">
<Dropdown>
<Dropdown.Toggle id="breadcrumbs-dropdown-section" variant="link" className="p-0 text-primary small">
<span className="small text-gray-700">{section.title}</span>
{ancestorXblocks.map(({ children, title, isLast }, index) => (
<li
className="d-flex"
key={title}
>
<Dropdown>
<Dropdown.Toggle
id="breadcrumbs-dropdown-section"
variant="link"
className="p-0 text-primary small"
>
<span className="small text-gray-700">
{title}
</span>
<Icon
src={ArrowDropDownIcon}
className="text-primary ml-1"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{children.map(({ url, displayName }) => (
<Dropdown.Item
as={Link}
key={url}
to={getPathToCoursePage(index < 2, url)}
className="small"
data-testid={`breadcrumbs-dropdown-item-level${index}`}
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
{!isLast && (
<Icon
src={ArrowDropDownIcon}
className="text-primary ml-1"
src={ChevronRightIcon}
size="md"
className="text-primary mx-2"
alt={intl.formatMessage(messages.altIconChevron)}
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{section.children.map(({ url, displayName }) => (
<Dropdown.Item
as={Link}
key={url}
to={getPathToCourseOutlinePage(url)}
className="small"
data-testid="breadcrumbs-section-dropdown-item"
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<Icon
src={ChevronRightIcon}
size="md"
className="text-primary mx-2"
alt={intl.formatMessage(messages.altIconChevron)}
/>
</li>
<li className="d-flex">
<Dropdown>
<Dropdown.Toggle id="breadcrumbs-dropdown-subsection" variant="link" className="p-0 text-primary">
<span className="small text-gray-700">{subsection.title}</span>
<Icon
src={ArrowDropDownIcon}
className="text-primary ml-1"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{subsection.children.map(({ url, displayName }) => (
<Dropdown.Item
as={Link}
key={url}
to={getPathToCourseOutlinePage(url)}
className="small"
data-testid="breadcrumbs-subsection-dropdown-item"
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
</li>
)}
</li>
))}
</ol>
</nav>
);
};

Breadcrumbs.propTypes = {
courseId: PropTypes.string.isRequired,
sequenceId: PropTypes.string.isRequired,
};

export default Breadcrumbs;
Loading

0 comments on commit f836c6c

Please sign in to comment.