Add subsection/unit to section/subsection full-page view within library [FC-0090] (#2152)
* Removes the "content type" tab bar from the "Add Existing Component/Unit/Subsection" modal, and in all cases where only one type of content is shown for selection. * Updates the section/subsection sidebar to show "Existing Library Content" + "New Subsection" / "New Unit" buttons. * Updates the "Add New Unit/Subsection" buttons to directly launch the new container modal, instead of going via the container sidebar. * Ensures that whenever a subsection/unit is created from within a section/subsection, that it is linked to the parent section/subsection after created.
This commit is contained in:
@@ -158,7 +158,9 @@ const LibraryAuthoringPage = ({
|
||||
insideCollections,
|
||||
insideComponents,
|
||||
insideUnits,
|
||||
insideSection,
|
||||
insideSections,
|
||||
insideSubsection,
|
||||
insideSubsections,
|
||||
navigateTo,
|
||||
} = useLibraryRoutes();
|
||||
@@ -251,8 +253,12 @@ const LibraryAuthoringPage = ({
|
||||
extraFilter.push(activeTypeFilters[activeKey]);
|
||||
}
|
||||
|
||||
// Disable filtering by block/problem type when viewing the Collections/Units/Sections/Subsections tab.
|
||||
const onlyOneType = (insideCollections || insideUnits || insideSections || insideSubsections);
|
||||
// Disable filtering by block/problem type when viewing the Collections/Units/Sections/Subsections tab,
|
||||
// or when inside a specific Section or Subsection.
|
||||
const onlyOneType = (
|
||||
insideCollections || insideUnits || insideSections || insideSubsections
|
||||
|| insideSection || insideSubsection
|
||||
);
|
||||
const overrideTypesFilter = onlyOneType
|
||||
? new TypesFilterData()
|
||||
: undefined;
|
||||
@@ -298,14 +304,16 @@ const LibraryAuthoringPage = ({
|
||||
headerActions={<HeaderActions />}
|
||||
hideBorder
|
||||
/>
|
||||
<Tabs
|
||||
variant="tabs"
|
||||
activeKey={activeKey}
|
||||
onSelect={handleTabChange}
|
||||
className="my-3"
|
||||
>
|
||||
{visibleTabsToRender}
|
||||
</Tabs>
|
||||
{visibleTabs.length > 1 && (
|
||||
<Tabs
|
||||
variant="tabs"
|
||||
activeKey={activeKey}
|
||||
onSelect={handleTabChange}
|
||||
className="my-3"
|
||||
>
|
||||
{visibleTabsToRender}
|
||||
</Tabs>
|
||||
)}
|
||||
<ActionRow className="my-3">
|
||||
<SearchKeywordsField className="mr-3" />
|
||||
<FilterByTags />
|
||||
|
||||
@@ -90,6 +90,7 @@ describe('<AddContent />', () => {
|
||||
expect(screen.queryByRole('button', { name: /video/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument();
|
||||
expect(await screen.queryByRole('button', { name: /existing library content/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render advanced content buttons', async () => {
|
||||
@@ -331,6 +332,7 @@ describe('<AddContent />', () => {
|
||||
const unitId = 'lct:orf1:lib1:unit:test-1';
|
||||
renderWithContainer(unitId);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /existing library content/i })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument();
|
||||
@@ -396,12 +398,16 @@ describe('<AddContent />', () => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this container.');
|
||||
});
|
||||
|
||||
it('should only show subsection button when inside a section', async () => {
|
||||
it('should only show subsection buttons when inside a section', async () => {
|
||||
mockClipboardEmpty.applyMock();
|
||||
const sectionId = 'lct:orf1:lib1:section:test-1';
|
||||
renderWithContainer(sectionId, 'section');
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Subsection' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /existing subsection/i })).toBeInTheDocument();
|
||||
|
||||
// Button is labeled "New Subsection" , not "Subsection"
|
||||
expect(await screen.findByRole('button', { name: /new subsection/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Subsection' })).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument();
|
||||
@@ -409,12 +415,15 @@ describe('<AddContent />', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Text' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should only show unit button when inside a subsection', async () => {
|
||||
it('should only show unit buttons when inside a subsection', async () => {
|
||||
mockClipboardEmpty.applyMock();
|
||||
const subsectionId = 'lct:orf1:lib1:subsection:test-1';
|
||||
renderWithContainer(subsectionId, 'subsection');
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Unit' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /existing unit/i })).toBeInTheDocument();
|
||||
// Button is labeled "New Unit" in this context, not just "Unit"
|
||||
expect(await screen.findByRole('button', { name: /new unit/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Subsection' })).not.toBeInTheDocument();
|
||||
|
||||
@@ -31,13 +31,13 @@ import { blockTypes } from '../../editors/data/constants/app';
|
||||
|
||||
import { useLibraryRoutes } from '../routes';
|
||||
import genericMessages from '../generic/messages';
|
||||
import messages from './messages';
|
||||
import { messages, getContentMessages } from './messages';
|
||||
import type { BlockTypeMetadata } from '../data/api';
|
||||
import { ContainerType } from '../../generic/key-utils';
|
||||
|
||||
type ContentType = {
|
||||
name: string,
|
||||
disabled: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.ComponentType,
|
||||
blockType: string,
|
||||
};
|
||||
@@ -64,7 +64,7 @@ type AddAdvancedContentViewProps = {
|
||||
const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonProps) => {
|
||||
const {
|
||||
name,
|
||||
disabled,
|
||||
disabled = false,
|
||||
icon,
|
||||
blockType,
|
||||
} = contentType;
|
||||
@@ -96,97 +96,132 @@ const AddContentView = ({
|
||||
insideSubsection,
|
||||
} = useLibraryRoutes();
|
||||
|
||||
const collectionButtonData = {
|
||||
name: intl.formatMessage(messages.collectionButton),
|
||||
disabled: false,
|
||||
blockType: 'collection',
|
||||
};
|
||||
const contentMessages = useMemo(() => (
|
||||
getContentMessages(insideSection, insideSubsection, insideUnit)
|
||||
), [insideSection, insideSubsection, insideUnit]);
|
||||
|
||||
const unitButtonData = {
|
||||
name: intl.formatMessage(messages.unitButton),
|
||||
disabled: false,
|
||||
blockType: 'vertical',
|
||||
};
|
||||
const collectionButton = (
|
||||
<AddContentButton
|
||||
key="collection"
|
||||
contentType={{
|
||||
name: intl.formatMessage(contentMessages.collectionButton),
|
||||
blockType: 'collection',
|
||||
}}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
);
|
||||
|
||||
const sectionButtonData = {
|
||||
name: intl.formatMessage(messages.sectionButton),
|
||||
disabled: false,
|
||||
blockType: 'chapter',
|
||||
};
|
||||
const unitButton = (
|
||||
<AddContentButton
|
||||
key="unit"
|
||||
contentType={{
|
||||
name: intl.formatMessage(contentMessages.unitButton),
|
||||
blockType: 'unit',
|
||||
}}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
);
|
||||
|
||||
const subsectionButtonData = {
|
||||
name: intl.formatMessage(messages.subsectionButton),
|
||||
disabled: false,
|
||||
blockType: 'sequential',
|
||||
};
|
||||
const sectionButton = (
|
||||
<AddContentButton
|
||||
key="section"
|
||||
contentType={{
|
||||
name: intl.formatMessage(contentMessages.sectionButton),
|
||||
blockType: 'section',
|
||||
}}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
);
|
||||
|
||||
const libraryContentButtonData = {
|
||||
name: intl.formatMessage(messages.libraryContentButton),
|
||||
disabled: false,
|
||||
blockType: 'libraryContent',
|
||||
};
|
||||
const subsectionButton = (
|
||||
<AddContentButton
|
||||
key="subsection"
|
||||
contentType={{
|
||||
name: intl.formatMessage(contentMessages.subsectionButton),
|
||||
blockType: 'subsection',
|
||||
}}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
);
|
||||
|
||||
/** List container content types that should be displayed based on current path */
|
||||
const visibleContentTypes = useMemo(() => {
|
||||
const existingContentButton = (
|
||||
<AddContentButton
|
||||
key="libraryContent"
|
||||
contentType={{
|
||||
name: intl.formatMessage(contentMessages.libraryContentButton),
|
||||
blockType: 'libraryContent',
|
||||
}}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
);
|
||||
|
||||
/* Note: for MVP we are hiding the unsupported types, not just disabling them. */
|
||||
const componentButtons = contentTypes.filter(ct => !ct.disabled).map((contentType) => (
|
||||
<AddContentButton
|
||||
key={`add-content-${contentType.blockType}`}
|
||||
contentType={contentType}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
));
|
||||
const separator = (
|
||||
<hr className="w-100 bg-gray-500" />
|
||||
);
|
||||
|
||||
/** List buttons that should be displayed based on current path */
|
||||
const visibleButtons = useMemo(() => {
|
||||
if (insideCollection) {
|
||||
// except for add collection button, show everthing.
|
||||
// except for add collection button, show everything.
|
||||
return [
|
||||
libraryContentButtonData,
|
||||
sectionButtonData,
|
||||
subsectionButtonData,
|
||||
unitButtonData,
|
||||
existingContentButton,
|
||||
sectionButton,
|
||||
subsectionButton,
|
||||
unitButton,
|
||||
separator,
|
||||
...componentButtons,
|
||||
];
|
||||
}
|
||||
if (insideUnit) {
|
||||
// Only show libraryContentButton
|
||||
return [libraryContentButtonData];
|
||||
// Only show existing content button + component buttons
|
||||
return [
|
||||
existingContentButton,
|
||||
separator,
|
||||
...componentButtons,
|
||||
];
|
||||
}
|
||||
if (insideSection) {
|
||||
// Should only allow adding subsections
|
||||
return [subsectionButtonData];
|
||||
// Only allow adding subsections
|
||||
return [
|
||||
existingContentButton,
|
||||
subsectionButton,
|
||||
];
|
||||
}
|
||||
if (insideSubsection) {
|
||||
// Should only allow adding units
|
||||
return [unitButtonData];
|
||||
// Only allow adding units
|
||||
return [
|
||||
existingContentButton,
|
||||
unitButton,
|
||||
];
|
||||
}
|
||||
// except for libraryContentButton, show everthing.
|
||||
// except for existing content, show everything.
|
||||
return [
|
||||
collectionButtonData,
|
||||
sectionButtonData,
|
||||
subsectionButtonData,
|
||||
unitButtonData,
|
||||
collectionButton,
|
||||
sectionButton,
|
||||
subsectionButton,
|
||||
unitButton,
|
||||
separator,
|
||||
...componentButtons,
|
||||
];
|
||||
}, [insideCollection, insideUnit, insideSection, insideSubsection]);
|
||||
}, [componentButtons, insideCollection, insideUnit, insideSection, insideSubsection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visibleContentTypes.map((contentType) => (
|
||||
<AddContentButton
|
||||
key={contentType.blockType}
|
||||
contentType={contentType}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
))}
|
||||
{componentPicker && visibleContentTypes.includes(libraryContentButtonData) && (
|
||||
/// Show the "Add Library Content" button for units and collections
|
||||
{visibleButtons}
|
||||
{componentPicker && visibleButtons.includes(existingContentButton) && (
|
||||
<PickLibraryContentModal
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
/>
|
||||
)}
|
||||
{(!insideSection && !insideSubsection) && (
|
||||
<>
|
||||
<hr className="w-100 bg-gray-500" />
|
||||
{/* Note: for MVP we are hiding the unuspported types, not just disabling them. */}
|
||||
{contentTypes.filter(ct => !ct.disabled).map((contentType) => (
|
||||
<AddContentButton
|
||||
key={`add-content-${contentType.blockType}`}
|
||||
contentType={contentType}
|
||||
onCreateContent={onCreateContent}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -423,9 +458,9 @@ const AddContent = () => {
|
||||
} else if (blockType === 'advancedXBlock') {
|
||||
showAdvancedList();
|
||||
} else if ([
|
||||
ContainerType.Vertical,
|
||||
ContainerType.Chapter,
|
||||
ContainerType.Sequential,
|
||||
ContainerType.Unit,
|
||||
ContainerType.Subsection,
|
||||
ContainerType.Section,
|
||||
].includes(blockType as ContainerType)) {
|
||||
setCreateContainerModalType(blockType as ContainerType);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { messages } from './messages';
|
||||
|
||||
const AddContentHeader = () => (
|
||||
<span className="font-weight-bold m-1.5">
|
||||
|
||||
@@ -81,12 +81,24 @@ describe('<PickLibraryContentModal />', () => {
|
||||
});
|
||||
|
||||
[
|
||||
'collection' as const,
|
||||
'unit' as const,
|
||||
'section' as const,
|
||||
'subsection' as const,
|
||||
].forEach((context) => {
|
||||
it(`can pick components from the modal (${context})`, async () => {
|
||||
{
|
||||
context: 'collection' as const,
|
||||
addType: 'component',
|
||||
},
|
||||
{
|
||||
context: 'unit' as const,
|
||||
addType: 'component',
|
||||
},
|
||||
{
|
||||
context: 'subsection' as const,
|
||||
addType: 'unit',
|
||||
},
|
||||
{
|
||||
context: 'section' as const,
|
||||
addType: 'subsection',
|
||||
},
|
||||
].forEach(({ context, addType }) => {
|
||||
it(`can pick content from the modal (${context})`, async () => {
|
||||
render(context);
|
||||
|
||||
// Wait for the content library to load
|
||||
@@ -95,11 +107,16 @@ describe('<PickLibraryContentModal />', () => {
|
||||
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();
|
||||
expect(screen.getByText(`Select ${addType}s`)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /add to .*/i }));
|
||||
// Select and add the first item
|
||||
fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[0]);
|
||||
expect(await screen.findByText(
|
||||
new RegExp(`1 selected ${addType}`, 'i'),
|
||||
)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', {
|
||||
name: new RegExp(`add to ${context}`, 'i'),
|
||||
}));
|
||||
|
||||
await waitFor(() => {
|
||||
switch (context) {
|
||||
@@ -143,11 +160,15 @@ describe('<PickLibraryContentModal />', () => {
|
||||
expect(screen.queryAllByText('Introduction to Testing')[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Select the first component
|
||||
// Select and add the first item
|
||||
fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[0]);
|
||||
expect(await screen.findByText('1 Selected Component')).toBeInTheDocument();
|
||||
expect(await screen.findByText(
|
||||
new RegExp(`1 selected ${addType}`, 'i'),
|
||||
)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /add to .*/i }));
|
||||
fireEvent.click(screen.getByRole('button', {
|
||||
name: new RegExp(`add to ${context}`, 'i'),
|
||||
}));
|
||||
|
||||
await waitFor(() => {
|
||||
switch (context) {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Button, StandardModal } from '@openedx/paragon';
|
||||
|
||||
@@ -8,27 +13,7 @@ import type { SelectedComponent } from '../common/context/ComponentPickerContext
|
||||
import { useAddItemsToCollection, useAddItemsToContainer } from '../data/apiHooks';
|
||||
import genericMessages from '../generic/messages';
|
||||
import { allLibraryPageTabs, ContentType, useLibraryRoutes } from '../routes';
|
||||
import messages from './messages';
|
||||
|
||||
interface PickLibraryContentModalFooterProps {
|
||||
onSubmit: () => void;
|
||||
selectedComponents: SelectedComponent[];
|
||||
buttonText: React.ReactNode;
|
||||
}
|
||||
|
||||
const PickLibraryContentModalFooter: React.FC<PickLibraryContentModalFooterProps> = ({
|
||||
onSubmit,
|
||||
selectedComponents,
|
||||
buttonText,
|
||||
}) => (
|
||||
<ActionRow>
|
||||
<FormattedMessage {...messages.selectedComponents} values={{ count: selectedComponents.length }} />
|
||||
<ActionRow.Spacer />
|
||||
<Button variant="primary" onClick={onSubmit}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
);
|
||||
import { messages, getContentMessages } from './messages';
|
||||
|
||||
interface PickLibraryContentModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -57,10 +42,14 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
|
||||
const { showToast } = useContext(ToastContext);
|
||||
|
||||
const [selectedComponents, setSelectedComponents] = useState<SelectedComponent[]>([]);
|
||||
const contentMessages = useMemo(() => (
|
||||
getContentMessages(insideSection, insideSubsection, insideUnit)
|
||||
), [insideSection, insideSubsection, insideUnit]);
|
||||
|
||||
const [selectedContent, setSelectedComponents] = useState<SelectedComponent[]>([]);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
const usageKeys = selectedComponents.map(({ usageKey }) => usageKey);
|
||||
const usageKeys = selectedContent.map(({ usageKey }) => usageKey);
|
||||
onClose();
|
||||
if (insideCollection && collectionId) {
|
||||
updateCollectionItemsMutation.mutateAsync(usageKeys)
|
||||
@@ -80,7 +69,7 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
});
|
||||
}
|
||||
}, [
|
||||
selectedComponents,
|
||||
selectedContent,
|
||||
insideSection,
|
||||
insideSubsection,
|
||||
insideUnit,
|
||||
@@ -91,16 +80,13 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
// determine filter an visibleTabs based on current location
|
||||
let extraFilter = ['NOT type = "collection"'];
|
||||
let visibleTabs = allLibraryPageTabs.filter((tab) => tab !== ContentType.collections);
|
||||
let addBtnText = messages.addToCollectionButton;
|
||||
if (insideSection) {
|
||||
// show only subsections
|
||||
extraFilter = ['block_type = "subsection"'];
|
||||
addBtnText = messages.addToSectionButton;
|
||||
visibleTabs = [ContentType.subsections];
|
||||
} else if (insideSubsection) {
|
||||
// show only units
|
||||
extraFilter = ['block_type = "unit"'];
|
||||
addBtnText = messages.addToSubsectionButton;
|
||||
visibleTabs = [ContentType.units];
|
||||
} else if (insideUnit) {
|
||||
// show only components
|
||||
@@ -109,7 +95,6 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
'NOT block_type = "subsection"',
|
||||
'NOT block_type = "section"',
|
||||
];
|
||||
addBtnText = messages.addToUnitButton;
|
||||
visibleTabs = [ContentType.components];
|
||||
}
|
||||
|
||||
@@ -120,17 +105,22 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
|
||||
return (
|
||||
<StandardModal
|
||||
title="Select components"
|
||||
title={intl.formatMessage(contentMessages.selectContentTitle)}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
footerNode={(
|
||||
<PickLibraryContentModalFooter
|
||||
onSubmit={onSubmit}
|
||||
selectedComponents={selectedComponents}
|
||||
buttonText={intl.formatMessage(addBtnText)}
|
||||
/>
|
||||
<ActionRow>
|
||||
<FormattedMessage
|
||||
{...contentMessages.selectedContent}
|
||||
values={{ count: selectedContent.length }}
|
||||
/>
|
||||
<ActionRow.Spacer />
|
||||
<Button variant="primary" onClick={onSubmit}>
|
||||
{intl.formatMessage(contentMessages.addToButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<ComponentPicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
export const messages = defineMessages({
|
||||
collectionButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.collection',
|
||||
defaultMessage: 'Collection',
|
||||
@@ -24,29 +24,19 @@ const messages = defineMessages({
|
||||
libraryContentButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-content',
|
||||
defaultMessage: 'Existing Library Content',
|
||||
description: 'Content of button to add existing library content to a collection.',
|
||||
description: 'Content of button to add existing library content to a collection or container.',
|
||||
},
|
||||
addToCollectionButton: {
|
||||
addToButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-content.add-to-collection',
|
||||
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.',
|
||||
selectContentTitle: {
|
||||
id: 'course-authoring.library-authoring.add-content.select-components',
|
||||
defaultMessage: 'Select components',
|
||||
description: 'Title for the content picker when selecting components in library.',
|
||||
},
|
||||
addToSectionButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-content.add-to-section',
|
||||
defaultMessage: 'Add to Section',
|
||||
description: 'Button to add library content to a section.',
|
||||
},
|
||||
addToSubsectionButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-content.add-to-subsection',
|
||||
defaultMessage: 'Add to Subsection',
|
||||
description: 'Button to add library content to a subsection.',
|
||||
},
|
||||
selectedComponents: {
|
||||
selectedContent: {
|
||||
id: 'course-authoring.library-authoring.add-content.selected-components',
|
||||
defaultMessage: '{count, plural, one {# Selected Component} other {# Selected Components}}',
|
||||
description: 'Title for selected components in library.',
|
||||
@@ -154,4 +144,97 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
export const unitMessages = defineMessages({
|
||||
addToButton: {
|
||||
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.',
|
||||
},
|
||||
});
|
||||
|
||||
export const subsectionMessages = defineMessages({
|
||||
unitButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.new-unit',
|
||||
defaultMessage: 'New Unit',
|
||||
description: 'Content of button to create a new Unit in a Subsection.',
|
||||
},
|
||||
libraryContentButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-unit',
|
||||
defaultMessage: 'Existing Unit',
|
||||
description: 'Content of button to add an existing Unit to a Subsection.',
|
||||
},
|
||||
addToButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-content.add-to-subsection',
|
||||
defaultMessage: 'Add to Subsection',
|
||||
description: 'Button to add Units to a Subsection.',
|
||||
},
|
||||
selectContentTitle: {
|
||||
id: 'course-authoring.library-authoring.add-content.select-units',
|
||||
defaultMessage: 'Select units',
|
||||
description: 'Title for the content picker when selecting units in library.',
|
||||
},
|
||||
selectedContent: {
|
||||
id: 'course-authoring.library-authoring.add-content.selected-units',
|
||||
defaultMessage: '{count, plural, one {# Selected Unit} other {# Selected Units}}',
|
||||
description: 'Title for selected units in library.',
|
||||
},
|
||||
});
|
||||
|
||||
export const sectionMessages = defineMessages({
|
||||
subsectionButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.new-subsection',
|
||||
defaultMessage: 'New Subsection',
|
||||
description: 'Content of button to create a new Subsection in a Section.',
|
||||
},
|
||||
libraryContentButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-subsection',
|
||||
defaultMessage: 'Existing Subsection',
|
||||
description: 'Content of button to add an existing Subsection to a Section.',
|
||||
},
|
||||
addToButton: {
|
||||
id: 'course-authoring.library-authoring.add-content.buttons.library-content.add-to-section',
|
||||
defaultMessage: 'Add to Section',
|
||||
description: 'Button to add library content to a section.',
|
||||
},
|
||||
selectContentTitle: {
|
||||
id: 'course-authoring.library-authoring.add-content.select-subsections',
|
||||
defaultMessage: 'Select subsections',
|
||||
description: 'Title for the content picker when selecting subsections in library.',
|
||||
},
|
||||
selectedContent: {
|
||||
id: 'course-authoring.library-authoring.add-content.selected-subsections',
|
||||
defaultMessage: '{count, plural, one {# Selected Subsections} other {# Selected Subsections}}',
|
||||
description: 'Title for selected subsections in library.',
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* Returns the appropriate message set for the given route conditions.
|
||||
*/
|
||||
export const getContentMessages = (
|
||||
insideSection: boolean,
|
||||
insideSubsection: boolean,
|
||||
insideUnit: boolean,
|
||||
) => {
|
||||
if (insideSection) {
|
||||
return {
|
||||
...messages,
|
||||
...sectionMessages,
|
||||
};
|
||||
}
|
||||
if (insideSubsection) {
|
||||
return {
|
||||
...messages,
|
||||
...subsectionMessages,
|
||||
};
|
||||
}
|
||||
if (insideUnit) {
|
||||
return {
|
||||
...messages,
|
||||
...unitMessages,
|
||||
};
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -377,16 +377,15 @@ describe('<ComponentPicker />', () => {
|
||||
expect(screen.getByRole('tab', { name: /units/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display only unit tab', async () => {
|
||||
it('should display only units', 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();
|
||||
expect(await screen.findByText('Published Test Unit')).toBeInTheDocument();
|
||||
// No tabs shown when only one tab is visible
|
||||
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display never published filter', async () => {
|
||||
|
||||
@@ -120,10 +120,10 @@ describe('<ContainerInfo />', () => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes');
|
||||
});
|
||||
|
||||
testIf(containerType === 'Unit')(`show only published ${containerType} content`, async () => {
|
||||
it(`show only published ${containerType} content`, async () => {
|
||||
render(containerId, true);
|
||||
expect(await screen.findByTestId('container-info-menu-toggle')).toBeInTheDocument();
|
||||
expect(screen.getByText(/text block published 1/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/block published 1/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`shows the ${containerType} Preview tab by default and the children are readonly`, async () => {
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { Button, useToggle } from '@openedx/paragon';
|
||||
import { Add } from '@openedx/paragon/icons';
|
||||
import { type ContainerType } from '../../generic/key-utils';
|
||||
import { PickLibraryContentModal } from '../add-content';
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
|
||||
interface FooterActionsProps {
|
||||
addContentType?: ContainerType;
|
||||
addContentBtnText: string;
|
||||
addExistingContentBtnText: string;
|
||||
}
|
||||
|
||||
export const FooterActions = ({
|
||||
addContentType,
|
||||
addContentBtnText,
|
||||
addExistingContentBtnText,
|
||||
}: FooterActionsProps) => {
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
const { openAddContentSidebar } = useSidebarContext();
|
||||
const { readOnly } = useLibraryContext();
|
||||
const { readOnly, setCreateContainerModalType } = useLibraryContext();
|
||||
const addContent = () => {
|
||||
if (addContentType) {
|
||||
setCreateContainerModalType(addContentType);
|
||||
} else {
|
||||
openAddContentSidebar();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className="w-100 mr-2">
|
||||
@@ -23,7 +33,7 @@ export const FooterActions = ({
|
||||
className="ml-2"
|
||||
iconBefore={Add}
|
||||
variant="outline-primary rounded-0"
|
||||
onClick={openAddContentSidebar}
|
||||
onClick={addContent}
|
||||
disabled={readOnly}
|
||||
block
|
||||
>
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('CreateContainerModal container linking', () => {
|
||||
{ containerId: newSectionId, routeType: 'section' },
|
||||
);
|
||||
|
||||
const subsectionButton = await screen.findByRole('button', { name: /^Subsection$/ });
|
||||
const subsectionButton = await screen.findByRole('button', { name: /New subsection/i });
|
||||
userEvent.click(subsectionButton);
|
||||
const nameInput = await screen.findByLabelText(/name your subsection/i);
|
||||
userEvent.type(nameInput, 'Test Subsection');
|
||||
|
||||
@@ -39,7 +39,7 @@ const CreateContainerModal = () => {
|
||||
|
||||
/** labels based on the type of modal open, i.e., section, subsection or unit */
|
||||
const labels = React.useMemo(() => {
|
||||
if (createContainerModalType === ContainerType.Chapter) {
|
||||
if (createContainerModalType === ContainerType.Section) {
|
||||
return {
|
||||
modalTitle: intl.formatMessage(messages.createSectionModalTitle),
|
||||
validationError: intl.formatMessage(messages.createSectionModalNameInvalid),
|
||||
@@ -49,7 +49,7 @@ const CreateContainerModal = () => {
|
||||
errorMsg: intl.formatMessage(messages.createSectionError),
|
||||
};
|
||||
}
|
||||
if (createContainerModalType === ContainerType.Sequential) {
|
||||
if (createContainerModalType === ContainerType.Subsection) {
|
||||
return {
|
||||
modalTitle: intl.formatMessage(messages.createSubsectionModalTitle),
|
||||
validationError: intl.formatMessage(messages.createSubsectionModalNameInvalid),
|
||||
@@ -72,40 +72,41 @@ const CreateContainerModal = () => {
|
||||
/** Call close for section, subsection and unit as the operation is idempotent */
|
||||
const handleClose = () => setCreateContainerModalType(undefined);
|
||||
|
||||
/** Calculate containerType based on type of open modal */
|
||||
const containerType = React.useMemo(() => {
|
||||
if (createContainerModalType === ContainerType.Chapter) {
|
||||
return ContainerType.Section;
|
||||
}
|
||||
if (createContainerModalType === ContainerType.Sequential) {
|
||||
return ContainerType.Subsection;
|
||||
}
|
||||
return ContainerType.Unit;
|
||||
}, [createContainerModalType]);
|
||||
|
||||
const handleCreate = React.useCallback(async (values) => {
|
||||
try {
|
||||
const canStandAlone = !(insideCollection || insideSection || insideSubsection);
|
||||
const container = await create.mutateAsync({
|
||||
canStandAlone,
|
||||
containerType,
|
||||
containerType: createContainerModalType,
|
||||
...values,
|
||||
});
|
||||
// link container to parent
|
||||
|
||||
// Navigate to the new container
|
||||
navigateTo({ containerId: container.id });
|
||||
|
||||
// Link container to parent after navigating -- we may still show an
|
||||
// error if this linking fails, but at least the user can see that
|
||||
// the container was created.
|
||||
if (collectionId && insideCollection) {
|
||||
await updateCollectionItemsMutation.mutateAsync([container.id]);
|
||||
} else if (containerId && (insideSection || insideSubsection)) {
|
||||
await updateContainerItemsMutation.mutateAsync([container.id]);
|
||||
}
|
||||
// Navigate to the new container
|
||||
navigateTo({ containerId: container.id });
|
||||
|
||||
showToast(labels.successMsg);
|
||||
} catch (error) {
|
||||
showToast(labels.errorMsg);
|
||||
} finally {
|
||||
handleClose();
|
||||
}
|
||||
}, [containerType, labels, handleClose, navigateTo]);
|
||||
}, [
|
||||
createContainerModalType,
|
||||
handleClose,
|
||||
labels,
|
||||
navigateTo,
|
||||
updateCollectionItemsMutation,
|
||||
updateContainerItemsMutation,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
type PathMatch,
|
||||
} from 'react-router-dom';
|
||||
import { ContainerType, getBlockType } from '../generic/key-utils';
|
||||
|
||||
@@ -62,15 +61,15 @@ export type NavigateToData = {
|
||||
};
|
||||
|
||||
export type LibraryRoutesData = {
|
||||
insideCollection: PathMatch<string> | null;
|
||||
insideCollections: PathMatch<string> | null;
|
||||
insideComponents: PathMatch<string> | null;
|
||||
insideSections: PathMatch<string> | null;
|
||||
insideSection: PathMatch<string> | null;
|
||||
insideSubsections: PathMatch<string> | null;
|
||||
insideSubsection: PathMatch<string> | null;
|
||||
insideUnits: PathMatch<string> | null;
|
||||
insideUnit: PathMatch<string> | null;
|
||||
insideCollection: boolean;
|
||||
insideCollections: boolean;
|
||||
insideComponents: boolean;
|
||||
insideSections: boolean;
|
||||
insideSection: boolean;
|
||||
insideSubsections: boolean;
|
||||
insideSubsection: boolean;
|
||||
insideUnits: boolean;
|
||||
insideUnit: boolean;
|
||||
|
||||
/** Navigate using the best route from the current location for the given parameters.
|
||||
* This function can be mutated if there are changes in the current route, so always include
|
||||
@@ -85,15 +84,17 @@ export const useLibraryRoutes = (): LibraryRoutesData => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const insideCollection = matchPath(BASE_ROUTE + ROUTES.COLLECTION, pathname);
|
||||
const insideCollections = matchPath(BASE_ROUTE + ROUTES.COLLECTIONS, pathname);
|
||||
const insideComponents = matchPath(BASE_ROUTE + ROUTES.COMPONENTS, pathname);
|
||||
const insideSections = matchPath(BASE_ROUTE + ROUTES.SECTIONS, pathname);
|
||||
const insideSection = matchPath(BASE_ROUTE + ROUTES.SECTION, pathname);
|
||||
const insideSubsections = matchPath(BASE_ROUTE + ROUTES.SUBSECTIONS, pathname);
|
||||
const insideSubsection = matchPath(BASE_ROUTE + ROUTES.SUBSECTION, pathname);
|
||||
const insideUnits = matchPath(BASE_ROUTE + ROUTES.UNITS, pathname);
|
||||
const insideUnit = matchPath(BASE_ROUTE + ROUTES.UNIT, pathname);
|
||||
// Convert the returned PathMatch<string> | null values to PathMatch<string> | false
|
||||
// to make it easier to return them as booleans below.
|
||||
const insideCollection = matchPath(BASE_ROUTE + ROUTES.COLLECTION, pathname) || false;
|
||||
const insideCollections = matchPath(BASE_ROUTE + ROUTES.COLLECTIONS, pathname) || false;
|
||||
const insideComponents = matchPath(BASE_ROUTE + ROUTES.COMPONENTS, pathname) || false;
|
||||
const insideSections = matchPath(BASE_ROUTE + ROUTES.SECTIONS, pathname) || false;
|
||||
const insideSection = matchPath(BASE_ROUTE + ROUTES.SECTION, pathname) || false;
|
||||
const insideSubsections = matchPath(BASE_ROUTE + ROUTES.SUBSECTIONS, pathname) || false;
|
||||
const insideSubsection = matchPath(BASE_ROUTE + ROUTES.SUBSECTION, pathname) || false;
|
||||
const insideUnits = matchPath(BASE_ROUTE + ROUTES.UNITS, pathname) || false;
|
||||
const insideUnit = matchPath(BASE_ROUTE + ROUTES.UNIT, pathname) || false;
|
||||
|
||||
// Sanity check to ensure that we are not inside more than one route at the same time.
|
||||
// istanbul ignore if: this is a developer error, not a user error.
|
||||
@@ -108,7 +109,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => {
|
||||
insideSubsection,
|
||||
insideUnits,
|
||||
insideUnit,
|
||||
].filter((match): match is PathMatch<string> => match !== null).length > 1) {
|
||||
].filter((match) => match).length > 1) {
|
||||
throw new Error('Cannot be inside more than one route at the same time.');
|
||||
}
|
||||
|
||||
@@ -247,15 +248,15 @@ export const useLibraryRoutes = (): LibraryRoutesData => {
|
||||
|
||||
return useMemo(() => ({
|
||||
navigateTo,
|
||||
insideCollection,
|
||||
insideCollections,
|
||||
insideComponents,
|
||||
insideSections,
|
||||
insideSection,
|
||||
insideSubsections,
|
||||
insideSubsection,
|
||||
insideUnits,
|
||||
insideUnit,
|
||||
insideCollection: !!insideCollection,
|
||||
insideCollections: !!insideCollections,
|
||||
insideComponents: !!insideComponents,
|
||||
insideSections: !!insideSections,
|
||||
insideSection: !!insideSection,
|
||||
insideSubsections: !!insideSubsections,
|
||||
insideSubsection: !!insideSubsection,
|
||||
insideUnits: !!insideUnits,
|
||||
insideUnit: !!insideUnit,
|
||||
}), [
|
||||
navigateTo,
|
||||
insideCollection,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useContainer, useContentLibrary } from '../data/apiHooks';
|
||||
import Loading from '../../generic/Loading';
|
||||
import NotFoundAlert from '../../generic/NotFoundAlert';
|
||||
import ErrorAlert from '../../generic/alert-error';
|
||||
import { ContainerType } from '../../generic/key-utils';
|
||||
import Header from '../../header';
|
||||
import SubHeader from '../../generic/sub-header/SubHeader';
|
||||
import { SubHeaderTitle } from '../LibraryAuthoringPage';
|
||||
@@ -105,6 +106,7 @@ export const LibrarySectionPage = () => {
|
||||
<Container className="px-4 py-4">
|
||||
<LibraryContainerChildren containerKey={containerId} />
|
||||
<FooterActions
|
||||
addContentType={ContainerType.Subsection}
|
||||
addContentBtnText={intl.formatMessage(sectionMessages.addContentButton)}
|
||||
addExistingContentBtnText={intl.formatMessage(sectionMessages.addExistingContentButton)}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
getLibraryContainerApiUrl,
|
||||
getLibraryContainerChildrenApiUrl,
|
||||
getLibraryContainersApiUrl,
|
||||
} from '../data/api';
|
||||
import {
|
||||
mockContentLibrary,
|
||||
@@ -390,6 +391,110 @@ describe('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
expect(await screen.findByRole('button', { name: new RegExp(`${childType} Info`, 'i') })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`${cType} sidebar should render "new ${childType}" and "existing ${childType}" buttons`, async () => {
|
||||
renderLibrarySectionPage(undefined, undefined, cType);
|
||||
const addChild = await screen.findByRole('button', { name: new RegExp(`add ${childType}`, 'i') });
|
||||
userEvent.click(addChild);
|
||||
const addNew = await screen.findByRole('button', { name: new RegExp(`^new ${childType}$`, 'i') });
|
||||
const addExisting = await screen.findByRole('button', { name: new RegExp(`^existing ${childType}$`, 'i') });
|
||||
|
||||
// Clicking "add new" shows create container modal (tested below)
|
||||
userEvent.click(addNew);
|
||||
expect(await screen.findByLabelText(new RegExp(`name your ${childType}`, 'i'))).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
|
||||
// Clicking "add existing" shows content picker modal
|
||||
userEvent.click(addExisting);
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: new RegExp(`add to ${cType}`, 'i') })).toBeInTheDocument();
|
||||
// No "Types" filter shown
|
||||
expect(screen.queryByRole('button', { name: /type/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`"add new" button should add ${childType} to the ${cType}`, async () => {
|
||||
const { libraryId } = mockContentLibrary;
|
||||
const containerId = cType === ContainerType.Section
|
||||
? mockGetContainerMetadata.sectionId
|
||||
: mockGetContainerMetadata.subsectionId;
|
||||
const childId = cType === ContainerType.Section
|
||||
? mockGetContainerMetadata.subsectionId
|
||||
: mockGetContainerMetadata.unitId;
|
||||
|
||||
axiosMock
|
||||
.onPost(getLibraryContainersApiUrl(libraryId))
|
||||
.reply(200, { id: childId });
|
||||
axiosMock
|
||||
.onPost(getLibraryContainerChildrenApiUrl(containerId))
|
||||
.reply(200);
|
||||
renderLibrarySectionPage(containerId, libraryId, cType);
|
||||
|
||||
const addChild = await screen.findByRole('button', { name: new RegExp(`add new ${childType}`, 'i') });
|
||||
userEvent.click(addChild);
|
||||
const textBox = await screen.findByLabelText(new RegExp(`name your ${childType}`, 'i'));
|
||||
fireEvent.change(textBox, { target: { value: `New ${childType} Title` } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post.length).toEqual(2);
|
||||
});
|
||||
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({
|
||||
can_stand_alone: false,
|
||||
container_type: childType,
|
||||
display_name: `New ${childType} Title`,
|
||||
}));
|
||||
expect(axiosMock.history.post[1].data).toEqual(JSON.stringify({ usage_keys: [childId] }));
|
||||
expect(textBox).not.toBeInTheDocument();
|
||||
const childTypeTitle = childType.charAt(0).toUpperCase() + childType.slice(1);
|
||||
expect(mockShowToast).toHaveBeenCalledWith(`${childTypeTitle} created successfully`);
|
||||
});
|
||||
|
||||
it(`"add new" button should show error when adding ${childType} to the ${cType}`, async () => {
|
||||
const { libraryId } = mockContentLibrary;
|
||||
const containerId = cType === ContainerType.Section
|
||||
? mockGetContainerMetadata.sectionId
|
||||
: mockGetContainerMetadata.subsectionId;
|
||||
const childId = cType === ContainerType.Section
|
||||
? mockGetContainerMetadata.subsectionId
|
||||
: mockGetContainerMetadata.unitId;
|
||||
|
||||
axiosMock
|
||||
.onPost(getLibraryContainersApiUrl(libraryId))
|
||||
.reply(200, { id: childId });
|
||||
axiosMock
|
||||
.onPost(getLibraryContainerChildrenApiUrl(containerId))
|
||||
.reply(500);
|
||||
renderLibrarySectionPage(containerId, libraryId, cType);
|
||||
|
||||
const addChild = await screen.findByRole('button', { name: new RegExp(`add new ${childType}`, 'i') });
|
||||
userEvent.click(addChild);
|
||||
const textBox = await screen.findByLabelText(new RegExp(`name your ${childType}`, 'i'));
|
||||
fireEvent.change(textBox, { target: { value: `New ${childType} Title` } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post.length).toEqual(2);
|
||||
});
|
||||
expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({
|
||||
can_stand_alone: false,
|
||||
container_type: childType,
|
||||
display_name: `New ${childType} Title`,
|
||||
}));
|
||||
expect(axiosMock.history.post[1].data).toEqual(JSON.stringify({ usage_keys: [childId] }));
|
||||
expect(textBox).not.toBeInTheDocument();
|
||||
expect(mockShowToast).toHaveBeenCalledWith(`There is an error when creating the library ${childType}`);
|
||||
});
|
||||
|
||||
it(`"add existing ${childType}" button should load ${cType} content picker modal`, async () => {
|
||||
renderLibrarySectionPage(undefined, undefined, cType);
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
const addChild = await screen.findByRole('button', { name: new RegExp(`add existing ${childType}`, 'i') });
|
||||
userEvent.click(addChild);
|
||||
|
||||
// Content picker loaded (modal behavior is tested elsewhere)
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: new RegExp(`add to ${cType}`, 'i') })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`should open manage tags on click tag count in ${cType} page`, async () => {
|
||||
const cId = cType === ContainerType.Section
|
||||
? mockGetContainerMetadata.sectionId
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useContentFromSearchIndex, useContentLibrary } from '../data/apiHooks';
|
||||
import Loading from '../../generic/Loading';
|
||||
import NotFoundAlert from '../../generic/NotFoundAlert';
|
||||
import ErrorAlert from '../../generic/alert-error';
|
||||
import { ContainerType } from '../../generic/key-utils';
|
||||
import Header from '../../header';
|
||||
import SubHeader from '../../generic/sub-header/SubHeader';
|
||||
import { SubHeaderTitle } from '../LibraryAuthoringPage';
|
||||
@@ -154,6 +155,7 @@ export const LibrarySubsectionPage = () => {
|
||||
<Container className="px-4 py-4">
|
||||
<LibraryContainerChildren containerKey={containerId} />
|
||||
<FooterActions
|
||||
addContentType={ContainerType.Unit}
|
||||
addContentBtnText={intl.formatMessage(subsectionMessages.addContentButton)}
|
||||
addExistingContentBtnText={intl.formatMessage(subsectionMessages.addExistingContentButton)}
|
||||
/>
|
||||
|
||||
@@ -442,4 +442,14 @@ describe('<LibraryUnitPage />', () => {
|
||||
userEvent.click(component.parentElement!.parentElement!.parentElement!, undefined, { clickCount: 2 });
|
||||
expect(await screen.findByRole('dialog', { name: 'Editor Dialog' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('"Add New Content" button should open "Add Content" sidebar', async () => {
|
||||
renderLibraryUnitPage();
|
||||
const addContent = await screen.findByRole('button', { name: /add new content/i });
|
||||
userEvent.click(addContent);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /existing library content/i })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /text/i })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /problem/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user