Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const CourseUnit = ({ courseId }) => {
isUnitVerticalType,
isUnitLibraryType,
isSplitTestType,
isProblemBankType,
staticFileNotices,
currentlyVisibleToStudents,
unitXBlockActions,
Expand Down Expand Up @@ -219,6 +220,7 @@ const CourseUnit = ({ courseId }) => {
parentLocator={blockId}
isSplitTestType={isSplitTestType}
isUnitVerticalType={isUnitVerticalType}
isProblemBankType={isProblemBankType}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
addComponentTemplateData={addComponentTemplateData}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}
/>
Expand Down Expand Up @@ -94,7 +92,7 @@ describe('<AddComponent />', () => {
),
});
expect(btn).toBeInTheDocument();
if (component.beta) {
if (componentTemplates[component].beta) {
expect(within(btn).queryByText('Beta')).toBeInTheDocument();
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,31 +20,67 @@ 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();

const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
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);
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -268,7 +303,7 @@ const AddComponent = ({
/>
</div>
</StandardModal>
{isXBlockEditorModalOpen && (
{isXBlockEditorModalOpen && courseId && blockType && newBlockId && (
<div className="editor-page">
<EditorPage
courseId={courseId}
Expand All @@ -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;
1 change: 1 addition & 0 deletions src/course-unit/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const getXBlockSupportMessages = (intl) => ({

export const messageTypes = {
refreshXBlock: 'refreshXBlock',
refreshIframe: 'refreshIframe',
Copy link
Contributor

@rpenido rpenido Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we calling this somewhere?

After adding a component, the iframe is not refreshing on my stack.

Adding it after the sendMessageToIframe(messageTypes.addSelectedComponentsToBank, { selectedComponents }); call didn't work.
Calling sendMessageToIframe(messageTypes.refreshXBlock, null); trigger a refresh, but some times it refresh before the component is fully added because of racing conditions.

Edit: My bad. The refresh worked after I ran tutor dev exec cms npm run build-dev.

showMoveXBlockModal: 'showMoveXBlockModal',
completeXBlockMoving: 'completeXBlockMoving',
rollbackMovedXBlock: 'rollbackMovedXBlock',
Expand Down
5 changes: 5 additions & 0 deletions src/course-unit/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {
Expand Down Expand Up @@ -254,6 +258,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
isUnitVerticalType,
isUnitLibraryType,
isSplitTestType,
isProblemBankType,
sharedClipboardData,
showPasteXBlock,
showPasteUnit,
Expand Down
1 change: 1 addition & 0 deletions src/course-unit/xblock-container-iframe/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const useMessageHandlers = ({
handleShowProcessingNotification,
handleHideProcessingNotification,
handleEditXBlock,
handleRefreshIframe,
}: UseMessageHandlersTypes): MessageHandlersTypes => {
const { copyToClipboard } = useClipboard();

Expand All @@ -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),
Expand Down
10 changes: 10 additions & 0 deletions src/course-unit/xblock-container-iframe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -199,6 +207,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
handleShowProcessingNotification,
handleHideProcessingNotification,
handleEditXBlock,
handleRefreshIframe,
});

useIframeMessages(messageHandlers);
Expand Down Expand Up @@ -268,6 +277,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
/>
) : null}
<iframe
key={iframeKey}
ref={iframeRef}
title={intl.formatMessage(messages.xblockIframeTitle)}
name="xblock-iframe"
Expand Down
15 changes: 4 additions & 11 deletions src/library-authoring/component-picker/ComponentPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down