Skip to content

Commit

Permalink
feat: update breadcrumbs on course unit page
Browse files Browse the repository at this point in the history
  • Loading branch information
ihor-romaniuk committed Jan 5, 2024
1 parent be5c8c2 commit 8bb2745
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 49 deletions.
10 changes: 6 additions & 4 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
React, useState, useEffect,
} from 'react';
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Expand Down Expand Up @@ -40,7 +38,9 @@ import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
import PublishModal from './publish-modal/PublishModal';
import ConfigureModal from './configure-modal/ConfigureModal';
import DeleteModal from './delete-modal/DeleteModal';
import { useCourseOutline } from './hooks';
import {
useCourseOutline, useScrollToLocatorElement,
} from './hooks';
import messages from './messages';

const CourseOutline = ({ courseId }) => {
Expand Down Expand Up @@ -88,6 +88,8 @@ const CourseOutline = ({ courseId }) => {
handleDragNDrop,
} = useCourseOutline({ courseId });

useScrollToLocatorElement({ isLoading });

const [sections, setSections] = useState(sectionsList);

const initialSections = [...sectionsList];
Expand Down
16 changes: 15 additions & 1 deletion src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import {
act, render, waitFor, cleanup, fireEvent, within,
} from '@testing-library/react';
Expand Down Expand Up @@ -46,6 +45,8 @@ let axiosMock;
let store;
const mockPathname = '/foo-bar';
const courseId = '123';
const locatorSectionName = 'Demo Course Overview';
const locatorSectionId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';

window.HTMLElement.prototype.scrollIntoView = jest.fn();

Expand All @@ -54,6 +55,7 @@ jest.mock('react-router-dom', () => ({
useLocation: () => ({
pathname: mockPathname,
}),
useSearchParams: () => [new URLSearchParams({ show: locatorSectionId })],
}));

jest.mock('../help-urls/hooks', () => ({
Expand Down Expand Up @@ -380,6 +382,18 @@ describe('<CourseOutline />', () => {
expect(await findAllByTestId('section-card')).toHaveLength(5);
});

it('check correct scrolling to the locator section when URL has a "show" param', async () => {
const scrollIntoViewFn = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewFn;
const { getByText } = render(<RootWrapper />);

await waitFor(() => {
expect(getByText(locatorSectionName)).toBeInTheDocument();
expect(scrollIntoViewFn).toHaveBeenCalled();
expect(scrollIntoViewFn).toHaveBeenCalledWith({ behavior: 'smooth' });
});
});

it('check whether subsection is duplicated successfully', async () => {
const { findAllByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
Expand Down
8 changes: 7 additions & 1 deletion src/course-outline/card-header/CardHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import messages from './messages';
const CardHeader = ({
title,
status,
sectionId,
hasChanges,
isExpanded,
onClickPublish,
Expand Down Expand Up @@ -58,7 +59,11 @@ const CardHeader = ({
});

return (
<div className="item-card-header" data-testid={`${namePrefix}-card-header`}>
<div
className="item-card-header"
data-locator={sectionId}
data-testid={`${namePrefix}-card-header`}
>
{isFormOpen ? (
<Form.Group className="m-0">
<Form.Control
Expand Down Expand Up @@ -167,6 +172,7 @@ const CardHeader = ({
CardHeader.propTypes = {
title: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
sectionId: PropTypes.string.isRequired,
hasChanges: PropTypes.bool.isRequired,
isExpanded: PropTypes.bool.isRequired,
onExpand: PropTypes.func.isRequired,
Expand Down
20 changes: 18 additions & 2 deletions src/course-outline/hooks.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
import { useToggle } from '@edx/paragon';

import { RequestStatus } from '../data/constants';
Expand Down Expand Up @@ -211,5 +212,20 @@ const useCourseOutline = ({ courseId }) => {
};
};

// eslint-disable-next-line import/prefer-default-export
export { useCourseOutline };
const useScrollToLocatorElement = ({ isLoading }) => {
const [searchParams] = useSearchParams();

useEffect(() => {
const locator = searchParams.get('show');
const locatorToShow = document.querySelector(`[data-locator="${locator}"]`);

if (locator && locatorToShow) {
locatorToShow.scrollIntoView({ behavior: 'smooth' });
}
}, [isLoading]);
};

export {
useCourseOutline,
useScrollToLocatorElement,
};
13 changes: 11 additions & 2 deletions src/course-outline/subsection-card/SubsectionCard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, useToggle } from '@edx/paragon';
import { Add as IconAdd } from '@edx/paragon/icons';
Expand All @@ -24,7 +25,10 @@ const SubsectionCard = ({
const currentRef = useRef(null);
const intl = useIntl();
const dispatch = useDispatch();
const [isExpanded, setIsExpanded] = useState(false);
const [searchParams] = useSearchParams();
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === subsection.id;
const [isExpanded, setIsExpanded] = useState(locatorId ? isScrolledToElement : false);
const [isFormOpen, openForm, closeForm] = useToggle(false);

const {
Expand Down Expand Up @@ -81,7 +85,12 @@ const SubsectionCard = ({
}, [savingStatus]);

return (
<div className="subsection-card" data-testid="subsection-card" ref={currentRef}>
<div
className="subsection-card"
data-locator={subsection.id}
data-testid="subsection-card"
ref={currentRef}
>
<CardHeader
title={displayName}
status={subsectionStatus}
Expand Down
51 changes: 31 additions & 20 deletions src/course-outline/subsection-card/SubsectionCard.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
Expand Down Expand Up @@ -38,25 +38,27 @@ const subsection = {

const onEditSubectionSubmit = jest.fn();

const renderComponent = (props) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<SubsectionCard
section={section}
subsection={subsection}
onOpenPublishModal={jest.fn()}
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
onEditClick={jest.fn()}
savingStatus=""
onEditSubmit={onEditSubectionSubmit}
onDuplicateSubmit={jest.fn()}
namePrefix="subsection"
{...props}
>
<span>children</span>
</SubsectionCard>
</IntlProvider>,
const renderComponent = (props, entry = '/') => render(
<AppProvider store={store} wrapWithRouter={false}>
<MemoryRouter initialEntries={[entry]}>
<IntlProvider locale="en">
<SubsectionCard
section={section}
subsection={subsection}
onOpenPublishModal={jest.fn()}
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
onEditClick={jest.fn()}
savingStatus=""
onEditSubmit={onEditSubectionSubmit}
onDuplicateSubmit={jest.fn()}
namePrefix="subsection"
{...props}
>
<span>children</span>
</SubsectionCard>
</IntlProvider>
</MemoryRouter>
</AppProvider>,
);

Expand Down Expand Up @@ -122,4 +124,13 @@ describe('<SubsectionCard />', () => {
fireEvent.keyDown(editField, { key: 'Enter', keyCode: 13 });
expect(onEditSubectionSubmit).toHaveBeenCalled();
});

it('check extended section when URL has a "show" param', async () => {
const { findByTestId } = renderComponent(null, `?show=${section.id}`);

const cardUnits = await findByTestId('subsection-card__units');
const newUnitButton = await findByTestId('new-unit-button');
expect(cardUnits).toBeInTheDocument();
expect(newUnitButton).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/course-unit/__mocks__/courseUnitIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ module.exports = {
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
display_name: 'Example Week 1: Getting Started',
category: 'chapter',
has_children: true,
Expand Down
50 changes: 32 additions & 18 deletions src/course-unit/breadcrumbs/Breadcrumbs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import messages from './messages';
const Breadcrumbs = ({ courseId }) => {
const intl = useIntl();
const { ancestorInfo } = useSelector(getCourseUnitData);
const { sectionsList } = useCourseOutline({ courseId });
const { sectionsList, isLoading: isLoadingCourseOutline } = useCourseOutline({ courseId });
const activeCourseSectionInfo = sectionsList.find((block) => block.id === ancestorInfo?.ancestors[1]?.id);

const breadcrumbs = {
section: {
id: ancestorInfo?.ancestors[1]?.id,
displayName: ancestorInfo?.ancestors[1]?.displayName,
dropdownItems: sectionsList || [],
dropdownItems: sectionsList,
},
subsection: {
id: ancestorInfo?.ancestors[0]?.id,
Expand All @@ -30,6 +30,12 @@ const Breadcrumbs = ({ courseId }) => {
},
};

const getLoadingPlaceholder = () => (
<div className="small px-3 py-2" role="status">
{intl.formatMessage(messages.loading)}
</div>
);

return (
<nav className="d-flex align-center mb-2.5">
<ol className="p-0 m-0 d-flex align-center">
Expand All @@ -43,14 +49,18 @@ const Breadcrumbs = ({ courseId }) => {
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{breadcrumbs.section.dropdownItems.map(({ studioUrl, displayName }) => (
<Dropdown.Item
href={studioUrl}
className="small"
>
{displayName}
</Dropdown.Item>
))}
{(isLoadingCourseOutline || !breadcrumbs.section.dropdownItems.length)
? getLoadingPlaceholder()
: breadcrumbs.section.dropdownItems.map(({ id, studioUrl, displayName }) => (
<Dropdown.Item
key={id}
href={studioUrl}
className="small"
data-testid="breadcrumbs-section-dropdown-item"
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<Icon
Expand All @@ -70,14 +80,18 @@ const Breadcrumbs = ({ courseId }) => {
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{breadcrumbs.subsection.dropdownItems.map(({ studioUrl, displayName }) => (
<Dropdown.Item
href={studioUrl}
className="small"
>
{displayName}
</Dropdown.Item>
))}
{(isLoadingCourseOutline || !breadcrumbs.subsection.dropdownItems.length)
? getLoadingPlaceholder()
: breadcrumbs.subsection.dropdownItems.map(({ id, studioUrl, displayName }) => (
<Dropdown.Item
key={id}
href={studioUrl}
className="small"
data-testid="breadcrumbs-subsection-dropdown-item"
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
</li>
Expand Down
Loading

0 comments on commit 8bb2745

Please sign in to comment.