feat: Add unit from library into course (#1829)

* feat: Initial worflow to add unit to course
* test: Add initial tests
* feat: Show only published units
* test: Update Subsection card test and ComponentPicker tests
* feat: Connect add unit from library API
* test: Test for Add unit from library in CourseOutline
* fix: create a new Vertical from a Library Unit
* docs: add a little note about avoiding 'vertical' where possible
* refactor: Use visibleTabs instead of showOnlyHomeTab

---------

Co-authored-by: Jillian Vogel <jill@opencraft.com>
Co-authored-by: Braden MacDonald <braden@opencraft.com>
This commit is contained in:
Chris Chávez
2025-04-24 14:07:54 -05:00
committed by GitHub
parent 1fe1f93314
commit d6b51ecf0c
12 changed files with 372 additions and 94 deletions

View File

@@ -103,6 +103,7 @@ const CourseOutline = ({ courseId }) => {
handleNewSectionSubmit,
handleNewSubsectionSubmit,
handleNewUnitSubmit,
handleAddUnitFromLibrary,
getUnitUrl,
handleVideoSharingOptionChange,
handlePasteClipboardClick,
@@ -383,6 +384,7 @@ const CourseOutline = ({ courseId }) => {
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
onOpenConfigureModal={openConfigureModal}
onNewUnitSubmit={handleNewUnitSubmit}
onAddUnitFromLibrary={handleAddUnitFromLibrary}
onOrderChange={updateSubsectionOrderByIndex}
onPasteClick={handlePasteClipboardClick}
>

View File

@@ -60,11 +60,14 @@ import {
moveSubsection,
moveUnit,
} from './drag-helper/utils';
import { postXBlockBaseApiUrl } from '../course-unit/data/api';
import { COMPONENT_TYPES } from '../generic/block-type-utils/constants';
let axiosMock;
let store;
const mockPathname = '/foo-bar';
const courseId = '123';
const containerKey = 'lct:org:lib:unit:1';
window.HTMLElement.prototype.scrollIntoView = jest.fn();
@@ -94,6 +97,30 @@ jest.mock('./data/api', () => ({
getTagsCount: () => jest.fn().mockResolvedValue({}),
}));
jest.mock('../studio-home/hooks', () => ({
useStudioHome: () => ({
librariesV2Enabled: true,
}),
}));
// Mock ComponentPicker to call onComponentSelected on click
jest.mock('../library-authoring/component-picker', () => ({
ComponentPicker: (props) => {
const onClick = () => {
// eslint-disable-next-line react/prop-types
props.onComponentSelected({
usageKey: containerKey,
blockType: 'unti',
});
};
return (
<button type="submit" onClick={onClick}>
Dummy button
</button>
);
},
}));
const queryClient = new QueryClient();
jest.mock('@dnd-kit/core', () => ({
@@ -390,6 +417,42 @@ describe('<CourseOutline />', () => {
}));
});
it('adds a unit from library correctly', async () => {
render(<RootWrapper />);
const [sectionElement] = await screen.findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const units = await within(subsectionElement).findAllByTestId('unit-card');
expect(units.length).toBe(1);
axiosMock
.onPost(postXBlockBaseApiUrl())
.reply(200, {
locator: 'some',
});
const addUnitFromLibraryButton = within(subsectionElement).getByRole('button', {
name: /use unit from library/i,
});
fireEvent.click(addUnitFromLibraryButton);
// click dummy button to execute onComponentSelected prop.
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
fireEvent.click(dummyBtn);
waitFor(() => expect(axiosMock.history.post.length).toBe(1));
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [subsection] = section.childInfo.children;
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
type: COMPONENT_TYPES.libraryV2,
category: 'vertical',
parent_locator: subsection.id,
library_content_key: containerKey,
}));
});
it('render checklist value correctly', async () => {
const { getByText } = render(<RootWrapper />);

View File

@@ -53,6 +53,7 @@ import {
setPasteFileNotices,
updateCourseLaunchQueryStatus,
} from './slice';
import { createCourseXblock } from '../../course-unit/data/api';
export function fetchCourseOutlineIndexQuery(courseId) {
return async (dispatch) => {
@@ -540,6 +541,26 @@ export function addNewUnitQuery(parentLocator, callback) {
};
}
export function addUnitFromLibrary(body, callback) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try {
await createCourseXblock(body).then(async (result) => {
if (result) {
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(hideProcessingNotification());
callback(result.locator);
}
});
} catch (error) /* istanbul ignore next */ {
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
}
function setBlockOrderListQuery(
parentId,
blockIds,

View File

@@ -53,6 +53,7 @@ import {
setUnitOrderListQuery,
pasteClipboardContent,
dismissNotificationQuery,
addUnitFromLibrary,
} from './data/thunk';
const useCourseOutline = ({ courseId }) => {
@@ -128,6 +129,10 @@ const useCourseOutline = ({ courseId }) => {
dispatch(addNewUnitQuery(subsectionId, openUnitPage));
};
const handleAddUnitFromLibrary = (body) => {
dispatch(addUnitFromLibrary(body, openUnitPage));
};
const headerNavigationsActions = {
handleNewSection: handleNewSectionSubmit,
handleReIndex: () => {
@@ -336,6 +341,7 @@ const useCourseOutline = ({ courseId }) => {
getUnitUrl,
openUnitPage,
handleNewUnitSubmit,
handleAddUnitFromLibrary,
handleVideoSharingOptionChange,
handlePasteClipboardClick,
notificationDismissUrl,

View File

@@ -1,12 +1,12 @@
// @ts-check
import React, {
useContext, useEffect, useState, useRef,
useContext, useEffect, useState, useRef, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, useToggle } from '@openedx/paragon';
import { Button, StandardModal, useToggle } from '@openedx/paragon';
import { Add as IconAdd } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
@@ -22,6 +22,11 @@ import TitleButton from '../card-header/TitleButton';
import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import messages from './messages';
import { ComponentPicker } from '../../library-authoring';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import { ContainerType } from '../../generic/key-utils';
import { useStudioHome } from '../../studio-home/hooks';
import { ContentType } from '../../library-authoring/routes';
const SubsectionCard = ({
section,
@@ -37,6 +42,7 @@ const SubsectionCard = ({
onOpenDeleteModal,
onDuplicateSubmit,
onNewUnitSubmit,
onAddUnitFromLibrary,
onOrderChange,
onOpenConfigureModal,
onPasteClick,
@@ -51,6 +57,12 @@ const SubsectionCard = ({
const [isFormOpen, openForm, closeForm] = useToggle(false);
const namePrefix = 'subsection';
const { sharedClipboardData, showPasteUnit } = useClipboard();
const { librariesV2Enabled } = useStudioHome();
const [
isAddLibraryUnitModalOpen,
openAddLibraryUnitModal,
closeAddLibraryUnitModal,
] = useToggle(false);
const {
id,
@@ -172,90 +184,129 @@ const SubsectionCard = ({
&& !(isHeaderVisible === false)
);
const handleSelectLibraryUnit = useCallback((selectedUnit) => {
onAddUnitFromLibrary({
type: COMPONENT_TYPES.libraryV2,
category: ContainerType.Vertical,
parentLocator: id,
libraryContentKey: selectedUnit.usageKey,
});
closeAddLibraryUnitModal();
}, []);
return (
<SortableItem
id={id}
category={category}
key={id}
isDraggable={isDraggable}
isDroppable={actions.childAddable}
componentStyle={{
background: '#f8f7f6',
...borderStyle,
}}
>
<div
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
data-testid="subsection-card"
ref={currentRef}
<>
<SortableItem
id={id}
category={category}
key={id}
isDraggable={isDraggable}
isDroppable={actions.childAddable}
componentStyle={{
background: '#f8f7f6',
...borderStyle,
}}
>
{isHeaderVisible && (
<>
<CardHeader
title={displayName}
status={subsectionStatus}
cardId={id}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
onClickMoveUp={handleSubsectionMoveUp}
onClickMoveDown={handleSubsectionMoveDown}
onClickConfigure={onOpenConfigureModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
isSequential
extraActionsComponent={extraActionsComponent}
/>
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={subsection}
<div
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
data-testid="subsection-card"
ref={currentRef}
>
{isHeaderVisible && (
<>
<CardHeader
title={displayName}
status={subsectionStatus}
cardId={id}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
onClickMoveUp={handleSubsectionMoveUp}
onClickMoveDown={handleSubsectionMoveDown}
onClickConfigure={onOpenConfigureModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
isSequential
extraActionsComponent={extraActionsComponent}
/>
</div>
</>
)}
{isExpanded && (
<div
data-testid="subsection-card__units"
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
>
{children}
{actions.childAddable && (
<>
<Button
data-testid="new-unit-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (
<PasteComponent
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={subsection}
/>
</div>
</>
)}
{isExpanded && (
<div
data-testid="subsection-card__units"
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
>
{children}
{actions.childAddable && (
<>
<Button
data-testid="new-unit-button"
className="mt-4"
text={intl.formatMessage(messages.pasteButton)}
clipboardData={sharedClipboardData}
onClick={handlePasteButtonClick}
/>
)}
</>
)}
</div>
)}
</div>
</SortableItem>
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (
<PasteComponent
className="mt-4"
text={intl.formatMessage(messages.pasteButton)}
clipboardData={sharedClipboardData}
onClick={handlePasteButtonClick}
/>
)}
{librariesV2Enabled && (
<Button
data-testid="use-unit-from-library"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={openAddLibraryUnitModal}
>
{intl.formatMessage(messages.useUnitFromLibraryButton)}
</Button>
)}
</>
)}
</div>
)}
</div>
</SortableItem>
<StandardModal
title={intl.formatMessage(messages.unitPickerModalTitle)}
isOpen={isAddLibraryUnitModalOpen}
onClose={closeAddLibraryUnitModal}
isOverflowVisible={false}
size="xl"
>
<ComponentPicker
showOnlyPublished
extraFilter={['block_type = "unit"']}
componentPickerMode="single"
onComponentSelected={handleSelectLibraryUnit}
visibleTabs={[ContentType.units]}
/>
</StandardModal>
</>
);
};
@@ -306,6 +357,7 @@ SubsectionCard.propTypes = {
onOpenDeleteModal: PropTypes.func.isRequired,
onDuplicateSubmit: PropTypes.func.isRequired,
onNewUnitSubmit: PropTypes.func.isRequired,
onAddUnitFromLibrary: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
getPossibleMoves: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired,

View File

@@ -1,6 +1,6 @@
import { MemoryRouter } from 'react-router-dom';
import {
act, render, fireEvent, within,
act, render, fireEvent, within, screen,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -10,9 +10,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import initializeStore from '../../store';
import SubsectionCard from './SubsectionCard';
import cardHeaderMessages from '../card-header/messages';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
let store;
const mockPathname = '/foo-bar';
const containerKey = 'lct:org:lib:unit:1';
const handleOnAddUnitFromLibrary = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -21,6 +24,30 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('../../studio-home/hooks', () => ({
useStudioHome: () => ({
librariesV2Enabled: true,
}),
}));
// Mock ComponentPicker to call onComponentSelected on click
jest.mock('../../library-authoring/component-picker', () => ({
ComponentPicker: (props) => {
const onClick = () => {
// eslint-disable-next-line react/prop-types
props.onComponentSelected({
usageKey: containerKey,
blockType: 'unti',
});
};
return (
<button type="submit" onClick={onClick}>
Dummy button
</button>
);
},
}));
const unit = {
id: 'unit-1',
};
@@ -80,6 +107,7 @@ const renderComponent = (props, entry = '/') => render(
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
onNewUnitSubmit={jest.fn()}
onAddUnitFromLibrary={handleOnAddUnitFromLibrary}
isCustomRelativeDatesActive={false}
onEditClick={jest.fn()}
savingStatus=""
@@ -247,4 +275,31 @@ describe('<SubsectionCard />', () => {
expect(cardUnits).toBeNull();
expect(newUnitButton).toBeNull();
});
it('should add unit from library', async () => {
renderComponent();
const expandButton = await screen.findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandButton);
const useUnitFromLibraryButton = screen.getByRole('button', {
name: /use unit from library/i,
});
expect(useUnitFromLibraryButton).toBeInTheDocument();
fireEvent.click(useUnitFromLibraryButton);
expect(await screen.findByText('Select unit'));
// click dummy button to execute onComponentSelected prop.
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
fireEvent.click(dummyBtn);
expect(handleOnAddUnitFromLibrary).toHaveBeenCalled();
expect(handleOnAddUnitFromLibrary).toHaveBeenCalledWith({
type: COMPONENT_TYPES.libraryV2,
parentLocator: '123',
category: 'vertical',
libraryContentKey: containerKey,
});
});
});

View File

@@ -4,10 +4,22 @@ const messages = defineMessages({
newUnitButton: {
id: 'course-authoring.course-outline.subsection.button.new-unit',
defaultMessage: 'New unit',
description: 'Message of the button to create a new unit in a subsection.',
},
pasteButton: {
id: 'course-authoring.course-outline.subsection.button.paste-unit',
defaultMessage: 'Paste unit',
description: 'Message of the button to paste a new unit in a subsection.',
},
useUnitFromLibraryButton: {
id: 'course-authoring.course-outline.subsection.button.use-unit-from-library',
defaultMessage: 'Use unit from library',
description: 'Message of the button to add a new unit from a library in a subsection.',
},
unitPickerModalTitle: {
id: 'course-authoring.course-outline.subsection.unit.modal.single-title.text',
defaultMessage: 'Select unit',
description: 'Library unit picker modal title.',
},
});

View File

@@ -52,6 +52,15 @@ export const buildCollectionUsageKey = (learningContextKey: string, collectionId
export enum ContainerType {
Unit = 'unit',
/**
* Vertical is the old name for Unit. Generally, **please avoid using this term entirely in any libraries code** or
* anything based on the new Learning Core "Containers" framework - just call it a unit. We do still need to use this
* in the modulestore-based courseware, and currently the /xblock/ API used to copy library containers into courses
* also requires specifying this, though that should change to a better API that does the unit->vertical conversion
* automatically in the future.
* TODO: we should probably move this to a separate enum/mapping, and keep this for the new container types only.
*/
Vertical = 'vertical',
}
/**

View File

@@ -43,7 +43,7 @@ import { LibrarySidebar } from './library-sidebar';
import { useComponentPickerContext } from './common/context/ComponentPickerContext';
import { useLibraryContext } from './common/context/LibraryContext';
import { SidebarBodyComponentId, useSidebarContext } from './common/context/SidebarContext';
import { ContentType, useLibraryRoutes } from './routes';
import { allLibraryPageTabs, ContentType, useLibraryRoutes } from './routes';
import messages from './messages';
@@ -129,9 +129,13 @@ export const SubHeaderTitle = ({ title }: { title: ReactNode }) => {
interface LibraryAuthoringPageProps {
returnToLibrarySelection?: () => void,
visibleTabs?: ContentType[],
}
const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPageProps) => {
const LibraryAuthoringPage = ({
returnToLibrarySelection,
visibleTabs = allLibraryPageTabs,
}: LibraryAuthoringPageProps) => {
const intl = useIntl();
const {
@@ -163,7 +167,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
// The activeKey determines the currently selected tab.
const getActiveKey = () => {
if (componentPickerMode) {
return ContentType.home;
return visibleTabs[0];
}
if (insideCollections) {
return ContentType.collections;
@@ -245,6 +249,16 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
// Disable filtering by block/problem type when viewing the Collections tab.
const overrideTypesFilter = (insideCollections || insideUnits) ? new TypesFilterData() : undefined;
const tabTitles = {
[ContentType.home]: intl.formatMessage(messages.homeTab),
[ContentType.collections]: intl.formatMessage(messages.collectionsTab),
[ContentType.components]: intl.formatMessage(messages.componentsTab),
[ContentType.units]: intl.formatMessage(messages.unitsTab),
};
const visibleTabsToRender = visibleTabs.map((contentType) => (
<Tab eventKey={contentType} title={tabTitles[contentType]} />
));
return (
<div className="d-flex">
<div className="flex-grow-1">
@@ -279,10 +293,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
onSelect={handleTabChange}
className="my-3"
>
<Tab eventKey={ContentType.home} title={intl.formatMessage(messages.homeTab)} />
<Tab eventKey={ContentType.collections} title={intl.formatMessage(messages.collectionsTab)} />
<Tab eventKey={ContentType.components} title={intl.formatMessage(messages.componentsTab)} />
<Tab eventKey={ContentType.units} title={intl.formatMessage(messages.unitsTab)} />
{visibleTabsToRender}
</Tabs>
<ActionRow className="my-3">
<SearchKeywordsField className="mr-3" />

View File

@@ -17,6 +17,7 @@ import {
} from '../data/api.mocks';
import { ComponentPicker } from './ComponentPicker';
import { ContentType } from '../routes';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -276,4 +277,29 @@ describe('<ComponentPicker />', () => {
// Wait for the content library to load
await screen.findByText(/Only published content is visible and available for reuse./i);
});
it('should display all tabs', async () => {
// Default `visibleTabs = allLibraryPageTabs`
render(<ComponentPicker />);
expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i));
expect(await screen.findByRole('tab', { name: /all content/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /collections/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /components/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /units/i })).toBeInTheDocument();
});
it('should display only unit tab', async () => {
render(<ComponentPicker visibleTabs={[ContentType.units]} />);
expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i));
expect(await screen.findByRole('tab', { name: /units/i })).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /all content/i })).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /collections/i })).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /components/i })).not.toBeInTheDocument();
});
});

View File

@@ -14,18 +14,28 @@ import LibraryAuthoringPage from '../LibraryAuthoringPage';
import LibraryCollectionPage from '../collections/LibraryCollectionPage';
import SelectLibrary from './SelectLibrary';
import messages from './messages';
import { ContentType, allLibraryPageTabs } from '../routes';
interface LibraryComponentPickerProps {
returnToLibrarySelection: () => void;
visibleTabs: ContentType[],
}
const InnerComponentPicker: React.FC<LibraryComponentPickerProps> = ({ returnToLibrarySelection }) => {
const InnerComponentPicker: React.FC<LibraryComponentPickerProps> = ({
returnToLibrarySelection,
visibleTabs,
}) => {
const { collectionId } = useLibraryContext();
if (collectionId) {
return <LibraryCollectionPage />;
}
return <LibraryAuthoringPage returnToLibrarySelection={returnToLibrarySelection} />;
return (
<LibraryAuthoringPage
returnToLibrarySelection={returnToLibrarySelection}
visibleTabs={visibleTabs}
/>
);
};
/** Default handler in single-select mode. Used by the legacy UI for adding a single selected component to a course. */
@@ -38,7 +48,12 @@ const defaultSelectionChangedCallback: ComponentSelectionChangedEvent = (selecti
window.parent.postMessage({ type: 'pickerSelectionChanged', selections }, '*');
};
type ComponentPickerProps = { libraryId?: string, showOnlyPublished?: boolean, extraFilter?: string[] } & (
type ComponentPickerProps = {
libraryId?: string,
showOnlyPublished?: boolean,
extraFilter?: string[],
visibleTabs?: ContentType[],
} & (
{
componentPickerMode?: 'single',
onComponentSelected?: ComponentSelectedEvent,
@@ -56,6 +71,7 @@ export const ComponentPicker: React.FC<ComponentPickerProps> = ({
showOnlyPublished,
extraFilter,
componentPickerMode = 'single',
visibleTabs = allLibraryPageTabs,
/** This default callback is used to send the selected component back to the parent window,
* when the component picker is used in an iframe.
*/
@@ -116,7 +132,10 @@ export const ComponentPicker: React.FC<ComponentPickerProps> = ({
<FormattedMessage {...messages.pickerInfoBanner} />
</Alert>
)}
<InnerComponentPicker returnToLibrarySelection={returnToLibrarySelection} />
<InnerComponentPicker
returnToLibrarySelection={returnToLibrarySelection}
visibleTabs={visibleTabs}
/>
</SidebarProvider>
</LibraryProvider>
</ComponentPickerProvider>

View File

@@ -41,6 +41,8 @@ export enum ContentType {
units = 'units',
}
export const allLibraryPageTabs: ContentType[] = Object.values(ContentType);
export type NavigateToData = {
componentId?: string,
collectionId?: string,