From d5b09700b629d5191659ee2eac84e99588e0693d Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 21 May 2025 16:42:23 +0530 Subject: [PATCH 01/11] feat: create section and subsection in library --- src/generic/block-type-utils/constants.ts | 20 ++- src/generic/block-type-utils/index.scss | 50 ++++++ src/generic/key-utils.ts | 7 +- src/library-authoring/LibraryLayout.tsx | 4 +- .../add-content/AddContent.tsx | 58 ++++-- src/library-authoring/add-content/messages.ts | 10 ++ .../common/context/LibraryContext.tsx | 16 ++ .../create-container/CreateContainerModal.tsx | 169 ++++++++++++++++++ .../create-container/index.tsx | 1 + .../create-container/messages.ts | 96 ++++++++++ .../create-unit/CreateUnitModal.tsx | 115 ------------ src/library-authoring/create-unit/index.tsx | 1 - src/library-authoring/create-unit/messages.ts | 46 ----- src/library-authoring/routes.ts | 22 +++ 14 files changed, 432 insertions(+), 183 deletions(-) create mode 100644 src/library-authoring/create-container/CreateContainerModal.tsx create mode 100644 src/library-authoring/create-container/index.tsx create mode 100644 src/library-authoring/create-container/messages.ts delete mode 100644 src/library-authoring/create-unit/CreateUnitModal.tsx delete mode 100644 src/library-authoring/create-unit/index.tsx delete mode 100644 src/library-authoring/create-unit/messages.ts diff --git a/src/generic/block-type-utils/constants.ts b/src/generic/block-type-utils/constants.ts index a9a87a0c7c..34a2346c88 100644 --- a/src/generic/block-type-utils/constants.ts +++ b/src/generic/block-type-utils/constants.ts @@ -6,7 +6,6 @@ import { ContentPaste as ContentPasteIcon, Edit as EditIcon, EditNote as EditNoteIcon, - CalendarViewDay, HelpOutline as HelpOutlineIcon, LibraryAdd as LibraryIcon, Lock as LockIcon, @@ -15,6 +14,9 @@ import { TextFields as TextFieldsIcon, VideoCamera as VideoCameraIcon, Folder, + ViewCarousel, + ViewDay, + WidthWide, } from '@openedx/paragon/icons'; import NewsstandIcon from '../NewsstandIcon'; @@ -36,7 +38,9 @@ export const COMPONENT_TYPES = { export const UNIT_TYPE_ICONS_MAP: Record = { video: VideoCameraIcon, other: BookOpenIcon, - vertical: CalendarViewDay, + vertical: ViewDay, + sequential: WidthWide, + chapter: ViewCarousel, problem: EditIcon, lock: LockIcon, }; @@ -57,8 +61,10 @@ export const COMPONENT_TYPE_ICON_MAP: Record = { export const STRUCTURAL_TYPE_ICONS: Record = { vertical: UNIT_TYPE_ICONS_MAP.vertical, unit: UNIT_TYPE_ICONS_MAP.vertical, - sequential: Folder, - chapter: Folder, + sequential: UNIT_TYPE_ICONS_MAP.sequential, + subsection: UNIT_TYPE_ICONS_MAP.sequential, + chapter: UNIT_TYPE_ICONS_MAP.chapter, + section: UNIT_TYPE_ICONS_MAP.chapter, collection: Folder, libraryContent: Folder, paste: ContentPasteIcon, @@ -75,8 +81,10 @@ export const COMPONENT_TYPE_STYLE_COLOR_MAP = { [COMPONENT_TYPES.dragAndDrop]: 'component-style-default', vertical: 'component-style-vertical', unit: 'component-style-vertical', - sequential: 'component-style-default', - chapter: 'component-style-default', + sequential: 'component-style-sequential', + subsection: 'component-style-sequential', + chapter: 'component-style-chapter', + section: 'component-style-chapter', collection: 'component-style-collection', other: 'component-style-other', }; diff --git a/src/generic/block-type-utils/index.scss b/src/generic/block-type-utils/index.scss index 713509c0ba..a33b2c578e 100644 --- a/src/generic/block-type-utils/index.scss +++ b/src/generic/block-type-utils/index.scss @@ -123,6 +123,56 @@ } } +.component-style-sequential { + background-color: #EA3E3E; + + .pgn__icon:not(.btn-icon-before) { + color: white; + } + + .btn-icon { + &:hover, &:active, &:focus { + background-color: darken(#EA3E3E, 15%); + } + } + + .btn { + background-color: lighten(#0B8E77, 10%); + border: 0; + + &:hover, &:active, &:focus { + background-color: lighten(#0B8E77, 20%); + border: 1px solid $primary; + margin: -1px; + } + } +} + +.component-style-chapter { + background-color: #45009E; + + .pgn__icon:not(.btn-icon-before) { + color: white; + } + + .btn-icon { + &:hover, &:active, &:focus { + background-color: darken(#45009E, 15%); + } + } + + .btn { + background-color: lighten(#0B8E77, 10%); + border: 0; + + &:hover, &:active, &:focus { + background-color: lighten(#0B8E77, 20%); + border: 1px solid $primary; + margin: -1px; + } + } +} + .component-style-other { background-color: #646464; diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index eeb5202d20..abd0111f96 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -51,15 +51,18 @@ export const buildCollectionUsageKey = (learningContextKey: string, collectionId }; export enum ContainerType { + Section = 'section', + Subsection = 'subsection', Unit = 'unit', /** - * Vertical is the old name for Unit. Generally, **please avoid using this term entirely in any libraries code** or + * Chapter, Sequential and Vertical is the old name for Unit. Generally, **please avoid using this term entirely in any libraries code** or * anything based on the new Learning Core "Containers" framework - just call it a unit. We do still need to use this * in the modulestore-based courseware, and currently the /xblock/ API used to copy library containers into courses * also requires specifying this, though that should change to a better API that does the unit->vertical conversion * automatically in the future. - * TODO: we should probably move this to a separate enum/mapping, and keep this for the new container types only. */ + Chapter = 'chapter', + Sequential = 'sequential', Vertical = 'vertical', } diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index f6f425912a..9a2896566a 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -12,7 +12,7 @@ import LibraryAuthoringPage from './LibraryAuthoringPage'; import { LibraryProvider } from './common/context/LibraryContext'; import { SidebarProvider } from './common/context/SidebarContext'; import { CreateCollectionModal } from './create-collection'; -import { CreateUnitModal } from './create-unit'; +import { CreateContainerModal } from './create-container'; import LibraryCollectionPage from './collections/LibraryCollectionPage'; import { ComponentPicker } from './component-picker'; import { ComponentEditorModal } from './components/ComponentEditorModal'; @@ -50,7 +50,7 @@ const LibraryLayout = () => { <> {childPage} - + diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index 8ddbdae354..7d0960241b 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -1,5 +1,5 @@ import React, { useContext, useMemo } from 'react'; -import type { MessageDescriptor } from 'react-intl'; +import type { IntlShape, MessageDescriptor } from 'react-intl'; import { useSelector } from 'react-redux'; import { Stack, @@ -29,7 +29,7 @@ import { useLibraryContext } from '../common/context/LibraryContext'; import { PickLibraryContentModal } from './PickLibraryContentModal'; import { blockTypes } from '../../editors/data/constants/app'; -import { ContentType as LibraryContentTypes } from '../routes'; +import { ContentType as LibraryContentTypes, useLibraryRoutes } from '../routes'; import genericMessages from '../generic/messages'; import messages from './messages'; import type { BlockTypeMetadata } from '../data/api'; @@ -58,7 +58,7 @@ type AddAdvancedContentViewProps = { closeAdvancedList: () => void, onCreateContent: (blockType: string) => void, advancedBlocks: Record, - isBlockTypeEnabled: (blockType) => boolean, + isBlockTypeEnabled: (blockType: string) => boolean, }; const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonProps) => { @@ -93,8 +93,14 @@ const AddContentView = ({ componentPicker, unitId, } = useLibraryContext(); + const { + insideCollection, + insideUnit, + insideSection, + insideSubsection, + } = useLibraryRoutes(); let upstreamContainerType: ContainerType | undefined; - if (unitId) { + if (unitId && insideUnit) { upstreamContainerType = getContainerTypeFromId(unitId); } @@ -110,6 +116,18 @@ const AddContentView = ({ blockType: 'vertical', }; + const sectionButtonData = { + name: intl.formatMessage(messages.sectionButton), + disabled: false, + blockType: 'chapter', + }; + + const subsectionButtonData = { + name: intl.formatMessage(messages.subsectionButton), + disabled: false, + blockType: 'sequential', + }; + const libraryContentButtonData = { name: intl.formatMessage(messages.libraryContentButton), disabled: false, @@ -121,7 +139,7 @@ const AddContentView = ({ return ( <> - {(collectionId || unitId) && componentPicker && ( + {((collectionId && insideCollection) || (unitId && insideUnit)) && componentPicker && ( /// Show the "Add Library Content" button for units and collections <> @@ -133,12 +151,20 @@ const AddContentView = ({ /> )} - {!collectionId && !unitId && ( + {!insideCollection && !insideUnit && !insideSubsection && !insideSection && ( // Doesn't show the "Collection" button if we are in a unit or collection )} - {upstreamContainerType !== ContainerType.Unit && ( - // Doesn't show the "Unit" button if we are in a unit + {!(insideUnit || insideSubsection || insideSection) && ( + // Doesn't show the "Section" button if we are in a unit or section or subsection + + )} + {!(insideSubsection || insideUnit) && ( + // Doesn't show the "Subsection" button if we are in a unit or Subsection + + )} + {!(insideUnit || insideSection) && ( + // Doesn't show the "Unit" button if we are in a unit or section )}
@@ -191,7 +217,7 @@ const AddAdvancedContentView = ({ }; export const parseErrorMsg = ( - intl, + intl: IntlShape, error: any, detailedMessage: MessageDescriptor, defaultMessage: MessageDescriptor, @@ -223,9 +249,15 @@ const AddContent = () => { collectionId, openCreateCollectionModal, openCreateUnitModal, + openCreateSectionModal, + openCreateSubsectionModal, openComponentEditor, unitId, } = useLibraryContext(); + const { + insideCollection, + insideUnit, + } = useLibraryRoutes(); const addComponentsToCollectionMutation = useAddItemsToCollection(libraryId, collectionId); const addComponentsToContainerMutation = useAddComponentsToContainer(unitId); const createBlockMutation = useCreateLibraryBlock(); @@ -306,12 +338,12 @@ const AddContent = () => { } const linkComponent = (opaqueKey: string) => { - if (collectionId) { + if (collectionId && insideCollection) { addComponentsToCollectionMutation.mutateAsync([opaqueKey]).catch(() => { showToast(intl.formatMessage(genericMessages.manageCollectionsFailed)); }); } - if (unitId) { + if (unitId && insideUnit) { addComponentsToContainerMutation.mutateAsync([opaqueKey]).catch(() => { showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage)); }); @@ -381,6 +413,10 @@ const AddContent = () => { showAdvancedList(); } else if (blockType === 'vertical') { openCreateUnitModal(); + } else if (blockType === 'chapter') { + openCreateSectionModal(); + } else if (blockType === 'sequential') { + openCreateSubsectionModal(); } else { onCreateBlock(blockType); } diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts index cd7e688c5e..4a2b176613 100644 --- a/src/library-authoring/add-content/messages.ts +++ b/src/library-authoring/add-content/messages.ts @@ -11,6 +11,16 @@ const messages = defineMessages({ defaultMessage: 'Unit', description: 'Content of button to create a Unit.', }, + sectionButton: { + id: 'course-authoring.library-authoring.add-content.buttons.section', + defaultMessage: 'Section', + description: 'Content of button to create a Section.', + }, + subsectionButton: { + id: 'course-authoring.library-authoring.add-content.buttons.subsection', + defaultMessage: 'Subsection', + description: 'Content of button to create a Subsection.', + }, libraryContentButton: { id: 'course-authoring.library-authoring.add-content.buttons.library-content', defaultMessage: 'Existing Library Content', diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 678ee310e5..a082bf305d 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -40,6 +40,14 @@ export type LibraryContextData = { isCreateCollectionModalOpen: boolean; openCreateCollectionModal: () => void; closeCreateCollectionModal: () => void; + // "Create New Section" modal + isCreateSectionModalOpen: boolean; + openCreateSectionModal: () => void; + closeCreateSectionModal: () => void; + // "Create New Subsection" modal + isCreateSubsectionModalOpen: boolean; + openCreateSubsectionModal: () => void; + closeCreateSubsectionModal: () => void; // "Create New Unit" modal isCreateUnitModalOpen: boolean; openCreateUnitModal: () => void; @@ -92,6 +100,8 @@ export const LibraryProvider = ({ }: LibraryProviderProps) => { const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false); const [isCreateUnitModalOpen, openCreateUnitModal, closeCreateUnitModal] = useToggle(false); + const [isCreateSectionModalOpen, openCreateSectionModal, closeCreateSectionModal] = useToggle(false); + const [isCreateSubsectionModalOpen, openCreateSubsectionModal, closeCreateSubsectionModal] = useToggle(false); const [componentBeingEdited, setComponentBeingEdited] = useState(); const closeComponentEditor = useCallback((data) => { setComponentBeingEdited((prev) => { @@ -150,6 +160,12 @@ export const LibraryProvider = ({ isCreateUnitModalOpen, openCreateUnitModal, closeCreateUnitModal, + isCreateSectionModalOpen, + openCreateSectionModal, + closeCreateSectionModal, + isCreateSubsectionModalOpen, + openCreateSubsectionModal, + closeCreateSubsectionModal, componentBeingEdited, openComponentEditor, closeComponentEditor, diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx new file mode 100644 index 0000000000..d863aaa008 --- /dev/null +++ b/src/library-authoring/create-container/CreateContainerModal.tsx @@ -0,0 +1,169 @@ +import React from 'react'; +import { + ActionRow, + Form, + ModalDialog, +} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Formik } from 'formik'; +import { useNavigate } from 'react-router'; +import * as Yup from 'yup'; +import FormikControl from '../../generic/FormikControl'; +import { useLibraryContext } from '../common/context/LibraryContext'; +import messages from './messages'; +import { useAddItemsToCollection, useCreateLibraryContainer } from '../data/apiHooks'; +import { ToastContext } from '../../generic/toast-context'; +import LoadingButton from '../../generic/loading-button'; +import { ContainerType } from '../../generic/key-utils'; +import { useLibraryRoutes } from '../routes'; + +const CreateContainerModal = () => { + const intl = useIntl(); + const navigate = useNavigate(); + const { + collectionId, + libraryId, + isCreateSectionModalOpen, + closeCreateSectionModal, + isCreateSubsectionModalOpen, + closeCreateSubsectionModal, + isCreateUnitModalOpen, + closeCreateUnitModal, + } = useLibraryContext(); + const { insideCollection, insideSubsection, insideSection } = useLibraryRoutes(); + const create = useCreateLibraryContainer(libraryId); + const updateItemsMutation = useAddItemsToCollection(libraryId, collectionId); + const { showToast } = React.useContext(ToastContext); + + const labels = React.useMemo(() => { + if (isCreateSectionModalOpen) { + return { + modalTitle: intl.formatMessage(messages.createSectionModalTitle), + validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + nameLabel: intl.formatMessage(messages.createSectionModalNameLabel), + placeholder: intl.formatMessage(messages.createSectionModalNamePlaceholder), + successMsg: intl.formatMessage(messages.createSectionSuccess), + errorMsg: intl.formatMessage(messages.createSectionError), + } + } else if (isCreateSubsectionModalOpen) { + return { + modalTitle: intl.formatMessage(messages.createSubsectionModalTitle), + validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + nameLabel: intl.formatMessage(messages.createSubsectionModalNameLabel), + placeholder: intl.formatMessage(messages.createSubsectionModalNamePlaceholder), + successMsg: intl.formatMessage(messages.createSubsectionSuccess), + errorMsg: intl.formatMessage(messages.createSubsectionError), + } + } else { + return { + modalTitle: intl.formatMessage(messages.createUnitModalTitle), + validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + nameLabel: intl.formatMessage(messages.createUnitModalNameLabel), + placeholder: intl.formatMessage(messages.createUnitModalNamePlaceholder), + successMsg: intl.formatMessage(messages.createUnitSuccess), + errorMsg: intl.formatMessage(messages.createUnitError), + } + } + }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); + + const handleClose = React.useCallback(() => { + closeCreateSectionModal(); + closeCreateSubsectionModal(); + closeCreateUnitModal(); + }, [closeCreateUnitModal, closeCreateSubsectionModal, closeCreateSectionModal]); + + const containerType = React.useMemo(() => { + if (isCreateSectionModalOpen) { + return ContainerType.Section; + } else if (isCreateSubsectionModalOpen) { + return ContainerType.Subsection; + } else { + return ContainerType.Unit; + } + }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); + + const handleCreate = React.useCallback(async (values) => { + try { + const container = await create.mutateAsync({ + containerType, + ...values, + }); + // link container to parent + if (collectionId && insideCollection) { + await updateItemsMutation.mutateAsync([container.id]); + } + // Navigate to the new container + navigate(`/library/${libraryId}/${containerType}/${container.id}`); + showToast(labels.successMsg); + } catch (error) { + showToast(labels.errorMsg); + } finally { + handleClose(); + } + }, [containerType, labels, handleClose]); + + return ( + + + + {labels.modalTitle} + + + + + {(formikProps) => ( +
+ + + {labels.nameLabel} + + )} + value={formikProps.values.displayName} + placeholder={labels.placeholder} + controlClasses="pb-2" + /> + + + + + {intl.formatMessage(messages.createModalCancel)} + + + + +
+ )} +
+
+ ); +}; + +export default CreateContainerModal; diff --git a/src/library-authoring/create-container/index.tsx b/src/library-authoring/create-container/index.tsx new file mode 100644 index 0000000000..f15e01cbea --- /dev/null +++ b/src/library-authoring/create-container/index.tsx @@ -0,0 +1 @@ +export { default as CreateContainerModal } from './CreateContainerModal'; diff --git a/src/library-authoring/create-container/messages.ts b/src/library-authoring/create-container/messages.ts new file mode 100644 index 0000000000..94e7b774f3 --- /dev/null +++ b/src/library-authoring/create-container/messages.ts @@ -0,0 +1,96 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + createUnitModalTitle: { + id: 'course-authoring.library-authoring.modals.create-unit.title', + defaultMessage: 'New Unit', + description: 'Title of the Create Unit modal', + }, + createSubsectionModalTitle: { + id: 'course-authoring.library-authoring.modals.create-subsection.title', + defaultMessage: 'New Subsection', + description: 'Title of the Create Subsection modal', + }, + createSectionModalTitle: { + id: 'course-authoring.library-authoring.modals.create-section.title', + defaultMessage: 'New Section', + description: 'Title of the Create Section modal', + }, + createModalCancel: { + id: 'course-authoring.library-authoring.modals.create-container.cancel', + defaultMessage: 'Cancel', + description: 'Label of the Cancel button of the Create container modal', + }, + createContainerModalCreate: { + id: 'course-authoring.library-authoring.modals.create-container.create', + defaultMessage: 'Create', + description: 'Label of the Create button of the Create container modal', + }, + createUnitModalNameLabel: { + id: 'course-authoring.library-authoring.modals.create-unit.form.name', + defaultMessage: 'Name your unit', + description: 'Label of the Name field of the Create Unit modal form', + }, + createSectionModalNameLabel: { + id: 'course-authoring.library-authoring.modals.create-section.form.name', + defaultMessage: 'Name your section', + description: 'Label of the Name field of the Create Section modal form', + }, + createSubsectionModalNameLabel: { + id: 'course-authoring.library-authoring.modals.create-subsection.form.name', + defaultMessage: 'Name your subsection', + description: 'Label of the Name field of the Create Subsection modal form', + }, + createUnitModalNamePlaceholder: { + id: 'course-authoring.library-authoring.modals.create-unit.form.name.placeholder', + defaultMessage: 'Give a descriptive title', + description: 'Placeholder of the Name field of the Create Unit modal form', + }, + createSubsectionModalNamePlaceholder: { + id: 'course-authoring.library-authoring.modals.create-subsection.form.name.placeholder', + defaultMessage: 'Give a descriptive title', + description: 'Placeholder of the Name field of the Create Subsection modal form', + }, + createSectionModalNamePlaceholder: { + id: 'course-authoring.library-authoring.modals.create-section.form.name.placeholder', + defaultMessage: 'Give a descriptive title', + description: 'Placeholder of the Name field of the Create Section modal form', + }, + createContainerModalNameInvalid: { + id: 'course-authoring.library-authoring.modals.create-container.form.name.invalid', + defaultMessage: 'Container name is required', + description: 'Message when the Name field of the Create Container modal form is invalid', + }, + createSectionSuccess: { + id: 'course-authoring.library-authoring.modals.create-section.success', + defaultMessage: 'Section created successfully', + description: 'Success message when creating a library section', + }, + createSectionError: { + id: 'course-authoring.library-authoring.modals.create-section.error', + defaultMessage: 'There is an error when creating the library section', + description: 'Error message when creating a library section', + }, + createSubsectionSuccess: { + id: 'course-authoring.library-authoring.modals.create-subsection.success', + defaultMessage: 'Subsection created successfully', + description: 'Success message when creating a library subsection', + }, + createSubsectionError: { + id: 'course-authoring.library-authoring.modals.create-subsection.error', + defaultMessage: 'There is an error when creating the library subsection', + description: 'Error message when creating a library subsection', + }, + createUnitSuccess: { + id: 'course-authoring.library-authoring.modals.create-unit.success', + defaultMessage: 'Unit created successfully', + description: 'Success message when creating a library unit', + }, + createUnitError: { + id: 'course-authoring.library-authoring.modals.create-unit.error', + defaultMessage: 'There is an error when creating the library unit', + description: 'Error message when creating a library unit', + }, +}); + +export default messages; diff --git a/src/library-authoring/create-unit/CreateUnitModal.tsx b/src/library-authoring/create-unit/CreateUnitModal.tsx deleted file mode 100644 index 0d83303d30..0000000000 --- a/src/library-authoring/create-unit/CreateUnitModal.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import { - ActionRow, - Form, - ModalDialog, -} from '@openedx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { Formik } from 'formik'; -import { useNavigate } from 'react-router'; -import * as Yup from 'yup'; -import FormikControl from '../../generic/FormikControl'; -import { useLibraryContext } from '../common/context/LibraryContext'; -import messages from './messages'; -import { useAddItemsToCollection, useCreateLibraryContainer } from '../data/apiHooks'; -import { ToastContext } from '../../generic/toast-context'; -import LoadingButton from '../../generic/loading-button'; -import { ContainerType } from '../../generic/key-utils'; - -const CreateUnitModal = () => { - const intl = useIntl(); - const navigate = useNavigate(); - const { - collectionId, - libraryId, - isCreateUnitModalOpen, - closeCreateUnitModal, - } = useLibraryContext(); - const create = useCreateLibraryContainer(libraryId); - const updateItemsMutation = useAddItemsToCollection(libraryId, collectionId); - const { showToast } = React.useContext(ToastContext); - - const handleCreate = React.useCallback(async (values) => { - try { - const container = await create.mutateAsync({ - containerType: ContainerType.Unit, - ...values, - }); - if (collectionId) { - await updateItemsMutation.mutateAsync([container.id]); - } - // Navigate to the new unit - navigate(`/library/${libraryId}/unit/${container.id}`); - showToast(intl.formatMessage(messages.createUnitSuccess)); - } catch (error) { - showToast(intl.formatMessage(messages.createUnitError)); - } finally { - closeCreateUnitModal(); - } - }, []); - - return ( - - - - {intl.formatMessage(messages.createUnitModalTitle)} - - - - - {(formikProps) => ( -
- - - {intl.formatMessage(messages.createUnitModalNameLabel)} - - )} - value={formikProps.values.displayName} - placeholder={intl.formatMessage(messages.createUnitModalNamePlaceholder)} - controlClasses="pb-2" - /> - - - - - {intl.formatMessage(messages.createUnitModalCancel)} - - - - -
- )} -
-
- ); -}; - -export default CreateUnitModal; diff --git a/src/library-authoring/create-unit/index.tsx b/src/library-authoring/create-unit/index.tsx deleted file mode 100644 index ee82f395fe..0000000000 --- a/src/library-authoring/create-unit/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as CreateUnitModal } from './CreateUnitModal'; diff --git a/src/library-authoring/create-unit/messages.ts b/src/library-authoring/create-unit/messages.ts deleted file mode 100644 index db52c1ed6e..0000000000 --- a/src/library-authoring/create-unit/messages.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - createUnitModalTitle: { - id: 'course-authoring.library-authoring.modals.create-unit.title', - defaultMessage: 'New Unit', - description: 'Title of the Create Unit modal', - }, - createUnitModalCancel: { - id: 'course-authoring.library-authoring.modals.create-unit.cancel', - defaultMessage: 'Cancel', - description: 'Label of the Cancel button of the Create Unit modal', - }, - createUnitModalCreate: { - id: 'course-authoring.library-authoring.modals.create-unit.create', - defaultMessage: 'Create', - description: 'Label of the Create button of the Create Unit modal', - }, - createUnitModalNameLabel: { - id: 'course-authoring.library-authoring.modals.create-unit.form.name', - defaultMessage: 'Name your unit', - description: 'Label of the Name field of the Create Unit modal form', - }, - createUnitModalNamePlaceholder: { - id: 'course-authoring.library-authoring.modals.create-unit.form.name.placeholder', - defaultMessage: 'Give a descriptive title', - description: 'Placeholder of the Name field of the Create Unit modal form', - }, - createUnitModalNameInvalid: { - id: 'course-authoring.library-authoring.modals.create-unit.form.name.invalid', - defaultMessage: 'Unit name is required', - description: 'Message when the Name field of the Create Unit modal form is invalid', - }, - createUnitSuccess: { - id: 'course-authoring.library-authoring.modals.create-unit.success', - defaultMessage: 'Unit created successfully', - description: 'Success message when creating a library unit', - }, - createUnitError: { - id: 'course-authoring.library-authoring.modals.create-unit.error', - defaultMessage: 'There is an error when creating the library unit', - description: 'Error message when creating a library unit', - }, -}); - -export default messages; diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index 4615f229d3..541648372f 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -21,6 +21,10 @@ export const ROUTES = { COMPONENTS: '/components/:componentId?', // * Collections tab, with an optionally selected collectionId in the sidebar. COLLECTIONS: '/collections/:collectionId?', + // * Sections tab, with an optionally selected sectionId in the sidebar. + SECTIONS: '/sections/:sectionId?', + // * Subsections tab, with an optionally selected subsectionId in the sidebar. + SUBSECTIONS: '/subsections/:subsectionId?', // * Units tab, with an optionally selected unitId in the sidebar. UNITS: '/units/:unitId?', // * All Content tab, with an optionally selected componentId in the sidebar. @@ -30,6 +34,12 @@ export const ROUTES = { // LibraryCollectionPage route: // * with a selected collectionId and/or an optionally selected componentId. COLLECTION: '/collection/:collectionId/:componentId?', + // LibrarySectionPage route: + // * with a selected sectionId and/or an optionally selected subsectionId. + SECTION: '/section/:sectionId/:subsectionId?', + // LibrarySubsectionPage route: + // * with a selected subsectionId and/or an optionally selected unitId. + SUBSECTION: '/subsection/:subsectionId/:unitId?', // LibraryUnitPage route: // * with a selected unitId and/or an optionally selected componentId. UNIT: '/unit/:unitId/:componentId?', @@ -56,6 +66,10 @@ export type LibraryRoutesData = { insideCollection: PathMatch | 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; @@ -73,6 +87,10 @@ export const useLibraryRoutes = (): LibraryRoutesData => { 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); @@ -189,6 +207,10 @@ export const useLibraryRoutes = (): LibraryRoutesData => { insideCollection, insideCollections, insideComponents, + insideSections, + insideSection, + insideSubsections, + insideSubsection, insideUnits, insideUnit, }; From 8c501a90995238881783fc604eddadde84c43d75 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 21 May 2025 17:00:14 +0530 Subject: [PATCH 02/11] chore: fix lint issues --- src/generic/key-utils.ts | 3 +- .../add-content/AddContent.tsx | 5 --- .../create-container/CreateContainerModal.tsx | 32 +++++++++---------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index abd0111f96..56e728c715 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -55,7 +55,8 @@ export enum ContainerType { Subsection = 'subsection', Unit = 'unit', /** - * Chapter, Sequential and Vertical is the old name for Unit. Generally, **please avoid using this term entirely in any libraries code** or + * Chapter, Sequential and Vertical is the old name for Unit. + * Generally, **please avoid using this term entirely in any libraries code** or * anything based on the new Learning Core "Containers" framework - just call it a unit. We do still need to use this * in the modulestore-based courseware, and currently the /xblock/ API used to copy library containers into courses * also requires specifying this, though that should change to a better API that does the unit->vertical conversion diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index 7d0960241b..1d501faf58 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -33,7 +33,6 @@ import { ContentType as LibraryContentTypes, useLibraryRoutes } from '../routes' import genericMessages from '../generic/messages'; import messages from './messages'; import type { BlockTypeMetadata } from '../data/api'; -import { getContainerTypeFromId, ContainerType } from '../../generic/key-utils'; type ContentType = { name: string, @@ -99,10 +98,6 @@ const AddContentView = ({ insideSection, insideSubsection, } = useLibraryRoutes(); - let upstreamContainerType: ContainerType | undefined; - if (unitId && insideUnit) { - upstreamContainerType = getContainerTypeFromId(unitId); - } const collectionButtonData = { name: intl.formatMessage(messages.collectionButton), diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx index d863aaa008..e530fb2825 100644 --- a/src/library-authoring/create-container/CreateContainerModal.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.tsx @@ -30,7 +30,7 @@ const CreateContainerModal = () => { isCreateUnitModalOpen, closeCreateUnitModal, } = useLibraryContext(); - const { insideCollection, insideSubsection, insideSection } = useLibraryRoutes(); + const { insideCollection } = useLibraryRoutes(); const create = useCreateLibraryContainer(libraryId); const updateItemsMutation = useAddItemsToCollection(libraryId, collectionId); const { showToast } = React.useContext(ToastContext); @@ -44,8 +44,9 @@ const CreateContainerModal = () => { placeholder: intl.formatMessage(messages.createSectionModalNamePlaceholder), successMsg: intl.formatMessage(messages.createSectionSuccess), errorMsg: intl.formatMessage(messages.createSectionError), - } - } else if (isCreateSubsectionModalOpen) { + }; + } + if (isCreateSubsectionModalOpen) { return { modalTitle: intl.formatMessage(messages.createSubsectionModalTitle), validationError: intl.formatMessage(messages.createContainerModalNameInvalid), @@ -53,17 +54,16 @@ const CreateContainerModal = () => { placeholder: intl.formatMessage(messages.createSubsectionModalNamePlaceholder), successMsg: intl.formatMessage(messages.createSubsectionSuccess), errorMsg: intl.formatMessage(messages.createSubsectionError), - } - } else { - return { - modalTitle: intl.formatMessage(messages.createUnitModalTitle), - validationError: intl.formatMessage(messages.createContainerModalNameInvalid), - nameLabel: intl.formatMessage(messages.createUnitModalNameLabel), - placeholder: intl.formatMessage(messages.createUnitModalNamePlaceholder), - successMsg: intl.formatMessage(messages.createUnitSuccess), - errorMsg: intl.formatMessage(messages.createUnitError), - } + }; } + return { + modalTitle: intl.formatMessage(messages.createUnitModalTitle), + validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + nameLabel: intl.formatMessage(messages.createUnitModalNameLabel), + placeholder: intl.formatMessage(messages.createUnitModalNamePlaceholder), + successMsg: intl.formatMessage(messages.createUnitSuccess), + errorMsg: intl.formatMessage(messages.createUnitError), + }; }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); const handleClose = React.useCallback(() => { @@ -75,11 +75,11 @@ const CreateContainerModal = () => { const containerType = React.useMemo(() => { if (isCreateSectionModalOpen) { return ContainerType.Section; - } else if (isCreateSubsectionModalOpen) { + } + if (isCreateSubsectionModalOpen) { return ContainerType.Subsection; - } else { - return ContainerType.Unit; } + return ContainerType.Unit; }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); const handleCreate = React.useCallback(async (values) => { From a0c8daa78cd44ff702542e4f98f009b0841f9f4c Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 21 May 2025 20:52:27 +0530 Subject: [PATCH 03/11] test: add new section and subsection --- .../LibraryAuthoringPage.test.tsx | 89 ++++++++++++++----- .../add-content/AddContent.test.tsx | 9 +- .../create-container/CreateContainerModal.tsx | 6 +- .../create-container/messages.ts | 18 +++- 4 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 5865d65a69..208d3db655 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -735,9 +735,22 @@ describe('', () => { expect(mockShowToast).toHaveBeenCalledWith('There is an error when creating the library collection'); }); - it('should create a unit', async () => { + test.each([ + { + label: 'should create a unit', + containerType: 'unit', + }, + { + label: 'should create a section', + containerType: 'section', + }, + { + label: 'should create a subsection', + containerType: 'subsection', + }, + ])('$label', async ({ containerType }) => { await renderLibraryPage(); - const title = 'This is a Test'; + const title = `This is a Test ${containerType}`; const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId); axiosMock.onPost(url).reply(200, { id: '1', @@ -753,11 +766,11 @@ describe('', () => { fireEvent.click(newButton); expect(screen.getByText(/add content/i)).toBeInTheDocument(); - // Open New unit Modal + // Open New container Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0]; + const newUnitButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i }); + const unitModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); expect(unitModalHeading).toBeInTheDocument(); // Click on Cancel button @@ -765,10 +778,10 @@ describe('', () => { fireEvent.click(cancelButton); expect(unitModalHeading).not.toBeInTheDocument(); - // Open new unit modal again and create a unit + // Open new unit modal again and create a container fireEvent.click(newUnitButton); const createButton = screen.getByRole('button', { name: /create/i }); - const nameField = screen.getByRole('textbox', { name: /name your unit/i }); + const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') }); fireEvent.change(nameField, { target: { value: title } }); fireEvent.click(createButton); @@ -778,14 +791,27 @@ describe('', () => { expect(axiosMock.history.post[0].url).toBe(url); expect(axiosMock.history.post[0].data).toContain(`"display_name":"${title}"`); - expect(axiosMock.history.post[0].data).toContain('"container_type":"unit"'); - expect(mockShowToast).toHaveBeenCalledWith('Unit created successfully'); + expect(axiosMock.history.post[0].data).toContain(`"container_type":"${containerType}"`); + expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${containerType} created successfully`, 'i'))); }); - it('should show validations in create unit', async () => { + test.each([ + { + label: 'should show validations in create unit', + containerType: 'unit', + }, + { + label: 'should show validations in create section', + containerType: 'section', + }, + { + label: 'should show validations in create subsection', + containerType: 'subsection', + }, + ])('$label', async ({ containerType }) => { await renderLibraryPage(); - const title = 'This is a Test'; + const title = `This is a Test ${containerType}`; const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId); axiosMock.onPost(url).reply(200, { id: '1', @@ -801,14 +827,14 @@ describe('', () => { fireEvent.click(newButton); expect(screen.getByText(/add content/i)).toBeInTheDocument(); - // Open New unit Modal + // Open New container Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0]; + const newUnitButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i }); + const unitModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); expect(unitModalHeading).toBeInTheDocument(); - const nameField = screen.getByRole('textbox', { name: /name your unit/i }); + const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') }); fireEvent.focus(nameField); fireEvent.blur(nameField); @@ -816,12 +842,25 @@ describe('', () => { const createButton = screen.getByRole('button', { name: /create/i }); fireEvent.click(createButton); - expect(await screen.findByText(/unit name is required/i)).toBeInTheDocument(); + expect(await screen.findByText(new RegExp(`${containerType} name is required`, 'i'))).toBeInTheDocument(); }); - it('should show error on create unit', async () => { + test.each([ + { + label: 'should show error on create unit', + containerType: 'unit', + }, + { + label: 'should show error on create section', + containerType: 'section', + }, + { + label: 'should show error on create subsection', + containerType: 'subsection', + }, + ])('$label', async ({ containerType }) => { await renderLibraryPage(); - const displayName = 'This is a Test'; + const displayName = `This is a Test ${containerType}`; const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId); axiosMock.onPost(url).reply(500); @@ -833,23 +872,25 @@ describe('', () => { fireEvent.click(newButton); expect(screen.getByText(/add content/i)).toBeInTheDocument(); - // Open New Unit Modal + // Open New container Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0]; + const newUnitButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i }); + const unitModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); expect(unitModalHeading).toBeInTheDocument(); - // Create a unit + // Create a container const createButton = screen.getByRole('button', { name: /create/i }); - const nameField = screen.getByRole('textbox', { name: /name your unit/i }); + const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') }); fireEvent.change(nameField, { target: { value: displayName } }); fireEvent.click(createButton); // Check error toast await waitFor(() => expect(axiosMock.history.post.length).toBe(1)); - expect(mockShowToast).toHaveBeenCalledWith('There is an error when creating the library unit'); + expect(mockShowToast).toHaveBeenCalledWith( + expect.stringMatching(new RegExp(`There is an error when creating the library ${containerType}`, 'i')) + ); }); it('shows a single block when usageKey query param is set', async () => { diff --git a/src/library-authoring/add-content/AddContent.test.tsx b/src/library-authoring/add-content/AddContent.test.tsx index 09f01bd174..3f12b6d27c 100644 --- a/src/library-authoring/add-content/AddContent.test.tsx +++ b/src/library-authoring/add-content/AddContent.test.tsx @@ -36,7 +36,7 @@ const { libraryId } = mockContentLibrary; const render = (collectionId?: string) => { const params: { libraryId: string, collectionId?: string } = { libraryId, collectionId }; return baseRender(, { - path: '/library/:libraryId/:collectionId?', + path: '/library/:libraryId/collection/:collectionId?', params, extraWrapper: ({ children }) => ( { const renderWithUnit = (unitId: string) => { const params: { libraryId: string, unitId?: string } = { libraryId, unitId }; return baseRender(, { - path: '/library/:libraryId/:unitId?', + path: '/library/:libraryId/unit/:unitId?', params, extraWrapper: ({ children }) => ( ', () => { }); }); - it('should not show collection/unit buttons when create component in container', async () => { + it('should not show collection, unit, section and subsection buttons when create component in unit', async () => { const unitId = 'lct:orf1:lib1:unit:test-1'; renderWithUnit(unitId); expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument(); + screen.debug(); expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Section' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Subsection' })).not.toBeInTheDocument(); }); it('should create a component in unit', async () => { diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx index e530fb2825..574410089c 100644 --- a/src/library-authoring/create-container/CreateContainerModal.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.tsx @@ -39,7 +39,7 @@ const CreateContainerModal = () => { if (isCreateSectionModalOpen) { return { modalTitle: intl.formatMessage(messages.createSectionModalTitle), - validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + validationError: intl.formatMessage(messages.createSectionModalNameInvalid), nameLabel: intl.formatMessage(messages.createSectionModalNameLabel), placeholder: intl.formatMessage(messages.createSectionModalNamePlaceholder), successMsg: intl.formatMessage(messages.createSectionSuccess), @@ -49,7 +49,7 @@ const CreateContainerModal = () => { if (isCreateSubsectionModalOpen) { return { modalTitle: intl.formatMessage(messages.createSubsectionModalTitle), - validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + validationError: intl.formatMessage(messages.createSubsectionModalNameInvalid), nameLabel: intl.formatMessage(messages.createSubsectionModalNameLabel), placeholder: intl.formatMessage(messages.createSubsectionModalNamePlaceholder), successMsg: intl.formatMessage(messages.createSubsectionSuccess), @@ -58,7 +58,7 @@ const CreateContainerModal = () => { } return { modalTitle: intl.formatMessage(messages.createUnitModalTitle), - validationError: intl.formatMessage(messages.createContainerModalNameInvalid), + validationError: intl.formatMessage(messages.createUnitModalNameInvalid), nameLabel: intl.formatMessage(messages.createUnitModalNameLabel), placeholder: intl.formatMessage(messages.createUnitModalNamePlaceholder), successMsg: intl.formatMessage(messages.createUnitSuccess), diff --git a/src/library-authoring/create-container/messages.ts b/src/library-authoring/create-container/messages.ts index 94e7b774f3..25d576f48d 100644 --- a/src/library-authoring/create-container/messages.ts +++ b/src/library-authoring/create-container/messages.ts @@ -56,10 +56,20 @@ const messages = defineMessages({ defaultMessage: 'Give a descriptive title', description: 'Placeholder of the Name field of the Create Section modal form', }, - createContainerModalNameInvalid: { - id: 'course-authoring.library-authoring.modals.create-container.form.name.invalid', - defaultMessage: 'Container name is required', - description: 'Message when the Name field of the Create Container modal form is invalid', + createUnitModalNameInvalid: { + id: 'course-authoring.library-authoring.modals.create-unit.form.name.invalid', + defaultMessage: 'Unit name is required', + description: 'Message when the Name field of the Create Unit modal form is invalid', + }, + createSectionModalNameInvalid: { + id: 'course-authoring.library-authoring.modals.create-section.form.name.invalid', + defaultMessage: 'Section name is required', + description: 'Message when the Name field of the Create Section modal form is invalid', + }, + createSubsectionModalNameInvalid: { + id: 'course-authoring.library-authoring.modals.create-subsection.form.name.invalid', + defaultMessage: 'Subsection name is required', + description: 'Message when the Name field of the Create Subsection modal form is invalid', }, createSectionSuccess: { id: 'course-authoring.library-authoring.modals.create-section.success', From 3e7c9a576c5cad573b8b6f7b7e326777f1f24cd1 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 21 May 2025 20:54:44 +0530 Subject: [PATCH 04/11] chore: fix lint issues --- src/library-authoring/LibraryAuthoringPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 208d3db655..ec6def40bf 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -889,7 +889,7 @@ describe('', () => { // Check error toast await waitFor(() => expect(axiosMock.history.post.length).toBe(1)); expect(mockShowToast).toHaveBeenCalledWith( - expect.stringMatching(new RegExp(`There is an error when creating the library ${containerType}`, 'i')) + expect.stringMatching(new RegExp(`There is an error when creating the library ${containerType}`, 'i')), ); }); From 115ccea8049ea376c6f3754de3439b24a2f1b9a1 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 22 May 2025 11:53:29 +0530 Subject: [PATCH 05/11] refactor: add content component --- .../add-content/AddContent.tsx | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index 1d501faf58..df03f8be67 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -88,7 +88,6 @@ const AddContentView = ({ }: AddContentViewProps) => { const intl = useIntl(); const { - collectionId, componentPicker, unitId, } = useLibraryContext(); @@ -132,35 +131,51 @@ const AddContentView = ({ const extraFilter = unitId ? ['NOT block_type = "unit"', 'NOT type = "collections"'] : undefined; const visibleTabs = unitId ? [LibraryContentTypes.components] : undefined; + /** List container content types that should be displayed based on current path */ + const visibleContentTypes = useMemo(() => { + if (insideCollection) { + // except for add collection button, show everthing. + return [ + libraryContentButtonData, + sectionButtonData, + subsectionButtonData, + unitButtonData, + ]; + } + if (insideUnit) { + // Only show libraryContentButton + return [libraryContentButtonData]; + } + if (insideSection) { + // Should only allow adding subsections + return [subsectionButtonData]; + } + if (insideSubsection) { + // Should only allow adding units + return [unitButtonData]; + } + // except for libraryContentButton, show everthing. + return [ + collectionButtonData, + sectionButtonData, + subsectionButtonData, + unitButtonData, + ]; + }, [insideCollection, insideUnit, insideSection, insideSubsection]); + return ( <> - {((collectionId && insideCollection) || (unitId && insideUnit)) && componentPicker && ( + {visibleContentTypes.map((contentType) => ( + + ))} + {componentPicker && visibleContentTypes.includes(libraryContentButtonData) && ( /// Show the "Add Library Content" button for units and collections - <> - - - - )} - {!insideCollection && !insideUnit && !insideSubsection && !insideSection && ( - // Doesn't show the "Collection" button if we are in a unit or collection - - )} - {!(insideUnit || insideSubsection || insideSection) && ( - // Doesn't show the "Section" button if we are in a unit or section or subsection - - )} - {!(insideSubsection || insideUnit) && ( - // Doesn't show the "Subsection" button if we are in a unit or Subsection - - )} - {!(insideUnit || insideSection) && ( - // Doesn't show the "Unit" button if we are in a unit or section - + )}
{/* Note: for MVP we are hiding the unuspported types, not just disabling them. */} From 56e4d2068c9c9e81461ab8ffcd2e4560a8f22c34 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 22 May 2025 12:06:33 +0530 Subject: [PATCH 06/11] docs: add docs to create container modal component --- .../create-container/CreateContainerModal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx index 574410089c..9fbddc47e6 100644 --- a/src/library-authoring/create-container/CreateContainerModal.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.tsx @@ -17,6 +17,7 @@ import LoadingButton from '../../generic/loading-button'; import { ContainerType } from '../../generic/key-utils'; import { useLibraryRoutes } from '../routes'; +/** Common modal to create section, subsection or unit in library */ const CreateContainerModal = () => { const intl = useIntl(); const navigate = useNavigate(); @@ -35,6 +36,7 @@ const CreateContainerModal = () => { const updateItemsMutation = useAddItemsToCollection(libraryId, collectionId); const { showToast } = React.useContext(ToastContext); + /** labels based on the type of modal open, i.e., section, subsection or unit */ const labels = React.useMemo(() => { if (isCreateSectionModalOpen) { return { @@ -66,12 +68,14 @@ const CreateContainerModal = () => { }; }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); + /** Call close for section, subsection and unit as the operation is idempotent */ const handleClose = React.useCallback(() => { closeCreateSectionModal(); closeCreateSubsectionModal(); closeCreateUnitModal(); }, [closeCreateUnitModal, closeCreateSubsectionModal, closeCreateSectionModal]); + /** Calculate containerType based on type of open modal */ const containerType = React.useMemo(() => { if (isCreateSectionModalOpen) { return ContainerType.Section; @@ -80,7 +84,7 @@ const CreateContainerModal = () => { return ContainerType.Subsection; } return ContainerType.Unit; - }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); + }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen]); const handleCreate = React.useCallback(async (values) => { try { From 8ad44bc3815ea02bafe1ef87931261b3cab8dff2 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 23 May 2025 15:13:12 +0530 Subject: [PATCH 07/11] fix: apply review suggestions --- src/generic/key-utils.ts | 2 +- .../add-content/AddContent.test.tsx | 1 - src/library-authoring/add-content/AddContent.tsx | 12 +++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index 56e728c715..4c0a3eaa4a 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -55,7 +55,7 @@ export enum ContainerType { Subsection = 'subsection', Unit = 'unit', /** - * Chapter, Sequential and Vertical is the old name for Unit. + * Chapter, Sequential and Vertical are the old names for section, subsection an unit. * Generally, **please avoid using this term entirely in any libraries code** or * anything based on the new Learning Core "Containers" framework - just call it a unit. We do still need to use this * in the modulestore-based courseware, and currently the /xblock/ API used to copy library containers into courses diff --git a/src/library-authoring/add-content/AddContent.test.tsx b/src/library-authoring/add-content/AddContent.test.tsx index 3f12b6d27c..ac686b1ed5 100644 --- a/src/library-authoring/add-content/AddContent.test.tsx +++ b/src/library-authoring/add-content/AddContent.test.tsx @@ -331,7 +331,6 @@ describe('', () => { renderWithUnit(unitId); expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument(); - screen.debug(); expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument(); diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index df03f8be67..cd73cbd14d 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -148,11 +148,13 @@ const AddContentView = ({ } if (insideSection) { // Should only allow adding subsections - return [subsectionButtonData]; + throw new Error('Not implemented'); + // return [subsectionButtonData]; } if (insideSubsection) { // Should only allow adding units - return [unitButtonData]; + throw new Error('Not implemented'); + // return [unitButtonData]; } // except for libraryContentButton, show everthing. return [ @@ -166,7 +168,11 @@ const AddContentView = ({ return ( <> {visibleContentTypes.map((contentType) => ( - + ))} {componentPicker && visibleContentTypes.includes(libraryContentButtonData) && ( /// Show the "Add Library Content" button for units and collections From 193b0fa42d642224be6fe8edfe7ae41508b51534 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 23 May 2025 15:29:13 +0530 Subject: [PATCH 08/11] temp: fix coverage --- src/library-authoring/add-content/AddContent.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index cd73cbd14d..10659abe23 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -146,11 +146,13 @@ const AddContentView = ({ // Only show libraryContentButton return [libraryContentButtonData]; } + // istanbul ignore if if (insideSection) { // Should only allow adding subsections throw new Error('Not implemented'); // return [subsectionButtonData]; } + // istanbul ignore if if (insideSubsection) { // Should only allow adding units throw new Error('Not implemented'); From ccf84c92103c61cb242305f351e06a768f694f8b Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Sat, 24 May 2025 11:34:58 +0530 Subject: [PATCH 09/11] docs: fix typo --- src/generic/key-utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/generic/key-utils.ts b/src/generic/key-utils.ts index 4c0a3eaa4a..63498927ae 100644 --- a/src/generic/key-utils.ts +++ b/src/generic/key-utils.ts @@ -55,12 +55,12 @@ export enum ContainerType { Subsection = 'subsection', Unit = 'unit', /** - * Chapter, Sequential and Vertical are the old names for section, subsection an unit. + * Chapter, Sequential and Vertical are the old names for section, subsection and unit. * Generally, **please avoid using this term entirely in any libraries code** or - * anything based on the new Learning Core "Containers" framework - just call it a unit. We do still need to use this - * in the modulestore-based courseware, and currently the /xblock/ API used to copy library containers into courses - * also requires specifying this, though that should change to a better API that does the unit->vertical conversion - * automatically in the future. + * anything based on the new Learning Core "Containers" framework - just call it a unit, section or subsection. We + * do still need to use this in the modulestore-based courseware, and currently the /xblock/ API used to copy + * library containers into courses also requires specifying this, though that should change to a better API + * that does the unit->vertical conversion automatically in the future. */ Chapter = 'chapter', Sequential = 'sequential', From 3e016c2b0ec3cdd7608069437bf702c74ef48e03 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Sat, 24 May 2025 12:13:48 +0530 Subject: [PATCH 10/11] refactor: replace multiple toggles by single state to open container modals --- .../add-content/AddContent.tsx | 17 +++++----- .../common/context/LibraryContext.tsx | 34 ++++--------------- .../create-container/CreateContainerModal.tsx | 28 ++++++--------- 3 files changed, 25 insertions(+), 54 deletions(-) diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index 10659abe23..48be2ba3d3 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -33,6 +33,7 @@ import { ContentType as LibraryContentTypes, useLibraryRoutes } from '../routes' import genericMessages from '../generic/messages'; import messages from './messages'; import type { BlockTypeMetadata } from '../data/api'; +import { ContainerType } from '../../generic/key-utils'; type ContentType = { name: string, @@ -266,9 +267,7 @@ const AddContent = () => { libraryId, collectionId, openCreateCollectionModal, - openCreateUnitModal, - openCreateSectionModal, - openCreateSubsectionModal, + setCreateContainerModalType, openComponentEditor, unitId, } = useLibraryContext(); @@ -429,12 +428,12 @@ const AddContent = () => { showAddLibraryContentModal(); } else if (blockType === 'advancedXBlock') { showAdvancedList(); - } else if (blockType === 'vertical') { - openCreateUnitModal(); - } else if (blockType === 'chapter') { - openCreateSectionModal(); - } else if (blockType === 'sequential') { - openCreateSubsectionModal(); + } else if ([ + ContainerType.Vertical, + ContainerType.Chapter, + ContainerType.Sequential, + ].includes(blockType as ContainerType)) { + setCreateContainerModalType(blockType as ContainerType); } else { onCreateBlock(blockType); } diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index a082bf305d..40bd3a69be 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -7,6 +7,7 @@ import { useState, } from 'react'; import { useParams } from 'react-router-dom'; +import { ContainerType } from '../../../generic/key-utils'; import type { ComponentPicker } from '../../component-picker'; import type { ContentLibrary, BlockTypeMetadata } from '../../data/api'; @@ -40,18 +41,9 @@ export type LibraryContextData = { isCreateCollectionModalOpen: boolean; openCreateCollectionModal: () => void; closeCreateCollectionModal: () => void; - // "Create New Section" modal - isCreateSectionModalOpen: boolean; - openCreateSectionModal: () => void; - closeCreateSectionModal: () => void; - // "Create New Subsection" modal - isCreateSubsectionModalOpen: boolean; - openCreateSubsectionModal: () => void; - closeCreateSubsectionModal: () => void; - // "Create New Unit" modal - isCreateUnitModalOpen: boolean; - openCreateUnitModal: () => void; - closeCreateUnitModal: () => void; + // "Create new container" modal + createContainerModalType: ContainerType | undefined; + setCreateContainerModalType: (containerType?: ContainerType) => void; // Editor modal - for editing some component /** If the editor is open and the user is editing some component, this is the component being edited. */ componentBeingEdited: ComponentEditorInfo | undefined; @@ -99,9 +91,7 @@ export const LibraryProvider = ({ componentPicker, }: LibraryProviderProps) => { const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false); - const [isCreateUnitModalOpen, openCreateUnitModal, closeCreateUnitModal] = useToggle(false); - const [isCreateSectionModalOpen, openCreateSectionModal, closeCreateSectionModal] = useToggle(false); - const [isCreateSubsectionModalOpen, openCreateSubsectionModal, closeCreateSubsectionModal] = useToggle(false); + const [createContainerModalType, setCreateContainerModalType] = useState(undefined); const [componentBeingEdited, setComponentBeingEdited] = useState(); const closeComponentEditor = useCallback((data) => { setComponentBeingEdited((prev) => { @@ -157,15 +147,8 @@ export const LibraryProvider = ({ isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal, - isCreateUnitModalOpen, - openCreateUnitModal, - closeCreateUnitModal, - isCreateSectionModalOpen, - openCreateSectionModal, - closeCreateSectionModal, - isCreateSubsectionModalOpen, - openCreateSubsectionModal, - closeCreateSubsectionModal, + createContainerModalType, + setCreateContainerModalType, componentBeingEdited, openComponentEditor, closeComponentEditor, @@ -189,9 +172,6 @@ export const LibraryProvider = ({ isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal, - isCreateUnitModalOpen, - openCreateUnitModal, - closeCreateUnitModal, componentBeingEdited, openComponentEditor, closeComponentEditor, diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx index 9fbddc47e6..752a450ad5 100644 --- a/src/library-authoring/create-container/CreateContainerModal.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.tsx @@ -24,12 +24,8 @@ const CreateContainerModal = () => { const { collectionId, libraryId, - isCreateSectionModalOpen, - closeCreateSectionModal, - isCreateSubsectionModalOpen, - closeCreateSubsectionModal, - isCreateUnitModalOpen, - closeCreateUnitModal, + createContainerModalType, + setCreateContainerModalType, } = useLibraryContext(); const { insideCollection } = useLibraryRoutes(); const create = useCreateLibraryContainer(libraryId); @@ -38,7 +34,7 @@ const CreateContainerModal = () => { /** labels based on the type of modal open, i.e., section, subsection or unit */ const labels = React.useMemo(() => { - if (isCreateSectionModalOpen) { + if (createContainerModalType === ContainerType.Chapter) { return { modalTitle: intl.formatMessage(messages.createSectionModalTitle), validationError: intl.formatMessage(messages.createSectionModalNameInvalid), @@ -48,7 +44,7 @@ const CreateContainerModal = () => { errorMsg: intl.formatMessage(messages.createSectionError), }; } - if (isCreateSubsectionModalOpen) { + if (createContainerModalType === ContainerType.Sequential) { return { modalTitle: intl.formatMessage(messages.createSubsectionModalTitle), validationError: intl.formatMessage(messages.createSubsectionModalNameInvalid), @@ -66,25 +62,21 @@ const CreateContainerModal = () => { successMsg: intl.formatMessage(messages.createUnitSuccess), errorMsg: intl.formatMessage(messages.createUnitError), }; - }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen, intl]); + }, [createContainerModalType]); /** Call close for section, subsection and unit as the operation is idempotent */ - const handleClose = React.useCallback(() => { - closeCreateSectionModal(); - closeCreateSubsectionModal(); - closeCreateUnitModal(); - }, [closeCreateUnitModal, closeCreateSubsectionModal, closeCreateSectionModal]); + const handleClose = () => setCreateContainerModalType(undefined); /** Calculate containerType based on type of open modal */ const containerType = React.useMemo(() => { - if (isCreateSectionModalOpen) { + if (createContainerModalType === ContainerType.Chapter) { return ContainerType.Section; } - if (isCreateSubsectionModalOpen) { + if (createContainerModalType === ContainerType.Sequential) { return ContainerType.Subsection; } return ContainerType.Unit; - }, [isCreateUnitModalOpen, isCreateSectionModalOpen, isCreateSubsectionModalOpen]); + }, [createContainerModalType]); const handleCreate = React.useCallback(async (values) => { try { @@ -109,7 +101,7 @@ const CreateContainerModal = () => { return ( Date: Sat, 24 May 2025 12:28:32 +0530 Subject: [PATCH 11/11] test: container creation in collection --- .../LibraryAuthoringPage.test.tsx | 30 +++++++------- .../LibraryCollectionPage.test.tsx | 41 ++++++++++++------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index ec6def40bf..1fc72f368a 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -768,18 +768,18 @@ describe('', () => { // Open New container Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; - fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); - expect(unitModalHeading).toBeInTheDocument(); + const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; + fireEvent.click(newContainerButton); + const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); + expect(containerModalHeading).toBeInTheDocument(); // Click on Cancel button const cancelButton = screen.getByRole('button', { name: /cancel/i }); fireEvent.click(cancelButton); - expect(unitModalHeading).not.toBeInTheDocument(); + expect(containerModalHeading).not.toBeInTheDocument(); - // Open new unit modal again and create a container - fireEvent.click(newUnitButton); + // Open new container modal again and create a container + fireEvent.click(newContainerButton); const createButton = screen.getByRole('button', { name: /create/i }); const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') }); @@ -829,10 +829,10 @@ describe('', () => { // Open New container Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; - fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); - expect(unitModalHeading).toBeInTheDocument(); + const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; + fireEvent.click(newContainerButton); + const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); + expect(containerModalHeading).toBeInTheDocument(); const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') }); fireEvent.focus(nameField); @@ -874,10 +874,10 @@ describe('', () => { // Open New container Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; - fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); - expect(unitModalHeading).toBeInTheDocument(); + const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; + fireEvent.click(newContainerButton); + const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); + expect(containerModalHeading).toBeInTheDocument(); // Create a container const createButton = screen.getByRole('button', { name: /create/i }); diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index d9a5768301..0e90d4d571 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -429,14 +429,27 @@ describe('', () => { await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument()); }); - it('should create a unit inside a collection', async () => { + test.each([ + { + label: 'should create a unit inside a collection', + containerType: 'unit', + }, + { + label: 'should create a section inside a collection', + containerType: 'section', + }, + { + label: 'should create a subsection inside a collection', + containerType: 'subsection', + }, + ])('$label', async ({ containerType }) => { await renderLibraryCollectionPage(); - const unitTitle = 'This is a Test'; + const containerTitle = `This is a Test ${containerType}`; const containerUrl = getLibraryContainersApiUrl(mockContentLibrary.libraryId); axiosMock.onPost(containerUrl).reply(200, { - id: 'unit-1', + id: 'container-id', slug: 'this-is-a-test', - title: unitTitle, + title: containerTitle, }); const collectionUrl = getLibraryCollectionItemsApiUrl( mockContentLibrary.libraryId, @@ -454,16 +467,16 @@ describe('', () => { // Open New unit Modal const sidebar = screen.getByTestId('library-sidebar'); - const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0]; - fireEvent.click(newUnitButton); - const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i }); - expect(unitModalHeading).toBeInTheDocument(); + const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0]; + fireEvent.click(newContainerButton); + const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') }); + expect(containerModalHeading).toBeInTheDocument(); // Fill the form const createButton = screen.getByRole('button', { name: /create/i }); - const nameField = screen.getByRole('textbox', { name: /name your unit/i }); + const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') }); - fireEvent.change(nameField, { target: { value: unitTitle } }); + fireEvent.change(nameField, { target: { value: containerTitle } }); fireEvent.click(createButton); // Check success @@ -471,13 +484,13 @@ describe('', () => { // Check that the unit was created expect(axiosMock.history.post[0].url).toBe(containerUrl); - expect(axiosMock.history.post[0].data).toContain(`"display_name":"${unitTitle}"`); - expect(axiosMock.history.post[0].data).toContain('"container_type":"unit"'); - expect(mockShowToast).toHaveBeenCalledWith('Unit created successfully'); + expect(axiosMock.history.post[0].data).toContain(`"display_name":"${containerTitle}"`); + expect(axiosMock.history.post[0].data).toContain(`"container_type":"${containerType}"`); + expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${containerType} created successfully`, 'i'))); // Check that the unit was added to the collection expect(axiosMock.history.patch.length).toBe(1); expect(axiosMock.history.patch[0].url).toBe(collectionUrl); - expect(axiosMock.history.patch[0].data).toContain('"usage_keys":["unit-1"]'); + expect(axiosMock.history.patch[0].data).toContain('"usage_keys":["container-id"]'); }); });