Skip to content

Commit

Permalink
feat: add library v2 component in course (#75)
Browse files Browse the repository at this point in the history
* feat: add library v2 component in course

* test: add test for lib v2 component

* test: fix failing tests

* refactor: remove ComponentPickerModal component

Replace with standard modal and component picker components.
  • Loading branch information
navinkarkera authored Nov 14, 2024
1 parent f8e00da commit b6ee727
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 83 deletions.
2 changes: 1 addition & 1 deletion src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 28 additions & 1 deletion src/course-unit/add-component/AddComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -38,6 +50,9 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
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,
Expand Down Expand Up @@ -122,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>
);
};
Expand Down
35 changes: 32 additions & 3 deletions src/course-unit/add-component/AddComponent.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -61,7 +66,11 @@ describe('<AddComponent />', () => {
expect(getByRole('heading', { name: messages.title.defaultMessage })).toBeInTheDocument();
Object.keys(componentTemplates).forEach((component) => {
const btn = 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',
),
});
expect(btn).toBeInTheDocument();
if (component.beta) {
Expand Down Expand Up @@ -115,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();
});
});
Expand Down Expand Up @@ -180,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);
Expand Down Expand Up @@ -399,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'];
Expand Down
10 changes: 9 additions & 1 deletion src/course-unit/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down
8 changes: 4 additions & 4 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/library-authoring/add-content/AddContentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const AddContentContainer = () => {
collectionId,
openCreateCollectionModal,
openComponentEditor,
componentPickerModal,
componentPicker,
} = useLibraryContext();
const createBlockMutation = useCreateLibraryBlock();
const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId);
Expand Down Expand Up @@ -239,7 +239,7 @@ const AddContentContainer = () => {
return (
<Stack direction="vertical">
{collectionId ? (
componentPickerModal && (
componentPicker && (
<>
<AddContentButton contentType={libraryContentButtonData} onCreateContent={onCreateContent} />
<PickLibraryContentModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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,
Expand Down Expand Up @@ -40,7 +40,7 @@ const render = () => baseRender(<PickLibraryContentModal isOpen onClose={onClose
<LibraryProvider
libraryId={libraryId}
collectionId="collectionId"
componentPickerModal={ComponentPickerModal}
componentPicker={ComponentPicker}
>
{children}
</LibraryProvider>
Expand Down
27 changes: 17 additions & 10 deletions src/library-authoring/add-content/PickLibraryContentModal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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>
);
};
26 changes: 13 additions & 13 deletions src/library-authoring/common/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 = {
Expand All @@ -39,7 +39,7 @@ type ComponentPickerSingleType = {
addComponentToSelectedComponents?: never;
removeComponentFromSelectedComponents?: never;
restrictToLibrary: boolean;
componentPickerModal?: never;
componentPicker?: never;
};

type ComponentPickerMultipleType = {
Expand All @@ -49,7 +49,7 @@ type ComponentPickerMultipleType = {
addComponentToSelectedComponents: ComponentSelectedEvent;
removeComponentFromSelectedComponents: ComponentSelectedEvent;
restrictToLibrary: boolean;
componentPickerModal?: never;
componentPicker?: never;
};

type ComponentPickerType = NoComponentPickerType | ComponentPickerSingleType | ComponentPickerMultipleType;
Expand Down Expand Up @@ -121,23 +121,23 @@ type NoComponentPickerProps = {
onComponentSelected?: never;
onChangeComponentSelection?: never;
restrictToLibrary?: never;
componentPickerModal?: typeof ComponentPickerModal;
componentPicker?: typeof ComponentPicker;
};

export type ComponentPickerSingleProps = {
componentPickerMode: 'single';
onComponentSelected: ComponentSelectedEvent;
onChangeComponentSelection?: never;
restrictToLibrary?: boolean;
componentPickerModal?: never;
componentPicker?: never;
};

export type ComponentPickerMultipleProps = {
componentPickerMode: 'multiple';
onComponentSelected?: never;
onChangeComponentSelection?: ComponentSelectionChangedEvent;
restrictToLibrary?: boolean;
componentPickerModal?: never;
componentPicker?: never;
};

type ComponentPickerProps = NoComponentPickerProps | ComponentPickerSingleProps | ComponentPickerMultipleProps;
Expand All @@ -150,7 +150,7 @@ type LibraryProviderProps = {
showOnlyPublished?: boolean;
/** Only used for testing */
initialSidebarComponentInfo?: SidebarComponentInfo;
componentPickerModal?: typeof ComponentPickerModal;
componentPicker?: typeof ComponentPicker;
} & ComponentPickerProps;

/**
Expand All @@ -166,7 +166,7 @@ export const LibraryProvider = ({
onChangeComponentSelection,
showOnlyPublished = false,
initialSidebarComponentInfo,
componentPickerModal,
componentPicker,
}: LibraryProviderProps) => {
const [collectionId, setCollectionId] = useState(collectionIdProp);
const [sidebarComponentInfo, setSidebarComponentInfo] = useState<SidebarComponentInfo | undefined>(
Expand Down Expand Up @@ -276,7 +276,7 @@ export const LibraryProvider = ({
if (!componentPickerMode) {
return {
...contextValue,
componentPickerModal,
componentPicker,
};
}
if (componentPickerMode === 'single') {
Expand Down Expand Up @@ -329,7 +329,7 @@ export const LibraryProvider = ({
openComponentEditor,
closeComponentEditor,
resetSidebarAdditionalActions,
componentPickerModal,
componentPicker,
]);

return (
Expand Down
Loading

0 comments on commit b6ee727

Please sign in to comment.