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 (