diff --git a/src/constants.js b/src/constants.js
index a641c8add..f9e84c19d 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -69,3 +69,8 @@ export const CLIPBOARD_STATUS = {
};
export const STRUCTURAL_XBLOCK_TYPES = ['vertical', 'sequential', 'chapter', 'course'];
+
+export const REGEX_RULES = {
+ specialCharsRule: /^[a-zA-Z0-9_\-.'*~\s]+$/,
+ noSpaceRule: /^\S*$/,
+};
diff --git a/src/generic/alert-error/AlertError.test.tsx b/src/generic/alert-error/AlertError.test.tsx
new file mode 100644
index 000000000..6cfee70db
--- /dev/null
+++ b/src/generic/alert-error/AlertError.test.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+
+import AlertError from '.';
+
+const RootWrapper = ({ error }: { error: unknown }) => (
+
+
+
+);
+
+describe('', () => {
+ test('render using a string', () => {
+ const error = 'This is a string error message';
+ const { getByText } = render();
+ expect(getByText('This is a string error message')).toBeInTheDocument();
+ });
+
+ test('render using an error', () => {
+ const error = new Error('This is an error message');
+ const { getByText } = render();
+ expect(getByText('This is an error message')).toBeInTheDocument();
+ });
+
+ test('render using an error with response', () => {
+ const error = {
+ message: 'This is an error message',
+ response: {
+ data: {
+ message: 'This is a response body',
+ },
+ },
+ };
+ const { getByText } = render();
+ screen.logTestingPlaygroundURL();
+ expect(getByText(/this is an error message/i)).toBeInTheDocument();
+ expect(getByText(/\{"message":"this is a response body"\}/i)).toBeInTheDocument();
+ });
+});
diff --git a/src/generic/alert-error/index.tsx b/src/generic/alert-error/index.tsx
new file mode 100644
index 000000000..a0612fc47
--- /dev/null
+++ b/src/generic/alert-error/index.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import {
+ Alert,
+} from '@openedx/paragon';
+
+const AlertError: React.FC<{ error: unknown }> = ({ error }) => (
+
+ {error instanceof Object && 'message' in error ? error.message : String(error)}
+
+ {error instanceof Object && (error as any).response?.data && JSON.stringify((error as any).response?.data)}
+
+);
+
+export default AlertError;
diff --git a/src/generic/create-or-rerun-course/hooks.jsx b/src/generic/create-or-rerun-course/hooks.jsx
index 5bb744aa4..a5c5196a8 100644
--- a/src/generic/create-or-rerun-course/hooks.jsx
+++ b/src/generic/create-or-rerun-course/hooks.jsx
@@ -5,6 +5,7 @@ import { useFormik } from 'formik';
import * as Yup from 'yup';
import { useNavigate } from 'react-router-dom';
+import { REGEX_RULES } from '../../constants';
import { RequestStatus } from '../../data/constants';
import { getStudioHomeData } from '../../studio-home/data/selectors';
import {
@@ -32,8 +33,8 @@ const useCreateOrRerunCourse = (initialValues) => {
const [isFormFilled, setFormFilled] = useState(false);
const [showErrorBanner, setShowErrorBanner] = useState(false);
const organizations = allowToCreateNewOrg ? allOrganizations : allowedOrganizations;
- const specialCharsRule = /^[a-zA-Z0-9_\-.'*~\s]+$/;
- const noSpaceRule = /^\S*$/;
+
+ const { specialCharsRule, noSpaceRule } = REGEX_RULES;
const validationSchema = Yup.object().shape({
displayName: Yup.string().required(
intl.formatMessage(messages.requiredFieldError),
diff --git a/src/generic/data/apiHooks.js b/src/generic/data/apiHooks.js
index e15ebc12a..7fec094dd 100644
--- a/src/generic/data/apiHooks.js
+++ b/src/generic/data/apiHooks.js
@@ -8,7 +8,7 @@ import { getOrganizations, getTagsCount } from './api';
export const useOrganizationListData = () => (
useQuery({
queryKey: ['organizationList'],
- queryFn: () => getOrganizations(),
+ queryFn: getOrganizations,
})
);
diff --git a/src/library-authoring/CreateLibrary.tsx b/src/library-authoring/CreateLibrary.tsx
deleted file mode 100644
index 227f14dbe..000000000
--- a/src/library-authoring/CreateLibrary.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
-import { Container } from '@openedx/paragon';
-
-import Header from '../header';
-import SubHeader from '../generic/sub-header/SubHeader';
-
-import messages from './messages';
-
-/* istanbul ignore next This is only a placeholder component */
-const CreateLibrary = () => (
- <>
-
-
- }
- />
-
-
-
-
- >
-);
-
-export default CreateLibrary;
diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx
index 958e3e448..26073ee23 100644
--- a/src/library-authoring/LibraryAuthoringPage.test.tsx
+++ b/src/library-authoring/LibraryAuthoringPage.test.tsx
@@ -62,6 +62,7 @@ const libraryData: ContentLibrary = {
hasUnpublishedChanges: true,
hasUnpublishedDeletes: false,
license: '',
+ canEditLibrary: false,
};
const RootWrapper = () => (
diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx
index 7f1643277..f369025a3 100644
--- a/src/library-authoring/LibraryAuthoringPage.tsx
+++ b/src/library-authoring/LibraryAuthoringPage.tsx
@@ -16,7 +16,7 @@ import NotFoundAlert from '../generic/NotFoundAlert';
import LibraryComponents from './LibraryComponents';
import LibraryCollections from './LibraryCollections';
import LibraryHome from './LibraryHome';
-import { useContentLibrary } from './data/apiHook';
+import { useContentLibrary } from './data/apiHooks';
import messages from './messages';
enum TabList {
diff --git a/src/library-authoring/LibraryComponents.tsx b/src/library-authoring/LibraryComponents.tsx
index fee8cb350..a8d2cd281 100644
--- a/src/library-authoring/LibraryComponents.tsx
+++ b/src/library-authoring/LibraryComponents.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { NoComponents, NoSearchResults } from './EmptyStates';
-import { useLibraryComponentCount } from './data/apiHook';
+import { useLibraryComponentCount } from './data/apiHooks';
import messages from './messages';
type LibraryComponentsProps = {
diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx
index 273d36fca..1a79c05cf 100644
--- a/src/library-authoring/LibraryHome.tsx
+++ b/src/library-authoring/LibraryHome.tsx
@@ -7,7 +7,7 @@ import {
import { NoComponents, NoSearchResults } from './EmptyStates';
import LibraryCollections from './LibraryCollections';
import LibraryComponents from './LibraryComponents';
-import { useLibraryComponentCount } from './data/apiHook';
+import { useLibraryComponentCount } from './data/apiHooks';
import messages from './messages';
const Section = ({ title, children } : { title: string, children: React.ReactNode }) => (
diff --git a/src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js b/src/library-authoring/__mocks__/contentLibrariesListV2.js
similarity index 100%
rename from src/studio-home/__mocks__/listStudioHomeV2LibrariesMock.js
rename to src/library-authoring/__mocks__/contentLibrariesListV2.js
diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx
new file mode 100644
index 000000000..b9a63ec02
--- /dev/null
+++ b/src/library-authoring/create-library/CreateLibrary.test.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import MockAdapter from 'axios-mock-adapter';
+import { initializeMockApp } from '@edx/frontend-platform';
+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 userEvent from '@testing-library/user-event';
+
+import initializeStore from '../../store';
+import CreateLibrary from './CreateLibrary';
+import { getContentLibraryV2CreateApiUrl } from './data/api';
+
+let store;
+const mockNavigate = jest.fn();
+let axiosMock: MockAdapter;
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: () => mockNavigate,
+}));
+
+jest.mock('../../generic/data/apiHooks', () => ({
+ ...jest.requireActual('../../generic/data/apiHooks'),
+ useOrganizationListData: () => ({
+ data: ['org1', 'org2', 'org3', 'org4', 'org5'],
+ isLoading: false,
+ }),
+}));
+
+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());
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ axiosMock.restore();
+ queryClient.clear();
+ });
+
+ test('call api data with correct data', async () => {
+ axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, {
+ id: 'library-id',
+ });
+
+ const { getByRole } = render();
+
+ const titleInput = getByRole('textbox', { name: /library name/i });
+ userEvent.click(titleInput);
+ userEvent.type(titleInput, 'Test Library Name');
+
+ const orgInput = getByRole('combobox', { name: /organization/i });
+ userEvent.click(orgInput);
+ userEvent.type(orgInput, 'org1');
+ userEvent.tab();
+
+ const slugInput = getByRole('textbox', { name: /library id/i });
+ userEvent.click(slugInput);
+ userEvent.type(slugInput, 'test_library_slug');
+
+ fireEvent.click(getByRole('button', { name: /create/i }));
+ await waitFor(() => {
+ expect(axiosMock.history.post.length).toBe(1);
+ expect(axiosMock.history.post[0].data).toBe(
+ '{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}',
+ );
+ expect(mockNavigate).toHaveBeenCalledWith('/library/library-id');
+ });
+ });
+
+ test('show api error', async () => {
+ axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, {
+ field: 'Error message',
+ });
+ const { getByRole, getByTestId } = render();
+
+ const titleInput = getByRole('textbox', { name: /library name/i });
+ userEvent.click(titleInput);
+ userEvent.type(titleInput, 'Test Library Name');
+
+ const orgInput = getByTestId('autosuggest-textbox-input');
+ userEvent.click(orgInput);
+ userEvent.type(orgInput, 'org1');
+ userEvent.tab();
+
+ const slugInput = getByRole('textbox', { name: /library id/i });
+ userEvent.click(slugInput);
+ userEvent.type(slugInput, 'test_library_slug');
+
+ fireEvent.click(getByRole('button', { name: /create/i }));
+ await waitFor(() => {
+ expect(axiosMock.history.post.length).toBe(1);
+ expect(axiosMock.history.post[0].data).toBe(
+ '{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}',
+ );
+ expect(mockNavigate).not.toHaveBeenCalled();
+ expect(getByRole('alert')).toHaveTextContent('Request failed with status code 400');
+ expect(getByRole('alert')).toHaveTextContent('{"field":"Error message"}');
+ });
+ });
+});
diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx
new file mode 100644
index 000000000..f705c73a4
--- /dev/null
+++ b/src/library-authoring/create-library/CreateLibrary.tsx
@@ -0,0 +1,138 @@
+import React from 'react';
+import { StudioFooter } from '@edx/frontend-component-footer';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Container,
+ Form,
+ StatefulButton,
+} from '@openedx/paragon';
+import { Formik } from 'formik';
+import { useNavigate } from 'react-router-dom';
+import * as Yup from 'yup';
+
+import { REGEX_RULES } from '../../constants';
+import Header from '../../header';
+import FormikControl from '../../generic/FormikControl';
+import FormikErrorFeedback from '../../generic/FormikErrorFeedback';
+import AlertError from '../../generic/alert-error';
+import { useOrganizationListData } from '../../generic/data/apiHooks';
+import SubHeader from '../../generic/sub-header/SubHeader';
+import { useCreateLibraryV2 } from './data/apiHooks';
+import messages from './messages';
+
+const CreateLibrary = () => {
+ const intl = useIntl();
+ const navigate = useNavigate();
+
+ const { noSpaceRule, specialCharsRule } = REGEX_RULES;
+ const validSlugIdRegex = /^[a-zA-Z\d]+(?:[\w-]*[a-zA-Z\d]+)*$/;
+
+ const {
+ mutate,
+ data,
+ isLoading,
+ isError,
+ error,
+ } = useCreateLibraryV2();
+
+ const {
+ data: organizationListData,
+ isLoading: isOrganizationListLoading,
+ } = useOrganizationListData();
+
+ if (data) {
+ navigate(`/library/${data.id}`);
+ }
+
+ return (
+ <>
+
+
+
+ mutate(values)}
+ >
+ {(formikProps) => (
+
+ )}
+
+ {isError && ()}
+
+
+ >
+ );
+};
+
+export default CreateLibrary;
diff --git a/src/library-authoring/create-library/data/api.ts b/src/library-authoring/create-library/data/api.ts
new file mode 100644
index 000000000..b529de5e9
--- /dev/null
+++ b/src/library-authoring/create-library/data/api.ts
@@ -0,0 +1,30 @@
+import { camelCaseObject, getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+
+import type { ContentLibrary } from '../../data/api';
+
+const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
+
+/**
+ * Get the URL for creating a new library.
+ */
+export const getContentLibraryV2CreateApiUrl = () => `${getApiBaseUrl()}/api/libraries/v2/`;
+
+export interface CreateContentLibraryArgs {
+ title: string,
+ org: string,
+ slug: string,
+}
+
+/**
+ * Create a new library
+ */
+export async function createLibraryV2(data: CreateContentLibraryArgs): Promise {
+ const client = getAuthenticatedHttpClient();
+ const url = getContentLibraryV2CreateApiUrl();
+
+ // Description field cannot be null, but we don't have a input in the form for it
+ const { data: newLibrary } = await client.post(url, { description: '', ...data });
+
+ return camelCaseObject(newLibrary);
+}
diff --git a/src/library-authoring/create-library/data/apiHooks.ts b/src/library-authoring/create-library/data/apiHooks.ts
new file mode 100644
index 000000000..3a58771b9
--- /dev/null
+++ b/src/library-authoring/create-library/data/apiHooks.ts
@@ -0,0 +1,22 @@
+import {
+ useMutation,
+ useQueryClient,
+} from '@tanstack/react-query';
+
+import { createLibraryV2 } from './api';
+import { libraryAuthoringQueryKeys } from '../../data/apiHooks';
+
+/**
+ * Hook that provides a "mutation" that can be used to create a new content library.
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const useCreateLibraryV2 = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createLibraryV2,
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibraryList() });
+ },
+ });
+};
diff --git a/src/library-authoring/create-library/index.ts b/src/library-authoring/create-library/index.ts
new file mode 100644
index 000000000..dedcc16dc
--- /dev/null
+++ b/src/library-authoring/create-library/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export { default as CreateLibrary } from './CreateLibrary';
diff --git a/src/library-authoring/create-library/messages.ts b/src/library-authoring/create-library/messages.ts
new file mode 100644
index 000000000..295808b56
--- /dev/null
+++ b/src/library-authoring/create-library/messages.ts
@@ -0,0 +1,88 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ createLibrary: {
+ id: 'course-authoring.library-authoring.create-library',
+ defaultMessage: 'Create new library',
+ description: 'Header for the create library form',
+ },
+ titleLabel: {
+ id: 'course-authoring.library-authoring.create-library.form.title.label',
+ defaultMessage: 'Library name',
+ description: 'Label for the title field.',
+ },
+ titlePlaceholder: {
+ id: 'course-authoring.library-authoring.create-library.form.title.placeholder',
+ defaultMessage: 'e.g. Computer Science Problems',
+ description: 'Placeholder text for the title field.',
+ },
+ titleHelp: {
+ id: 'course-authoring.library-authoring.create-library.form.title.help',
+ defaultMessage: 'The name for your library',
+ description: 'Help text for the title field.',
+ },
+ orgLabel: {
+ id: 'course-authoring.library-authoring.create-library.form.org.label',
+ defaultMessage: 'Organization',
+ description: 'Label for the organization field.',
+ },
+ orgPlaceholder: {
+ id: 'course-authoring.library-authoring.create-library.form.org.placeholder',
+ defaultMessage: 'e.g. UniversityX or OrganizationX',
+ description: 'Placeholder text for the organization field.',
+ },
+ orgHelp: {
+ id: 'course-authoring.library-authoring.create-library.form.org.help',
+ defaultMessage: 'The public organization name for your library. This cannot be changed.',
+ description: 'Help text for the organization field.',
+ },
+ slugLabel: {
+ id: 'course-authoring.library-authoring.create-library.form.slug.label',
+ defaultMessage: 'Library ID',
+ description: 'Label for the slug field.',
+ },
+ slugPlaceholder: {
+ id: 'course-authoring.library-authoring.create-library.form.slug.placeholder',
+ defaultMessage: 'e.g. CSPROB',
+ description: 'Placeholder text for the slug field.',
+ },
+ slugHelp: {
+ id: 'course-authoring.library-authoring.create-library.form.slug.help',
+ defaultMessage: `The unique code that identifies this library. Note: This is
+ part of your library URL, so no spaces or special characters are allowed.
+ This cannot be changed.`,
+ description: 'Help text for the slug field.',
+ },
+ invalidSlugError: {
+ id: 'course-authoring.library-authoring.create-library.form.invalid-slug.error',
+ defaultMessage: 'Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens.',
+ description: 'Text to display when slug id has invalid symbols.',
+ },
+ requiredFieldError: {
+ id: 'course-authoring.library-authoring.create-library.form.required.error',
+ defaultMessage: 'Required field.',
+ description: 'Error message to display when a required field is missing.',
+ },
+ disallowedCharsError: {
+ id: 'course-authoring.library-authoring.create-library.form.disallowed-chars.error',
+ defaultMessage: 'Please do not use any spaces or special characters in this field.',
+ description: 'Error message to display when a field contains disallowed characters.',
+ },
+ noSpaceError: {
+ id: 'course-authoring.library-authoring.create-library.form.no-space.error',
+ defaultMessage: 'Please do not use any spaces in this field.',
+ description: 'Error message to display when a field contains spaces.',
+ },
+ createLibraryButton: {
+ id: 'course-authoring.library-authoring.create-library.form.create-library.button',
+ defaultMessage: 'Create',
+ description: 'Button text for creating a new library.',
+ },
+ createLibraryButtonPending: {
+ id: 'course-authoring.library-authoring.create-library.form.create-library.button.pending',
+ defaultMessage: 'Creating..',
+ description: 'Button text while the library is being created.',
+ },
+});
+
+export default messages;
diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts
index 95126d826..be3ec564f 100644
--- a/src/library-authoring/data/api.ts
+++ b/src/library-authoring/data/api.ts
@@ -1,11 +1,13 @@
-import { camelCaseObject, getConfig } from '@edx/frontend-platform';
+import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
+
/**
* Get the URL for the content library API.
*/
export const getContentLibraryApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/`;
+export const getContentLibraryV2ListApiUrl = () => `${getApiBaseUrl()}/api/libraries/v2/`;
export interface ContentLibrary {
id: string;
@@ -23,6 +25,7 @@ export interface ContentLibrary {
hasUnpublishedChanges: boolean;
hasUnpublishedDeletes: boolean;
license: string;
+ canEditLibrary: boolean;
}
/**
@@ -36,3 +39,48 @@ export async function getContentLibrary(libraryId?: string): Promise {
+ // Set default params if not passed in
+ const customParamsDefaults = {
+ type: customParams.type || 'complex',
+ page: customParams.page || 1,
+ pageSize: customParams.pageSize || 50,
+ pagination: customParams.pagination !== undefined ? customParams.pagination : true,
+ order: customParams.order || 'title',
+ textSearch: customParams.search,
+ };
+ const customParamsFormated = snakeCaseObject(customParamsDefaults);
+ const { data } = await getAuthenticatedHttpClient()
+ .get(getContentLibraryV2ListApiUrl(), { params: customParamsFormated });
+ return camelCaseObject(data);
+}
diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHooks.ts
similarity index 61%
rename from src/library-authoring/data/apiHook.ts
rename to src/library-authoring/data/apiHooks.ts
index 56a6791d2..4b887bb4a 100644
--- a/src/library-authoring/data/apiHook.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -3,14 +3,27 @@ import { useQuery } from '@tanstack/react-query';
import { MeiliSearch } from 'meilisearch';
import { useContentSearchConnection, useContentSearchResults } from '../../search-modal';
-import { getContentLibrary } from './api';
+import { type GetLibrariesV2CustomParams, getContentLibrary, getContentLibraryV2List } from './api';
+
+export const libraryAuthoringQueryKeys = {
+ all: ['contentLibrary'],
+ /**
+ * Base key for data specific to a contentLibrary
+ */
+ contentLibrary: (contentLibraryId?: string) => [...libraryAuthoringQueryKeys.all, contentLibraryId],
+ contentLibraryList: (customParams?: GetLibrariesV2CustomParams) => [
+ ...libraryAuthoringQueryKeys.all,
+ 'list',
+ ...(customParams ? [customParams] : []),
+ ],
+};
/**
* Hook to fetch a content library by its ID.
*/
export const useContentLibrary = (libraryId?: string) => (
useQuery({
- queryKey: ['contentLibrary', libraryId],
+ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId),
queryFn: () => getContentLibrary(libraryId),
})
);
@@ -46,3 +59,14 @@ export const useLibraryComponentCount = (libraryId: string, searchKeywords: stri
collectionCount,
};
};
+
+/**
+ * Builds the query to fetch list of V2 Libraries
+ */
+export const useContentLibraryV2List = (customParams: GetLibrariesV2CustomParams) => (
+ useQuery({
+ queryKey: libraryAuthoringQueryKeys.contentLibraryList(customParams),
+ queryFn: () => getContentLibraryV2List(customParams),
+ keepPreviousData: true,
+ })
+);
diff --git a/src/library-authoring/index.ts b/src/library-authoring/index.ts
index 40da2db4a..817a85737 100644
--- a/src/library-authoring/index.ts
+++ b/src/library-authoring/index.ts
@@ -1,2 +1,3 @@
export { default as LibraryAuthoringPage } from './LibraryAuthoringPage';
-export { default as CreateLibrary } from './CreateLibrary';
+export { CreateLibrary } from './create-library';
+export { libraryAuthoringQueryKeys, useContentLibraryV2List } from './data/apiHooks';
diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts
index c63d66b3e..0cc321738 100644
--- a/src/library-authoring/messages.ts
+++ b/src/library-authoring/messages.ts
@@ -65,16 +65,6 @@ const messages = defineMessages({
defaultMessage: 'Recently modified components and collections will be displayed here.',
description: 'Temp placeholder for the recent components container. This will be replaced with the actual list.',
},
- createLibrary: {
- id: 'course-authoring.library-authoring.create-library',
- defaultMessage: 'Create library',
- description: 'Header for the create library form',
- },
- createLibraryTempPlaceholder: {
- id: 'course-authoring.library-authoring.create-library-temp-placeholder',
- defaultMessage: 'This is a placeholder for the create library form. This will be replaced with the actual form.',
- description: 'Temp placeholder for the create library container. This will be replaced with the new library form.',
- },
recentlyModifiedTitle: {
id: 'course-authoring.library-authoring.recently-modified-title',
defaultMessage: 'Recently Modified',
diff --git a/src/library/data/api.ts b/src/library/data/api.ts
deleted file mode 100644
index 2a31db309..000000000
--- a/src/library/data/api.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { camelCaseObject, snakeCaseObject, getConfig } from '@edx/frontend-platform';
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-
-export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
-
-export interface LibraryV2 {
- id: string,
- type: string,
- org: string,
- slug: string,
- title: string,
- description: string,
- numBlocks: number,
- version: number,
- lastPublished: string | null,
- allowLti: boolean,
- allowPublicLearning: boolean,
- allowPublicRead: boolean,
- hasUnpublishedChanges: boolean,
- hasUnpublishedDeletes: boolean,
- license: string,
-}
-
-export interface LibrariesV2Response {
- next: string | null,
- previous: string | null,
- count: number,
- numPages: number,
- currentPage: number,
- start: number,
- results: LibraryV2[],
-}
-
-/* Additional custom parameters for the API request. */
-export interface GetLibrariesV2CustomParams {
- /* (optional) Library type, default `complex` */
- type?: string,
- /* (optional) Page number of results */
- page?: number,
- /* (optional) The number of results on each page, default `50` */
- pageSize?: number,
- /* (optional) Whether pagination is supported, default `true` */
- pagination?: boolean,
- /* (optional) Library field to order results by. Prefix with '-' for descending */
- order?: string,
- /* (optional) Search query to filter v2 Libraries by */
- search?: string,
-}
-
-/**
- * Get's studio home v2 Libraries.
- */
-export async function getStudioHomeLibrariesV2(customParams: GetLibrariesV2CustomParams): Promise {
- // Set default params if not passed in
- const customParamsDefaults = {
- type: customParams.type || 'complex',
- page: customParams.page || 1,
- pageSize: customParams.pageSize || 50,
- pagination: customParams.pagination !== undefined ? customParams.pagination : true,
- order: customParams.order || 'title',
- textSearch: customParams.search,
- };
- const customParamsFormat = snakeCaseObject(customParamsDefaults);
- const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`, { params: customParamsFormat });
- return camelCaseObject(data);
-}
diff --git a/src/studio-home/__mocks__/index.js b/src/studio-home/__mocks__/index.js
index af2a85b39..92461eb0b 100644
--- a/src/studio-home/__mocks__/index.js
+++ b/src/studio-home/__mocks__/index.js
@@ -1,2 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
export { default as studioHomeMock } from './studioHomeMock';
-export { default as listStudioHomeV2LibrariesMock } from './listStudioHomeV2LibrariesMock';
diff --git a/src/studio-home/data/api.test.js b/src/studio-home/data/api.test.js
index 5d255d7fb..329d05b3b 100644
--- a/src/studio-home/data/api.test.js
+++ b/src/studio-home/data/api.test.js
@@ -14,12 +14,10 @@ import {
getStudioHomeCoursesV2,
getStudioHomeLibraries,
} from './api';
-import { getStudioHomeLibrariesV2 } from '../../library/data/api';
import {
generateGetStudioCoursesApiResponse,
generateGetStudioHomeDataApiResponse,
generateGetStudioHomeLibrariesApiResponse,
- generateGetStudioHomeLibrariesV2ApiResponse,
} from '../factories/mockApiResponses';
let axiosMock;
@@ -80,16 +78,6 @@ describe('studio-home api calls', () => {
expect(result).toEqual(expected);
});
- it('should get studio v2 libraries data', async () => {
- const apiLink = `${getApiBaseUrl()}/api/libraries/v2/`;
- axiosMock.onGet(apiLink).reply(200, generateGetStudioHomeLibrariesV2ApiResponse());
- const result = await getStudioHomeLibrariesV2({});
- const expected = generateGetStudioHomeLibrariesV2ApiResponse();
-
- expect(axiosMock.history.get[0].url).toEqual(apiLink);
- expect(result).toEqual(expected);
- });
-
it('should handle course notification request', async () => {
const dismissLink = 'to://dismiss-link';
const successResponse = { status: 'OK' };
diff --git a/src/studio-home/data/apiHooks.ts b/src/studio-home/data/apiHooks.ts
deleted file mode 100644
index 99e9606fb..000000000
--- a/src/studio-home/data/apiHooks.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-
-import { GetLibrariesV2CustomParams, getStudioHomeLibrariesV2 } from '../../library/data/api';
-
-/**
- * Builds the query to fetch list of V2 Libraries
- */
-const useListStudioHomeV2Libraries = (customParams: GetLibrariesV2CustomParams) => (
- useQuery({
- queryKey: ['listV2Libraries', customParams],
- queryFn: () => getStudioHomeLibrariesV2(customParams),
- keepPreviousData: true,
- })
-);
-
-export default useListStudioHomeV2Libraries;
diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.jsx
index 90f47d5e8..6f40c9212 100644
--- a/src/studio-home/tabs-section/TabsSection.test.jsx
+++ b/src/studio-home/tabs-section/TabsSection.test.jsx
@@ -11,7 +11,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
import MockAdapter from 'axios-mock-adapter';
import initializeStore from '../../store';
-import { studioHomeMock, listStudioHomeV2LibrariesMock } from '../__mocks__';
+import { studioHomeMock } from '../__mocks__';
import messages from '../messages';
import tabMessages from './messages';
import TabsSection from '.';
@@ -25,26 +25,8 @@ import {
import { getApiBaseUrl, getStudioHomeApiUrl } from '../data/api';
import { executeThunk } from '../../utils';
import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks';
-
-import useListStudioHomeV2Libraries from '../data/apiHooks';
-
-jest.mock('../data/apiHooks', () => ({
- // Since only useListStudioHomeV2Libraries is exported as default
- __esModule: true,
- default: jest.fn(() => ({
- data: {
- next: null,
- previous: null,
- count: 2,
- num_pages: 1,
- current_page: 1,
- start: 0,
- results: [],
- },
- isLoading: false,
- isError: false,
- })),
-}));
+import { getContentLibraryV2ListApiUrl } from '../../library-authoring/data/api';
+import contentLibrariesListV2 from '../../library-authoring/__mocks__/contentLibrariesListV2';
const { studioShortName } = studioHomeMock;
@@ -108,6 +90,7 @@ describe('', () => {
...getConfig(),
LIBRARY_MODE: 'mixed',
});
+ axiosMock.onGet(getContentLibraryV2ListApiUrl()).reply(200, contentLibrariesListV2);
});
it('should render all tabs correctly', async () => {
@@ -384,12 +367,6 @@ describe('', () => {
});
it('should switch to Libraries tab and render specific v2 library details', async () => {
- useListStudioHomeV2Libraries.mockReturnValue({
- data: listStudioHomeV2LibrariesMock,
- isLoading: false,
- isError: false,
- });
-
render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -403,14 +380,14 @@ describe('', () => {
expect(screen.getByText('Showing 2 of 2')).toBeVisible();
- expect(screen.getByText(listStudioHomeV2LibrariesMock.results[0].title)).toBeVisible();
+ expect(screen.getByText(contentLibrariesListV2.results[0].title)).toBeVisible();
expect(screen.getByText(
- `${listStudioHomeV2LibrariesMock.results[0].org} / ${listStudioHomeV2LibrariesMock.results[0].slug}`,
+ `${contentLibrariesListV2.results[0].org} / ${contentLibrariesListV2.results[0].slug}`,
)).toBeVisible();
- expect(screen.getByText(listStudioHomeV2LibrariesMock.results[1].title)).toBeVisible();
+ expect(screen.getByText(contentLibrariesListV2.results[1].title)).toBeVisible();
expect(screen.getByText(
- `${listStudioHomeV2LibrariesMock.results[1].org} / ${listStudioHomeV2LibrariesMock.results[1].slug}`,
+ `${contentLibrariesListV2.results[1].org} / ${contentLibrariesListV2.results[1].slug}`,
)).toBeVisible();
});
@@ -444,12 +421,6 @@ describe('', () => {
LIBRARY_MODE: 'v2 only',
});
- useListStudioHomeV2Libraries.mockReturnValue({
- data: listStudioHomeV2LibrariesMock,
- isLoading: false,
- isError: false,
- });
-
render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -463,14 +434,14 @@ describe('', () => {
expect(screen.getByText('Showing 2 of 2')).toBeVisible();
- expect(screen.getByText(listStudioHomeV2LibrariesMock.results[0].title)).toBeVisible();
+ expect(screen.getByText(contentLibrariesListV2.results[0].title)).toBeVisible();
expect(screen.getByText(
- `${listStudioHomeV2LibrariesMock.results[0].org} / ${listStudioHomeV2LibrariesMock.results[0].slug}`,
+ `${contentLibrariesListV2.results[0].org} / ${contentLibrariesListV2.results[0].slug}`,
)).toBeVisible();
- expect(screen.getByText(listStudioHomeV2LibrariesMock.results[1].title)).toBeVisible();
+ expect(screen.getByText(contentLibrariesListV2.results[1].title)).toBeVisible();
expect(screen.getByText(
- `${listStudioHomeV2LibrariesMock.results[1].org} / ${listStudioHomeV2LibrariesMock.results[1].slug}`,
+ `${contentLibrariesListV2.results[1].org} / ${contentLibrariesListV2.results[1].slug}`,
)).toBeVisible();
});
diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx
index 8678a69ea..f29fa1d6c 100644
--- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx
+++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx
@@ -10,8 +10,8 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig, getPath } from '@edx/frontend-platform';
import { Error } from '@openedx/paragon/icons';
+import { useContentLibraryV2List } from '../../../library-authoring';
import { constructLibraryAuthoringURL } from '../../../utils';
-import useListStudioHomeV2Libraries from '../../data/apiHooks';
import { LoadingSpinner } from '../../../generic/Loading';
import AlertMessage from '../../../generic/alert-message';
import CardItem from '../../card-item';
@@ -45,7 +45,7 @@ const LibrariesV2Tab: React.FC<{
data,
isLoading,
isError,
- } = useListStudioHomeV2Libraries({ page: currentPage, ...filterParams });
+ } = useContentLibraryV2List({ page: currentPage, ...filterParams });
if (isLoading && !isFiltered) {
return (