diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx
index d29a805aaf..6097fb7f7c 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 mockAddItemsToCollection = jest.fn();
+const mockAddComponentsToContainer = jest.fn();
+jest.spyOn(api, 'addItemsToCollection').mockImplementation(mockAddItemsToCollection);
+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 mockAddItemsToCollection = jest.fn();
- jest.spyOn(api, 'addItemsToCollection').mockImplementation(mockAddItemsToCollection);
-
- 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(mockAddItemsToCollection).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(mockAddItemsToCollection).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 mockAddItemsToCollection = jest.fn().mockRejectedValue(new Error('Failed to add components'));
- jest.spyOn(api, 'addItemsToCollection').mockImplementation(mockAddItemsToCollection);
- 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(mockAddItemsToCollection).toHaveBeenCalledWith(
- libraryId,
- 'collectionId',
- ['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'],
- );
+ it(`show error when api call fails (${context})`, async () => {
+ if (context === 'collection') {
+ mockAddItemsToCollection.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(mockAddItemsToCollection).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 176dba8ba7..f71f40d081 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 { useAddItemsToCollection } from '../data/apiHooks';
+import { useAddItemsToCollection, useAddComponentsToContainer } from '../data/apiHooks';
import messages from './messages';
interface PickLibraryContentModalFooterProps {
onSubmit: () => void;
selectedComponents: SelectedComponent[];
+ buttonText: React.ReactNode;
}
const PickLibraryContentModalFooter: React.FC = ({
onSubmit,
selectedComponents,
+ buttonText,
}) => (
);
@@ -29,17 +31,20 @@ const PickLibraryContentModalFooter: React.FC void;
+ extraFilter?: string[];
}
export const PickLibraryContentModal: React.FC = ({
isOpen,
onClose,
+ extraFilter,
}) => {
const intl = useIntl();
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 +52,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 = useAddItemsToCollection(libraryId, collectionId);
+ const updateCollectionItemsMutation = useAddItemsToCollection(libraryId, collectionId);
+ const updateUnitComponentsMutation = useAddComponentsToContainer(unitId);
const { showToast } = useContext(ToastContext);
@@ -60,13 +66,24 @@ 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));
+ });
+ }
+ if (unitId) {
+ updateUnitComponentsMutation.mutateAsync(usageKeys)
+ .then(() => {
+ showToast(intl.formatMessage(messages.successAssociateComponentMessage));
+ })
+ .catch(() => {
+ showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage));
+ });
+ }
}, [selectedComponents]);
return (
@@ -76,12 +93,22 @@ export const PickLibraryContentModal: React.FC = (
size="xl"
isOpen={isOpen}
onClose={onClose}
- footerNode={}
+ footerNode={(
+
+ )}
>
);
diff --git a/src/library-authoring/add-content/index.ts b/src/library-authoring/add-content/index.ts
index a3bd82be41..9dd2de3456 100644
--- a/src/library-authoring/add-content/index.ts
+++ b/src/library-authoring/add-content/index.ts
@@ -1,2 +1,3 @@
export { default as AddContent } from './AddContent';
export { default as AddContentHeader } from './AddContentHeader';
+export { PickLibraryContentModal } from './PickLibraryContentModal';
diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts
index c42146aa49..120b5896fb 100644
--- a/src/library-authoring/add-content/messages.ts
+++ b/src/library-authoring/add-content/messages.ts
@@ -21,6 +21,11 @@ const messages = defineMessages({
defaultMessage: 'Add to Collection',
description: 'Button to add library content to a collection.',
},
+ addToUnitButton: {
+ id: 'course-authoring.library-authoring.add-content.buttons.library-content.add-to-unit',
+ defaultMessage: 'Add to Unit',
+ description: 'Button to add library content to a unit.',
+ },
selectedComponents: {
id: 'course-authoring.library-authoring.add-content.selected-components',
defaultMessage: '{count, plural, one {# Selected Component} other {# Selected Components}}',
diff --git a/src/library-authoring/containers/UnitInfo.tsx b/src/library-authoring/containers/UnitInfo.tsx
index a6385009a2..a991f3d163 100644
--- a/src/library-authoring/containers/UnitInfo.tsx
+++ b/src/library-authoring/containers/UnitInfo.tsx
@@ -10,6 +10,7 @@ import {
useToggle,
} from '@openedx/paragon';
import { useEffect, useCallback } from 'react';
+import { Link } from 'react-router-dom';
import { MoreVert } from '@openedx/paragon/icons';
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
@@ -70,7 +71,7 @@ const UnitMenu = ({ containerId, displayName }: ContainerMenuProps) => {
const UnitInfo = () => {
const intl = useIntl();
- const { setUnitId } = useLibraryContext();
+ const { libraryId } = useLibraryContext();
const { componentPickerMode } = useComponentPickerContext();
const {
defaultTab,
@@ -81,7 +82,7 @@ const UnitInfo = () => {
sidebarAction,
} = useSidebarContext();
const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections;
- const { insideUnit, navigateTo } = useLibraryRoutes();
+ const { insideUnit } = useLibraryRoutes();
const tab: UnitInfoTab = (
sidebarTab && isUnitInfoTab(sidebarTab)
@@ -90,15 +91,7 @@ const UnitInfo = () => {
const unitId = sidebarComponentInfo?.id;
const { data: container } = useContainer(unitId);
- const handleOpenUnit = useCallback(() => {
- if (componentPickerMode) {
- setUnitId(unitId);
- } else {
- navigateTo({ unitId });
- }
- }, [componentPickerMode, navigateTo, unitId]);
-
- const showOpenUnitButton = !insideUnit || componentPickerMode;
+ const showOpenUnitButton = !insideUnit && !componentPickerMode;
const renderTab = useCallback((infoTab: UnitInfoTab, component: React.ReactNode, title: string) => {
if (hiddenTabs.includes(infoTab)) {
@@ -130,7 +123,8 @@ const UnitInfo = () => {
@@ -147,7 +141,7 @@ const UnitInfo = () => {
activeKey={tab}
onSelect={setSidebarTab}
>
- {renderTab(UNIT_INFO_TABS.Preview, , intl.formatMessage(messages.previewTabTitle))}
+ {renderTab(UNIT_INFO_TABS.Preview, , intl.formatMessage(messages.previewTabTitle))}
{renderTab(UNIT_INFO_TABS.Organize, , intl.formatMessage(messages.organizeTabTitle))}
{renderTab(UNIT_INFO_TABS.Settings, 'Unit Settings', intl.formatMessage(messages.settingsTabTitle))}
diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx
index 4e8b261e6a..f1873fb08b 100644
--- a/src/library-authoring/units/LibraryUnitBlocks.tsx
+++ b/src/library-authoring/units/LibraryUnitBlocks.tsx
@@ -17,6 +17,7 @@ import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
import Loading from '../../generic/Loading';
import TagCount from '../../generic/tag-count';
import { useLibraryContext } from '../common/context/LibraryContext';
+import { PickLibraryContentModal } from '../add-content';
import ComponentMenu from '../components';
import { LibraryBlockMetadata } from '../data/api';
import { libraryAuthoringQueryKeys, useContainerChildren } from '../data/apiHooks';
@@ -34,10 +35,16 @@ const LARGE_COMPONENTS = [
'lti_consumer',
];
-export const LibraryUnitBlocks = () => {
+interface LibraryUnitBlocksProps {
+ preview?: boolean;
+}
+
+export const LibraryUnitBlocks = ({ preview = false }: LibraryUnitBlocksProps) => {
const intl = useIntl();
const [orderedBlocks, setOrderedBlocks] = useState([]);
const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false);
+ const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
+
const { navigateTo } = useLibraryRoutes();
const {
@@ -153,31 +160,39 @@ export const LibraryUnitBlocks = () => {
{renderedBlocks}
-
-
-
-
-
-
+ { !preview && (
+
+
+
+
+
+
+
+
-
+ )}