From 3685dbd6a1343ae63b3193358cea899430f592e4 Mon Sep 17 00:00:00 2001 From: Ihor Romaniuk Date: Tue, 1 Apr 2025 16:37:14 +0200 Subject: [PATCH] feat: [FC-0070] rendering split test content in unit page (#1492) Introduces functionality to display Split Test Content within the new Unit page interface. --- src/constants.js | 1 + src/course-unit/CourseUnit.jsx | 24 +- src/course-unit/CourseUnit.scss | 1 + src/course-unit/CourseUnit.test.jsx | 331 ++++++++++++++---- .../add-component/AddComponent.jsx | 243 +++++++------ .../add-component/AddComponent.test.jsx | 3 + .../ComponentModalView.jsx | 24 +- src/course-unit/breadcrumbs/Breadcrumbs.scss | 2 +- src/course-unit/breadcrumbs/Breadcrumbs.tsx | 66 ++-- src/course-unit/constants.js | 8 + src/course-unit/context/iFrameContext.tsx | 9 +- src/course-unit/data/thunk.js | 21 +- .../header-navigations/HeaderNavigations.jsx | 9 +- .../HeaderNavigations.test.jsx | 21 +- src/course-unit/header-title/HeaderTitle.jsx | 13 +- src/course-unit/hooks.jsx | 29 +- src/course-unit/sidebar/Sidebar.scss | 15 + .../sidebar/SplitTestSidebarInfo.tsx | 58 +++ src/course-unit/sidebar/messages.js | 55 +++ .../xblock-container-iframe/hooks/types.ts | 3 + .../hooks/useMessageHandlers.tsx | 11 +- .../xblock-container-iframe/index.scss | 4 + .../xblock-container-iframe/index.tsx | 43 ++- .../xblock-container-iframe/types.ts | 1 + .../configure-modal/ConfigureModal.jsx | 4 +- src/generic/configure-modal/UnitTab.jsx | 20 +- src/generic/configure-modal/messages.js | 6 +- src/generic/sub-header/SubHeader.jsx | 6 +- src/utils.js | 2 +- 29 files changed, 775 insertions(+), 258 deletions(-) create mode 100644 src/course-unit/sidebar/SplitTestSidebarInfo.tsx create mode 100644 src/course-unit/xblock-container-iframe/index.scss diff --git a/src/constants.js b/src/constants.js index 7439961ff..849247d39 100644 --- a/src/constants.js +++ b/src/constants.js @@ -59,6 +59,7 @@ export const COURSE_BLOCK_NAMES = ({ sequential: { id: 'sequential', name: 'Subsection' }, vertical: { id: 'vertical', name: 'Unit' }, libraryContent: { id: 'library_content', name: 'Library content' }, + splitTest: { id: 'split_test', name: 'Split Test' }, component: { id: 'component', name: 'Component' }, }); diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index b4932e8e1..87f2d7646 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -28,6 +28,7 @@ import Breadcrumbs from './breadcrumbs/Breadcrumbs'; import HeaderNavigations from './header-navigations/HeaderNavigations'; import Sequence from './course-sequence'; import Sidebar from './sidebar'; +import SplitTestSidebarInfo from './sidebar/SplitTestSidebarInfo'; import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks'; import messages from './messages'; import PublishControls from './sidebar/PublishControls'; @@ -52,6 +53,7 @@ const CourseUnit = ({ courseId }) => { isTitleEditFormOpen, isUnitVerticalType, isUnitLibraryType, + isSplitTestType, staticFileNotices, currentlyVisibleToStudents, unitXBlockActions, @@ -72,6 +74,7 @@ const CourseUnit = ({ courseId }) => { handleRollbackMovedXBlock, handleCloseXBlockMovedAlert, handleNavigateToTargetUnit, + addComponentTemplateData, } = useCourseUnit({ courseId, blockId }); const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType); @@ -155,7 +158,7 @@ const CourseUnit = ({ courseId }) => { )} headerActions={( )} @@ -188,16 +191,18 @@ const CourseUnit = ({ courseId }) => { - {isUnitVerticalType && ( - - )} + {showPasteXBlock && canPasteComponent && isUnitVerticalType && ( { )} + {isSplitTestType && ( + + + + )} diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index abc649b98..bd0806454 100644 --- a/src/course-unit/CourseUnit.scss +++ b/src/course-unit/CourseUnit.scss @@ -5,6 +5,7 @@ @import "./header-title/HeaderTitle"; @import "./move-modal"; @import "./preview-changes"; +@import "./xblock-container-iframe"; .course-unit { min-width: 900px; diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 083cc8c80..020c1e2b0 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -48,10 +48,8 @@ import { executeThunk } from '../utils'; import { IFRAME_FEATURE_POLICY } from '../constants'; import pasteComponentMessages from '../generic/clipboard/paste-component/messages'; import pasteNotificationsMessages from './clipboard/paste-notification/messages'; -import headerNavigationsMessages from './header-navigations/messages'; import headerTitleMessages from './header-title/messages'; import courseSequenceMessages from './course-sequence/messages'; -import sidebarMessages from './sidebar/messages'; import { extractCourseUnitId } from './sidebar/utils'; import CourseUnit from './CourseUnit'; @@ -64,6 +62,8 @@ import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants import { IframeProvider } from './context/iFrameContext'; import moveModalMessages from './move-modal/messages'; import xblockContainerIframeMessages from './xblock-container-iframe/messages'; +import headerNavigationsMessages from './header-navigations/messages'; +import sidebarMessages from './sidebar/messages'; import messages from './messages'; let axiosMock; @@ -196,7 +196,7 @@ describe('', () => { const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`); expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY); - expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;'); + expect(iframe).toHaveAttribute('style', 'height: 0px;'); expect(iframe).toHaveAttribute('scrolling', 'no'); expect(iframe).toHaveAttribute('referrerpolicy', 'origin'); expect(iframe).toHaveAttribute('loading', 'lazy'); @@ -209,11 +209,11 @@ describe('', () => { await waitFor(() => { const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); - expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;'); + expect(iframe).toHaveAttribute('style', 'height: 0px;'); simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, { courseXBlockDropdownHeight: 200, }); - expect(iframe).toHaveAttribute('style', 'width: 100%; height: 200px;'); + expect(iframe).toHaveAttribute('style', 'height: 200px;'); }); }); @@ -1821,7 +1821,7 @@ describe('', () => { }); describe('XBlock restrict access', () => { - it('opens xblock restrict access modal successfully', () => { + it('opens xblock restrict access modal successfully', async () => { const { getByTitle, getByTestId, } = render(); @@ -1830,7 +1830,7 @@ describe('', () => { const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage; const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage; - waitFor(() => { + await waitFor(() => { const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const usageId = courseVerticalChildrenMock.children[0].block_id; expect(iframe).toBeInTheDocument(); @@ -1840,7 +1840,7 @@ describe('', () => { }); }); - waitFor(() => { + await waitFor(() => { const configureModal = getByTestId('configure-modal'); expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument(); @@ -1854,7 +1854,7 @@ describe('', () => { getByTitle, queryByTestId, getByTestId, } = render(); - waitFor(() => { + await waitFor(() => { const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); expect(iframe).toBeInTheDocument(); simulatePostMessageEvent(messageTypes.manageXBlockAccess, { @@ -1862,7 +1862,7 @@ describe('', () => { }); }); - waitFor(() => { + await waitFor(() => { const configureModal = getByTestId('configure-modal'); expect(configureModal).toBeInTheDocument(); userEvent.click(within(configureModal).getByRole('button', { @@ -1883,85 +1883,104 @@ describe('', () => { .reply(200, { dummy: 'value' }); const { - getByTitle, getByRole, getByTestId, + getByTitle, getByRole, getByTestId, queryByTestId, } = render(); const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name; const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name; - waitFor(() => { + await waitFor(() => { const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); expect(iframe).toBeInTheDocument(); + }); + + await act(async () => { simulatePostMessageEvent(messageTypes.manageXBlockAccess, { usageId: courseVerticalChildrenMock.children[0].block_id, }); }); - waitFor(() => { - const configureModal = getByTestId('configure-modal'); - expect(configureModal).toBeInTheDocument(); + const configureModal = await waitFor(() => getByTestId('configure-modal')); + expect(configureModal).toBeInTheDocument(); - expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument(); - expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument(); + expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument(); + expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument(); - const restrictAccessSelect = getByRole('combobox', { - name: configureModalMessages.restrictAccessTo.defaultMessage, - }); - - userEvent.selectOptions(restrictAccessSelect, '0'); - - // eslint-disable-next-line array-callback-return - userPartitionInfoFormatted.selectablePartitions[0].groups.map((group) => { - expect(within(configureModal).getByRole('checkbox', { name: group.name })).not.toBeChecked(); - expect(within(configureModal).queryByText(group.name)).toBeInTheDocument(); - }); - - const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 }); - userEvent.click(group1Checkbox); - expect(group1Checkbox).toBeChecked(); - - const saveModalBtnText = within(configureModal).getByRole('button', { - name: configureModalMessages.saveButton.defaultMessage, - }); - expect(saveModalBtnText).toBeInTheDocument(); - - userEvent.click(saveModalBtnText); - expect(handleConfigureSubmitMock).toHaveBeenCalledTimes(1); + const restrictAccessSelect = getByRole('combobox', { + name: configureModalMessages.restrictAccessTo.defaultMessage, }); + + await userEvent.selectOptions(restrictAccessSelect, '0'); + + await waitFor(() => { + userPartitionInfoFormatted.selectablePartitions[0].groups.forEach((group) => { + const checkbox = within(configureModal).getByRole('checkbox', { name: group.name }); + expect(checkbox).not.toBeChecked(); + expect(checkbox).toBeInTheDocument(); + }); + }); + + const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 }); + await userEvent.click(group1Checkbox); + expect(group1Checkbox).toBeChecked(); + + const saveModalBtnText = within(configureModal).getByRole('button', { + name: configureModalMessages.saveButton.defaultMessage, + }); + + expect(saveModalBtnText).toBeInTheDocument(); + await userEvent.click(saveModalBtnText); + + await waitFor(() => { + expect(axiosMock.history.post.length).toBeGreaterThan(0); + expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id)); + }); + + expect(queryByTestId('configure-modal')).not.toBeInTheDocument(); }); }); - it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => { - const { getByTitle } = render(); - const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock)); - const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id; - - updatedCourseVerticalChildrenMock.children = updatedCourseVerticalChildrenMock.children - .map((child) => (child.block_id === targetBlockId - ? { ...child, block_type: 'html' } - : child)); - - axiosMock - .onGet(getCourseVerticalChildrenApiUrl(blockId)) - .reply(200, updatedCourseVerticalChildrenMock); - - await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + const checkLegacyEditModalOnEditMessage = async () => { + const { getByTitle, getByTestId } = render(); await waitFor(() => { - const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); - expect(iframe).toBeInTheDocument(); - simulatePostMessageEvent(messageTypes.currentXBlockId, { - id: targetBlockId, - }); + const editButton = getByTestId('header-edit-button'); + expect(editButton).toBeInTheDocument(); + const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); + expect(xblocksIframe).toBeInTheDocument(); + userEvent.click(editButton); + }); + }; + + const checkRenderVisibilityModal = async (headingMessageId) => { + const { getByRole, getByTestId } = render(); + let configureModal; + let restrictAccessSelect; + + await waitFor(() => { + const headerConfigureBtn = getByRole('button', { name: /settings/i }); + expect(headerConfigureBtn).toBeInTheDocument(); + userEvent.click(headerConfigureBtn); }); - waitFor(() => { - simulatePostMessageEvent(messageTypes.duplicateXBlock, {}); - simulatePostMessageEvent(messageTypes.newXBlockEditor, {}); - expect(mockedUsedNavigate) - .toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true }); + await waitFor(() => { + configureModal = getByTestId('configure-modal'); + restrictAccessSelect = within(configureModal) + .getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage }); + expect(within(configureModal) + .getByRole('heading', { name: configureModalMessages[headingMessageId].defaultMessage })).toBeInTheDocument(); + expect(within(configureModal) + .queryByText(configureModalMessages.unitVisibility.defaultMessage)).not.toBeInTheDocument(); + expect(within(configureModal) + .getByText(configureModalMessages.restrictAccessTo.defaultMessage)).toBeInTheDocument(); + expect(restrictAccessSelect).toBeInTheDocument(); + expect(restrictAccessSelect).toHaveValue('-1'); }); - }); + + const modalSaveBtn = within(configureModal) + .getByRole('button', { name: configureModalMessages.saveButton.defaultMessage }); + userEvent.click(modalSaveBtn); + }; describe('Library Content page', () => { const newUnitId = '12345'; @@ -1982,6 +2001,20 @@ describe('', () => { }, }); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); + axiosMock + .onGet(getCourseUnitApiUrl(courseId)) + .reply(200, { + ...courseUnitIndexMock, + category: 'library_content', + ancestor_info: { + ...courseUnitIndexMock.ancestor_info, + child_info: { + ...courseUnitIndexMock.ancestor_info.child_info, + category: 'library_content', + }, + }, + }); + await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); }); it('navigates to library content page on receive window event', async () => { @@ -2020,5 +2053,171 @@ describe('', () => { expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument(); }); }); + + it('should display visibility modal correctly', async () => ( + checkRenderVisibilityModal('libraryContentAccess') + )); + + it('opens legacy edit modal on edit button click', checkLegacyEditModalOnEditMessage); + }); + + describe('Split Test Content page', () => { + const newUnitId = '12345'; + const sequenceId = courseSectionVerticalMock.subsection_location; + + beforeEach(async () => { + axiosMock + .onGet(getCourseSectionVerticalApiUrl(blockId)) + .reply(200, { + ...courseSectionVerticalMock, + xblock: { + ...courseSectionVerticalMock.xblock, + category: 'split_test', + }, + xblock_info: { + ...courseSectionVerticalMock.xblock_info, + category: 'split_test', + }, + }); + await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); + axiosMock + .onGet(getCourseUnitApiUrl(courseId)) + .reply(200, { + ...courseUnitIndexMock, + category: 'split_test', + ancestor_info: { + ...courseUnitIndexMock.ancestor_info, + child_info: { + ...courseUnitIndexMock.ancestor_info.child_info, + category: 'split_test', + }, + }, + }); + await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); + }); + + it('navigates to split test content page on receive window event', () => { + render(); + + simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId }); + expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`); + }); + + it('navigates to group configuration page on receive window event', () => { + const groupId = 12345; + render(); + + simulatePostMessageEvent(messageTypes.handleViewGroupConfigurations, { usageId: `${courseId}#${groupId}` }); + expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/group_configurations#${groupId}`); + }); + + it('displays processing notification on receiving post message', async () => { + const { getByText, queryByText } = render(); + + await waitFor(() => { + simulatePostMessageEvent(messageTypes.addNewComponent); + expect(getByText(('Adding'))).toBeInTheDocument(); + + simulatePostMessageEvent(messageTypes.hideProcessingNotification); + expect(queryByText(('Adding'))).not.toBeInTheDocument(); + + simulatePostMessageEvent(messageTypes.pasteNewComponent); + expect(getByText(('Pasting'))).toBeInTheDocument(); + + simulatePostMessageEvent(messageTypes.hideProcessingNotification); + expect(queryByText(('Pasting'))).not.toBeInTheDocument(); + }); + }); + + it('should render split test content page correctly', async () => { + const { + getByText, + getByRole, + queryByRole, + getByTestId, + queryByText, + } = render(); + + const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; + const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; + const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components'; + + await waitFor(() => { + const unitHeaderTitle = getByTestId('unit-header-title'); + expect(getByText(unitDisplayName)).toBeInTheDocument(); + expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument(); + expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument(); + expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument(); + + expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument(); + + expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument(); + expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument(); + + // Sidebar + const sidebarContent = [ + { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage }, + { query: queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') }, + { query: queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage }, + { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage }, + { query: queryByText, name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', '') }, + { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage }, + { query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage }, + { query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage }, + { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage }, + { query: queryByText, name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage }, + { query: queryByRole, type: 'link', name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }, + ]; + + sidebarContent.forEach(({ query, type, name }) => { + expect(type ? query(type, { name }) : query(name)).toBeInTheDocument(); + }); + + expect( + queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }), + ).toHaveAttribute('href', helpLinkUrl); + }); + }); + + it('should display visibility modal correctly', async () => ( + checkRenderVisibilityModal('splitTestAccess') + )); + + it('opens legacy edit modal on edit button click', checkLegacyEditModalOnEditMessage); + }); + + it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => { + const { getByTitle } = render(); + const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock)); + const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id; + + updatedCourseVerticalChildrenMock.children = updatedCourseVerticalChildrenMock.children + .map((child) => (child.block_id === targetBlockId + ? { ...child, block_type: 'html' } + : child)); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, updatedCourseVerticalChildrenMock); + + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + + await waitFor(() => { + const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); + expect(iframe).toBeInTheDocument(); + simulatePostMessageEvent(messageTypes.currentXBlockId, { + id: targetBlockId, + }); + }); + + waitFor(() => { + simulatePostMessageEvent(messageTypes.duplicateXBlock, {}); + simulatePostMessageEvent(messageTypes.newXBlockEditor, {}); + expect(mockedUsedNavigate) + .toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true }); + }); }); }); diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx index 4f71d1b63..be9481bad 100644 --- a/src/course-unit/add-component/AddComponent.jsx +++ b/src/course-unit/add-component/AddComponent.jsx @@ -17,23 +17,35 @@ import { messageTypes } from '../constants'; import { useIframe } from '../context/hooks'; import { useEventListener } from '../../generic/hooks'; -const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => { +const AddComponent = ({ + parentLocator, + isSplitTestType, + isUnitVerticalType, + addComponentTemplateData, + handleCreateNewCourseXBlock, +}) => { const navigate = useNavigate(); const intl = useIntl(); 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 [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle(); const [selectedComponents, setSelectedComponents] = useState([]); + const [usageId, setUsageId] = useState(null); const { sendMessageToIframe } = useIframe(); - const receiveMessage = useCallback(({ data: { type } }) => { + const receiveMessage = useCallback(({ data: { type, payload } }) => { if (type === messageTypes.showMultipleComponentPicker) { showSelectLibraryContentModal(); } - }, [showSelectLibraryContentModal]); + if (type === messageTypes.showSingleComponentPicker) { + setUsageId(payload.usageId); + showAddLibraryContentModal(); + } + }, [showSelectLibraryContentModal, showAddLibraryContentModal, setUsageId]); useEventListener('message', receiveMessage); @@ -46,11 +58,11 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => { handleCreateNewCourseXBlock({ type: COMPONENT_TYPES.libraryV2, category: selection.blockType, - parentLocator: blockId, + parentLocator: usageId || blockId, libraryContentKey: selection.usageKey, }); closeAddLibraryContentModal(); - }, []); + }, [usageId]); const handleCreateNewXBlock = (type, moduleName) => { switch (type) { @@ -77,14 +89,10 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => { showAddLibraryContentModal(); break; case COMPONENT_TYPES.advanced: - handleCreateNewCourseXBlock({ - type: moduleName, category: moduleName, parentLocator: blockId, - }); + handleCreateNewCourseXBlock({ type: moduleName, category: moduleName, parentLocator: blockId }); break; case COMPONENT_TYPES.openassessment: - handleCreateNewCourseXBlock({ - boilerplate: moduleName, category: type, parentLocator: blockId, - }); + handleCreateNewCourseXBlock({ boilerplate: moduleName, category: type, parentLocator: blockId }); break; case COMPONENT_TYPES.html: handleCreateNewCourseXBlock({ @@ -100,104 +108,135 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => { } }; - if (!Object.keys(componentTemplates).length) { - return null; + if (isUnitVerticalType || isSplitTestType) { + return ( +
+ {Object.keys(componentTemplates).length && isUnitVerticalType ? ( + <> +
{intl.formatMessage(messages.title)}
+
    + {componentTemplates.map((component) => { + const { type, displayName, beta } = component; + let modalParams; + + if (!component.templates.length) { + return null; + } + + switch (type) { + case COMPONENT_TYPES.advanced: + modalParams = { + open: openAdvanced, + close: closeAdvanced, + isOpen: isOpenAdvanced, + }; + break; + case COMPONENT_TYPES.html: + modalParams = { + open: openHtml, + close: closeHtml, + isOpen: isOpenHtml, + }; + break; + case COMPONENT_TYPES.openassessment: + modalParams = { + open: openOpenAssessment, + close: closeOpenAssessment, + isOpen: isOpenOpenAssessment, + }; + break; + default: + return ( +
  • + handleCreateNewXBlock(type)} + displayName={displayName} + type={type} + beta={beta} + /> +
  • + ); + } + + return ( + + ); + })} +
+ + ) : null} + { + closeAddLibraryContentModal(); + closeSelectLibraryContentModal(); + }} + isOverflowVisible={false} + size="xl" + footerNode={ + isSelectLibraryContentModalOpen && ( + + + + ) + } + > + + +
+ ); } - return ( -
-
{intl.formatMessage(messages.title)}
-
    - {componentTemplates.map((component) => { - const { type, displayName, beta } = component; - let modalParams; + return null; +}; - if (!component.templates.length) { - return null; - } - - switch (type) { - case COMPONENT_TYPES.advanced: - modalParams = { - open: openAdvanced, - close: closeAdvanced, - isOpen: isOpenAdvanced, - }; - break; - case COMPONENT_TYPES.html: - modalParams = { - open: openHtml, - close: closeHtml, - isOpen: isOpenHtml, - }; - break; - case COMPONENT_TYPES.openassessment: - modalParams = { - open: openOpenAssessment, - close: closeOpenAssessment, - isOpen: isOpenOpenAssessment, - }; - break; - default: - return ( -
  • - handleCreateNewXBlock(type)} - displayName={displayName} - type={type} - beta={beta} - /> -
  • - ); - } - - return ( - - ); - })} -
