From ea2ba754223101ce62fb79d4cf8d149b10f15ccc Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Sat, 7 Sep 2024 14:31:24 -0500 Subject: [PATCH] feat: Connect CreateCollectionModal with API --- .../CreateCollectionModal.tsx | 54 +++++++++++++++---- .../create-collection/messages.ts | 15 ++++++ src/library-authoring/data/api.ts | 26 +++++++-- src/library-authoring/data/apiHooks.ts | 17 +++++- 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/library-authoring/create-collection/CreateCollectionModal.tsx b/src/library-authoring/create-collection/CreateCollectionModal.tsx index e49a2fdb35..45c83b3a2d 100644 --- a/src/library-authoring/create-collection/CreateCollectionModal.tsx +++ b/src/library-authoring/create-collection/CreateCollectionModal.tsx @@ -6,41 +6,73 @@ import { ModalDialog, } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { useParams } from 'react-router-dom'; import { LibraryContext } from '../common/context'; import messages from './messages'; +import { useCreateLibraryCollection } from '../data/apiHooks'; +import { ToastContext } from '../../generic/toast-context'; const CreateCollectionModal = () => { const intl = useIntl(); + const { libraryId } = useParams(); + const create = useCreateLibraryCollection(libraryId); const { isCreateCollectionModalOpen, closeCreateCollectionModal, } = React.useContext(LibraryContext); + const { showToast } = React.useContext(ToastContext); const [collectionName, setCollectionName] = React.useState(null); - const [isCollectionNameInvalid, setIsCollectionNameInvalid] = React.useState(false); + const [collectionNameInvalidMsg, setCollectionNameInvalidMsg] = React.useState(null); const [collectionDescription, setCollectionDescription] = React.useState(null); + const [isCreatingCollection, setIsCreatingCollection] = React.useState(false); const handleNameOnChange = React.useCallback((value : string) => { setCollectionName(value); - setIsCollectionNameInvalid(false); + setCollectionNameInvalidMsg(null); + }, []); + + const handleOnClose = React.useCallback(() => { + closeCreateCollectionModal(); + setCollectionNameInvalidMsg(null); + setCollectionName(null); + setCollectionDescription(null); + setIsCreatingCollection(false); }, []); const handleCreate = React.useCallback(() => { if (collectionName === null || collectionName === '') { - setIsCollectionNameInvalid(true); + setCollectionNameInvalidMsg( + intl.formatMessage(messages.createCollectionModalNameInvalid), + ); return; } - // TODO call API - setCollectionName(null); - setCollectionDescription(null); - closeCreateCollectionModal(); + + setIsCreatingCollection(true); + + create.mutateAsync({ + title: collectionName, + description: collectionDescription || '', + }).then(() => { + handleOnClose(); + showToast(intl.formatMessage(messages.createCollectionSuccess)); + }).catch((err) => { + setIsCreatingCollection(false); + if (err.customAttributes.httpErrorStatus === 409) { + setCollectionNameInvalidMsg( + intl.formatMessage(messages.createCollectionModalNameConflict), + ); + } else { + showToast(intl.formatMessage(messages.createCollectionError)); + } + }); }, [collectionName, collectionDescription]); return ( { value={collectionName} onChange={(e) => handleNameOnChange(e.target.value)} /> - { isCollectionNameInvalid && ( + { collectionNameInvalidMsg && ( - {intl.formatMessage(messages.createCollectionModalNameInvalid)} + {collectionNameInvalidMsg} )} @@ -90,7 +122,7 @@ const CreateCollectionModal = () => { {intl.formatMessage(messages.createCollectionModalCancel)} - diff --git a/src/library-authoring/create-collection/messages.ts b/src/library-authoring/create-collection/messages.ts index 7ee82070ad..1f5ad0757d 100644 --- a/src/library-authoring/create-collection/messages.ts +++ b/src/library-authoring/create-collection/messages.ts @@ -35,6 +35,11 @@ const messages = defineMessages({ defaultMessage: 'Collection name is required', description: 'Mesasge when the Name field of the Create Collection modal form is invalid', }, + createCollectionModalNameConflict: { + id: 'course-authoring.library-authoring.modals.create-collection.form.name.conflict', + defaultMessage: 'There is another collection with the same name', + description: 'Mesasge when the Name field of the Create Collection modal form is not unique', + }, createCollectionModalDescriptionLabel: { id: 'course-authoring.library-authoring.modals.create-collection.form.description', defaultMessage: 'Add a description (optional)', @@ -50,6 +55,16 @@ const messages = defineMessages({ defaultMessage: 'Descriptions can help you and your team better organize and find what you are looking for', description: 'Details of the Description field of the Create Collection modal form', }, + createCollectionSuccess: { + id: 'course-authoring.library-authoring.modals.create-collection.success', + defaultMessage: 'Collection created successfully', + description: 'Success message when creating a library collection', + }, + createCollectionError: { + id: 'course-authoring.library-authoring.modals.create-collection.error', + defaultMessage: 'There is an error when creating the library collection', + description: 'Error message when creating a library collection', + }, }); export default messages; diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index e622e6addf..0d5fdf4e11 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -25,9 +25,13 @@ export const getCommitLibraryChangesUrl = (libraryId: string) => `${getApiBaseUr */ export const getLibraryPasteClipboardUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/paste_clipboard/`; /** - * Get the URL for the xblock metadata API. - */ + * Get the URL for the xblock metadata API. + */ export const getXBlockFieldsApiUrl = (usageKey: string) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}/fields/`; +/** + * Get the URL for the Library Collections API. + */ +export const getLibraryCollectionsApiUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/collections/`; export interface ContentLibrary { id: string; @@ -127,6 +131,11 @@ export interface UpdateXBlockFieldsRequest { }; } +export interface CreateLibraryCollectionDataRequest { + title: string; + description: string | null; +} + /** * Fetch block types of a library */ @@ -240,7 +249,18 @@ export async function getXBlockFields(usageKey: string): Promise { /** * Update xblock fields. */ -export async function updateXBlockFields(usageKey:string, xblockData: UpdateXBlockFieldsRequest) { +export async function updateXBlockFields(usageKey: string, xblockData: UpdateXBlockFieldsRequest) { const client = getAuthenticatedHttpClient(); await client.post(getXBlockFieldsApiUrl(usageKey), xblockData); } + +export async function createCollection(collectionData: CreateLibraryCollectionDataRequest, libraryId?: string) { + if (!libraryId) { + throw new Error('libraryId is required'); + } + + const client = getAuthenticatedHttpClient(); + const { data } = await client.post(getLibraryCollectionsApiUrl(libraryId), collectionData); + + return camelCaseObject(data); +} diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 2ebed19ff9..263f326c26 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -18,9 +18,11 @@ import { libraryPasteClipboard, getXBlockFields, updateXBlockFields, + createCollection, + CreateLibraryCollectionDataRequest, } from './api'; -const libraryQueryPredicate = (query: Query, libraryId: string): boolean => { +const libraryQueryPredicate = (query: Query, libraryId?: string): boolean => { // Invalidate all content queries related to this library. // If we allow searching "all courses and libraries" in the future, // then we'd have to invalidate all `["content_search", "results"]` @@ -209,3 +211,16 @@ export const useUpdateXBlockFields = (contentLibraryId: string, usageKey: string }, }); }; + +/** + * Use this mutation to create a library collection + */ +export const useCreateLibraryCollection = (libraryId?: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateLibraryCollectionDataRequest) => createCollection(data, libraryId), + onSettled: () => { + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); + }, + }); +};