-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add existing content to a collection [FC-0062] (#1416)
Allows library components to be added to a collection using the add-content sidebar. For the Libraries Relaunch Beta. For: #1173
- Loading branch information
Showing
13 changed files
with
351 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
src/library-authoring/add-content/PickLibraryContentModal.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/data/api.mock'; | ||
import { | ||
fireEvent, | ||
render as baseRender, | ||
waitFor, | ||
screen, | ||
initializeMocks, | ||
} from '../../testUtils'; | ||
import mockResult from '../__mocks__/library-search.json'; | ||
import { LibraryProvider } from '../common/context'; | ||
import { ComponentPickerModal } from '../component-picker'; | ||
import * as api from '../data/api'; | ||
import { | ||
mockContentLibrary, | ||
mockGetCollectionMetadata, | ||
} from '../data/api.mocks'; | ||
import { PickLibraryContentModal } from './PickLibraryContentModal'; | ||
|
||
initializeMocks(); | ||
mockContentSearchConfig.applyMock(); | ||
mockContentLibrary.applyMock(); | ||
mockGetCollectionMetadata.applyMock(); | ||
mockSearchResult(mockResult); | ||
|
||
const { libraryId } = mockContentLibrary; | ||
|
||
const onClose = jest.fn(); | ||
let mockShowToast: (message: string) => void; | ||
|
||
const render = () => baseRender(<PickLibraryContentModal isOpen onClose={onClose} />, { | ||
path: '/library/:libraryId/collection/:collectionId/*', | ||
params: { libraryId, collectionId: 'collectionId' }, | ||
extraWrapper: ({ children }) => ( | ||
<LibraryProvider | ||
libraryId={libraryId} | ||
collectionId="collectionId" | ||
componentPickerModal={ComponentPickerModal} | ||
> | ||
{children} | ||
</LibraryProvider> | ||
), | ||
}); | ||
|
||
describe('<PickLibraryContentModal />', () => { | ||
beforeEach(() => { | ||
const mocks = initializeMocks(); | ||
mockShowToast = mocks.mockShowToast; | ||
}); | ||
|
||
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'], | ||
); | ||
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'], | ||
); | ||
expect(onClose).toHaveBeenCalled(); | ||
expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this collection.'); | ||
}); | ||
}); | ||
}); |
81 changes: 81 additions & 0 deletions
81
src/library-authoring/add-content/PickLibraryContentModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React, { useCallback, useContext, useState } from 'react'; | ||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; | ||
import { ActionRow, Button } from '@openedx/paragon'; | ||
|
||
import { ToastContext } from '../../generic/toast-context'; | ||
import { type SelectedComponent, useLibraryContext } from '../common/context'; | ||
import { useAddComponentsToCollection } from '../data/apiHooks'; | ||
import messages from './messages'; | ||
|
||
interface PickLibraryContentModalFooterProps { | ||
onSubmit: () => void; | ||
selectedComponents: SelectedComponent[]; | ||
} | ||
|
||
const PickLibraryContentModalFooter: React.FC<PickLibraryContentModalFooterProps> = ({ | ||
onSubmit, | ||
selectedComponents, | ||
}) => ( | ||
<ActionRow> | ||
<FormattedMessage {...messages.selectedComponents} values={{ count: selectedComponents.length }} /> | ||
<ActionRow.Spacer /> | ||
<Button variant="primary" onClick={onSubmit}> | ||
<FormattedMessage {...messages.addToCollectionButton} /> | ||
</Button> | ||
</ActionRow> | ||
); | ||
|
||
interface PickLibraryContentModalProps { | ||
isOpen: boolean; | ||
onClose: () => void; | ||
} | ||
|
||
// eslint-disable-next-line import/prefer-default-export | ||
export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = ({ | ||
isOpen, | ||
onClose, | ||
}) => { | ||
const intl = useIntl(); | ||
|
||
const { | ||
libraryId, | ||
collectionId, | ||
/** We need to get it as a reference instead of directly importing it to avoid the import cycle: | ||
* ComponentPickerModal > ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > | ||
* Sidebar > AddContentContainer > ComponentPickerModal */ | ||
componentPickerModal: ComponentPickerModal, | ||
} = useLibraryContext(); | ||
|
||
// istanbul ignore if: this should never happen | ||
if (!collectionId || !ComponentPickerModal) { | ||
throw new Error('libraryId and componentPickerModal are required'); | ||
} | ||
|
||
const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId); | ||
|
||
const { showToast } = useContext(ToastContext); | ||
|
||
const [selectedComponents, setSelectedComponents] = useState<SelectedComponent[]>([]); | ||
|
||
const onSubmit = useCallback(() => { | ||
const usageKeys = selectedComponents.map(({ usageKey }) => usageKey); | ||
onClose(); | ||
updateComponentsMutation.mutateAsync(usageKeys) | ||
.then(() => { | ||
showToast(intl.formatMessage(messages.successAssociateComponentMessage)); | ||
}) | ||
.catch(() => { | ||
showToast(intl.formatMessage(messages.errorAssociateComponentMessage)); | ||
}); | ||
}, [selectedComponents]); | ||
|
||
return ( | ||
<ComponentPickerModal | ||
libraryId={libraryId} | ||
isOpen={isOpen} | ||
onClose={onClose} | ||
onChangeComponentSelection={setSelectedComponents} | ||
footerNode={<PickLibraryContentModalFooter onSubmit={onSubmit} selectedComponents={selectedComponents} />} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
// eslint-disable-next-line import/prefer-default-export | ||
export { default as AddContentContainer } from './AddContentContainer'; | ||
export { default as AddContentHeader } from './AddContentHeader'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.