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,