refactor: Unit page - refactoring breadcrumbs, view live and preview links buttons (#827)
This commit is contained in:
1
.env
1
.env
@@ -9,7 +9,6 @@ EXAMS_BASE_URL=''
|
||||
FAVICON_URL=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
LMS_BASE_URL=''
|
||||
PREVIEW_BASE_URL=''
|
||||
LEARNING_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
|
||||
@@ -42,4 +42,3 @@ HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=true
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
|
||||
PREVIEW_BASE_URL='http://preview.localhost:18000'
|
||||
|
||||
@@ -8,7 +8,6 @@ EXAMS_BASE_URL=
|
||||
FAVICON_URL='https://edx-cdn.org/v3/default/favicon.ico'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
PREVIEW_BASE_URL='http://preview.localhost:18000'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
|
||||
@@ -66,9 +66,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
/>
|
||||
)}
|
||||
breadcrumbs={(
|
||||
<Breadcrumbs
|
||||
courseId={courseId}
|
||||
/>
|
||||
<Breadcrumbs />
|
||||
)}
|
||||
headerActions={(
|
||||
<HeaderNavigations
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
@@ -28,14 +28,11 @@ import { executeThunk } from '../utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
import headerNavigationsMessages from './header-navigations/messages';
|
||||
import headerTitleMessages from './header-title/messages';
|
||||
import { getUnitPreviewPath, getUnitViewLivePath } from './utils';
|
||||
import messages from './add-component/messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseId = '123';
|
||||
const sectionId = 'graded_interactions';
|
||||
const subsectionId = '19a30717eff543078a5d94ae9d6c18a5';
|
||||
const blockId = '567890';
|
||||
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@@ -97,20 +94,21 @@ describe('<CourseUnit />', () => {
|
||||
const { open } = window;
|
||||
window.open = jest.fn();
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const {
|
||||
draft_preview_link: draftPreviewLink,
|
||||
published_preview_link: publishedPreviewLink,
|
||||
} = courseSectionVerticalMock;
|
||||
|
||||
await waitFor(() => {
|
||||
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||
userEvent.click(viewLiveButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
const VIEW_LIVE_LINK = getConfig().LMS_BASE_URL + getUnitViewLivePath(courseId, blockId);
|
||||
expect(window.open).toHaveBeenCalledWith(VIEW_LIVE_LINK, '_blank');
|
||||
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
|
||||
|
||||
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||
userEvent.click(previewButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
// eslint-disable-next-line max-len
|
||||
const PREVIEW_LINK = getConfig().PREVIEW_BASE_URL + getUnitPreviewPath(courseId, sectionId, subsectionId, blockId);
|
||||
expect(window.open).toHaveBeenCalledWith(PREVIEW_LINK, '_blank');
|
||||
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
|
||||
});
|
||||
|
||||
window.open = open;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown, Icon } from '@edx/paragon';
|
||||
@@ -7,34 +6,14 @@ import {
|
||||
ChevronRight as ChevronRightIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { useCourseOutline } from '../../course-outline/hooks';
|
||||
import { getCourseUnitData } from '../data/selectors';
|
||||
import { createCorrectInternalRoute } from '../../utils';
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const Breadcrumbs = ({ courseId }) => {
|
||||
const Breadcrumbs = () => {
|
||||
const intl = useIntl();
|
||||
const { ancestorInfo } = useSelector(getCourseUnitData);
|
||||
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,
|
||||
},
|
||||
subsection: {
|
||||
id: ancestorInfo?.ancestors[0]?.id,
|
||||
displayName: ancestorInfo?.ancestors[0]?.displayName,
|
||||
dropdownItems: activeCourseSectionInfo?.childInfo.children || [],
|
||||
},
|
||||
};
|
||||
|
||||
const getLoadingPlaceholder = () => (
|
||||
<div className="small px-3 py-2" role="status">
|
||||
{intl.formatMessage(messages.loading)}
|
||||
</div>
|
||||
);
|
||||
const { ancestorXblocks } = useSelector(getCourseSectionVertical);
|
||||
const [section, subsection] = ancestorXblocks ?? [];
|
||||
|
||||
return (
|
||||
<nav className="d-flex align-center mb-2.5">
|
||||
@@ -42,25 +21,23 @@ const Breadcrumbs = ({ courseId }) => {
|
||||
<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">{breadcrumbs.section.displayName}</span>
|
||||
<span className="small text-gray-700">{section.title}</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{(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>
|
||||
))}
|
||||
{section.children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
key={url}
|
||||
href={createCorrectInternalRoute(url)}
|
||||
className="small"
|
||||
data-testid="breadcrumbs-section-dropdown-item"
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<Icon
|
||||
@@ -73,25 +50,23 @@ const Breadcrumbs = ({ courseId }) => {
|
||||
<li className="d-flex">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="breadcrumbs-dropdown-subsection" variant="link" className="p-0 text-primary">
|
||||
<span className="small text-gray-700">{breadcrumbs.subsection.displayName}</span>
|
||||
<span className="small text-gray-700">{subsection.title}</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{(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>
|
||||
))}
|
||||
{subsection.children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
key={url}
|
||||
href={createCorrectInternalRoute(url)}
|
||||
className="small"
|
||||
data-testid="breadcrumbs-subsection-dropdown-item"
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</li>
|
||||
@@ -100,8 +75,4 @@ const Breadcrumbs = ({ courseId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Breadcrumbs.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.course-unit {
|
||||
.sub-header {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sub-header-title .sub-header-breadcrumbs {
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
|
||||
@@ -6,18 +6,13 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { getCourseOutlineIndexApiUrl } from '../../course-outline/data/api';
|
||||
import { fetchCourseOutlineIndexQuery } from '../../course-outline/data/thunk';
|
||||
import { courseOutlineIndexMock } from '../../course-outline/__mocks__';
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseUnitApiUrl } from '../data/api';
|
||||
import { fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseUnitIndexMock } from '../__mocks__';
|
||||
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
|
||||
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
|
||||
import Breadcrumbs from './Breadcrumbs';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseId = '123';
|
||||
@@ -56,6 +51,10 @@ describe('<Breadcrumbs />', () => {
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render Breadcrumbs component correctly', async () => {
|
||||
@@ -67,26 +66,7 @@ describe('<Breadcrumbs />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('render dropdown loading placeholder on pending', async () => {
|
||||
const { getByText, queryAllByTestId } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
expect(getByText(breadcrumbsExpected.subsection.displayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const button = getByText(breadcrumbsExpected.section.displayName);
|
||||
userEvent.click(button);
|
||||
|
||||
expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(0);
|
||||
expect(getByText(messages.loading.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render Breadcrumbs\'s dropdown menus correctly', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(200, courseOutlineIndexMock);
|
||||
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
||||
const { getByText, queryAllByTestId } = renderComponent();
|
||||
|
||||
expect(getByText(breadcrumbsExpected.section.displayName)).toBeInTheDocument();
|
||||
@@ -97,10 +77,10 @@ describe('<Breadcrumbs />', () => {
|
||||
const button = getByText(breadcrumbsExpected.section.displayName);
|
||||
userEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(4);
|
||||
expect(queryAllByTestId('breadcrumbs-section-dropdown-item')).toHaveLength(5);
|
||||
});
|
||||
|
||||
userEvent.click(getByText(breadcrumbsExpected.subsection.displayName));
|
||||
expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(3);
|
||||
expect(queryAllByTestId('breadcrumbs-subsection-dropdown-item')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
@@ -13,21 +12,21 @@ import {
|
||||
fetchCourseSectionVerticalData,
|
||||
} from './data/thunk';
|
||||
import {
|
||||
getCourseSectionVertical,
|
||||
getCourseUnitData,
|
||||
getLoadingStatus,
|
||||
getSavingStatus,
|
||||
} from './data/selectors';
|
||||
import { updateSavingStatus } from './data/slice';
|
||||
import { getUnitViewLivePath, getUnitPreviewPath } from './utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { config } = useContext(AppContext);
|
||||
const courseUnit = useSelector(getCourseUnitData);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const { draftPreviewLink, publishedPreviewLink } = useSelector(getCourseSectionVertical);
|
||||
const navigate = useNavigate();
|
||||
const [isTitleEditFormOpen, toggleTitleEditForm] = useState(false);
|
||||
|
||||
@@ -36,12 +35,10 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
|
||||
const headerNavigationsActions = {
|
||||
handleViewLive: () => {
|
||||
window.open(config.LMS_BASE_URL + getUnitViewLivePath(courseId, blockId), '_blank');
|
||||
window.open(publishedPreviewLink, '_blank');
|
||||
},
|
||||
handlePreview: () => {
|
||||
const subsectionId = courseUnit.ancestorInfo?.ancestors[0]?.id.split('@').pop();
|
||||
const sectionId = courseUnit.ancestorInfo?.ancestors[1]?.id.split('@').pop();
|
||||
window.open(config.PREVIEW_BASE_URL + getUnitPreviewPath(courseId, sectionId, subsectionId, blockId), '_blank');
|
||||
window.open(draftPreviewLink, '_blank');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable max-len */
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Method to return course unit view live URL path.
|
||||
* @param {string} courseId
|
||||
* @param {string} blockId
|
||||
* @returns {string} {`/courses/${string}/jump_to/${string}`}
|
||||
*/
|
||||
export const getUnitViewLivePath = (courseId, blockId) => (
|
||||
`/courses/${courseId}/jump_to/${blockId}`
|
||||
);
|
||||
|
||||
/**
|
||||
* Method to return course unit preview URL path.
|
||||
* @param {string} courseId
|
||||
* @param {string} sectionId
|
||||
* @param {string} blockId
|
||||
* @returns {string} {`/courses/${courseId}/courseware/interactive_demonstrations/${sectionId}/1?activate_block_id=${blockId}`}
|
||||
*/
|
||||
export const getUnitPreviewPath = (courseId, sectionId, subsectionId, blockId) => (
|
||||
`/courses/${courseId}/courseware/${sectionId}/${subsectionId}/1?activate_block_id=${blockId}`
|
||||
);
|
||||
@@ -104,7 +104,6 @@ initialize({
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
SUPPORT_EMAIL: process.env.SUPPORT_EMAIL || null,
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
PREVIEW_BASE_URL: process.env.PREVIEW_BASE_URL,
|
||||
EXAMS_BASE_URL: process.env.EXAMS_BASE_URL || null,
|
||||
CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null,
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false',
|
||||
|
||||
@@ -46,7 +46,6 @@ mergeConfig({
|
||||
CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null,
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false',
|
||||
ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true',
|
||||
PREVIEW_BASE_URL: process.env.PREVIEW_BASE_URL || '',
|
||||
}, 'CourseAuthoringConfig');
|
||||
|
||||
// Mock the plugins repo so jest will stop complaining about ES6 syntax
|
||||
|
||||
Reference in New Issue
Block a user