feat: add ParentBreadcrumbs component [FC-0090] (#2223)
Adds the `ParentBreadcrumbs` component to show a list of parent containers on the Unit and Subsection breadcrumbs.
This commit is contained in:
60
src/library-authoring/__mocks__/unit-single.json
Normal file
60
src/library-authoring/__mocks__/unit-single.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"comment": "This mock is captured from a real search result and roughly edited to match the mocks in src/library-authoring/data/api.mocks.ts",
|
||||
"note": "The _formatted fields have been removed from this result and should be re-added programatically when mocking.",
|
||||
"results": [
|
||||
{
|
||||
"indexUid": "studio_content",
|
||||
"hits": [
|
||||
{
|
||||
"display_name": "Test Unit",
|
||||
"block_id": "test-unit-9284e2",
|
||||
"id": "lctAximTESTunittest-unit-9284e2-a9a4386e",
|
||||
"type": "library_container",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Test Library"
|
||||
}
|
||||
],
|
||||
"created": 1742221203.895054,
|
||||
"modified": 1742221203.895054,
|
||||
"usage_key": "lct:org:lib:unit:test-unit-9a207",
|
||||
"block_type": "unit",
|
||||
"context_key": "lib:Axim:TEST",
|
||||
"org": "Axim",
|
||||
"access_id": 15,
|
||||
"num_children": 0,
|
||||
"_formatted": {
|
||||
"display_name": "Test Unit",
|
||||
"block_id": "test-unit-9284e2",
|
||||
"id": "lctAximTESTunittest-unit-9284e2-a9a4386e",
|
||||
"type": "library_container",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "Test Library"
|
||||
}
|
||||
],
|
||||
"created": "1742221203.895054",
|
||||
"modified": "1742221203.895054",
|
||||
"usage_key": "lct:org:lib:unit:test-unit-9a207",
|
||||
"block_type": "unit",
|
||||
"context_key": "lib:Axim:TEST",
|
||||
"org": "Axim",
|
||||
"access_id": "15",
|
||||
"num_children": "0",
|
||||
"published": {
|
||||
"display_name": "Published Test Unit"
|
||||
}
|
||||
},
|
||||
"published": {
|
||||
"display_name": "Published Test Unit"
|
||||
}
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
@import "./history-widget/HistoryWidget";
|
||||
@import "./status-widget/StatusWidget";
|
||||
@import "./parent-breadcrumbs";
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { ContainerType } from '@src/generic/key-utils';
|
||||
|
||||
import { type ContainerParents, ParentBreadcrumbs } from '.';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
const renderComponent = (containerType: ContainerType, parents: ContainerParents) => (
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<IntlProvider locale="en">
|
||||
<ParentBreadcrumbs
|
||||
libraryData={{ id: 'library-id', title: 'Library Title' }}
|
||||
containerType={containerType}
|
||||
parents={parents}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</BrowserRouter>,
|
||||
)
|
||||
);
|
||||
|
||||
describe('<ParentBreadcrumbs />', () => {
|
||||
it('show breadcrumb without parent', async () => {
|
||||
renderComponent(ContainerType.Unit, { displayName: [], key: [] });
|
||||
const links = screen.queryAllByRole('link');
|
||||
expect(links).toHaveLength(2); // Library link + Empty link
|
||||
|
||||
expect(links[0]).toHaveTextContent('Library Title');
|
||||
expect(links[0]).toHaveProperty('href', 'http://localhost/library/library-id');
|
||||
|
||||
expect(links[1]).toHaveTextContent(''); // Empty link for no parent
|
||||
expect(links[1]).toHaveProperty('href', 'http://localhost/');
|
||||
});
|
||||
|
||||
it('show breadcrumb to a unit without one parent', async () => {
|
||||
renderComponent(ContainerType.Unit, { displayName: ['Parent Subsection'], key: ['subsection-key'] });
|
||||
const links = screen.queryAllByRole('link');
|
||||
expect(links).toHaveLength(2); // Library link + Parent Subsection link
|
||||
|
||||
expect(links[0]).toHaveTextContent('Library Title');
|
||||
expect(links[0]).toHaveProperty('href', 'http://localhost/library/library-id');
|
||||
|
||||
expect(links[1]).toHaveTextContent('Parent Subsection');
|
||||
expect(links[1]).toHaveProperty('href', 'http://localhost/library/library-id/subsection/subsection-key');
|
||||
});
|
||||
|
||||
it('show breadcrumb to a subsection without one parent', async () => {
|
||||
renderComponent(ContainerType.Subsection, { displayName: ['Parent Section'], key: ['section-key'] });
|
||||
const links = screen.queryAllByRole('link');
|
||||
expect(links).toHaveLength(2); // Library link + Parent Subsection link
|
||||
|
||||
expect(links[0]).toHaveTextContent('Library Title');
|
||||
expect(links[0]).toHaveProperty('href', 'http://localhost/library/library-id');
|
||||
|
||||
expect(links[1]).toHaveTextContent('Parent Section');
|
||||
expect(links[1]).toHaveProperty('href', 'http://localhost/library/library-id/section/section-key');
|
||||
});
|
||||
|
||||
it('should throw an error if displayName and key arrays are not the same length', async () => {
|
||||
expect(() => renderComponent(ContainerType.Unit, {
|
||||
displayName: ['Parent 1'],
|
||||
key: ['key1', 'key2'],
|
||||
})).toThrow('Parents key and displayName arrays must have the same length.');
|
||||
});
|
||||
|
||||
it('show breadcrumb with multiple parents', async () => {
|
||||
renderComponent(ContainerType.Unit, {
|
||||
displayName: ['Parent Subsection 1', 'Parent Subsection 2'],
|
||||
key: ['subsection-key-1', 'subsection-key-2'],
|
||||
});
|
||||
const links = screen.queryAllByRole('link');
|
||||
expect(links).toHaveLength(1); // Library link only. Parents are displayed in a dropdown.
|
||||
|
||||
expect(links[0]).toHaveTextContent('Library Title');
|
||||
expect(links[0]).toHaveProperty('href', 'http://localhost/library/library-id');
|
||||
|
||||
const dropdown = screen.getByRole('button', { name: '2 Subsections' });
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(dropdown);
|
||||
|
||||
const subsectionLinks = screen.queryAllByRole('link');
|
||||
expect(subsectionLinks).toHaveLength(2); // Library link only. Parents are displayed in a dropdown.
|
||||
|
||||
expect(subsectionLinks[0]).toHaveTextContent('Parent Subsection 1');
|
||||
expect(subsectionLinks[0]).toHaveProperty('href', 'http://localhost/library/library-id/subsection/subsection-key-1');
|
||||
|
||||
expect(subsectionLinks[1]).toHaveTextContent('Parent Subsection 2');
|
||||
expect(subsectionLinks[1]).toHaveProperty('href', 'http://localhost/library/library-id/subsection/subsection-key-2');
|
||||
});
|
||||
});
|
||||
13
src/library-authoring/generic/parent-breadcrumbs/index.scss
Normal file
13
src/library-authoring/generic/parent-breadcrumbs/index.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.breadcrumb-menu {
|
||||
button {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.parents-breadcrumb {
|
||||
max-width: 700px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
121
src/library-authoring/generic/parent-breadcrumbs/index.tsx
Normal file
121
src/library-authoring/generic/parent-breadcrumbs/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Breadcrumb, MenuItem, SelectMenu,
|
||||
} from '@openedx/paragon';
|
||||
import { ContainerType } from '@src/generic/key-utils';
|
||||
import type { ContentLibrary } from '../../data/api';
|
||||
import messages from './messages';
|
||||
|
||||
interface OverflowLinksProps {
|
||||
to: string | string[];
|
||||
children: ReactNode | ReactNode[];
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
const OverflowLinks = ({ children, to, containerType }: OverflowLinksProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (typeof to === 'string') {
|
||||
return (
|
||||
<Link className="parents-breadcrumb link-muted" to={to}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!Array.isArray(to) || !Array.isArray(children) || to.length !== children.length) {
|
||||
throw new Error('Both "to" and "children" should have the same length.');
|
||||
}
|
||||
|
||||
// to is string[] that should be converted to overflow menu
|
||||
const items = to.map((link, index) => (
|
||||
<MenuItem key={link} to={link} as={Link}>
|
||||
{children[index]}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
const containerTypeName = containerType === ContainerType.Unit
|
||||
? intl.formatMessage(messages.breadcrumbsSubsectionsDropdown)
|
||||
: intl.formatMessage(messages.breadcrumbsSectionsDropdown);
|
||||
|
||||
return (
|
||||
<SelectMenu
|
||||
className="breadcrumb-menu"
|
||||
variant="link"
|
||||
defaultMessage={`${items.length} ${containerTypeName}`}
|
||||
>
|
||||
{items}
|
||||
</SelectMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ContainerParents {
|
||||
displayName?: string[];
|
||||
key?: string[];
|
||||
}
|
||||
|
||||
type ContentLibraryPartial = Pick<ContentLibrary, 'id' | 'title'> & Partial<ContentLibrary>;
|
||||
|
||||
interface ParentBreadcrumbsProps {
|
||||
libraryData: ContentLibraryPartial;
|
||||
parents?: ContainerParents;
|
||||
containerType: ContainerType;
|
||||
}
|
||||
|
||||
export const ParentBreadcrumbs = ({ libraryData, parents, containerType }: ParentBreadcrumbsProps) => {
|
||||
const intl = useIntl();
|
||||
const { id: libraryId, title: libraryTitle } = libraryData;
|
||||
|
||||
const links: Array<{ label: string | string[], to: string | string[], containerType: ContainerType }> = [
|
||||
{
|
||||
label: libraryTitle,
|
||||
to: `/library/${libraryId}`,
|
||||
containerType,
|
||||
},
|
||||
];
|
||||
|
||||
const parentLength = parents?.key?.length || 0;
|
||||
const parentNameLength = parents?.displayName?.length || 0;
|
||||
|
||||
if (parentLength !== parentNameLength) {
|
||||
throw new Error('Parents key and displayName arrays must have the same length.');
|
||||
}
|
||||
|
||||
const parentType = containerType === ContainerType.Unit
|
||||
? 'subsection'
|
||||
: 'section';
|
||||
|
||||
if (parentLength === 0 || !parents) {
|
||||
// Adding empty breadcrumb to add the last `>` spacer.
|
||||
links.push({
|
||||
label: '',
|
||||
to: '',
|
||||
containerType,
|
||||
});
|
||||
} else if (parentLength === 1) {
|
||||
links.push({
|
||||
label: parents.displayName?.[0] || '',
|
||||
to: `/library/${libraryId}/${parentType}/${parents.key?.[0]}`,
|
||||
containerType,
|
||||
});
|
||||
} else {
|
||||
// Add all parents as a single object containing list of links
|
||||
// This is converted to overflow menu by OverflowLinks component
|
||||
links.push({
|
||||
label: parents.displayName || [],
|
||||
to: parents.key?.map((parentKey) => `/library/${libraryId}/${parentType}/${parentKey}`) || [],
|
||||
containerType,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbsAriaLabel)}
|
||||
links={links}
|
||||
linkAs={OverflowLinks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
21
src/library-authoring/generic/parent-breadcrumbs/messages.ts
Normal file
21
src/library-authoring/generic/parent-breadcrumbs/messages.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
breadcrumbsAriaLabel: {
|
||||
id: 'course-authoring.library-authoring.parent-breadcrumbs.label.text',
|
||||
defaultMessage: 'Navigation breadcrumbs',
|
||||
description: 'Aria label for navigation breadcrumbs',
|
||||
},
|
||||
breadcrumbsSectionsDropdown: {
|
||||
id: 'course-authoring.library-authoring.parent-breadcrumbs.dropdown.sections',
|
||||
defaultMessage: 'Sections',
|
||||
description: 'Title for dropdown menu containing sections',
|
||||
},
|
||||
breadcrumbsSubsectionsDropdown: {
|
||||
id: 'course-authoring.library-authoring.parent-breadcrumbs.dropdown.subsections',
|
||||
defaultMessage: 'Subsections',
|
||||
description: 'Title for dropdown menu containing subsections',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import {
|
||||
Breadcrumb, Container, MenuItem, SelectMenu,
|
||||
} from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Container } from '@openedx/paragon';
|
||||
|
||||
import type { ContainerHit } from '@src/search-manager';
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { useContentFromSearchIndex, useContentLibrary } from '../data/apiHooks';
|
||||
@@ -15,41 +13,11 @@ import { ContainerType } from '../../generic/key-utils';
|
||||
import Header from '../../header';
|
||||
import SubHeader from '../../generic/sub-header/SubHeader';
|
||||
import { SubHeaderTitle } from '../LibraryAuthoringPage';
|
||||
import { messages, subsectionMessages } from './messages';
|
||||
import { subsectionMessages } from './messages';
|
||||
import { LibrarySidebar } from '../library-sidebar';
|
||||
import { ParentBreadcrumbs } from '../generic/parent-breadcrumbs';
|
||||
import { LibraryContainerChildren } from './LibraryContainerChildren';
|
||||
import { ContainerEditableTitle, FooterActions, HeaderActions } from '../containers';
|
||||
import { ContainerHit } from '../../search-manager';
|
||||
|
||||
interface OverflowLinksProps {
|
||||
to: string | string[];
|
||||
children: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
const OverflowLinks = ({ children, to }: OverflowLinksProps) => {
|
||||
if (typeof to === 'string') {
|
||||
return (
|
||||
<Link className="subsection-breadcrumb link-muted" to={to}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
// to is string[] that should be converted to overflow menu
|
||||
const items = to?.map((link, index) => (
|
||||
<MenuItem key={link} to={link} as={Link}>
|
||||
{children?.[index]}
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<SelectMenu
|
||||
className="breadcrumb-menu"
|
||||
variant="link"
|
||||
defaultMessage={`${items.length} Sections`}
|
||||
>
|
||||
{items}
|
||||
</SelectMenu>
|
||||
);
|
||||
};
|
||||
|
||||
/** Full library subsection page */
|
||||
export const LibrarySubsectionPage = () => {
|
||||
@@ -64,42 +32,6 @@ export const LibrarySubsectionPage = () => {
|
||||
} = useContentFromSearchIndex(containerId ? [containerId] : []);
|
||||
const subsectionData = (hits as ContainerHit[])?.[0];
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
const links: Array<{ label: string | string[], to: string | string[] }> = [
|
||||
{
|
||||
label: libraryData?.title || '',
|
||||
to: `/library/${libraryId}`,
|
||||
},
|
||||
];
|
||||
const sectionLength = subsectionData?.sections?.displayName?.length || 0;
|
||||
if (sectionLength === 1) {
|
||||
links.push({
|
||||
label: subsectionData.sections?.displayName?.[0] || '',
|
||||
to: `/library/${libraryId}/section/${subsectionData?.sections?.key?.[0]}`,
|
||||
});
|
||||
} else if (sectionLength > 1) {
|
||||
// Add all sections as a single object containing list of links
|
||||
// This is converted to overflow menu by OverflowLinks component
|
||||
links.push({
|
||||
label: subsectionData?.sections?.displayName || '',
|
||||
to: subsectionData?.sections?.key?.map((link) => `/library/${libraryId}/section/${link}`) || '',
|
||||
});
|
||||
} else {
|
||||
// Adding empty breadcrumb to add the last `>` spacer.
|
||||
links.push({
|
||||
label: '',
|
||||
to: '',
|
||||
});
|
||||
}
|
||||
return (
|
||||
<Breadcrumb
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbsAriaLabel)}
|
||||
links={links}
|
||||
linkAs={OverflowLinks}
|
||||
/>
|
||||
);
|
||||
}, [libraryData, subsectionData, libraryId]);
|
||||
|
||||
if (!containerId || !libraryId) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
throw new Error('Rendered without containerId or libraryId URL parameter');
|
||||
@@ -141,7 +73,13 @@ export const LibrarySubsectionPage = () => {
|
||||
<div className="px-4 bg-light-200 border-bottom mb-2">
|
||||
<SubHeader
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={containerId} />} />}
|
||||
breadcrumbs={breadcrumbs}
|
||||
breadcrumbs={(
|
||||
<ParentBreadcrumbs
|
||||
libraryData={libraryData}
|
||||
parents={subsectionData.sections}
|
||||
containerType={subsectionData.blockType}
|
||||
/>
|
||||
)}
|
||||
headerActions={(
|
||||
<HeaderActions
|
||||
containerKey={containerId}
|
||||
|
||||
@@ -30,17 +30,3 @@
|
||||
box-shadow: 0 .125rem .25rem rgb(0 0 0 / .15), 0 .125rem .5rem rgb(0 0 0 / .15);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-menu {
|
||||
button {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-breadcrumb {
|
||||
max-width: 700px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { act } from 'react';
|
||||
|
||||
import { ToastActionData } from '@src/generic/toast-context';
|
||||
import { mockClipboardEmpty } from '@src/generic/data/api.mock';
|
||||
import { mockContentSearchConfig, mockGetBlockTypes, mockSearchResult } from '@src/search-manager/data/api.mock';
|
||||
import {
|
||||
initializeMocks,
|
||||
fireEvent,
|
||||
@@ -9,7 +12,8 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '../../testUtils';
|
||||
} from '@src/testUtils';
|
||||
|
||||
import {
|
||||
getLibraryContainerApiUrl,
|
||||
getLibraryContainerChildrenApiUrl,
|
||||
@@ -22,10 +26,8 @@ import {
|
||||
mockGetContainerChildren,
|
||||
mockLibraryBlockMetadata,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
|
||||
import { mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import LibraryLayout from '../LibraryLayout';
|
||||
import { ToastActionData } from '../../generic/toast-context';
|
||||
import mockResult from '../__mocks__/unit-single.json';
|
||||
|
||||
const path = '/library/:libraryId/*';
|
||||
const libraryTitle = mockContentLibrary.libraryData.title;
|
||||
@@ -41,6 +43,19 @@ mockGetBlockTypes.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
mockXBlockFields.applyMock();
|
||||
mockLibraryBlockMetadata.applyMock();
|
||||
const searchFilterfn = (requestData: any) => {
|
||||
const queryFilter = requestData?.queries[0]?.filter?.[1];
|
||||
const subsectionId = queryFilter?.split('usage_key IN ["')[1].split('"]')[0];
|
||||
switch (subsectionId) {
|
||||
case mockGetContainerMetadata.unitIdLoading:
|
||||
return new Promise<any>(() => {});
|
||||
case mockGetContainerMetadata.unitIdError:
|
||||
return Promise.reject(new Error('Not found'));
|
||||
default:
|
||||
return mockResult;
|
||||
}
|
||||
};
|
||||
mockSearchResult(mockResult, searchFilterfn);
|
||||
|
||||
const verticalSortableListCollisionDetection = jest.fn();
|
||||
jest.mock('../../generic/DraggableList/verticalSortableList', () => ({
|
||||
@@ -107,13 +122,13 @@ describe('<LibraryUnitPage />', () => {
|
||||
|
||||
it('shows empty unit', async () => {
|
||||
renderLibraryUnitPage(mockGetContainerMetadata.unitIdEmpty);
|
||||
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('Test Unit'))).toHaveLength(2); // Header + Sidebar
|
||||
expect(await screen.findByText('This unit is empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can rename unit', async () => {
|
||||
renderLibraryUnitPage();
|
||||
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('Test Unit'))).toHaveLength(2); // Header + Sidebar
|
||||
|
||||
const editUnitTitleButton = screen.getAllByRole(
|
||||
'button',
|
||||
@@ -144,7 +159,7 @@ describe('<LibraryUnitPage />', () => {
|
||||
|
||||
it('show error if renaming unit fails', async () => {
|
||||
renderLibraryUnitPage();
|
||||
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('Test Unit'))).toHaveLength(2); // Header + Sidebar
|
||||
|
||||
const editUnitTitleButton = screen.getAllByRole(
|
||||
'button',
|
||||
@@ -161,6 +176,8 @@ describe('<LibraryUnitPage />', () => {
|
||||
|
||||
const textBox = screen.getByRole('textbox', { name: /text input/i });
|
||||
expect(textBox).toBeInTheDocument();
|
||||
expect(textBox).toHaveValue('Test Unit');
|
||||
screen.logTestingPlaygroundURL();
|
||||
fireEvent.change(textBox, { target: { value: 'New Unit Title' } });
|
||||
fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
|
||||
|
||||
@@ -195,7 +212,7 @@ describe('<LibraryUnitPage />', () => {
|
||||
|
||||
it('should open and close component sidebar on component selection', async () => {
|
||||
renderLibraryUnitPage();
|
||||
expect((await screen.findAllByText('Test Unit')).length).toBeGreaterThan(1);
|
||||
expect((await screen.findAllByText('Test Unit'))).toHaveLength(2); // Header + Sidebar
|
||||
// No Preview tab shown in sidebar
|
||||
expect(screen.queryByText('Preview')).not.toBeInTheDocument();
|
||||
|
||||
@@ -220,6 +237,7 @@ describe('<LibraryUnitPage />', () => {
|
||||
const url = getXBlockFieldsApiUrl('lb:org1:Demo_course_generated:html:text-0');
|
||||
axiosMock.onPost(url).reply(200);
|
||||
renderLibraryUnitPage();
|
||||
expect((await screen.findAllByText('Test Unit'))).toHaveLength(2); // Header + Sidebar
|
||||
|
||||
// Wait loading of the component
|
||||
await screen.findByText('text block 0');
|
||||
@@ -254,6 +272,7 @@ describe('<LibraryUnitPage />', () => {
|
||||
const url = getXBlockFieldsApiUrl('lb:org1:Demo_course_generated:html:text-0');
|
||||
axiosMock.onPost(url).reply(400);
|
||||
renderLibraryUnitPage();
|
||||
expect((await screen.findAllByText('Test Unit'))).toHaveLength(2); // Header + Sidebar
|
||||
|
||||
// Wait loading of the component
|
||||
await screen.findByText('text block 0');
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Container,
|
||||
} from '@openedx/paragon';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ErrorAlert from '@src/generic/alert-error';
|
||||
import { ContainerType } from '@src/generic/key-utils';
|
||||
import type { ContainerHit } from '@src/search-manager';
|
||||
import Loading from '../../generic/Loading';
|
||||
import NotFoundAlert from '../../generic/NotFoundAlert';
|
||||
import SubHeader from '../../generic/sub-header/SubHeader';
|
||||
import ErrorAlert from '../../generic/alert-error';
|
||||
import Header from '../../header';
|
||||
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { useContainer, useContentLibrary } from '../data/apiHooks';
|
||||
import { useContentFromSearchIndex, useContentLibrary } from '../data/apiHooks';
|
||||
import { LibrarySidebar } from '../library-sidebar';
|
||||
import { ParentBreadcrumbs } from '../generic/parent-breadcrumbs';
|
||||
import { SubHeaderTitle } from '../LibraryAuthoringPage';
|
||||
import { LibraryUnitBlocks } from './LibraryUnitBlocks';
|
||||
import messages from './messages';
|
||||
@@ -36,12 +36,11 @@ export const LibraryUnitPage = () => {
|
||||
const { sidebarItemInfo } = useSidebarContext();
|
||||
|
||||
const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId);
|
||||
// fetch unitData from index as it includes its parent subsections as well.
|
||||
const {
|
||||
data: unitData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useContainer(containerId);
|
||||
hits, isLoading, isError, error,
|
||||
} = useContentFromSearchIndex(containerId ? [containerId] : []);
|
||||
const unitData = (hits as ContainerHit[])?.[0];
|
||||
|
||||
if (!containerId || !libraryId) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
@@ -62,24 +61,6 @@ export const LibraryUnitPage = () => {
|
||||
return <ErrorAlert error={error} />;
|
||||
}
|
||||
|
||||
const breadcrumbs = (
|
||||
<Breadcrumb
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbsAriaLabel)}
|
||||
links={[
|
||||
{
|
||||
label: libraryData.title,
|
||||
to: `/library/${libraryId}`,
|
||||
},
|
||||
// Adding empty breadcrumb to add the last `>` spacer.
|
||||
{
|
||||
label: '',
|
||||
to: '',
|
||||
},
|
||||
]}
|
||||
linkAs={Link}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className="flex-grow-1">
|
||||
@@ -105,7 +86,13 @@ export const LibraryUnitPage = () => {
|
||||
addContentBtnText={intl.formatMessage(messages.addContentButton)}
|
||||
/>
|
||||
)}
|
||||
breadcrumbs={breadcrumbs}
|
||||
breadcrumbs={(
|
||||
<ParentBreadcrumbs
|
||||
libraryData={libraryData}
|
||||
parents={unitData.subsections}
|
||||
containerType={ContainerType.Unit}
|
||||
/>
|
||||
)}
|
||||
hideBorder
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user