diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx
index 26073ee23..fcd8c60b9 100644
--- a/src/library-authoring/LibraryAuthoringPage.test.tsx
+++ b/src/library-authoring/LibraryAuthoringPage.test.tsx
@@ -5,15 +5,20 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { fireEvent, render, waitFor } from '@testing-library/react';
+import {
+ fireEvent,
+ render,
+ waitFor,
+ screen,
+} from '@testing-library/react';
import fetchMock from 'fetch-mock-jest';
import initializeStore from '../store';
import { getContentSearchConfigUrl } from '../search-modal/data/api';
import mockResult from '../search-modal/__mocks__/search-result.json';
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
-import LibraryAuthoringPage from './LibraryAuthoringPage';
import { getContentLibraryApiUrl, type ContentLibrary } from './data/api';
+import LibraryLayout from './LibraryLayout';
let store;
const mockUseParams = jest.fn();
@@ -61,15 +66,15 @@ const libraryData: ContentLibrary = {
allowPublicRead: false,
hasUnpublishedChanges: true,
hasUnpublishedDeletes: false,
+ canEditLibrary: true,
license: '',
- canEditLibrary: false,
};
const RootWrapper = () => (
-
+
@@ -206,6 +211,16 @@ describe('
', () => {
expect(getByText('You have not added any content to this library yet.')).toBeInTheDocument();
});
+ it('show new content button', async () => {
+ mockUseParams.mockReturnValue({ libraryId: libraryData.id });
+ axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
+
+ render(
);
+
+ expect(await screen.findByRole('heading')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
+ });
+
it('show library without search results', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
@@ -234,4 +249,24 @@ describe('
', () => {
// This step is necessary to avoid the url change leak to other tests
fireEvent.click(getByRole('tab', { name: 'Home' }));
});
+
+ it('should open and close new content sidebar', async () => {
+ mockUseParams.mockReturnValue({ libraryId: libraryData.id });
+ axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
+
+ render(
);
+
+ expect(await screen.findByRole('heading')).toBeInTheDocument();
+ expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
+
+ const newButton = screen.getByRole('button', { name: /new/i });
+ fireEvent.click(newButton);
+
+ expect(screen.getByText(/add content/i)).toBeInTheDocument();
+
+ const closeButton = screen.getByRole('button', { name: /close/i });
+ fireEvent.click(closeButton);
+
+ expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
+ });
});
diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx
index f369025a3..524737275 100644
--- a/src/library-authoring/LibraryAuthoringPage.tsx
+++ b/src/library-authoring/LibraryAuthoringPage.tsx
@@ -1,10 +1,18 @@
-import React, { useState } from 'react';
+import React, { useContext } from 'react';
import { StudioFooter } from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
- Container, Icon, IconButton, SearchField, Tab, Tabs,
+ Button,
+ Container,
+ Icon,
+ IconButton,
+ SearchField,
+ Tab,
+ Tabs,
+ Row,
+ Col,
} from '@openedx/paragon';
-import { InfoOutline } from '@openedx/paragon/icons';
+import { Add, InfoOutline } from '@openedx/paragon/icons';
import {
Routes, Route, useLocation, useNavigate, useParams,
} from 'react-router-dom';
@@ -18,6 +26,8 @@ import LibraryCollections from './LibraryCollections';
import LibraryHome from './LibraryHome';
import { useContentLibrary } from './data/apiHooks';
import messages from './messages';
+import { LibrarySidebar } from './library-sidebar';
+import { LibraryContext } from './common/context';
enum TabList {
home = '',
@@ -44,7 +54,7 @@ const LibraryAuthoringPage = () => {
const intl = useIntl();
const location = useLocation();
const navigate = useNavigate();
- const [searchKeywords, setSearchKeywords] = useState('');
+ const [searchKeywords, setSearchKeywords] = React.useState('');
const { libraryId } = useParams();
@@ -52,6 +62,7 @@ const LibraryAuthoringPage = () => {
const currentPath = location.pathname.split('/').pop();
const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home;
+ const { sidebarBodyComponent, openAddContentSidebar } = useContext(LibraryContext);
if (isLoading) {
return
;
@@ -62,62 +73,80 @@ const LibraryAuthoringPage = () => {
}
const handleTabChange = (key: string) => {
- // setTabKey(key);
navigate(key);
};
return (
- <>
-
-
- }
- subtitle={intl.formatMessage(messages.headingSubtitle)}
- />
- setSearchKeywords(value)}
- onSubmit={() => {}}
- className="w-50"
- />
-
-
-
-
-
-
- }
+
+
+
+
- }
- />
- }
- />
- }
- />
-
-
-
- >
+
+ }
+ subtitle={intl.formatMessage(messages.headingSubtitle)}
+ headerActions={[
+ ,
+ ]}
+ />
+ setSearchKeywords(value)}
+ onSubmit={() => {}}
+ className="w-50"
+ />
+
+
+
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+
+ { sidebarBodyComponent !== null && (
+
+
+
+ )}
+
+
);
};
diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx
new file mode 100644
index 000000000..95d829606
--- /dev/null
+++ b/src/library-authoring/LibraryLayout.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import LibraryAuthoringPage from './LibraryAuthoringPage';
+import { LibraryProvider } from './common/context';
+
+const LibraryLayout = () => (
+
+
+
+);
+
+export default LibraryLayout;
diff --git a/src/library-authoring/add-content/AddContentContainer.test.tsx b/src/library-authoring/add-content/AddContentContainer.test.tsx
new file mode 100644
index 000000000..51b07843c
--- /dev/null
+++ b/src/library-authoring/add-content/AddContentContainer.test.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { initializeMockApp } from '@edx/frontend-platform';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import {
+ render, screen, fireEvent, waitFor,
+} from '@testing-library/react';
+import { AppProvider } from '@edx/frontend-platform/react';
+import MockAdapter from 'axios-mock-adapter';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import AddContentContainer from './AddContentContainer';
+import initializeStore from '../../store';
+import { getCreateLibraryBlockUrl } from '../data/api';
+
+const mockUseParams = jest.fn();
+let axiosMock;
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
+ useParams: () => mockUseParams(),
+}));
+
+const libraryId = '1';
+let store;
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+const RootWrapper = () => (
+
+
+
+
+
+
+
+);
+
+describe('
', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+ store = initializeStore();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ mockUseParams.mockReturnValue({ libraryId });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render content buttons', () => {
+ render(
);
+ expect(screen.getByRole('button', { name: /collection/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /text/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /problem/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /open reponse/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /drag drop/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /video/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument();
+ });
+
+ it('should create a content', async () => {
+ const url = getCreateLibraryBlockUrl(libraryId);
+ axiosMock.onPost(url).reply(200);
+
+ render(
);
+
+ const textButton = screen.getByRole('button', { name: /text/i });
+ fireEvent.click(textButton);
+
+ await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
+ });
+});
diff --git a/src/library-authoring/add-content/AddContentContainer.tsx b/src/library-authoring/add-content/AddContentContainer.tsx
new file mode 100644
index 000000000..9af31593c
--- /dev/null
+++ b/src/library-authoring/add-content/AddContentContainer.tsx
@@ -0,0 +1,108 @@
+import React, { useContext } from 'react';
+import {
+ Stack,
+ Button,
+} from '@openedx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Article,
+ AutoAwesome,
+ BookOpen,
+ Create,
+ ThumbUpOutline,
+ Question,
+ VideoCamera,
+} from '@openedx/paragon/icons';
+import { v4 as uuid4 } from 'uuid';
+import { useParams } from 'react-router-dom';
+import { ToastContext } from '../../generic/toast-context';
+import { useCreateLibraryBlock } from '../data/apiHooks';
+import messages from './messages';
+
+const AddContentContainer = () => {
+ const intl = useIntl();
+ const { libraryId } = useParams();
+ const createBlockMutation = useCreateLibraryBlock();
+ const { showToast } = useContext(ToastContext);
+
+ const contentTypes = [
+ {
+ name: intl.formatMessage(messages.textTypeButton),
+ disabled: false,
+ icon: Article,
+ blockType: 'html',
+ },
+ {
+ name: intl.formatMessage(messages.problemTypeButton),
+ disabled: false,
+ icon: Question,
+ blockType: 'problem',
+ },
+ {
+ name: intl.formatMessage(messages.openResponseTypeButton),
+ disabled: false,
+ icon: Create,
+ blockType: 'openassessment',
+ },
+ {
+ name: intl.formatMessage(messages.dragDropTypeButton),
+ disabled: false,
+ icon: ThumbUpOutline,
+ blockType: 'drag-and-drop-v2',
+ },
+ {
+ name: intl.formatMessage(messages.videoTypeButton),
+ disabled: false,
+ icon: VideoCamera,
+ blockType: 'video',
+ },
+ {
+ name: intl.formatMessage(messages.otherTypeButton),
+ disabled: true,
+ icon: AutoAwesome,
+ blockType: 'other', // This block doesn't exist yet.
+ },
+ ];
+
+ const onCreateContent = (blockType: string) => {
+ if (libraryId) {
+ createBlockMutation.mutateAsync({
+ libraryId,
+ blockType,
+ definitionId: `${uuid4()}`,
+ }).then(() => {
+ showToast(intl.formatMessage(messages.successCreateMessage));
+ }).catch(() => {
+ showToast(intl.formatMessage(messages.errorCreateMessage));
+ });
+ }
+ };
+
+ return (
+
+
+
+ {contentTypes.map((contentType) => (
+
+ ))}
+
+ );
+};
+
+export default AddContentContainer;
diff --git a/src/library-authoring/add-content/index.ts b/src/library-authoring/add-content/index.ts
new file mode 100644
index 000000000..876828e16
--- /dev/null
+++ b/src/library-authoring/add-content/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export { default as AddContentContainer } from './AddContentContainer';
diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts
new file mode 100644
index 000000000..1d13635e5
--- /dev/null
+++ b/src/library-authoring/add-content/messages.ts
@@ -0,0 +1,55 @@
+import { defineMessages as _defineMessages } from '@edx/frontend-platform/i18n';
+import type { defineMessages as defineMessagesType } from 'react-intl';
+
+// frontend-platform currently doesn't provide types... do it ourselves.
+const defineMessages = _defineMessages as typeof defineMessagesType;
+
+const messages = defineMessages({
+ collectionButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.collection',
+ defaultMessage: 'Collection',
+ description: 'Content of button to create a Collection.',
+ },
+ textTypeButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.types.text',
+ defaultMessage: 'Text',
+ description: 'Content of button to create a Text component.',
+ },
+ problemTypeButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.types.problem',
+ defaultMessage: 'Problem',
+ description: 'Content of button to create a Problem component.',
+ },
+ openResponseTypeButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.types.open-response',
+ defaultMessage: 'Open Reponse',
+ description: 'Content of button to create a Open Response component.',
+ },
+ dragDropTypeButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.types.drag-drop',
+ defaultMessage: 'Drag Drop',
+ description: 'Content of button to create a Drag Drod component.',
+ },
+ videoTypeButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.types.video',
+ defaultMessage: 'Video',
+ description: 'Content of button to create a Video component.',
+ },
+ otherTypeButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.types.other',
+ defaultMessage: 'Advanced / Other',
+ description: 'Content of button to create a Advanced / Other component.',
+ },
+ successCreateMessage: {
+ id: 'course-authoring.library-authoring.add-content.success.text',
+ defaultMessage: 'Content created successfully.',
+ description: 'Message when creation of content in library is success',
+ },
+ errorCreateMessage: {
+ id: 'course-authoring.library-authoring.add-content.error.text',
+ defaultMessage: 'There was an error creating the content.',
+ description: 'Message when creation of content in library is on error',
+ },
+});
+
+export default messages;
diff --git a/src/library-authoring/common/context.tsx b/src/library-authoring/common/context.tsx
new file mode 100644
index 000000000..241ed67d2
--- /dev/null
+++ b/src/library-authoring/common/context.tsx
@@ -0,0 +1,40 @@
+/* eslint-disable react/require-default-props */
+import React from 'react';
+
+enum SidebarBodyComponentId {
+ AddContent = 'add-content',
+}
+
+export interface LibraryContextData {
+ sidebarBodyComponent: SidebarBodyComponentId | null;
+ closeLibrarySidebar: () => void;
+ openAddContentSidebar: () => void;
+}
+
+export const LibraryContext = React.createContext({
+ sidebarBodyComponent: null,
+ closeLibrarySidebar: () => {},
+ openAddContentSidebar: () => {},
+} as LibraryContextData);
+
+/**
+ * React component to provide `LibraryContext`
+ */
+export const LibraryProvider = (props: { children?: React.ReactNode }) => {
+ const [sidebarBodyComponent, setSidebarBodyComponent] = React.useState
(null);
+
+ const closeLibrarySidebar = React.useCallback(() => setSidebarBodyComponent(null), []);
+ const openAddContentSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.AddContent), []);
+
+ const context = React.useMemo(() => ({
+ sidebarBodyComponent,
+ closeLibrarySidebar,
+ openAddContentSidebar,
+ }), [sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar]);
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/src/library-authoring/data/api.test.ts b/src/library-authoring/data/api.test.ts
new file mode 100644
index 000000000..66736ad24
--- /dev/null
+++ b/src/library-authoring/data/api.test.ts
@@ -0,0 +1,38 @@
+import MockAdapter from 'axios-mock-adapter';
+import { initializeMockApp } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { createLibraryBlock, getCreateLibraryBlockUrl } from './api';
+
+let axiosMock;
+
+describe('library api calls', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should create library block', async () => {
+ const libraryId = 'lib:org:1';
+ const url = getCreateLibraryBlockUrl(libraryId);
+ axiosMock.onPost(url).reply(200);
+ await createLibraryBlock({
+ libraryId,
+ blockType: 'html',
+ definitionId: '1',
+ });
+
+ expect(axiosMock.history.post[0].url).toEqual(url);
+ });
+});
diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts
index be3ec564f..37eb4eb3d 100644
--- a/src/library-authoring/data/api.ts
+++ b/src/library-authoring/data/api.ts
@@ -7,6 +7,10 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
* Get the URL for the content library API.
*/
export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`;
+/**
+ * Get the URL for create content in library.
+ */
+export const getCreateLibraryBlockUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/blocks/`;
export const getContentLibraryV2ListApiUrl = () => `${getApiBaseUrl()}/api/libraries/v2/`;
export interface ContentLibrary {
@@ -24,20 +28,8 @@ export interface ContentLibrary {
allowPublicRead: boolean;
hasUnpublishedChanges: boolean;
hasUnpublishedDeletes: boolean;
- license: string;
canEditLibrary: boolean;
-}
-
-/**
- * Fetch a content library by its ID.
- */
-export async function getContentLibrary(libraryId?: string): Promise {
- if (!libraryId) {
- throw new Error('libraryId is required');
- }
-
- const { data } = await getAuthenticatedHttpClient().get(getContentLibraryApiUrl(libraryId));
- return camelCaseObject(data);
+ license: string;
}
export interface LibrariesV2Response {
@@ -66,6 +58,49 @@ export interface GetLibrariesV2CustomParams {
search?: string,
}
+export interface CreateBlockDataRequest {
+ libraryId: string;
+ blockType: string;
+ definitionId: string;
+}
+
+export interface CreateBlockDataResponse {
+ id: string;
+ blockType: string;
+ defKey: string | null;
+ displayName: string;
+ hasUnpublishedChanges: boolean;
+ tagsCount: number;
+}
+
+/**
+ * Fetch a content library by its ID.
+ */
+export async function getContentLibrary(libraryId?: string): Promise {
+ if (!libraryId) {
+ throw new Error('libraryId is required');
+ }
+
+ const { data } = await getAuthenticatedHttpClient().get(getContentLibraryApiUrl(libraryId));
+ return camelCaseObject(data);
+}
+
+export async function createLibraryBlock({
+ libraryId,
+ blockType,
+ definitionId,
+}: CreateBlockDataRequest): Promise {
+ const client = getAuthenticatedHttpClient();
+ const { data } = await client.post(
+ getCreateLibraryBlockUrl(libraryId),
+ {
+ block_type: blockType,
+ definition_id: definitionId,
+ },
+ );
+ return data;
+}
+
/**
* Get a list of content libraries.
*/
diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx
new file mode 100644
index 000000000..679842376
--- /dev/null
+++ b/src/library-authoring/data/apiHooks.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import { initializeMockApp } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { renderHook } from '@testing-library/react-hooks';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import MockAdapter from 'axios-mock-adapter';
+import { getCreateLibraryBlockUrl } from './api';
+import { useCreateLibraryBlock } from './apiHooks';
+
+let axiosMock;
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+describe('library api hooks', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+
+ it('should create library block', async () => {
+ const libraryId = 'lib:org:1';
+ const url = getCreateLibraryBlockUrl(libraryId);
+ axiosMock.onPost(url).reply(200);
+ const { result } = renderHook(() => useCreateLibraryBlock(), { wrapper });
+ await result.current.mutateAsync({
+ libraryId,
+ blockType: 'html',
+ definitionId: '1',
+ });
+
+ expect(axiosMock.history.post[0].url).toEqual(url);
+ });
+});
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 4b887bb4a..d2b4cbd80 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -1,9 +1,14 @@
import React from 'react';
-import { useQuery } from '@tanstack/react-query';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { MeiliSearch } from 'meilisearch';
import { useContentSearchConnection, useContentSearchResults } from '../../search-modal';
-import { type GetLibrariesV2CustomParams, getContentLibrary, getContentLibraryV2List } from './api';
+import {
+ type GetLibrariesV2CustomParams,
+ createLibraryBlock,
+ getContentLibrary,
+ getContentLibraryV2List,
+} from './api';
export const libraryAuthoringQueryKeys = {
all: ['contentLibrary'],
@@ -28,6 +33,20 @@ export const useContentLibrary = (libraryId?: string) => (
})
);
+/**
+ * Use this mutation to create a block in a library
+ */
+export const useCreateLibraryBlock = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: createLibraryBlock,
+ onSettled: (_data, _error, variables) => {
+ queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.libraryId) });
+ queryClient.invalidateQueries({ queryKey: ['content_search'] });
+ },
+ });
+};
+
/**
* Hook to fetch the count of components and collections in a library.
*/
diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts
index 817a85737..d541704ee 100644
--- a/src/library-authoring/index.ts
+++ b/src/library-authoring/index.ts
@@ -1,3 +1,3 @@
-export { default as LibraryAuthoringPage } from './LibraryAuthoringPage';
+export { default as LibraryLayout } from './LibraryLayout';
export { CreateLibrary } from './create-library';
export { libraryAuthoringQueryKeys, useContentLibraryV2List } from './data/apiHooks';
diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx
new file mode 100644
index 000000000..734379f51
--- /dev/null
+++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx
@@ -0,0 +1,52 @@
+import React, { useContext } from 'react';
+import {
+ Stack,
+ Icon,
+ IconButton,
+} from '@openedx/paragon';
+import { Close } from '@openedx/paragon/icons';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import messages from '../messages';
+import { AddContentContainer } from '../add-content';
+import { LibraryContext } from '../common/context';
+
+/**
+ * Sidebar container for library pages.
+ *
+ * It's designed to "squash" the page when open.
+ * Uses `sidebarBodyComponent` of the `store` to
+ * choose which component is rendered.
+ * You can add more components in `bodyComponentMap`.
+ * Use the slice actions to open and close this sidebar.
+ */
+const LibrarySidebar = () => {
+ const intl = useIntl();
+ const { sidebarBodyComponent, closeLibrarySidebar } = useContext(LibraryContext);
+
+ const bodyComponentMap = {
+ 'add-content': ,
+ unknown: null,
+ };
+
+ const buildBody = () : React.ReactNode | null => bodyComponentMap[sidebarBodyComponent || 'unknown'];
+
+ return (
+
+
+
+ {intl.formatMessage(messages.addContentTitle)}
+
+
+
+ {buildBody()}
+
+ );
+};
+
+export default LibrarySidebar;
diff --git a/src/library-authoring/library-sidebar/index.ts b/src/library-authoring/library-sidebar/index.ts
new file mode 100644
index 000000000..087b1e1d9
--- /dev/null
+++ b/src/library-authoring/library-sidebar/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export { default as LibrarySidebar } from './LibrarySidebar';
diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts
index 0cc321738..d127be9ed 100644
--- a/src/library-authoring/messages.ts
+++ b/src/library-authoring/messages.ts
@@ -80,6 +80,21 @@ const messages = defineMessages({
defaultMessage: 'Components ({componentCount})',
description: 'Title for the components container',
},
+ addContentTitle: {
+ id: 'course-authoring.library-authoring.drawer.title.add-content',
+ defaultMessage: 'Add Content',
+ description: 'Title of add content in library container.',
+ },
+ newContentButton: {
+ id: 'course-authoring.library-authoring.buttons.new-content.text',
+ defaultMessage: 'New',
+ description: 'Text of button to open "Add content drawer"',
+ },
+ closeButtonAlt: {
+ id: 'course-authoring.library-authoring.buttons.close.alt',
+ defaultMessage: 'Close',
+ description: 'Alt text of close button',
+ },
});
export default messages;