diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index accb72e9bf..361947cf7b 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -116,25 +116,25 @@ const AddContentView = ({ return ( <> - {upstreamContainerType !== ContainerType.Unit && ( + {(collectionId || unitId) && componentPicker && ( + /// Show the "Add Library Content" button for units and collections <> - {collectionId ? ( - componentPicker && ( - <> - - - - ) - ) : ( - - )} - -
+ + )} + {!collectionId && !unitId && ( + // Doesn't show the "Collection" button if we are in a unit or collection + + )} + {upstreamContainerType !== ContainerType.Unit && ( + // Doesn't show the "Unit" button if we are in a unit + + )} +
{/* Note: for MVP we are hiding the unuspported types, not just disabling them. */} {contentTypes.filter(ct => !ct.disabled).map((contentType) => ( void; -const render = () => baseRender(, { - path: '/library/:libraryId/collection/:collectionId/*', - params: { libraryId, collectionId: 'collectionId' }, +const mockAddComponentsToCollection = jest.fn(); +const mockAddComponentsToContainer = jest.fn(); +jest.spyOn(api, 'addComponentsToCollection').mockImplementation(mockAddComponentsToCollection); +jest.spyOn(api, 'addComponentsToContainer').mockImplementation(mockAddComponentsToContainer); + +const render = (context: 'collection' | 'unit') => baseRender(, { + path: context === 'collection' + ? '/library/:libraryId/collection/:collectionId/*' + : '/library/:libraryId/container/:unitId/*', + params: { + libraryId, + ...(context === 'collection' && { collectionId: 'collectionId' }), + ...(context === 'unit' && { unitId: 'unitId' }), + }, extraWrapper: ({ children }) => ( ', () => { const mocks = initializeMocks(); mockShowToast = mocks.mockShowToast; mocks.axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + jest.clearAllMocks(); }); - it('can pick components from the modal', async () => { - const mockAddComponentsToCollection = jest.fn(); - jest.spyOn(api, 'addComponentsToCollection').mockImplementation(mockAddComponentsToCollection); - - render(); - - // Wait for the content library to load - await waitFor(() => { - expect(screen.getByText('Test Library')).toBeInTheDocument(); - expect(screen.queryAllByText('Introduction to Testing')[0]).toBeInTheDocument(); - }); - - // Select the first component - fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[0]); - expect(await screen.findByText('1 Selected Component')).toBeInTheDocument(); - - fireEvent.click(screen.queryAllByRole('button', { name: 'Add to Collection' })[0]); - - await waitFor(() => { - expect(mockAddComponentsToCollection).toHaveBeenCalledWith( - libraryId, - 'collectionId', - ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'], - ); + ['collection' as const, 'unit' as const].forEach((context) => { + it(`can pick components from the modal (${context})`, async () => { + render(context); + + // Wait for the content library to load + await waitFor(() => { + expect(screen.getByText('Test Library')).toBeInTheDocument(); + expect(screen.queryAllByText('Introduction to Testing')[0]).toBeInTheDocument(); + }); + + // Select the first component + fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[0]); + expect(await screen.findByText('1 Selected Component')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /add to .*/i })); + + await waitFor(() => { + if (context === 'collection') { + expect(mockAddComponentsToCollection).toHaveBeenCalledWith( + libraryId, + 'collectionId', + ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'], + ); + } else { + expect(mockAddComponentsToContainer).toHaveBeenCalledWith( + 'unitId', + ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'], + ); + } + }); expect(onClose).toHaveBeenCalled(); expect(mockShowToast).toHaveBeenCalledWith('Content linked successfully.'); }); - }); - - it('show error when api call fails', async () => { - const mockAddComponentsToCollection = jest.fn().mockRejectedValue(new Error('Failed to add components')); - jest.spyOn(api, 'addComponentsToCollection').mockImplementation(mockAddComponentsToCollection); - render(); - - // Wait for the content library to load - await waitFor(() => { - expect(screen.getByText('Test Library')).toBeInTheDocument(); - expect(screen.queryAllByText('Introduction to Testing')[0]).toBeInTheDocument(); - }); - - // Select the first component - fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[0]); - expect(await screen.findByText('1 Selected Component')).toBeInTheDocument(); - - fireEvent.click(screen.queryAllByRole('button', { name: 'Add to Collection' })[0]); - await waitFor(() => { - expect(mockAddComponentsToCollection).toHaveBeenCalledWith( - libraryId, - 'collectionId', - ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'], - ); + it(`show error when api call fails (${context})`, async () => { + if (context === 'collection') { + mockAddComponentsToCollection.mockRejectedValueOnce(new Error('Error')); + } else { + mockAddComponentsToContainer.mockRejectedValueOnce(new Error('Error')); + } + render(context); + + // Wait for the content library to load + await waitFor(() => { + expect(screen.getByText('Test Library')).toBeInTheDocument(); + expect(screen.queryAllByText('Introduction to Testing')[0]).toBeInTheDocument(); + }); + + // Select the first component + fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[0]); + expect(await screen.findByText('1 Selected Component')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /add to .*/i })); + + await waitFor(() => { + if (context === 'collection') { + expect(mockAddComponentsToCollection).toHaveBeenCalledWith( + libraryId, + 'collectionId', + ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'], + ); + } else { + expect(mockAddComponentsToContainer).toHaveBeenCalledWith( + 'unitId', + ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'], + ); + } + }); expect(onClose).toHaveBeenCalled(); - expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this collection.'); + const name = context === 'collection' ? 'collection' : 'container'; + expect(mockShowToast).toHaveBeenCalledWith(`There was an error linking the content to this ${name}.`); }); }); }); diff --git a/src/library-authoring/add-content/PickLibraryContentModal.tsx b/src/library-authoring/add-content/PickLibraryContentModal.tsx index 5436bf2d78..4b9cded02f 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.tsx @@ -5,23 +5,25 @@ import { ActionRow, Button, StandardModal } from '@openedx/paragon'; import { ToastContext } from '../../generic/toast-context'; import { useLibraryContext } from '../common/context/LibraryContext'; import type { SelectedComponent } from '../common/context/ComponentPickerContext'; -import { useAddComponentsToCollection } from '../data/apiHooks'; +import { useAddComponentsToCollection, useAddComponentsToContainer } from '../data/apiHooks'; import messages from './messages'; interface PickLibraryContentModalFooterProps { onSubmit: () => void; selectedComponents: SelectedComponent[]; + buttonText: React.ReactNode; } const PickLibraryContentModalFooter: React.FC = ({ onSubmit, selectedComponents, + buttonText, }) => ( ); @@ -40,6 +42,7 @@ export const PickLibraryContentModal: React.FC = ( const { libraryId, collectionId, + unitId, /** We need to get it as a reference instead of directly importing it to avoid the import cycle: * ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > * Sidebar > AddContent > ComponentPicker */ @@ -47,11 +50,12 @@ export const PickLibraryContentModal: React.FC = ( } = useLibraryContext(); // istanbul ignore if: this should never happen - if (!collectionId || !ComponentPicker) { - throw new Error('libraryId and componentPicker are required'); + if (!(collectionId || unitId) || !ComponentPicker) { + throw new Error('collectionId/unitId and componentPicker are required'); } - const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId); + const updateCollectionItemsMutation = useAddComponentsToCollection(libraryId, collectionId); + const updateUnitComponentsMutation = useAddComponentsToContainer(libraryId, unitId); const { showToast } = useContext(ToastContext); @@ -60,13 +64,23 @@ export const PickLibraryContentModal: React.FC = ( const onSubmit = useCallback(() => { const usageKeys = selectedComponents.map(({ usageKey }) => usageKey); onClose(); - updateComponentsMutation.mutateAsync(usageKeys) - .then(() => { - showToast(intl.formatMessage(messages.successAssociateComponentMessage)); - }) - .catch(() => { - showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage)); - }); + if (collectionId) { + updateCollectionItemsMutation.mutateAsync(usageKeys) + .then(() => { + showToast(intl.formatMessage(messages.successAssociateComponentMessage)); + }) + .catch(() => { + showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage)); + }); + } else if (unitId) { + updateUnitComponentsMutation.mutateAsync(usageKeys) + .then(() => { + showToast(intl.formatMessage(messages.successAssociateComponentMessage)); + }) + .catch(() => { + showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage)); + }); + } }, [selectedComponents]); return ( @@ -76,7 +90,16 @@ export const PickLibraryContentModal: React.FC = ( size="xl" isOpen={isOpen} onClose={onClose} - footerNode={} + footerNode={( + + )} > { }); it('should add components to container', async () => { + const libraryId = 'lib:org:1'; const componentId = 'lb:org:lib:html:1'; const containerId = 'ltc:org:lib:unit:1'; const url = getLibraryContainerChildrenApiUrl(containerId); axiosMock.onPost(url).reply(200); - const { result } = renderHook(() => useAddComponentsToContainer(containerId), { wrapper }); + const { result } = renderHook(() => useAddComponentsToContainer(libraryId, containerId), { wrapper }); await result.current.mutateAsync([componentId]); expect(axiosMock.history.post[0].url).toEqual(url);