feat: enable problem bank button functionality on unit page (#1480)
This commit is contained in:
@@ -282,7 +282,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const problemButton = getByRole('button', {
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(problemButton);
|
||||
|
||||
@@ -82,6 +82,7 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'discussion',
|
||||
@@ -101,6 +102,7 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'library',
|
||||
@@ -114,12 +116,13 @@ module.exports = {
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Library Content',
|
||||
display_name: 'Legacy Library Content',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
@@ -179,6 +182,7 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'openassessment',
|
||||
@@ -230,6 +234,7 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'problem',
|
||||
@@ -249,6 +254,7 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
@@ -268,6 +274,7 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'drag-and-drop-v2',
|
||||
@@ -287,6 +294,47 @@ module.exports = {
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: false,
|
||||
},
|
||||
{
|
||||
type: 'library_v2',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Library Content',
|
||||
category: 'library_v2',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Library Content',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
type: 'itembank',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Problem Bank',
|
||||
category: 'itembank',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Problem Bank',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
beta: true,
|
||||
},
|
||||
],
|
||||
course_sequence_ids: [
|
||||
|
||||
@@ -2,13 +2,14 @@ import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { StandardModal, useToggle } from '@openedx/paragon';
|
||||
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||
import ComponentModalView from './add-component-modals/ComponentModalView';
|
||||
import AddComponentButton from './add-component-btn';
|
||||
import messages from './messages';
|
||||
import { ComponentPicker } from '../../library-authoring/component-picker';
|
||||
|
||||
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -17,6 +18,17 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
||||
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
|
||||
const { componentTemplates } = useSelector(getCourseSectionVertical);
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
|
||||
const handleLibraryV2Selection = (selection) => {
|
||||
handleCreateNewCourseXBlock({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
category: selection.blockType,
|
||||
parentLocator: blockId,
|
||||
libraryContentKey: selection.usageKey,
|
||||
});
|
||||
closeAddLibraryContentModal();
|
||||
};
|
||||
|
||||
const handleCreateNewXBlock = (type, moduleName) => {
|
||||
switch (type) {
|
||||
@@ -35,6 +47,12 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
case COMPONENT_TYPES.library:
|
||||
handleCreateNewCourseXBlock({ type, category: 'library_content', parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.itembank:
|
||||
handleCreateNewCourseXBlock({ type, category: 'itembank', parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.libraryV2:
|
||||
showAddLibraryContentModal();
|
||||
break;
|
||||
case COMPONENT_TYPES.advanced:
|
||||
handleCreateNewCourseXBlock({
|
||||
type: moduleName, category: moduleName, parentLocator: blockId,
|
||||
@@ -67,7 +85,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
|
||||
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
|
||||
{componentTemplates.map((component) => {
|
||||
const { type, displayName } = component;
|
||||
const { type, displayName, beta } = component;
|
||||
let modalParams;
|
||||
|
||||
if (!component.templates.length) {
|
||||
@@ -103,6 +121,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
onClick={() => handleCreateNewXBlock(type)}
|
||||
displayName={displayName}
|
||||
type={type}
|
||||
beta={beta}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -118,6 +137,18 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<StandardModal
|
||||
title="Select component"
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
onComponentSelected={handleLibraryV2Selection}
|
||||
/>
|
||||
</StandardModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,11 @@ let axiosMock;
|
||||
const blockId = '123';
|
||||
const handleCreateNewCourseXBlockMock = jest.fn();
|
||||
|
||||
// Mock ComponentPicker to call onComponentSelected on load
|
||||
jest.mock('../../library-authoring/component-picker', () => ({
|
||||
ComponentPicker: (props) => props.onComponentSelected({ usageKey: 'test-usage-key', blockType: 'html' }),
|
||||
}));
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
@@ -59,11 +64,19 @@ describe('<AddComponent />', () => {
|
||||
const componentTemplates = courseSectionVerticalMock.component_templates;
|
||||
|
||||
expect(getByRole('heading', { name: messages.title.defaultMessage })).toBeInTheDocument();
|
||||
Object.keys(componentTemplates).map((component) => (
|
||||
expect(getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'),
|
||||
})).toBeInTheDocument()
|
||||
));
|
||||
Object.keys(componentTemplates).forEach((component) => {
|
||||
const btn = getByRole('button', {
|
||||
name: new RegExp(
|
||||
`${componentTemplates[component].type
|
||||
} ${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`,
|
||||
'i',
|
||||
),
|
||||
});
|
||||
expect(btn).toBeInTheDocument();
|
||||
if (component.beta) {
|
||||
expect(within(btn).queryByText('Beta')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('AddComponent component doesn\'t render when there aren\'t componentTemplates', async () => {
|
||||
@@ -111,7 +124,11 @@ describe('<AddComponent />', () => {
|
||||
}
|
||||
|
||||
return expect(getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'),
|
||||
name: new RegExp(
|
||||
`${componentTemplates[component].type
|
||||
} ${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`,
|
||||
'i',
|
||||
),
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -176,7 +193,7 @@ describe('<AddComponent />', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
const discussionButton = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
name: new RegExp(`problem ${messages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(discussionButton);
|
||||
@@ -187,6 +204,22 @@ describe('<AddComponent />', () => {
|
||||
}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('calls handleCreateNewCourseXBlock with correct parameters when Problem bank xblock create button is clicked', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
const problemBankBtn = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Problem Bank`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(problemBankBtn);
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
|
||||
parentLocator: '123',
|
||||
type: COMPONENT_TYPES.itembank,
|
||||
category: 'itembank',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls handleCreateNewCourseXBlock with correct parameters when Video xblock create button is clicked', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
@@ -206,7 +239,7 @@ describe('<AddComponent />', () => {
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
const libraryButton = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Library Content`, 'i'),
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Legacy Library Content`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(libraryButton);
|
||||
@@ -379,6 +412,22 @@ describe('<AddComponent />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows library picker on clicking v2 library content btn', async () => {
|
||||
const { findByRole } = renderComponent();
|
||||
const libBtn = await findByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(libBtn);
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
|
||||
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
parentLocator: '123',
|
||||
category: 'html',
|
||||
libraryContentKey: 'test-usage-key',
|
||||
});
|
||||
});
|
||||
|
||||
describe('component support label', () => {
|
||||
it('component support label is hidden if component support legend is disabled', async () => {
|
||||
const supportLevels = ['fs', 'ps'];
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { Badge, Button } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import AddComponentIcon from './AddComponentIcon';
|
||||
|
||||
const AddComponentButton = ({ type, displayName, onClick }) => {
|
||||
const AddComponentButton = ({
|
||||
type, displayName, onClick, beta,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -17,14 +19,20 @@ const AddComponentButton = ({ type, displayName, onClick }) => {
|
||||
<AddComponentIcon type={type} />
|
||||
<span className="sr-only">{intl.formatMessage(messages.buttonText)}</span>
|
||||
<span className="small mt-2">{displayName}</span>
|
||||
{beta && <Badge className="pb-1 mt-1" variant="primary">Beta</Badge>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
AddComponentButton.defaultProps = {
|
||||
beta: false,
|
||||
};
|
||||
|
||||
AddComponentButton.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
beta: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AddComponentButton;
|
||||
|
||||
@@ -58,9 +58,11 @@ const ComponentModalView = ({
|
||||
const isDisplaySupportLabel = supportLegend.showLegend && supportLabels[componentTemplate.supportLevel];
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-between w-100 mb-2.5 align-items-end">
|
||||
<div
|
||||
key={componentTemplate.displayName}
|
||||
className="d-flex justify-content-between w-100 mb-2.5 align-items-end"
|
||||
>
|
||||
<Form.Radio
|
||||
key={componentTemplate.displayName}
|
||||
className="add-component-modal-radio"
|
||||
value={value}
|
||||
>
|
||||
|
||||
@@ -63,9 +63,16 @@ export async function getCourseSectionVerticalData(unitId) {
|
||||
* @param {string} [options.displayName] - The display name.
|
||||
* @param {string} [options.boilerplate] - The boilerplate.
|
||||
* @param {string} [options.stagedContent] - The staged content.
|
||||
* @param {string} [options.libraryContentKey] - component key from library if being imported.
|
||||
*/
|
||||
export async function createCourseXblock({
|
||||
type, category, parentLocator, displayName, boilerplate, stagedContent,
|
||||
type,
|
||||
category,
|
||||
parentLocator,
|
||||
displayName,
|
||||
boilerplate,
|
||||
stagedContent,
|
||||
libraryContentKey,
|
||||
}) {
|
||||
const body = {
|
||||
type,
|
||||
@@ -74,6 +81,7 @@ export async function createCourseXblock({
|
||||
parent_locator: parentLocator,
|
||||
display_name: displayName,
|
||||
staged_content: stagedContent,
|
||||
library_content_key: libraryContentKey,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
|
||||
17
src/generic/NewsstandIcon.tsx
Normal file
17
src/generic/NewsstandIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
// This file will be no longer needed one https://github.com/openedx/paragon/pull/3045 is merged.
|
||||
const NewsstandIcon = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 -960 960 960"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default NewsstandIcon;
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import {
|
||||
BackHand as BackHandIcon,
|
||||
BookOpen as BookOpenIcon,
|
||||
Casino as ProblemBankIcon,
|
||||
Edit as EditIcon,
|
||||
EditNote as EditNoteIcon,
|
||||
FormatListBulleted as FormatListBulletedIcon,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
VideoCamera as VideoCameraIcon,
|
||||
Folder,
|
||||
} from '@openedx/paragon/icons';
|
||||
import NewsstandIcon from '../NewsstandIcon';
|
||||
|
||||
export const UNIT_ICON_TYPES = ['video', 'other', 'vertical', 'problem', 'lock'];
|
||||
|
||||
@@ -21,6 +23,8 @@ export const COMPONENT_TYPES = {
|
||||
advanced: 'advanced',
|
||||
discussion: 'discussion',
|
||||
library: 'library',
|
||||
libraryV2: 'library_v2',
|
||||
itembank: 'itembank',
|
||||
html: 'html',
|
||||
openassessment: 'openassessment',
|
||||
problem: 'problem',
|
||||
@@ -40,6 +44,8 @@ export const COMPONENT_TYPE_ICON_MAP: Record<string, React.ComponentType> = {
|
||||
[COMPONENT_TYPES.advanced]: ScienceIcon,
|
||||
[COMPONENT_TYPES.discussion]: QuestionAnswerOutlineIcon,
|
||||
[COMPONENT_TYPES.library]: LibraryIcon,
|
||||
[COMPONENT_TYPES.itembank]: ProblemBankIcon,
|
||||
[COMPONENT_TYPES.libraryV2]: NewsstandIcon,
|
||||
[COMPONENT_TYPES.html]: TextFieldsIcon,
|
||||
[COMPONENT_TYPES.openassessment]: EditNoteIcon,
|
||||
[COMPONENT_TYPES.problem]: HelpOutlineIcon,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LibraryProvider } from './common/context';
|
||||
import { CreateCollectionModal } from './create-collection';
|
||||
import { LibraryTeamModal } from './library-team';
|
||||
import LibraryCollectionPage from './collections/LibraryCollectionPage';
|
||||
import { ComponentPickerModal } from './component-picker';
|
||||
import { ComponentPicker } from './component-picker';
|
||||
import { ComponentEditorModal } from './components/ComponentEditorModal';
|
||||
|
||||
const LibraryLayout = () => {
|
||||
@@ -32,9 +32,9 @@ const LibraryLayout = () => {
|
||||
collectionId={collectionId}
|
||||
/** The component picker modal to use. We need to pass it as a reference instead of
|
||||
* directly importing it to avoid the import cycle:
|
||||
* ComponentPickerModal > ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage >
|
||||
* Sidebar > AddContentContainer > ComponentPickerModal */
|
||||
componentPickerModal={ComponentPickerModal}
|
||||
* ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage >
|
||||
* Sidebar > AddContentContainer > ComponentPicker */
|
||||
componentPicker={ComponentPicker}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
@@ -70,7 +70,7 @@ const AddContentContainer = () => {
|
||||
collectionId,
|
||||
openCreateCollectionModal,
|
||||
openComponentEditor,
|
||||
componentPickerModal,
|
||||
componentPicker,
|
||||
} = useLibraryContext();
|
||||
const createBlockMutation = useCreateLibraryBlock();
|
||||
const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId);
|
||||
@@ -235,7 +235,7 @@ const AddContentContainer = () => {
|
||||
return (
|
||||
<Stack direction="vertical">
|
||||
{collectionId ? (
|
||||
componentPickerModal && (
|
||||
componentPicker && (
|
||||
<>
|
||||
<AddContentButton contentType={libraryContentButtonData} onCreateContent={onCreateContent} />
|
||||
<PickLibraryContentModal
|
||||
|
||||
@@ -10,7 +10,7 @@ import { studioHomeMock } from '../../studio-home/__mocks__';
|
||||
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
|
||||
import mockResult from '../__mocks__/library-search.json';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { ComponentPickerModal } from '../component-picker';
|
||||
import { ComponentPicker } from '../component-picker';
|
||||
import * as api from '../data/api';
|
||||
import {
|
||||
mockContentLibrary,
|
||||
@@ -36,7 +36,7 @@ const render = () => baseRender(<PickLibraryContentModal isOpen onClose={onClose
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
collectionId="collectionId"
|
||||
componentPickerModal={ComponentPickerModal}
|
||||
componentPicker={ComponentPicker}
|
||||
>
|
||||
{children}
|
||||
</LibraryProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Button } from '@openedx/paragon';
|
||||
import { ActionRow, Button, StandardModal } from '@openedx/paragon';
|
||||
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { type SelectedComponent, useLibraryContext } from '../common/context';
|
||||
@@ -41,14 +41,14 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
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,
|
||||
* ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage >
|
||||
* Sidebar > AddContentContainer > ComponentPicker */
|
||||
componentPicker: ComponentPicker,
|
||||
} = useLibraryContext();
|
||||
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!collectionId || !ComponentPickerModal) {
|
||||
throw new Error('libraryId and componentPickerModal are required');
|
||||
if (!collectionId || !ComponentPicker) {
|
||||
throw new Error('libraryId and componentPicker are required');
|
||||
}
|
||||
|
||||
const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId);
|
||||
@@ -70,12 +70,19 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
}, [selectedComponents]);
|
||||
|
||||
return (
|
||||
<ComponentPickerModal
|
||||
libraryId={libraryId}
|
||||
<StandardModal
|
||||
title="Select components"
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
footerNode={<PickLibraryContentModalFooter onSubmit={onSubmit} selectedComponents={selectedComponents} />}
|
||||
/>
|
||||
>
|
||||
<ComponentPicker
|
||||
libraryId={libraryId}
|
||||
componentPickerMode="multiple"
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { ComponentPickerModal } from '../component-picker';
|
||||
import type { ComponentPicker } from '../component-picker';
|
||||
import type { ContentLibrary } from '../data/api';
|
||||
import { useContentLibrary } from '../data/apiHooks';
|
||||
|
||||
@@ -27,9 +27,9 @@ type NoComponentPickerType = {
|
||||
restrictToLibrary?: never;
|
||||
/** The component picker modal to use. We need to pass it as a reference instead of
|
||||
* directly importing it to avoid the import cycle:
|
||||
* ComponentPickerModal > ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage >
|
||||
* Sidebar > AddContentContainer > ComponentPickerModal */
|
||||
componentPickerModal?: typeof ComponentPickerModal;
|
||||
* ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage >
|
||||
* Sidebar > AddContentContainer > ComponentPicker */
|
||||
componentPicker?: typeof ComponentPicker;
|
||||
};
|
||||
|
||||
type ComponentPickerSingleType = {
|
||||
@@ -39,7 +39,7 @@ type ComponentPickerSingleType = {
|
||||
addComponentToSelectedComponents?: never;
|
||||
removeComponentFromSelectedComponents?: never;
|
||||
restrictToLibrary: boolean;
|
||||
componentPickerModal?: never;
|
||||
componentPicker?: never;
|
||||
};
|
||||
|
||||
type ComponentPickerMultipleType = {
|
||||
@@ -49,7 +49,7 @@ type ComponentPickerMultipleType = {
|
||||
addComponentToSelectedComponents: ComponentSelectedEvent;
|
||||
removeComponentFromSelectedComponents: ComponentSelectedEvent;
|
||||
restrictToLibrary: boolean;
|
||||
componentPickerModal?: never;
|
||||
componentPicker?: never;
|
||||
};
|
||||
|
||||
type ComponentPickerType = NoComponentPickerType | ComponentPickerSingleType | ComponentPickerMultipleType;
|
||||
@@ -127,7 +127,7 @@ type NoComponentPickerProps = {
|
||||
onComponentSelected?: never;
|
||||
onChangeComponentSelection?: never;
|
||||
restrictToLibrary?: never;
|
||||
componentPickerModal?: typeof ComponentPickerModal;
|
||||
componentPicker?: typeof ComponentPicker;
|
||||
};
|
||||
|
||||
export type ComponentPickerSingleProps = {
|
||||
@@ -135,7 +135,7 @@ export type ComponentPickerSingleProps = {
|
||||
onComponentSelected: ComponentSelectedEvent;
|
||||
onChangeComponentSelection?: never;
|
||||
restrictToLibrary?: boolean;
|
||||
componentPickerModal?: never;
|
||||
componentPicker?: never;
|
||||
};
|
||||
|
||||
export type ComponentPickerMultipleProps = {
|
||||
@@ -143,7 +143,7 @@ export type ComponentPickerMultipleProps = {
|
||||
onComponentSelected?: never;
|
||||
onChangeComponentSelection?: ComponentSelectionChangedEvent;
|
||||
restrictToLibrary?: boolean;
|
||||
componentPickerModal?: never;
|
||||
componentPicker?: never;
|
||||
};
|
||||
|
||||
type ComponentPickerProps = NoComponentPickerProps | ComponentPickerSingleProps | ComponentPickerMultipleProps;
|
||||
@@ -156,7 +156,7 @@ type LibraryProviderProps = {
|
||||
showOnlyPublished?: boolean;
|
||||
/** Only used for testing */
|
||||
initialSidebarComponentInfo?: SidebarComponentInfo;
|
||||
componentPickerModal?: typeof ComponentPickerModal;
|
||||
componentPicker?: typeof ComponentPicker;
|
||||
} & ComponentPickerProps;
|
||||
|
||||
/**
|
||||
@@ -172,7 +172,7 @@ export const LibraryProvider = ({
|
||||
onChangeComponentSelection,
|
||||
showOnlyPublished = false,
|
||||
initialSidebarComponentInfo,
|
||||
componentPickerModal,
|
||||
componentPicker,
|
||||
}: LibraryProviderProps) => {
|
||||
const [collectionId, setCollectionId] = useState(collectionIdProp);
|
||||
const [sidebarComponentInfo, setSidebarComponentInfo] = useState<SidebarComponentInfo | undefined>(
|
||||
@@ -290,7 +290,7 @@ export const LibraryProvider = ({
|
||||
if (!componentPickerMode) {
|
||||
return {
|
||||
...contextValue,
|
||||
componentPickerModal,
|
||||
componentPicker,
|
||||
};
|
||||
}
|
||||
if (componentPickerMode === 'single') {
|
||||
@@ -343,7 +343,7 @@ export const LibraryProvider = ({
|
||||
openComponentEditor,
|
||||
closeComponentEditor,
|
||||
resetSidebarAdditionalActions,
|
||||
componentPickerModal,
|
||||
componentPicker,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,7 +37,7 @@ const defaultSelectionChangedCallback: ComponentSelectionChangedEvent = (selecti
|
||||
window.parent.postMessage({ type: 'pickerSelectionChanged', selections }, '*');
|
||||
};
|
||||
|
||||
type ComponentPickerProps = { libraryId?: string } & (
|
||||
type ComponentPickerProps = { libraryId?: string, showOnlyPublished?: boolean } & (
|
||||
{
|
||||
componentPickerMode?: 'single',
|
||||
onComponentSelected?: ComponentSelectedEvent,
|
||||
@@ -53,6 +53,7 @@ type ComponentPickerProps = { libraryId?: string } & (
|
||||
export const ComponentPicker: React.FC<ComponentPickerProps> = ({
|
||||
/** Restrict the component picker to a specific library */
|
||||
libraryId,
|
||||
showOnlyPublished,
|
||||
componentPickerMode = 'single',
|
||||
/** This default callback is used to send the selected component back to the parent window,
|
||||
* when the component picker is used in an iframe.
|
||||
@@ -67,7 +68,7 @@ export const ComponentPicker: React.FC<ComponentPickerProps> = ({
|
||||
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const variant = queryParams.get('variant') || 'draft';
|
||||
const showOnlyPublished = variant === 'published';
|
||||
const calcShowOnlyPublished = variant === 'published' || showOnlyPublished;
|
||||
|
||||
const handleLibrarySelection = (library: string) => {
|
||||
setCurrentStep('pick-components');
|
||||
@@ -102,10 +103,10 @@ export const ComponentPicker: React.FC<ComponentPickerProps> = ({
|
||||
<Stepper.Step eventKey="pick-components" title="Pick some components">
|
||||
<LibraryProvider
|
||||
libraryId={selectedLibrary}
|
||||
showOnlyPublished={showOnlyPublished}
|
||||
showOnlyPublished={calcShowOnlyPublished}
|
||||
{...libraryProviderProps}
|
||||
>
|
||||
{ showOnlyPublished
|
||||
{ calcShowOnlyPublished
|
||||
&& (
|
||||
<Alert variant="info" className="m-2">
|
||||
<FormattedMessage {...messages.pickerInfoBanner} />
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StandardModal } from '@openedx/paragon';
|
||||
|
||||
import type { ComponentSelectionChangedEvent } from '../common/context';
|
||||
import { ComponentPicker } from './ComponentPicker';
|
||||
|
||||
interface ComponentPickerModalProps {
|
||||
libraryId?: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onChangeComponentSelection: ComponentSelectionChangedEvent;
|
||||
footerNode?: React.ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
||||
libraryId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onChangeComponentSelection,
|
||||
footerNode,
|
||||
}) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardModal
|
||||
title="Select components"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
footerNode={footerNode}
|
||||
>
|
||||
<ComponentPicker
|
||||
libraryId={libraryId}
|
||||
componentPickerMode="multiple"
|
||||
onChangeComponentSelection={onChangeComponentSelection}
|
||||
/>
|
||||
</StandardModal>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { ComponentPicker } from './ComponentPicker';
|
||||
export { ComponentPickerModal } from './ComponentPickerModal';
|
||||
|
||||
Reference in New Issue
Block a user