fix: add components button in item bank children page (#2491)

`Add components` button in item bank children page was not working.
This commit is contained in:
Navin Karkera
2025-10-20 22:39:58 +05:30
committed by GitHub
parent 66dad5ff32
commit e80930e06f
10 changed files with 91 additions and 70 deletions

View File

@@ -62,6 +62,8 @@ export const COURSE_BLOCK_NAMES = ({
libraryContent: { id: 'library_content', name: 'Library content' },
splitTest: { id: 'split_test', name: 'Split Test' },
component: { id: 'component', name: 'Component' },
itembank: { id: 'itembank', name: 'Problem Bank' },
legacyLibraryContent: { id: 'library_content', name: 'Randomized Content Block' },
});
export const STUDIO_CLIPBOARD_CHANNEL = 'studio_clipboard_channel';

View File

@@ -50,6 +50,7 @@ const CourseUnit = ({ courseId }) => {
isUnitVerticalType,
isUnitLibraryType,
isSplitTestType,
isProblemBankType,
staticFileNotices,
currentlyVisibleToStudents,
unitXBlockActions,
@@ -219,6 +220,7 @@ const CourseUnit = ({ courseId }) => {
parentLocator={blockId}
isSplitTestType={isSplitTestType}
isUnitVerticalType={isUnitVerticalType}
isProblemBankType={isProblemBankType}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
addComponentTemplateData={addComponentTemplateData}
/>

View File

@@ -14,7 +14,7 @@ import { fetchCourseSectionVerticalData } from '../data/thunk';
import { getCourseSectionVerticalApiUrl } from '../data/api';
import { courseSectionVerticalMock } from '../__mocks__';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import AddComponent from './AddComponent';
import AddComponent, { AddComponentProps } from './AddComponent';
import messages from './messages';
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
import { messageTypes } from '../constants';
@@ -56,13 +56,11 @@ jest.mock('../../generic/hooks/context/hooks', () => ({
}),
}));
const renderComponent = (props) => render(
const renderComponent = (props?: AddComponentProps) => render(
<IframeProvider>
<AddComponent
blockId={blockId}
isUnitVerticalType
parentLocator={blockId}
addComponentTemplateData={{}}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
{...props}
/>
@@ -94,7 +92,7 @@ describe('<AddComponent />', () => {
),
});
expect(btn).toBeInTheDocument();
if (component.beta) {
if (componentTemplates[component].beta) {
expect(within(btn).queryByText('Beta')).toBeInTheDocument();
}
});

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
@@ -7,28 +6,64 @@ import {
ActionRow, Button, StandardModal, useToggle,
} from '@openedx/paragon';
import { getCourseSectionVertical, getCourseUnitData } from '../data/selectors';
import { useWaffleFlags } from '../../data/apiHooks';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn';
import messages from './messages';
import { ComponentPicker } from '../../library-authoring/component-picker';
import { ContentType } from '../../library-authoring/routes';
import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useEventListener } from '../../generic/hooks';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
import { useWaffleFlags } from '@src/data/apiHooks';
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
import { ComponentPicker } from '@src/library-authoring/component-picker';
import { ContentType } from '@src/library-authoring/routes';
import { useIframe } from '@src/generic/hooks/context/hooks';
import { useEventListener } from '@src/generic/hooks';
import VideoSelectorPage from '@src/editors/VideoSelectorPage';
import EditorPage from '@src/editors/EditorPage';
import { SelectedComponent } from '@src/library-authoring';
import { fetchCourseSectionVerticalData } from '../data/thunk';
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';
type ComponentTemplateData = {
displayName: string,
category?: string,
type: string,
beta?: boolean,
templates: Array<{
boilerplateName?: string,
category?: string,
displayName: string,
supportLevel?: string | boolean,
}>,
supportLegend: {
allowUnsupportedXblocks?: boolean,
documentationLabel?: string,
showLegend?: boolean,
},
};
export interface AddComponentProps {
isSplitTestType?: boolean,
isUnitVerticalType?: boolean,
parentLocator: string,
handleCreateNewCourseXBlock: (
args: object,
callback?: (args: { courseKey: string, locator: string }) => void
) => void,
isProblemBankType?: boolean,
addComponentTemplateData?: {
blockId: string,
parentLocator?: string,
model: ComponentTemplateData,
},
}
const AddComponent = ({
parentLocator,
isSplitTestType,
isUnitVerticalType,
isProblemBankType,
addComponentTemplateData,
handleCreateNewCourseXBlock,
}) => {
}: AddComponentProps) => {
const intl = useIntl();
const dispatch = useDispatch();
@@ -36,16 +71,16 @@ const AddComponent = ({
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator;
const blockId = addComponentTemplateData?.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState(null);
const [courseId, setCourseId] = useState(null);
const [newBlockId, setNewBlockId] = useState(null);
const [blockType, setBlockType] = useState<string | null>(null);
const [courseId, setCourseId] = useState<string | null>(null);
const [newBlockId, setNewBlockId] = useState<string | null>(null);
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]);
const [selectedComponents, setSelectedComponents] = useState<SelectedComponent[]>([]);
const [usageId, setUsageId] = useState(null);
const { sendMessageToIframe } = useIframe();
const { useVideoGalleryFlow } = useWaffleFlags(courseId ?? undefined);
@@ -84,7 +119,7 @@ const AddComponent = ({
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe, blockId, sequenceId]);
const handleLibraryV2Selection = useCallback((selection) => {
const handleLibraryV2Selection = useCallback((selection: SelectedComponent) => {
handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2,
category: selection.blockType,
@@ -94,7 +129,7 @@ const AddComponent = ({
closeAddLibraryContentModal();
}, [usageId]);
const handleCreateNewXBlock = (type, moduleName) => {
const handleCreateNewXBlock = (type: string, moduleName?: string) => {
switch (type) {
case COMPONENT_TYPES.discussion:
case COMPONENT_TYPES.dragAndDrop:
@@ -156,16 +191,16 @@ const AddComponent = ({
}
};
if (isUnitVerticalType || isSplitTestType) {
if (isUnitVerticalType || isSplitTestType || isProblemBankType) {
return (
<div className="py-4">
{Object.keys(componentTemplates).length && isUnitVerticalType ? (
<>
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
{componentTemplates.map((component) => {
{componentTemplates.map((component: ComponentTemplateData) => {
const { type, displayName, beta } = component;
let modalParams;
let modalParams: { open: () => void, close: () => void, isOpen: boolean };
if (!component.templates.length) {
return null;
@@ -268,7 +303,7 @@ const AddComponent = ({
/>
</div>
</StandardModal>
{isXBlockEditorModalOpen && (
{isXBlockEditorModalOpen && courseId && blockType && newBlockId && (
<div className="editor-page">
<EditorPage
courseId={courseId}
@@ -288,32 +323,4 @@ const AddComponent = ({
return null;
};
AddComponent.propTypes = {
isSplitTestType: PropTypes.bool.isRequired,
isUnitVerticalType: PropTypes.bool.isRequired,
parentLocator: PropTypes.string.isRequired,
handleCreateNewCourseXBlock: PropTypes.func.isRequired,
addComponentTemplateData: {
blockId: PropTypes.string.isRequired,
model: PropTypes.shape({
displayName: PropTypes.string.isRequired,
category: PropTypes.string,
type: PropTypes.string.isRequired,
templates: PropTypes.arrayOf(
PropTypes.shape({
boilerplateName: PropTypes.string,
category: PropTypes.string,
displayName: PropTypes.string.isRequired,
supportLevel: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
}),
),
supportLegend: PropTypes.shape({
allowUnsupportedXblocks: PropTypes.bool,
documentationLabel: PropTypes.string,
showLegend: PropTypes.bool,
}),
}),
},
};
export default AddComponent;

View File

@@ -41,6 +41,7 @@ export const getXBlockSupportMessages = (intl) => ({
export const messageTypes = {
refreshXBlock: 'refreshXBlock',
refreshIframe: 'refreshIframe',
showMoveXBlockModal: 'showMoveXBlockModal',
completeXBlockMoving: 'completeXBlockMoving',
rollbackMovedXBlock: 'rollbackMovedXBlock',

View File

@@ -72,6 +72,10 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
const isSplitTestType = unitCategory === COURSE_BLOCK_NAMES.splitTest.id;
const isProblemBankType = [
COURSE_BLOCK_NAMES.legacyLibraryContent.id,
COURSE_BLOCK_NAMES.itembank.id,
].includes(unitCategory);
const headerNavigationsActions = {
handleViewLive: () => {
@@ -254,6 +258,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
isUnitVerticalType,
isUnitLibraryType,
isSplitTestType,
isProblemBankType,
sharedClipboardData,
showPasteXBlock,
showPasteUnit,

View File

@@ -15,6 +15,7 @@ export type UseMessageHandlersTypes = {
handleOpenManageTagsModal: (id: string) => void;
handleShowProcessingNotification: (variant: string) => void;
handleHideProcessingNotification: () => void;
handleRefreshIframe: () => void;
};
export type MessageHandlersTypes = Record<string, (payload: any) => void>;

View File

@@ -31,6 +31,7 @@ export const useMessageHandlers = ({
handleShowProcessingNotification,
handleHideProcessingNotification,
handleEditXBlock,
handleRefreshIframe,
}: UseMessageHandlersTypes): MessageHandlersTypes => {
const { copyToClipboard } = useClipboard();
@@ -50,6 +51,7 @@ export const useMessageHandlers = ({
[messageTypes.saveEditedXBlockData]: handleSaveEditedXBlockData,
[messageTypes.studioAjaxError]: ({ error }) => handleResponseErrors(error, dispatch, updateSavingStatus),
[messageTypes.refreshPositions]: handleFinishXBlockDragging,
[messageTypes.refreshIframe]: handleRefreshIframe,
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),

View File

@@ -46,6 +46,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
const intl = useIntl();
const dispatch = useDispatch();
// Useful to reload iframe
const [iframeKey, setIframeKey] = useState(0);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isUnlinkModalOpen, openUnlinkModal, closeUnlinkModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
@@ -182,6 +184,12 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
dispatch(hideProcessingNotification());
};
const handleRefreshIframe = () => {
// Updating iframeKey forces the iframe to re-render.
/* istanbul ignore next */
setIframeKey((prev) => prev + 1);
};
const messageHandlers = useMessageHandlers({
courseId,
dispatch,
@@ -199,6 +207,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
handleShowProcessingNotification,
handleHideProcessingNotification,
handleEditXBlock,
handleRefreshIframe,
});
useIframeMessages(messageHandlers);
@@ -268,6 +277,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
/>
) : null}
<iframe
key={iframeKey}
ref={iframeRef}
title={intl.formatMessage(messages.xblockIframeTitle)}
name="xblock-iframe"

View File

@@ -53,17 +53,10 @@ type ComponentPickerProps = {
showOnlyPublished?: boolean,
extraFilter?: string[],
visibleTabs?: ContentType[],
} & (
{
componentPickerMode?: 'single',
onComponentSelected?: ComponentSelectedEvent,
onChangeComponentSelection?: never,
} | {
componentPickerMode: 'multiple'
onComponentSelected?: never,
onChangeComponentSelection?: ComponentSelectionChangedEvent,
}
);
componentPickerMode?: 'single' | 'multiple',
onComponentSelected?: ComponentSelectedEvent,
onChangeComponentSelection?: ComponentSelectionChangedEvent,
};
export const ComponentPicker: React.FC<ComponentPickerProps> = ({
/** Restrict the component picker to a specific library */