diff --git a/src/course-unit/CourseUnit.tsx b/src/course-unit/CourseUnit.tsx index 5e4c879dd0..75522ee767 100644 --- a/src/course-unit/CourseUnit.tsx +++ b/src/course-unit/CourseUnit.tsx @@ -191,6 +191,7 @@ const CourseUnit = () => { isUnitLegacyLibraryType, isSplitTestType, isProblemBankType, + isGenericContainerType, staticFileNotices, currentlyVisibleToStudents, unitXBlockActions, @@ -375,6 +376,7 @@ const CourseUnit = () => { isSplitTestType={isSplitTestType} isUnitVerticalType={isUnitVerticalType} isProblemBankType={isProblemBankType} + isGenericContainerType={isGenericContainerType} handleCreateNewCourseXBlock={handleCreateNewCourseXBlock} addComponentTemplateData={addComponentTemplateData} /> diff --git a/src/course-unit/add-component/AddComponent.tsx b/src/course-unit/add-component/AddComponent.tsx index 9dd8078f41..b4456e5c5b 100644 --- a/src/course-unit/add-component/AddComponent.tsx +++ b/src/course-unit/add-component/AddComponent.tsx @@ -23,7 +23,7 @@ import { messageTypes } from '../constants'; import messages from './messages'; import AddComponentButton from './add-component-btn'; import ComponentModalView from './add-component-modals/ComponentModalView'; -import { getCourseSectionVertical, getCourseUnitData } from '../data/selectors'; +import { getCourseSectionVertical, getCourseUnitData, getCourseVerticalChildren } from '../data/selectors'; type ComponentTemplateData = { displayName: string; @@ -35,6 +35,9 @@ type ComponentTemplateData = { category?: string; displayName: string; supportLevel?: string | boolean; + disabled?: boolean; + disabledReason?: string; + singleInstance?: boolean; }>; supportLegend: { allowUnsupportedXblocks?: boolean; @@ -46,6 +49,7 @@ type ComponentTemplateData = { export interface AddComponentProps { isSplitTestType?: boolean; isUnitVerticalType?: boolean; + isGenericContainerType?: boolean; parentLocator: string; handleCreateNewCourseXBlock: ( args: object, @@ -64,6 +68,7 @@ const AddComponent = ({ isSplitTestType, isUnitVerticalType, isProblemBankType, + isGenericContainerType, addComponentTemplateData, handleCreateNewCourseXBlock, }: AddComponentProps) => { @@ -74,6 +79,12 @@ const AddComponent = ({ const [isOpenHtml, openHtml, closeHtml] = useToggle(false); const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false); const { componentTemplates = {} } = useSelector(getCourseSectionVertical); + const courseVerticalChildren = useSelector(getCourseVerticalChildren); + const existingBlockTypes = new Set( + (courseVerticalChildren?.children ?? []).map((child: { blockType?: string; category?: string; }) => + child.blockType ?? child.category + ), + ); const blockId = addComponentTemplateData?.parentLocator || parentLocator; const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle(); @@ -215,10 +226,10 @@ const AddComponent = ({ } }; - if (isUnitVerticalType || isSplitTestType || isProblemBankType) { + if (isUnitVerticalType || isSplitTestType || isProblemBankType || isGenericContainerType) { return (
- {Object.keys(componentTemplates).length && isUnitVerticalType ? + {Object.keys(componentTemplates).length && (isUnitVerticalType || isGenericContainerType) ? ( <>
{intl.formatMessage(messages.title)}
@@ -253,7 +264,13 @@ const AddComponent = ({ isOpen: isOpenOpenAssessment, }; break; - default: + default: { + const firstTemplate = component.templates[0]; + const isSingleInstanceSatisfied = !!firstTemplate?.singleInstance + && existingBlockTypes.has(firstTemplate.category ?? type); + const isDisabled = !!firstTemplate?.disabled || isSingleInstanceSatisfied; + const disabledReason = firstTemplate?.disabledReason + ?? (isSingleInstanceSatisfied ? 'Only one instance of this component is allowed.' : undefined); return (
  • ); + } } + const allTemplatesDisabled = component.templates.every( + (t) => !!t.disabled, + ); + const firstDisabledReason = component.templates.find((t) => t.disabledReason)?.disabledReason; return ( ); })} diff --git a/src/course-unit/add-component/add-component-btn/index.jsx b/src/course-unit/add-component/add-component-btn/index.jsx index 05ca8fa953..288cea526e 100644 --- a/src/course-unit/add-component/add-component-btn/index.jsx +++ b/src/course-unit/add-component/add-component-btn/index.jsx @@ -1,5 +1,10 @@ import PropTypes from 'prop-types'; -import { Badge, Button } from '@openedx/paragon'; +import { + Badge, + Button, + OverlayTrigger, + Tooltip, +} from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from '../messages'; @@ -10,14 +15,17 @@ const AddComponentButton = ({ displayName, onClick, beta, + disabled, + disabledReason, }) => { const intl = useIntl(); - return ( + const button = ( ); + + if (disabled && disabledReason) { + return ( + {disabledReason}} + > + {button} + + ); + } + return button; }; AddComponentButton.defaultProps = { beta: false, + disabled: false, + disabledReason: null, }; - AddComponentButton.propTypes = { type: PropTypes.string.isRequired, displayName: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, beta: PropTypes.bool, + disabled: PropTypes.bool, + disabledReason: PropTypes.string, }; export default AddComponentButton; diff --git a/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx b/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx index 6ca35afe5e..93a86ea5d7 100644 --- a/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx +++ b/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx @@ -15,6 +15,8 @@ const ComponentModalView = ({ modalParams, handleCreateNewXBlock, isRequestedModalView, + disabled, + disabledReason, }) => { const intl = useIntl(); const dispatch = useDispatch(); @@ -37,9 +39,11 @@ const ComponentModalView = ({ const renderAddComponentButton = () => (
  • ); @@ -102,6 +106,8 @@ const ComponentModalView = ({ ComponentModalView.defaultProps = { isRequestedModalView: false, + disabled: false, + disabledReason: null, }; ComponentModalView.propTypes = { @@ -111,6 +117,9 @@ ComponentModalView.propTypes = { isOpen: PropTypes.bool, }).isRequired, handleCreateNewXBlock: PropTypes.func.isRequired, + isRequestedModalView: PropTypes.bool, + disabled: PropTypes.bool, + disabledReason: PropTypes.string, component: PropTypes.shape({ displayName: PropTypes.string.isRequired, category: PropTypes.string, @@ -129,7 +138,6 @@ ComponentModalView.propTypes = { showLegend: PropTypes.bool, }), }).isRequired, - isRequestedModalView: PropTypes.bool, }; export default ComponentModalView; diff --git a/src/course-unit/hooks.tsx b/src/course-unit/hooks.tsx index c7724e4b6a..972da8d97d 100644 --- a/src/course-unit/hooks.tsx +++ b/src/course-unit/hooks.tsx @@ -84,6 +84,10 @@ export const useCourseUnit = ({ COURSE_BLOCK_NAMES.legacyLibraryContent.id, COURSE_BLOCK_NAMES.itembank.id, ].includes(unitCategory); + // True for generic container XBlocks that are not one of the specially-handled types above. + // These containers can also host add-component buttons via the MFE native strip. + const isGenericContainerType = !isUnitVerticalType && !isSplitTestType && !isProblemBankType && + !isUnitLegacyLibraryType && !!unitCategory; const headerNavigationsActions = { handleViewLive: () => { @@ -259,6 +263,7 @@ export const useCourseUnit = ({ isUnitLegacyLibraryType, isSplitTestType, isProblemBankType, + isGenericContainerType, sharedClipboardData, showPasteXBlock, showPasteUnit,