From 15e0b50fec5a1e5392b4feb3844cdba0993479c7 Mon Sep 17 00:00:00 2001 From: ihor-romaniuk Date: Fri, 8 Nov 2024 18:34:20 +0100 Subject: [PATCH] feat: [FC-0070] rendering split test content in unit page --- src/constants.js | 1 + src/course-unit/CourseUnit.jsx | 17 +- src/course-unit/CourseUnit.scss | 1 + src/course-unit/CourseUnit.test.jsx | 81 +++++- .../add-component/AddComponent.jsx | 250 +++++++++++------- .../add-component/AddComponent.test.jsx | 3 + .../ComponentModalView.jsx | 26 +- src/course-unit/breadcrumbs/Breadcrumbs.scss | 2 +- src/course-unit/breadcrumbs/Breadcrumbs.tsx | 66 +++-- src/course-unit/constants.js | 4 + src/course-unit/data/thunk.js | 17 +- .../header-navigations/HeaderNavigations.jsx | 8 +- .../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 | 61 +++++ src/course-unit/sidebar/messages.js | 55 ++++ .../xblock-container-iframe/hooks/types.ts | 2 + .../hooks/useMessageHandlers.tsx | 5 + .../xblock-container-iframe/index.scss | 4 + .../xblock-container-iframe/index.tsx | 42 ++- .../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 +- 28 files changed, 574 insertions(+), 188 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 7439961ffa..849247d395 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 a09b985966..8ae9319368 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 } 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,8 @@ const CourseUnit = ({ courseId }) => { handleRollbackMovedXBlock, handleCloseXBlockMovedAlert, handleNavigateToTargetUnit, + addComponentTemplateData, + setAddComponentTemplateData, } = useCourseUnit({ courseId, blockId }); const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType); @@ -153,7 +157,7 @@ const CourseUnit = ({ courseId }) => { )} headerActions={( )} @@ -186,14 +190,18 @@ const CourseUnit = ({ courseId }) => { {isUnitVerticalType && ( )} {showPasteXBlock && canPasteComponent && isUnitVerticalType && ( @@ -228,6 +236,11 @@ const CourseUnit = ({ courseId }) => { )} + {isSplitTestType && ( + + + + )} diff --git a/src/course-unit/CourseUnit.scss b/src/course-unit/CourseUnit.scss index abc649b986..bd08064547 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 7d81a9c7b0..143c43a4e5 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -50,7 +50,6 @@ 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'; @@ -62,6 +61,7 @@ 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 sidebarMessages from './sidebar/messages'; import messages from './messages'; let axiosMock; @@ -212,7 +212,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'); @@ -225,11 +225,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;'); }); }); @@ -2015,4 +2015,77 @@ describe('', () => { }); }); }); + + 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); + }); + + 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('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 + expect(queryByRole('heading', { name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage })).toBeInTheDocument(); + expect(queryByText(sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', ''))).toBeInTheDocument(); + expect(queryByText(sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage)).toBeInTheDocument(); + expect(queryByRole('heading', { name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage })).toBeInTheDocument(); + expect(queryByText(sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', ''))).toBeInTheDocument(); + expect(queryByRole('heading', { name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage })).toBeInTheDocument(); + expect(queryByText(sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage)).toBeInTheDocument(); + expect(queryByText(sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage)).toBeInTheDocument(); + expect(queryByRole('heading', { name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage })).toBeInTheDocument(); + expect(queryByText(sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage)).toBeInTheDocument(); + expect(queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage })).toBeInTheDocument(); + expect(queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage })).toHaveAttribute('href', helpLinkUrl); + }); + }); + }); }); diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx index 7d74995e08..68a6314887 100644 --- a/src/course-unit/add-component/AddComponent.jsx +++ b/src/course-unit/add-component/AddComponent.jsx @@ -17,13 +17,21 @@ import { messageTypes } from '../constants'; import { useIframe } from '../context/hooks'; import { useEventListener } from '../../generic/hooks'; -const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => { +const AddComponent = ({ + parentLocator, + isUnitVerticalType, + addComponentTemplateData, + handleCreateNewCourseXBlock, + setAddComponentTemplateData, +}) => { 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 isRequestedModalView = addComponentTemplateData?.model?.type; + const blockId = addComponentTemplateData.parentLocator || parentLocator; const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle(); const [selectedComponents, setSelectedComponents] = useState([]); @@ -77,126 +85,164 @@ 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({ - type, - boilerplate: moduleName, - parentLocator: blockId, - }, ({ courseKey, locator }) => { - navigate(`/course/${courseKey}/editor/html/${locator}`); - }); + handleCreateNewCourseXBlock( + { type, boilerplate: moduleName, parentLocator: blockId }, + ({ courseKey, locator }) => navigate(`/course/${courseKey}/editor/html/${locator}`), + ); break; default: } }; - if (!Object.keys(componentTemplates).length) { - return null; + if (isRequestedModalView && !isUnitVerticalType) { + return ( + {}, + close: () => setAddComponentTemplateData({}), + isOpen: addComponentTemplateData.model, + }} + /> + ); } - return ( -
-
{intl.formatMessage(messages.title)}
-
    - {componentTemplates.map((component) => { - const { type, displayName, beta } = component; - let modalParams; + if (Object.keys(componentTemplates).length && isUnitVerticalType) { + return ( +
    +
    {intl.formatMessage(messages.title)}
    +
      + {componentTemplates.map((component) => { + const { type, displayName, beta } = component; + let modalParams; - if (!component.templates.length) { - 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} + /> +
    • + ); + } - 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 && ( + + + + ) + } + > + + +
    + ); + } - return ( - - ); - })} -
- { - closeAddLibraryContentModal(); - closeSelectLibraryContentModal(); - }} - isOverflowVisible={false} - size="xl" - footerNode={ - isSelectLibraryContentModalOpen && ( - - - - ) - } - > - - -
- ); + return null; +}; + + +AddComponent.defaultProps = { + addComponentTemplateData: {}, + setAddComponentTemplateData: () => {}, }; AddComponent.propTypes = { - blockId: PropTypes.string.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, + }), + }), + }, + setAddComponentTemplateData: PropTypes.func, }; export default AddComponent; diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.jsx index 0971c20088..bda1ee1ea4 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 dcbd9e45c3..e7a94a0ba0 100644 --- a/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx +++ b/src/course-unit/add-component/add-component-modals/ComponentModalView.jsx @@ -14,13 +14,14 @@ const ComponentModalView = ({ component, modalParams, handleCreateNewXBlock, + isRequestedModalView, }) => { const intl = useIntl(); const dispatch = useDispatch(); const [moduleTitle, setModuleTitle] = useState(''); const { open, close, isOpen } = modalParams; const { - type, displayName, templates, supportLegend, + type, displayName, templates = [], supportLegend, } = component; const supportLabels = getXBlockSupportMessages(intl); @@ -30,15 +31,19 @@ const ComponentModalView = ({ setModuleTitle(''); }; + const renderAddComponentButton = () => ( +
  • + +
  • + ); + return ( <> -
  • - -
  • + {!isRequestedModalView && renderAddComponentButton()} ( + !!children.filter((child : any) => child?.url).length + ); + return (