diff --git a/src/constants.ts b/src/constants.ts index 8c6bc544e0..12e65d401d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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'; diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index 10a9ad25b0..ee03c1bd09 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -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} /> diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.tsx similarity index 99% rename from src/course-unit/add-component/AddComponent.test.jsx rename to src/course-unit/add-component/AddComponent.test.tsx index fafffb0bb8..a9c55b9056 100644 --- a/src/course-unit/add-component/AddComponent.test.jsx +++ b/src/course-unit/add-component/AddComponent.test.tsx @@ -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( @@ -94,7 +92,7 @@ describe('', () => { ), }); expect(btn).toBeInTheDocument(); - if (component.beta) { + if (componentTemplates[component].beta) { expect(within(btn).queryByText('Beta')).toBeInTheDocument(); } }); diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.tsx similarity index 86% rename from src/course-unit/add-component/AddComponent.jsx rename to src/course-unit/add-component/AddComponent.tsx index e0770515ea..17695fb938 100644 --- a/src/course-unit/add-component/AddComponent.jsx +++ b/src/course-unit/add-component/AddComponent.tsx @@ -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'; @@ -21,14 +20,50 @@ import { useEventListener } from '../../generic/hooks'; import VideoSelectorPage from '../../editors/VideoSelectorPage'; import EditorPage from '../../editors/EditorPage'; import { fetchCourseSectionVerticalData } from '../data/thunk'; +import { SelectedComponent } from '../../library-authoring'; + +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(null); + const [courseId, setCourseId] = useState(null); + const [newBlockId, setNewBlockId] = useState(null); const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle(); - const [selectedComponents, setSelectedComponents] = useState([]); + const [selectedComponents, setSelectedComponents] = useState([]); 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 (
{Object.keys(componentTemplates).length && isUnitVerticalType ? ( <>
{intl.formatMessage(messages.title)}
    - {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 = ({ />
- {isXBlockEditorModalOpen && ( + {isXBlockEditorModalOpen && courseId && blockType && newBlockId && (
({ export const messageTypes = { refreshXBlock: 'refreshXBlock', + refreshIframe: 'refreshIframe', showMoveXBlockModal: 'showMoveXBlockModal', completeXBlockMoving: 'completeXBlockMoving', rollbackMovedXBlock: 'rollbackMovedXBlock', diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index dac48c5389..1109c5c8a6 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -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, diff --git a/src/course-unit/xblock-container-iframe/hooks/types.ts b/src/course-unit/xblock-container-iframe/hooks/types.ts index 9d06598fba..45d7fee5fa 100644 --- a/src/course-unit/xblock-container-iframe/hooks/types.ts +++ b/src/course-unit/xblock-container-iframe/hooks/types.ts @@ -15,6 +15,7 @@ export type UseMessageHandlersTypes = { handleOpenManageTagsModal: (id: string) => void; handleShowProcessingNotification: (variant: string) => void; handleHideProcessingNotification: () => void; + handleRefreshIframe: () => void; }; export type MessageHandlersTypes = Record void>; diff --git a/src/course-unit/xblock-container-iframe/hooks/useMessageHandlers.tsx b/src/course-unit/xblock-container-iframe/hooks/useMessageHandlers.tsx index afbab8a2a0..fdba8ecdc5 100644 --- a/src/course-unit/xblock-container-iframe/hooks/useMessageHandlers.tsx +++ b/src/course-unit/xblock-container-iframe/hooks/useMessageHandlers.tsx @@ -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), diff --git a/src/course-unit/xblock-container-iframe/index.tsx b/src/course-unit/xblock-container-iframe/index.tsx index 2507c2f8a1..5b67b340af 100644 --- a/src/course-unit/xblock-container-iframe/index.tsx +++ b/src/course-unit/xblock-container-iframe/index.tsx @@ -46,6 +46,8 @@ const XBlockContainerIframe: FC = ({ 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 = ({ 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 = ({ handleShowProcessingNotification, handleHideProcessingNotification, handleEditXBlock, + handleRefreshIframe, }); useIframeMessages(messageHandlers); @@ -268,6 +277,7 @@ const XBlockContainerIframe: FC = ({ /> ) : null}