diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 678b721093..6811e3244b 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -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 { @@ -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 }) => { @@ -88,6 +88,8 @@ const CourseOutline = ({ courseId }) => { handleDragNDrop, } = useCourseOutline({ courseId }); + useScrollToLocatorElement({ isLoading }); + const [sections, setSections] = useState(sectionsList); const initialSections = [...sectionsList]; diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 0f4ee33e17..b8bd9982f2 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import { act, render, waitFor, cleanup, fireEvent, within, } from '@testing-library/react'; @@ -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(); @@ -54,6 +55,7 @@ jest.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: mockPathname, }), + useSearchParams: () => [new URLSearchParams({ show: locatorSectionId })], })); jest.mock('../help-urls/hooks', () => ({ @@ -380,6 +382,18 @@ describe('', () => { 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(); + + 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(); const section = courseOutlineIndexMock.courseStructure.childInfo.children[0]; diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index 2c06e02220..a38fb778bc 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -27,6 +27,7 @@ import messages from './messages'; const CardHeader = ({ title, status, + sectionId, hasChanges, isExpanded, onClickPublish, @@ -58,7 +59,11 @@ const CardHeader = ({ }); return ( -
+
{isFormOpen ? ( { }; }; -// eslint-disable-next-line import/prefer-default-export -export { useCourseOutline }; +const useScrollToLocatorElement = ({ isLoading }) => { + const [searchParams] = useSearchParams(); + + useEffect(() => { + const locator = searchParams.get('show'); + if (!locator) { + return; + } + + const locatorToShow = document.querySelector(`[data-locator="${locator}"]`); + if (!locatorToShow) { + return; + } + locatorToShow.scrollIntoView({ behavior: 'smooth' }); + }, [isLoading]); +}; + +export { + useCourseOutline, + useScrollToLocatorElement, +}; diff --git a/src/course-outline/subsection-card/SubsectionCard.jsx b/src/course-outline/subsection-card/SubsectionCard.jsx index 2dbf1e5e01..236b9b175b 100644 --- a/src/course-outline/subsection-card/SubsectionCard.jsx +++ b/src/course-outline/subsection-card/SubsectionCard.jsx @@ -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'; @@ -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 { @@ -81,7 +85,12 @@ const SubsectionCard = ({ }, [savingStatus]); return ( -
+
render( - - - - children - - , +const renderComponent = (props, entry = '/') => render( + + + + + children + + + , ); @@ -122,4 +124,13 @@ describe('', () => { 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(); + }); }); diff --git a/src/course-unit/__mocks__/courseUnitIndex.js b/src/course-unit/__mocks__/courseUnitIndex.js index 9d449c08f9..71fb8b1d04 100644 --- a/src/course-unit/__mocks__/courseUnitIndex.js +++ b/src/course-unit/__mocks__/courseUnitIndex.js @@ -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, diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.jsx b/src/course-unit/breadcrumbs/Breadcrumbs.jsx index bc38088fe3..ff09ce98ea 100644 --- a/src/course-unit/breadcrumbs/Breadcrumbs.jsx +++ b/src/course-unit/breadcrumbs/Breadcrumbs.jsx @@ -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, @@ -30,6 +30,12 @@ const Breadcrumbs = ({ courseId }) => { }, }; + const getLoadingPlaceholder = () => ( +
+ {intl.formatMessage(messages.loading)} +
+ ); + return (