Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FC-0059] Library info sidebar #1138

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@import "export-page/CourseExportPage";
@import "import-page/CourseImportPage";
@import "taxonomy";
@import "library-authoring";
@import "files-and-videos";
@import "content-tags-drawer";
@import "course-outline/CourseOutline";
Expand All @@ -30,7 +31,6 @@
@import "search-manager";
@import "certificates/scss/Certificates";
@import "group-configurations/GroupConfigurations";
@import "library-authoring";

// To apply the glow effect to the selected Section/Subsection, in the Course Outline
div.row:has(> div > div.highlight) {
Expand Down
74 changes: 62 additions & 12 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import mockResult from '../search-modal/__mocks__/search-result.json';
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
import { getContentLibraryApiUrl, type ContentLibrary } from './data/api';
import { LibraryLayout } from '.';
import { convertToDateFromString } from '../utils';

let store;
const mockUseParams = jest.fn();
Expand Down Expand Up @@ -83,13 +84,18 @@ const libraryData: ContentLibrary = {
numBlocks: 2,
version: 0,
lastPublished: null,
lastDraftCreated: convertToDateFromString('2024-07-22') as Date,
publishedBy: 'staff',
lastDraftCreatedBy: 'staff',
allowLti: false,
allowPublicLearning: false,
allowPublicRead: false,
hasUnpublishedChanges: true,
hasUnpublishedDeletes: false,
canEditLibrary: true,
license: '',
created: convertToDateFromString('2024-06-26') as Date,
updated: convertToDateFromString('2024-07-20') as Date,
};

const RootWrapper = () => (
Expand Down Expand Up @@ -177,23 +183,23 @@ describe('<LibraryAuthoringPage />', () => {
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

const {
getByRole, getByText, getAllByText, queryByText,
getByRole, getByText, queryByText, findByText, findAllByText,
} = render(<RootWrapper />);

// Ensure the search endpoint is called:
// Call 1: To fetch searchable/filterable/sortable library data
// Call 2: To fetch the recently modified components only
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });

expect(getByText('Content library')).toBeInTheDocument();
expect(getByText(libraryData.title)).toBeInTheDocument();
expect(await findByText('Content library')).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();

expect(getByText('Recently Modified')).toBeInTheDocument();
expect(getByText('Collections (0)')).toBeInTheDocument();
expect(getByText('Components (6)')).toBeInTheDocument();
expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument();
expect((await findAllByText('Test HTML Block'))[0]).toBeInTheDocument();

// Navigate to the components tab
fireEvent.click(getByRole('tab', { name: 'Components' }));
Expand Down Expand Up @@ -222,10 +228,10 @@ describe('<LibraryAuthoringPage />', () => {
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });

const { findByText, getByText } = render(<RootWrapper />);
const { findByText, getByText, findAllByText } = render(<RootWrapper />);

expect(await findByText('Content library')).toBeInTheDocument();
expect(await findByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

// Ensure the search endpoint is called:
// Call 1: To fetch searchable/filterable/sortable library data
Expand All @@ -250,10 +256,15 @@ describe('<LibraryAuthoringPage />', () => {
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });

const { findByText, getByRole, getByText } = render(<RootWrapper />);
const {
findByText,
getByRole,
getByText,
findAllByText,
} = render(<RootWrapper />);

expect(await findByText('Content library')).toBeInTheDocument();
expect(await findByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

// Ensure the search endpoint is called:
// Call 1: To fetch searchable/filterable/sortable library data
Expand Down Expand Up @@ -297,12 +308,51 @@ describe('<LibraryAuthoringPage />', () => {
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
});

it('should open Library Info by default', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

render(<RootWrapper />);

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();

expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
expect(screen.getByText('staff')).toBeInTheDocument();
expect(screen.getByText(libraryData.org)).toBeInTheDocument();
expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
});

it('should close and open Library Info', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

render(<RootWrapper />);

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();

const closeButton = screen.getByRole('button', { name: /close/i });
fireEvent.click(closeButton);

ChrisChV marked this conversation as resolved.
Show resolved Hide resolved
const libraryInfoButton = screen.getByRole('button', { name: /library info/i });
fireEvent.click(libraryInfoButton);

expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
});

it('show the "View All" button when viewing library with many components', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

const {
getByRole, getByText, queryByText, getAllByText,
getByRole, getByText, queryByText, getAllByText, findAllByText,
} = render(<RootWrapper />);

// Ensure the search endpoint is called:
Expand All @@ -311,7 +361,7 @@ describe('<LibraryAuthoringPage />', () => {
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });

expect(getByText('Content library')).toBeInTheDocument();
expect(getByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
expect(getByText('Collections (0)')).toBeInTheDocument();
Expand Down Expand Up @@ -344,7 +394,7 @@ describe('<LibraryAuthoringPage />', () => {
fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true });

const {
getByText, queryByText, getAllByText,
getByText, queryByText, getAllByText, findAllByText,
} = render(<RootWrapper />);

// Ensure the search endpoint is called:
Expand All @@ -353,7 +403,7 @@ describe('<LibraryAuthoringPage />', () => {
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });

expect(getByText('Content library')).toBeInTheDocument();
expect(getByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
expect(getByText('Collections (0)')).toBeInTheDocument();
Expand Down
63 changes: 38 additions & 25 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { useContext } from 'react';
import React, { useContext, useEffect } from 'react';
import { StudioFooter } from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button,
Col,
Container,
Icon,
IconButton,
Row,
Tab,
Tabs,
Expand Down Expand Up @@ -42,17 +40,34 @@ enum TabList {
collections = 'collections',
}

const SubHeaderTitle = ({ title }: { title: string }) => {
interface HeaderActionsProps {
canEditLibrary: boolean;
}

const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
const intl = useIntl();
const {
openAddContentSidebar,
openInfoSidebar,
} = useContext(LibraryContext);

return (
<>
{title}
<IconButton
src={InfoOutline}
iconAs={Icon}
alt={intl.formatMessage(messages.headingInfoAlt)}
className="mr-2"
/>
<Button
iconBefore={InfoOutline}
variant="outline-primary rounded-0"
onClick={() => openInfoSidebar()}
>
ChrisChV marked this conversation as resolved.
Show resolved Hide resolved
{intl.formatMessage(messages.libraryInfoButton)}
</Button>
<Button
iconBefore={Add}
variant="primary rounded-0"
onClick={() => openAddContentSidebar()}
disabled={!canEditLibrary}
ChrisChV marked this conversation as resolved.
Show resolved Hide resolved
>
{intl.formatMessage(messages.newContentButton)}
</Button>
</>
);
};
Expand All @@ -67,7 +82,14 @@ const LibraryAuthoringPage = () => {

const currentPath = location.pathname.split('/').pop();
const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home;
const { sidebarBodyComponent, openAddContentSidebar } = useContext(LibraryContext);
const {
sidebarBodyComponent,
openInfoSidebar,
} = useContext(LibraryContext);

useEffect(() => {
openInfoSidebar();
}, []);

const [searchParams] = useSearchParams();

Expand Down Expand Up @@ -102,18 +124,9 @@ const LibraryAuthoringPage = () => {
>
<Container size="xl" className="p-4 mt-3">
<SubHeader
title={<SubHeaderTitle title={libraryData.title} />}
title={libraryData.title}
subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={[
<Button
iconBefore={Add}
variant="primary rounded-0"
onClick={openAddContentSidebar}
disabled={!libraryData.canEditLibrary}
>
{intl.formatMessage(messages.newContentButton)}
</Button>,
]}
headerActions={<HeaderActions canEditLibrary={libraryData.canEditLibrary} />}
/>
<SearchKeywordsField className="w-50" />
<div className="d-flex mt-3 align-items-center">
Expand Down Expand Up @@ -162,8 +175,8 @@ const LibraryAuthoringPage = () => {
<StudioFooter />
</Col>
{ sidebarBodyComponent !== null && (
<Col xs={6} md={4} className="box-shadow-left-1">
<LibrarySidebar />
<Col xs={3} md={3} className="box-shadow-left-1">
<LibrarySidebar library={libraryData} />
</Col>
)}
</Row>
Expand Down
11 changes: 11 additions & 0 deletions src/library-authoring/add-content/AddContentHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';

const AddContentHeader = () => (
<span className="font-weight-bold m-1.5">
<FormattedMessage {...messages.addContentTitle} />
</span>
);

export default AddContentHeader;
1 change: 1 addition & 0 deletions src/library-authoring/add-content/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
ChrisChV marked this conversation as resolved.
Show resolved Hide resolved
export { default as AddContentContainer } from './AddContentContainer';
export { default as AddContentHeader } from './AddContentHeader';
5 changes: 5 additions & 0 deletions src/library-authoring/add-content/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const messages = defineMessages({
defaultMessage: 'There was an error creating the content.',
description: 'Message when creation of content in library is on error',
},
addContentTitle: {
id: 'course-authoring.library-authoring.sidebar.title.add-content',
defaultMessage: 'Add Content',
description: 'Title of add content in library container.',
},
});

export default messages;
14 changes: 12 additions & 2 deletions src/library-authoring/common/context.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
/* eslint-disable react/require-default-props */
import React from 'react';

enum SidebarBodyComponentId {
export enum SidebarBodyComponentId {
AddContent = 'add-content',
Info = 'info',
}

export interface LibraryContextData {
sidebarBodyComponent: SidebarBodyComponentId | null;
closeLibrarySidebar: () => void;
openAddContentSidebar: () => void;
openInfoSidebar: () => void;
}

export const LibraryContext = React.createContext({
sidebarBodyComponent: null,
closeLibrarySidebar: () => {},
openAddContentSidebar: () => {},
openInfoSidebar: () => {},

Check warning on line 20 in src/library-authoring/common/context.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/common/context.tsx#L20

Added line #L20 was not covered by tests
} as LibraryContextData);

/**
Expand All @@ -25,12 +28,19 @@

const closeLibrarySidebar = React.useCallback(() => setSidebarBodyComponent(null), []);
const openAddContentSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.AddContent), []);
const openInfoSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.Info), []);

const context = React.useMemo(() => ({
sidebarBodyComponent,
closeLibrarySidebar,
openAddContentSidebar,
}), [sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar]);
openInfoSidebar,
}), [
sidebarBodyComponent,
closeLibrarySidebar,
openAddContentSidebar,
openInfoSidebar,
]);

return (
<LibraryContext.Provider value={context}>
Expand Down
Loading
Loading