@@ -23,7 +33,7 @@ export const FooterActions = ({
className="ml-2"
iconBefore={Add}
variant="outline-primary rounded-0"
- onClick={openAddContentSidebar}
+ onClick={addContent}
disabled={readOnly}
block
>
diff --git a/src/library-authoring/create-container/CreateContainerModal.test.tsx b/src/library-authoring/create-container/CreateContainerModal.test.tsx
index bff91e8dc..7c7fb96fa 100644
--- a/src/library-authoring/create-container/CreateContainerModal.test.tsx
+++ b/src/library-authoring/create-container/CreateContainerModal.test.tsx
@@ -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');
diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx
index 8773e8c14..8861b7736 100644
--- a/src/library-authoring/create-container/CreateContainerModal.tsx
+++ b/src/library-authoring/create-container/CreateContainerModal.tsx
@@ -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 (
| null;
- insideCollections: PathMatch | null;
- insideComponents: PathMatch | null;
- insideSections: PathMatch | null;
- insideSection: PathMatch | null;
- insideSubsections: PathMatch | null;
- insideSubsection: PathMatch | null;
- insideUnits: PathMatch | null;
- insideUnit: PathMatch | 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 | null values to PathMatch | 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 => 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,
diff --git a/src/library-authoring/section-subsections/LibrarySectionPage.tsx b/src/library-authoring/section-subsections/LibrarySectionPage.tsx
index 95da17add..63055a2c2 100644
--- a/src/library-authoring/section-subsections/LibrarySectionPage.tsx
+++ b/src/library-authoring/section-subsections/LibrarySectionPage.tsx
@@ -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 = () => {
diff --git a/src/library-authoring/section-subsections/LibrarySectionSubsectionPage.test.tsx b/src/library-authoring/section-subsections/LibrarySectionSubsectionPage.test.tsx
index a24ba9276..4e3b365c0 100644
--- a/src/library-authoring/section-subsections/LibrarySectionSubsectionPage.test.tsx
+++ b/src/library-authoring/section-subsections/LibrarySectionSubsectionPage.test.tsx
@@ -13,6 +13,7 @@ import {
import {
getLibraryContainerApiUrl,
getLibraryContainerChildrenApiUrl,
+ getLibraryContainersApiUrl,
} from '../data/api';
import {
mockContentLibrary,
@@ -390,6 +391,110 @@ describe('', () => {
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
diff --git a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx
index 0881f92b0..661477afe 100644
--- a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx
+++ b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx
@@ -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 = () => {
diff --git a/src/library-authoring/units/LibraryUnitPage.test.tsx b/src/library-authoring/units/LibraryUnitPage.test.tsx
index 1009147fb..dc331d266 100644
--- a/src/library-authoring/units/LibraryUnitPage.test.tsx
+++ b/src/library-authoring/units/LibraryUnitPage.test.tsx
@@ -442,4 +442,14 @@ describe('', () => {
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();
+ });
});