- { - closeAddLibraryContentModal(); - closeSelectLibraryContentModal(); - }} - isOverflowVisible={false} - size="xl" - footerNode={ - isSelectLibraryContentModalOpen && ( - - - - ) - } - > - - -
- ); +AddComponent.defaultProps = { + addComponentTemplateData: {}, }; AddComponent.propTypes = { - blockId: PropTypes.string.isRequired, + 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; diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.jsx index 0971c2008..bda1ee1ea 100644 --- a/src/course-unit/add-component/AddComponent.test.jsx +++ b/src/course-unit/add-component/AddComponent.test.jsx @@ -64,6 +64,9 @@ const renderComponent = (props) => render( 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 dcbd9e45c..a0ae6a186 100644 --- a/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx +++ b/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx @@ -14,6 +14,7 @@ const ComponentModalView = ({ component, modalParams, handleCreateNewXBlock, + isRequestedModalView, }) => { const intl = useIntl(); const dispatch = useDispatch(); @@ -30,15 +31,19 @@ const ComponentModalView = ({ setModuleTitle(''); }; + const renderAddComponentButton = () => ( +
  • + +
  • + ); + return ( <> -
  • - -
  • + {!isRequestedModalView && renderAddComponentButton()} ( + !!children.filter((child : any) => child?.url).length + ); + return (