Skip to content

Commit

Permalink
feat: Library info sidebar - allows lib rename+publish (#1138)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisChV authored Aug 13, 2024
1 parent 8285f8e commit 4f5346e
Show file tree
Hide file tree
Showing 22 changed files with 1,124 additions and 65 deletions.
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
76 changes: 64 additions & 12 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,18 @@ const libraryData: ContentLibrary = {
numBlocks: 2,
version: 0,
lastPublished: null,
lastDraftCreated: '2024-07-22',
publishedBy: 'staff',
lastDraftCreatedBy: 'staff',
allowLti: false,
allowPublicLearning: false,
allowPublicRead: false,
hasUnpublishedChanges: true,
hasUnpublishedDeletes: false,
canEditLibrary: true,
license: '',
created: '2024-06-26',
updated: '2024-07-20',
};

const RootWrapper = () => (
Expand Down Expand Up @@ -177,23 +182,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 +227,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 Down Expand Up @@ -282,10 +287,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 @@ -329,12 +339,54 @@ 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);

expect(screen.queryByText('Draft')).not.toBeInTheDocument();
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();

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 @@ -343,7 +395,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 @@ -376,7 +428,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 @@ -385,7 +437,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
54 changes: 31 additions & 23 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
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 {
Badge,
Button,
Col,
Container,
Icon,
IconButton,
Row,
Stack,
Tab,
Expand Down Expand Up @@ -52,37 +50,40 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
const intl = useIntl();
const {
openAddContentSidebar,
openInfoSidebar,
} = useContext(LibraryContext);

if (!canEditLibrary) {
return null;
}

return (
<Button
iconBefore={Add}
variant="primary rounded-0"
onClick={() => openAddContentSidebar()}
disabled={!canEditLibrary}
>
{intl.formatMessage(messages.newContentButton)}
</Button>
<>
<Button
iconBefore={InfoOutline}
variant="outline-primary rounded-0"
onClick={openInfoSidebar}
>
{intl.formatMessage(messages.libraryInfoButton)}
</Button>
<Button
iconBefore={Add}
variant="primary rounded-0"
onClick={openAddContentSidebar}
disabled={!canEditLibrary}
>
{intl.formatMessage(messages.newContentButton)}
</Button>
</>
);
};

const SubHeaderTitle = ({ title, canEditLibrary }: { title: string, canEditLibrary: boolean }) => {
const intl = useIntl();

return (
<Stack direction="vertical">
<Stack direction="horizontal">
{title}
<IconButton
src={InfoOutline}
iconAs={Icon}
alt={intl.formatMessage(messages.headingInfoAlt)}
className="mr-2"
/>
</Stack>
{title}
{ !canEditLibrary && (
<div>
<Badge variant="primary" style={{ fontSize: '50%' }}>
Expand All @@ -104,7 +105,14 @@ const LibraryAuthoringPage = () => {

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

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

const [searchParams] = useSearchParams();

Expand Down Expand Up @@ -190,8 +198,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;
2 changes: 1 addition & 1 deletion src/library-authoring/add-content/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
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: () => {},
} as LibraryContextData);

/**
Expand All @@ -25,12 +28,19 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => {

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
29 changes: 28 additions & 1 deletion src/library-authoring/data/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { createLibraryBlock, getCreateLibraryBlockUrl } from './api';
import {
commitLibraryChanges,
createLibraryBlock,
getCommitLibraryChangesUrl,
getCreateLibraryBlockUrl,
revertLibraryChanges,
} from './api';

let axiosMock;

Expand All @@ -21,6 +27,7 @@ describe('library api calls', () => {

afterEach(() => {
jest.clearAllMocks();
axiosMock.restore();
});

it('should create library block', async () => {
Expand All @@ -35,4 +42,24 @@ describe('library api calls', () => {

expect(axiosMock.history.post[0].url).toEqual(url);
});

it('should commit library changes', async () => {
const libraryId = 'lib:org:1';
const url = getCommitLibraryChangesUrl(libraryId);
axiosMock.onPost(url).reply(200);

await commitLibraryChanges(libraryId);

expect(axiosMock.history.post[0].url).toEqual(url);
});

it('should revert library changes', async () => {
const libraryId = 'lib:org:1';
const url = getCommitLibraryChangesUrl(libraryId);
axiosMock.onDelete(url).reply(200);

await revertLibraryChanges(libraryId);

expect(axiosMock.history.delete[0].url).toEqual(url);
});
});
Loading

0 comments on commit 4f5346e

Please sign in to comment.