feat: [FC-0070] rendering split test content in unit page (#1492)
Introduces functionality to display Split Test Content within the new Unit page interface.
This commit is contained in:
@@ -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' },
|
||||
});
|
||||
|
||||
|
||||
@@ -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={(
|
||||
<HeaderNavigations
|
||||
unitCategory={unitCategory}
|
||||
category={unitCategory}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
/>
|
||||
)}
|
||||
@@ -188,16 +191,18 @@ const CourseUnit = ({ courseId }) => {
|
||||
<XBlockContainerIframe
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
courseVerticalChildren={courseVerticalChildren.children}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
{isUnitVerticalType && (
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
/>
|
||||
)}
|
||||
<AddComponent
|
||||
parentLocator={blockId}
|
||||
isSplitTestType={isSplitTestType}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
addComponentTemplateData={addComponentTemplateData}
|
||||
/>
|
||||
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
|
||||
<PasteComponent
|
||||
clipboardData={sharedClipboardData}
|
||||
@@ -232,6 +237,11 @@ const CourseUnit = ({ courseId }) => {
|
||||
</Sidebar>
|
||||
</>
|
||||
)}
|
||||
{isSplitTestType && (
|
||||
<Sidebar data-testid="course-split-test-sidebar">
|
||||
<SplitTestSidebarInfo />
|
||||
</Sidebar>
|
||||
)}
|
||||
</Stack>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@import "./header-title/HeaderTitle";
|
||||
@import "./move-modal";
|
||||
@import "./preview-changes";
|
||||
@import "./xblock-container-iframe";
|
||||
|
||||
.course-unit {
|
||||
min-width: 900px;
|
||||
|
||||
@@ -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('<CourseUnit />', () => {
|
||||
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('<CourseUnit />', () => {
|
||||
|
||||
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('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
describe('XBlock restrict access', () => {
|
||||
it('opens xblock restrict access modal successfully', () => {
|
||||
it('opens xblock restrict access modal successfully', async () => {
|
||||
const {
|
||||
getByTitle, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
@@ -1830,7 +1830,7 @@ describe('<CourseUnit />', () => {
|
||||
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('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
|
||||
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
||||
@@ -1854,7 +1854,7 @@ describe('<CourseUnit />', () => {
|
||||
getByTitle, queryByTestId, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
@@ -1862,7 +1862,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
userEvent.click(within(configureModal).getByRole('button', {
|
||||
@@ -1883,85 +1883,104 @@ describe('<CourseUnit />', () => {
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const {
|
||||
getByTitle, getByRole, getByTestId,
|
||||
getByTitle, getByRole, getByTestId, queryByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
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('<CourseUnit />', () => {
|
||||
},
|
||||
});
|
||||
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('<CourseUnit />', () => {
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
|
||||
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(<RootWrapper />);
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<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) => {
|
||||
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 (
|
||||
<li key={type}>
|
||||
<AddComponentButton
|
||||
onClick={() => handleCreateNewXBlock(type)}
|
||||
displayName={displayName}
|
||||
type={type}
|
||||
beta={beta}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentModalView
|
||||
key={type}
|
||||
component={component}
|
||||
handleCreateNewXBlock={handleCreateNewXBlock}
|
||||
modalParams={modalParams}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
<StandardModal
|
||||
title={
|
||||
isAddLibraryContentModalOpen
|
||||
? intl.formatMessage(messages.singleComponentPickerModalTitle)
|
||||
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
|
||||
}
|
||||
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
|
||||
onClose={() => {
|
||||
closeAddLibraryContentModal();
|
||||
closeSelectLibraryContentModal();
|
||||
}}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
footerNode={
|
||||
isSelectLibraryContentModalOpen && (
|
||||
<ActionRow>
|
||||
<Button onClick={onComponentSelectionSubmit}>
|
||||
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
|
||||
onComponentSelected={handleLibraryV2Selection}
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<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) => {
|
||||
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 (
|
||||
<li key={type}>
|
||||
<AddComponentButton
|
||||
onClick={() => handleCreateNewXBlock(type)}
|
||||
displayName={displayName}
|
||||
type={type}
|
||||
beta={beta}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentModalView
|
||||
key={type}
|
||||
component={component}
|
||||
handleCreateNewXBlock={handleCreateNewXBlock}
|
||||
modalParams={modalParams}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<StandardModal
|
||||
title={
|
||||
isAddLibraryContentModalOpen
|
||||
? intl.formatMessage(messages.singleComponentPickerModalTitle)
|
||||
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
|
||||
}
|
||||
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
|
||||
onClose={() => {
|
||||
closeAddLibraryContentModal();
|
||||
closeSelectLibraryContentModal();
|
||||
}}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
footerNode={
|
||||
isSelectLibraryContentModalOpen && (
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onComponentSelectionSubmit}>
|
||||
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
|
||||
onComponentSelected={handleLibraryV2Selection}
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
</div>
|
||||
);
|
||||
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;
|
||||
|
||||
@@ -64,6 +64,9 @@ const renderComponent = (props) => render(
|
||||
<IframeProvider>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
isUnitVerticalType
|
||||
parentLocator={blockId}
|
||||
addComponentTemplateData={{}}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -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 = () => (
|
||||
<li>
|
||||
<AddComponentButton
|
||||
onClick={open}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<AddComponentButton
|
||||
onClick={open}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</li>
|
||||
{!isRequestedModalView && renderAddComponentButton()}
|
||||
<ModalContainer
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
@@ -92,6 +97,10 @@ const ComponentModalView = ({
|
||||
);
|
||||
};
|
||||
|
||||
ComponentModalView.defaultProps = {
|
||||
isRequestedModalView: false,
|
||||
};
|
||||
|
||||
ComponentModalView.propTypes = {
|
||||
modalParams: PropTypes.shape({
|
||||
open: PropTypes.func,
|
||||
@@ -117,6 +126,7 @@ ComponentModalView.propTypes = {
|
||||
showLegend: PropTypes.bool,
|
||||
}),
|
||||
}).isRequired,
|
||||
isRequestedModalView: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ComponentModalView;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sub-header-title .sub-header-breadcrumbs {
|
||||
.sub-header-breadcrumbs {
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -26,43 +26,55 @@ const Breadcrumbs = ({ courseId, parentUnitId }: { courseId: string, parentUnitI
|
||||
isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url)
|
||||
);
|
||||
|
||||
const hasChildWithUrl = (children = []) => (
|
||||
!!children.filter((child : any) => child?.url).length
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="d-flex align-center mb-2.5">
|
||||
<ol className="p-0 m-0 d-flex align-center">
|
||||
<ol className="p-0 m-0 d-flex align-center flex-wrap">
|
||||
{ancestorXblocks.map(({ children, title, isLast }, index) => (
|
||||
<li
|
||||
className="d-flex"
|
||||
className="d-flex mb-2.5"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${title}-${index}`}
|
||||
>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="breadcrumbs-dropdown-section"
|
||||
variant="link"
|
||||
className="p-0 text-primary small"
|
||||
>
|
||||
{hasChildWithUrl(children) ? (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="breadcrumbs-dropdown-section"
|
||||
variant="link"
|
||||
className="p-0 text-primary small"
|
||||
>
|
||||
<span className="small text-gray-700">
|
||||
{title}
|
||||
</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
key={url}
|
||||
to={getPathToCoursePage(index < 2, url)}
|
||||
className="small"
|
||||
data-testid={`breadcrumbs-dropdown-item-level-${index}`}
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<span className="p-0 text-primary small btn btn-link text-decoration-none">
|
||||
<span className="small text-gray-700">
|
||||
{title}
|
||||
</span>
|
||||
<Icon
|
||||
src={ArrowDropDownIcon}
|
||||
className="text-primary ml-1"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{children.map(({ url, displayName }) => (
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
key={url}
|
||||
to={getPathToCoursePage(index < 2, url)}
|
||||
className="small"
|
||||
data-testid={`breadcrumbs-dropdown-item-level-${index}`}
|
||||
>
|
||||
{displayName}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</span>
|
||||
)}
|
||||
{!isLast && (
|
||||
<Icon
|
||||
src={ChevronRightIcon}
|
||||
|
||||
@@ -55,6 +55,7 @@ export const messageTypes = {
|
||||
completeXBlockMoving: 'completeXBlockMoving',
|
||||
rollbackMovedXBlock: 'rollbackMovedXBlock',
|
||||
showMultipleComponentPicker: 'showMultipleComponentPicker',
|
||||
showSingleComponentPicker: 'showSingleComponentPicker',
|
||||
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
|
||||
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
|
||||
copyXBlock: 'copyXBlock',
|
||||
@@ -69,6 +70,7 @@ export const messageTypes = {
|
||||
addXBlock: 'addXBlock',
|
||||
scrollToXBlock: 'scrollToXBlock',
|
||||
handleViewXBlockContent: 'handleViewXBlockContent',
|
||||
handleViewGroupConfigurations: 'handleViewGroupConfigurations',
|
||||
editXBlock: 'editXBlock',
|
||||
closeXBlockEditorModal: 'closeXBlockEditorModal',
|
||||
saveEditedXBlockData: 'saveEditedXBlockData',
|
||||
@@ -76,4 +78,10 @@ export const messageTypes = {
|
||||
studioAjaxError: 'studioAjaxError',
|
||||
refreshPositions: 'refreshPositions',
|
||||
openManageTags: 'openManageTags',
|
||||
showComponentTemplates: 'showComponentTemplates',
|
||||
addNewComponent: 'addNewComponent',
|
||||
pasteNewComponent: 'pasteComponent',
|
||||
copyXBlockLegacy: 'copyXBlockLegacy',
|
||||
hideProcessingNotification: 'hideProcessingNotification',
|
||||
handleRedirectToXBlockEditPage: 'handleRedirectToXBlockEditPage',
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
export interface IframeContextType {
|
||||
setIframeRef: (ref: MutableRefObject<HTMLIFrameElement | null>) => void;
|
||||
sendMessageToIframe: (messageType: string, payload: unknown) => void;
|
||||
sendMessageToIframe: (messageType: string, payload: unknown, consumerWindow?: Window | null) => void;
|
||||
}
|
||||
|
||||
export const IframeContext = createContext<IframeContextType | undefined>(undefined);
|
||||
@@ -16,11 +16,12 @@ export const IframeProvider: React.FC = ({ children }: { children: ReactNode })
|
||||
iframeRef.current = ref.current;
|
||||
}, []);
|
||||
|
||||
const sendMessageToIframe = useCallback((messageType: string, payload: any) => {
|
||||
const sendMessageToIframe = useCallback((messageType: string, payload: any, consumerWindow?: Window | null) => {
|
||||
const iframeWindow = iframeRef?.current?.contentWindow;
|
||||
if (iframeWindow) {
|
||||
const targetWindow = consumerWindow || iframeWindow;
|
||||
if (targetWindow) {
|
||||
try {
|
||||
iframeWindow.postMessage({ type: messageType, payload }, '*');
|
||||
targetWindow.postMessage({ type: messageType, payload }, '*');
|
||||
} catch (error) {
|
||||
logError('Failed to send message to iframe:', error);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
fetchSequenceSuccess,
|
||||
fetchCourseSectionVerticalDataSuccess,
|
||||
updateLoadingCourseSectionVerticalDataStatus,
|
||||
updateLoadingCourseXblockStatus,
|
||||
updateCourseVerticalChildren,
|
||||
updateCourseVerticalChildrenLoadingStatus,
|
||||
updateQueryPendingStatus,
|
||||
@@ -200,18 +199,30 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.FAILED }));
|
||||
handleResponseErrors(error, dispatch, updateSavingStatus);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourseVerticalChildrenData(itemId) {
|
||||
export function fetchCourseVerticalChildrenData(itemId, isSplitTestType, skipPageLoading) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
if (!skipPageLoading) {
|
||||
dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
}
|
||||
|
||||
try {
|
||||
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
|
||||
if (isSplitTestType) {
|
||||
const blockIds = courseVerticalChildrenData.children.map(child => child.blockId);
|
||||
const childrenDataArray = await Promise.all(
|
||||
blockIds.map(blockId => getCourseVerticalChildren(blockId)),
|
||||
);
|
||||
const allChildren = childrenDataArray.reduce(
|
||||
(acc, data) => acc.concat(data.children || []),
|
||||
[],
|
||||
);
|
||||
courseVerticalChildrenData.children = [...courseVerticalChildrenData.children, ...allChildren];
|
||||
}
|
||||
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
||||
dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
@@ -300,13 +311,13 @@ export function patchUnitItemQuery({
|
||||
dispatch(updateMovedXBlockParams(xBlockParams));
|
||||
dispatch(updateCourseOutlineInfo({}));
|
||||
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
callbackFn(sourceLocator);
|
||||
try {
|
||||
const courseUnit = await getCourseUnitData(currentParentLocator);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
} catch (error) {
|
||||
handleResponseErrors(error, dispatch, updateSavingStatus);
|
||||
}
|
||||
callbackFn(sourceLocator);
|
||||
} catch (error) {
|
||||
handleResponseErrors(error, dispatch, updateSavingStatus);
|
||||
} finally {
|
||||
|
||||
@@ -6,13 +6,13 @@ import { Edit as EditIcon } from '@openedx/paragon/icons';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import messages from './messages';
|
||||
|
||||
const HeaderNavigations = ({ headerNavigationsActions, unitCategory }) => {
|
||||
const HeaderNavigations = ({ headerNavigationsActions, category }) => {
|
||||
const intl = useIntl();
|
||||
const { handleViewLive, handlePreview, handleEdit } = headerNavigationsActions;
|
||||
|
||||
return (
|
||||
<nav className="header-navigations ml-auto flex-shrink-0">
|
||||
{unitCategory === COURSE_BLOCK_NAMES.vertical.id && (
|
||||
{category === COURSE_BLOCK_NAMES.vertical.id && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
@@ -28,11 +28,12 @@ const HeaderNavigations = ({ headerNavigationsActions, unitCategory }) => {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{unitCategory === COURSE_BLOCK_NAMES.libraryContent.id && (
|
||||
{[COURSE_BLOCK_NAMES.libraryContent.id, COURSE_BLOCK_NAMES.splitTest.id].includes(category) && (
|
||||
<Button
|
||||
iconBefore={EditIcon}
|
||||
variant="outline-primary"
|
||||
onClick={handleEdit}
|
||||
data-testid="header-edit-button"
|
||||
>
|
||||
{intl.formatMessage(messages.editButton)}
|
||||
</Button>
|
||||
@@ -47,7 +48,7 @@ HeaderNavigations.propTypes = {
|
||||
handlePreview: PropTypes.func.isRequired,
|
||||
handleEdit: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
unitCategory: PropTypes.string.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default HeaderNavigations;
|
||||
|
||||
@@ -18,6 +18,7 @@ const headerNavigationsActions = {
|
||||
const renderComponent = (props) => render(
|
||||
<IntlProvider locale="en">
|
||||
<HeaderNavigations
|
||||
category={COURSE_BLOCK_NAMES.vertical.id}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
{...props}
|
||||
/>
|
||||
@@ -47,17 +48,17 @@ describe('<HeaderNavigations />', () => {
|
||||
expect(editButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls the correct handlers when clicking buttons for library page', () => {
|
||||
const { getByRole, queryByRole } = renderComponent({ unitCategory: COURSE_BLOCK_NAMES.libraryContent.id });
|
||||
['libraryContent', 'splitTest'].forEach((category) => {
|
||||
it(`calls the correct handlers when clicking buttons for ${category} page`, () => {
|
||||
const { getByRole, queryByRole } = renderComponent({ category: COURSE_BLOCK_NAMES[category].id });
|
||||
|
||||
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
|
||||
fireEvent.click(editButton);
|
||||
expect(handleViewLiveFn).toHaveBeenCalledTimes(1);
|
||||
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
|
||||
fireEvent.click(editButton);
|
||||
expect(handleViewLiveFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
const viewLiveButton = queryByRole('button', { name: messages.viewLiveButton.defaultMessage });
|
||||
expect(viewLiveButton).not.toBeInTheDocument();
|
||||
|
||||
const previewButton = queryByRole('button', { name: messages.previewButton.defaultMessage });
|
||||
expect(previewButton).not.toBeInTheDocument();
|
||||
[messages.viewLiveButton.defaultMessage, messages.previewButton.defaultMessage].forEach((btnName) => {
|
||||
expect(queryByRole('button', { name: btnName })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,13 @@ const HeaderTitle = ({
|
||||
const [titleValue, setTitleValue] = useState(unitTitle);
|
||||
const currentItemData = useSelector(getCourseUnitData);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const { selectedPartitionIndex, selectedGroupsLabel } = currentItemData.userPartitionInfo;
|
||||
const { selectedPartitionIndex, selectedGroupsLabel } = currentItemData.userPartitionInfo ?? {};
|
||||
|
||||
const isXBlockComponent = [
|
||||
COURSE_BLOCK_NAMES.libraryContent.id,
|
||||
COURSE_BLOCK_NAMES.splitTest.id,
|
||||
COURSE_BLOCK_NAMES.component.id,
|
||||
].includes(currentItemData.category);
|
||||
|
||||
const onConfigureSubmit = (...arg) => {
|
||||
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
|
||||
@@ -87,9 +93,8 @@ const HeaderTitle = ({
|
||||
onConfigureSubmit={onConfigureSubmit}
|
||||
currentItemData={currentItemData}
|
||||
isSelfPaced={false}
|
||||
isXBlockComponent={
|
||||
[COURSE_BLOCK_NAMES.libraryContent.id, COURSE_BLOCK_NAMES.component.id].includes(currentItemData.category)
|
||||
}
|
||||
isXBlockComponent={isXBlockComponent}
|
||||
userPartitionInfo={currentItemData?.userPartitionInfo || {}}
|
||||
/>
|
||||
</div>
|
||||
{getVisibilityMessage()}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { camelCaseObject } from '@edx/frontend-platform/utils';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useClipboard } from '../generic/clipboard';
|
||||
@@ -46,6 +47,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
const [addComponentTemplateData, setAddComponentTemplateData] = useState({});
|
||||
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
|
||||
|
||||
const courseUnit = useSelector(getCourseUnitData);
|
||||
@@ -68,6 +70,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id;
|
||||
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 headerNavigationsActions = {
|
||||
handleViewLive: () => {
|
||||
@@ -76,7 +79,9 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
handlePreview: () => {
|
||||
window.open(draftPreviewLink, '_blank');
|
||||
},
|
||||
handleEdit: () => {},
|
||||
handleEdit: () => {
|
||||
sendMessageToIframe(messageTypes.editXBlock, { id: courseUnit.id }, window);
|
||||
},
|
||||
};
|
||||
|
||||
const handleTitleEdit = () => {
|
||||
@@ -170,6 +175,16 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const { usageId } = payload;
|
||||
navigate(`/course/${courseId}/container/${usageId}/${sequenceId}`);
|
||||
}
|
||||
|
||||
if (type === messageTypes.handleViewGroupConfigurations) {
|
||||
const { usageId } = payload;
|
||||
const groupId = usageId.split('#').pop();
|
||||
navigate(`/course/${courseId}/group_configurations#${groupId}`);
|
||||
}
|
||||
|
||||
if (type === messageTypes.showComponentTemplates) {
|
||||
setAddComponentTemplateData(camelCaseObject(payload));
|
||||
}
|
||||
}, [courseId, sequenceId]);
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
@@ -183,12 +198,17 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseUnitQuery(blockId));
|
||||
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
|
||||
dispatch(fetchCourseVerticalChildrenData(blockId));
|
||||
|
||||
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
||||
handleNavigate(sequenceId);
|
||||
dispatch(updateMovedXBlockParams({ isSuccess: false }));
|
||||
}, [courseId, blockId, sequenceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSplitTestType) {
|
||||
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
||||
}
|
||||
}, [isSplitTestType, blockId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMoveModalOpen && !Object.keys(courseOutlineInfo).length) {
|
||||
dispatch(getCourseOutlineInfoQuery(courseId));
|
||||
@@ -209,6 +229,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
isTitleEditFormOpen,
|
||||
isUnitVerticalType,
|
||||
isUnitLibraryType,
|
||||
isSplitTestType,
|
||||
sharedClipboardData,
|
||||
showPasteXBlock,
|
||||
showPasteUnit,
|
||||
@@ -227,6 +248,8 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
handleCloseXBlockMovedAlert,
|
||||
movedXBlockParams,
|
||||
handleNavigateToTargetUnit,
|
||||
addComponentTemplateData,
|
||||
setAddComponentTemplateData,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -81,4 +81,19 @@
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.course-split-test-sidebar {
|
||||
padding: $spacer;
|
||||
|
||||
@extend %base-font-params;
|
||||
|
||||
.course-split-test-sidebar-title {
|
||||
font-size: $font-size-base;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
.course-split-test-sidebar-devider {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
src/course-unit/sidebar/SplitTestSidebarInfo.tsx
Normal file
58
src/course-unit/sidebar/SplitTestSidebarInfo.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Card, Hyperlink, Stack } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SplitTestSidebarInfo = () => {
|
||||
const intl = useIntl();
|
||||
const boldTagWrapper = (chunks: React.ReactNode) => <strong>{chunks}</strong>;
|
||||
|
||||
return (
|
||||
<Card.Body className="course-split-test-sidebar">
|
||||
<Stack>
|
||||
<h3 className="course-split-test-sidebar-title">
|
||||
{intl.formatMessage(messages.sidebarSplitTestAddComponentTitle)}
|
||||
</h3>
|
||||
<p>
|
||||
{intl.formatMessage(messages.sidebarSplitTestSelectComponentType, { bold_tag: boldTagWrapper })}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages.sidebarSplitTestComponentAdded)}
|
||||
</p>
|
||||
<h3 className="course-split-test-sidebar-title">
|
||||
{intl.formatMessage(messages.sidebarSplitTestEditComponentTitle)}
|
||||
</h3>
|
||||
<p>
|
||||
{intl.formatMessage(messages.sidebarSplitTestEditComponentInstruction, { bold_tag: boldTagWrapper })}
|
||||
</p>
|
||||
<h3 className="course-split-test-sidebar-title">
|
||||
{intl.formatMessage(messages.sidebarSplitTestReorganizeComponentTitle)}
|
||||
</h3>
|
||||
<p>
|
||||
{intl.formatMessage(messages.sidebarSplitTestReorganizeComponentInstruction)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages.sidebarSplitTestReorganizeGroupsInstruction)}
|
||||
</p>
|
||||
<h3 className="course-split-test-sidebar-title">
|
||||
{intl.formatMessage(messages.sidebarSplitTestExperimentComponentTitle)}
|
||||
</h3>
|
||||
<p className="mb-0">
|
||||
{intl.formatMessage(messages.sidebarSplitTestExperimentComponentInstruction)}
|
||||
</p>
|
||||
<hr className="course-split-test-sidebar-devider my-4" />
|
||||
<Hyperlink
|
||||
showLaunchIcon={false}
|
||||
destination="https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components"
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
target="_blank"
|
||||
>
|
||||
{intl.formatMessage(messages.sidebarSplitTestLearnMoreLinkLabel)}
|
||||
</Hyperlink>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
);
|
||||
};
|
||||
|
||||
export default SplitTestSidebarInfo;
|
||||
@@ -137,6 +137,61 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-unit.modal.make-visibility.description',
|
||||
defaultMessage: 'If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students. Do you want to proceed?',
|
||||
},
|
||||
sidebarSplitTestAddComponentTitle: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.add-component.title',
|
||||
defaultMessage: 'Adding components',
|
||||
description: 'Title for the section that explains how to add components to a split test',
|
||||
},
|
||||
sidebarSplitTestSelectComponentType: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.add-component.select-type',
|
||||
defaultMessage: 'Select a component type under {bold_tag}Add New Component{bold_tag}. Then select a template.',
|
||||
description: 'Instruction text for selecting a component type and template when adding new components',
|
||||
},
|
||||
sidebarSplitTestComponentAdded: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.add-component.component-added',
|
||||
defaultMessage: 'The new component is added at the bottom of the page or group. You can then edit and move the component.',
|
||||
description: 'Instruction text indicating that the component has been added and can be moved or edited',
|
||||
},
|
||||
sidebarSplitTestEditComponentTitle: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.edit-component.title',
|
||||
defaultMessage: 'Editing components',
|
||||
description: 'Title for the section that explains how to edit components in a split test',
|
||||
},
|
||||
sidebarSplitTestEditComponentInstruction: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.edit-component.instruction',
|
||||
defaultMessage: 'Click the {bold_tag}Edit{bold_tag} icon in a component to edit its content.',
|
||||
description: 'Instruction text for editing a component by clicking the edit icon',
|
||||
},
|
||||
sidebarSplitTestReorganizeComponentTitle: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.reorganize-component.title',
|
||||
defaultMessage: 'Reorganizing components',
|
||||
description: 'Title for the section that explains how to reorganize components within a split test',
|
||||
},
|
||||
sidebarSplitTestReorganizeComponentInstruction: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.reorganize-component.instruction',
|
||||
defaultMessage: 'Drag components to new locations within this component.',
|
||||
description: 'Instruction text for reorganizing components by dragging them to new locations within a split test',
|
||||
},
|
||||
sidebarSplitTestReorganizeGroupsInstruction: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.reorganize-component.drag-to-groups',
|
||||
defaultMessage: 'For content experiments, you can drag components to other groups.',
|
||||
description: 'Instruction text for dragging components to other groups for content experiments',
|
||||
},
|
||||
sidebarSplitTestExperimentComponentTitle: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.experiment-component.title',
|
||||
defaultMessage: 'Working with content experiments',
|
||||
description: 'Title for the section that explains how to work with content experiments',
|
||||
},
|
||||
sidebarSplitTestExperimentComponentInstruction: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.experiment-component.confirm-config',
|
||||
defaultMessage: 'Confirm that you have properly configured content in each of your experiment groups.',
|
||||
description: 'Instruction text reminding users to check content configuration in each experiment group',
|
||||
},
|
||||
sidebarSplitTestLearnMoreLinkLabel: {
|
||||
id: 'course-authoring.course-unit.split-test.sidebar.learn-more-link.label',
|
||||
defaultMessage: 'Learn more about component containers',
|
||||
description: 'Text for a link that directs users to more information about component containers in the split test setup.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -12,6 +12,9 @@ export type UseMessageHandlersTypes = {
|
||||
handleSaveEditedXBlockData: () => void;
|
||||
handleFinishXBlockDragging: () => void;
|
||||
handleOpenManageTagsModal: (id: string) => void;
|
||||
handleShowProcessingNotification: (variant: string) => void;
|
||||
handleHideProcessingNotification: () => void;
|
||||
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
|
||||
};
|
||||
|
||||
export type MessageHandlersTypes = Record<string, (payload: any) => void>;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMemo } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { useClipboard } from '../../../generic/clipboard';
|
||||
import { handleResponseErrors } from '../../../generic/saving-error-alert/utils';
|
||||
import { handleResponseErrors } from '../../../generic/saving-error-alert';
|
||||
import { NOTIFICATION_MESSAGES } from '../../../constants';
|
||||
import { updateSavingStatus } from '../../data/slice';
|
||||
import { messageTypes } from '../../constants';
|
||||
import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
|
||||
@@ -27,6 +28,9 @@ export const useMessageHandlers = ({
|
||||
handleSaveEditedXBlockData,
|
||||
handleFinishXBlockDragging,
|
||||
handleOpenManageTagsModal,
|
||||
handleShowProcessingNotification,
|
||||
handleHideProcessingNotification,
|
||||
handleRedirectToXBlockEditPage,
|
||||
}: UseMessageHandlersTypes): MessageHandlersTypes => {
|
||||
const { copyToClipboard } = useClipboard();
|
||||
|
||||
@@ -46,6 +50,11 @@ export const useMessageHandlers = ({
|
||||
[messageTypes.studioAjaxError]: ({ error }) => handleResponseErrors(error, dispatch, updateSavingStatus),
|
||||
[messageTypes.refreshPositions]: handleFinishXBlockDragging,
|
||||
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
|
||||
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
|
||||
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
|
||||
[messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
|
||||
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
|
||||
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
|
||||
}), [
|
||||
courseId,
|
||||
handleDeleteXBlock,
|
||||
|
||||
4
src/course-unit/xblock-container-iframe/index.scss
Normal file
4
src/course-unit/xblock-container-iframe/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.xblock-container-iframe {
|
||||
width: calc(100% + ($spacer * .3125));
|
||||
margin: 0 (($spacer * .3125) * -1);
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import { useToggle, Sheet } from '@openedx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
hideProcessingNotification,
|
||||
showProcessingNotification,
|
||||
} from '../../generic/processing-notification/data/slice';
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
||||
import ModalIframe from '../../generic/modal-iframe';
|
||||
@@ -13,24 +17,27 @@ import { IFRAME_FEATURE_POLICY } from '../../constants';
|
||||
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
|
||||
import supportedEditors from '../../editors/supportedEditors';
|
||||
import { useIframe } from '../context/hooks';
|
||||
import { updateCourseUnitSidebar } from '../data/thunk';
|
||||
import {
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseVerticalChildrenData,
|
||||
updateCourseUnitSidebar,
|
||||
} from '../data/thunk';
|
||||
import { messageTypes } from '../constants';
|
||||
import {
|
||||
useMessageHandlers,
|
||||
useIframeContent,
|
||||
useIframeMessages,
|
||||
useIFrameBehavior,
|
||||
} from './hooks';
|
||||
import { formatAccessManagedXBlockData, getIframeUrl, getLegacyEditModalUrl } from './utils';
|
||||
import messages from './messages';
|
||||
import { messageTypes } from '../constants';
|
||||
|
||||
import {
|
||||
XBlockContainerIframeProps,
|
||||
AccessManagedXBlockDataTypes,
|
||||
} from './types';
|
||||
import { formatAccessManagedXBlockData, getIframeUrl, getLegacyEditModalUrl } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit,
|
||||
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
@@ -116,6 +123,9 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
const handleSaveEditedXBlockData = () => {
|
||||
sendMessageToIframe(messageTypes.completeXBlockEditing, { locator: configureXBlockId });
|
||||
dispatch(updateCourseUnitSidebar(blockId));
|
||||
if (!isUnitVerticalType) {
|
||||
dispatch(fetchCourseSectionVerticalData(blockId));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinishXBlockDragging = () => {
|
||||
@@ -127,6 +137,21 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
openManageTagsModal();
|
||||
};
|
||||
|
||||
const handleShowProcessingNotification = (variant: string) => {
|
||||
if (variant) {
|
||||
dispatch(showProcessingNotification(variant));
|
||||
}
|
||||
};
|
||||
|
||||
const handleHideProcessingNotification = () => {
|
||||
dispatch(fetchCourseVerticalChildrenData(blockId, true, true));
|
||||
dispatch(hideProcessingNotification());
|
||||
};
|
||||
|
||||
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
|
||||
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
|
||||
};
|
||||
|
||||
const messageHandlers = useMessageHandlers({
|
||||
courseId,
|
||||
navigate,
|
||||
@@ -141,6 +166,9 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
handleSaveEditedXBlockData,
|
||||
handleFinishXBlockDragging,
|
||||
handleOpenManageTagsModal,
|
||||
handleShowProcessingNotification,
|
||||
handleHideProcessingNotification,
|
||||
handleRedirectToXBlockEditPage,
|
||||
});
|
||||
|
||||
useIframeMessages(messageHandlers);
|
||||
@@ -181,9 +209,10 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
allow={IFRAME_FEATURE_POLICY}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
style={{ width: '100%', height: iframeHeight + iframeOffset }}
|
||||
style={{ height: iframeHeight + iframeOffset }}
|
||||
scrolling="no"
|
||||
referrerPolicy="origin"
|
||||
className="xblock-container-iframe"
|
||||
aria-label={intl.formatMessage(messages.xblockIframeLabel, { xblockCount: courseVerticalChildren.length })}
|
||||
/>
|
||||
{configureXBlockId && (
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface XBlockTypes {
|
||||
export interface XBlockContainerIframeProps {
|
||||
courseId: string;
|
||||
blockId: string;
|
||||
isUnitVerticalType: boolean,
|
||||
unitXBlockActions: {
|
||||
handleDelete: (XBlockId: string | null) => void;
|
||||
handleDuplicate: (XBlockId: string | null) => void;
|
||||
|
||||
@@ -170,6 +170,7 @@ const ConfigureModal = ({
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
case COURSE_BLOCK_NAMES.libraryContent.id:
|
||||
case COURSE_BLOCK_NAMES.splitTest.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
|
||||
if (data.selectedPartitionIndex >= 0) {
|
||||
@@ -248,11 +249,12 @@ const ConfigureModal = ({
|
||||
);
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
case COURSE_BLOCK_NAMES.libraryContent.id:
|
||||
case COURSE_BLOCK_NAMES.splitTest.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
return (
|
||||
<UnitTab
|
||||
isXBlockComponent={isXBlockComponent}
|
||||
isLibraryContent={COURSE_BLOCK_NAMES.libraryContent.id === category}
|
||||
category={category}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY && !ancestorHasStaffLock}
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
import { Field } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import messages from './messages';
|
||||
|
||||
const UnitTab = ({
|
||||
isXBlockComponent,
|
||||
isLibraryContent,
|
||||
category,
|
||||
values,
|
||||
setFieldValue,
|
||||
showWarning,
|
||||
@@ -44,6 +45,17 @@ const UnitTab = ({
|
||||
return group.deleted && isGroupSelected;
|
||||
};
|
||||
|
||||
const getAccessBlockTitle = () => {
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.libraryContent.id:
|
||||
return messages.libraryContentAccess;
|
||||
case COURSE_BLOCK_NAMES.splitTest.id:
|
||||
return messages.splitTestAccess;
|
||||
default:
|
||||
return messages.unitAccess;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isXBlockComponent && (
|
||||
@@ -63,7 +75,7 @@ const UnitTab = ({
|
||||
{userPartitionInfo.selectablePartitions.length > 0 && (
|
||||
<Form.Group controlId="groupSelect">
|
||||
<h4 className="mt-3">
|
||||
<FormattedMessage {...messages[isLibraryContent ? 'libraryContentAccess' : 'unitAccess']} />
|
||||
<FormattedMessage {...getAccessBlockTitle()} />
|
||||
</h4>
|
||||
<hr />
|
||||
<Form.Label as="legend" className="font-weight-bold">
|
||||
@@ -149,12 +161,12 @@ const UnitTab = ({
|
||||
|
||||
UnitTab.defaultProps = {
|
||||
isXBlockComponent: false,
|
||||
isLibraryContent: false,
|
||||
category: undefined,
|
||||
};
|
||||
|
||||
UnitTab.propTypes = {
|
||||
isXBlockComponent: PropTypes.bool,
|
||||
isLibraryContent: PropTypes.bool,
|
||||
category: PropTypes.string,
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
discussionEnabled: PropTypes.bool,
|
||||
|
||||
@@ -47,9 +47,13 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Unit access',
|
||||
},
|
||||
libraryContentAccess: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.lib-content-access',
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.library-content-access',
|
||||
defaultMessage: 'Library content access',
|
||||
},
|
||||
splitTestAccess: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.split-test-access',
|
||||
defaultMessage: 'Split Test access',
|
||||
},
|
||||
discussionEnabledSectionTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.section-title',
|
||||
defaultMessage: 'Discussion',
|
||||
|
||||
@@ -15,12 +15,12 @@ const SubHeader = ({
|
||||
withSubHeaderContent,
|
||||
}) => (
|
||||
<div className={`${!hideBorder && 'border-bottom border-light-400'} mb-3`}>
|
||||
{breadcrumbs && (
|
||||
<div className="sub-header-breadcrumbs">{breadcrumbs}</div>
|
||||
)}
|
||||
<header className="sub-header">
|
||||
<h2 className="sub-header-title">
|
||||
<small className="sub-header-title-subtitle">{subtitle}</small>
|
||||
{breadcrumbs && (
|
||||
<div className="sub-header-breadcrumbs">{breadcrumbs}</div>
|
||||
)}
|
||||
{title}
|
||||
{titleActions && (
|
||||
<ActionRow className="ml-auto mt-2 justify-content-start">
|
||||
|
||||
@@ -107,7 +107,7 @@ export const createCorrectInternalRoute = (checkPath) => {
|
||||
basePath = basePath.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!checkPath.startsWith(basePath)) {
|
||||
if (!checkPath?.startsWith(basePath)) {
|
||||
return `${basePath}${checkPath}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user