fix: do open editor of new xblock when duplicating (#1887)

Fixes bug where after duplicating an xblock, the editor modal of the old xblock is being open instead of the new copied xblock.
This commit is contained in:
Daniel Valenzuela
2025-05-20 18:51:42 -04:00
committed by GitHub
parent 503642be8c
commit 1568067980
15 changed files with 574 additions and 325 deletions

View File

@@ -65,6 +65,7 @@ import xblockContainerIframeMessages from './xblock-container-iframe/messages';
import headerNavigationsMessages from './header-navigations/messages';
import sidebarMessages from './sidebar/messages';
import messages from './messages';
import * as selectors from '../data/selectors';
let axiosMock;
let store;
@@ -166,27 +167,27 @@ describe('<CourseUnit />', () => {
});
it('render CourseUnit component correctly', async () => {
const { getByText, getByRole, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
const unitHeaderTitle = screen.getByTestId('unit-header-title');
expect(screen.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: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
});
});
it('renders the course unit iframe with correct attributes', async () => {
const { getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.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', 'height: 0px;');
@@ -210,27 +211,27 @@ describe('<CourseUnit />', () => {
});
it('displays an error alert when a studioAjaxError message is received', async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.studioAjaxError, {
error: 'Some error text...',
});
});
expect(getByTestId('saving-error-alert')).toBeInTheDocument();
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument();
});
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
const { getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
const legacyXBlockEditModalIframe = getByTitle(
const legacyXBlockEditModalIframe = screen.getByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
);
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
@@ -248,14 +249,14 @@ describe('<CourseUnit />', () => {
});
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
const { getByTitle, queryByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
const legacyXBlockEditModalIframe = queryByTitle(
const legacyXBlockEditModalIframe = screen.queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
);
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -263,14 +264,14 @@ describe('<CourseUnit />', () => {
});
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
const legacyXBlockEditModalIframe = queryByTitle(
const legacyXBlockEditModalIframe = screen.queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
);
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -285,7 +286,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const courseUnitSidebar = getByTestId('course-unit-sidebar');
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument();
@@ -304,10 +305,10 @@ describe('<CourseUnit />', () => {
});
it('updates course unit sidebar after receiving refreshPositions message', async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.refreshPositions);
});
@@ -321,7 +322,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const courseUnitSidebar = getByTestId('course-unit-sidebar');
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument();
@@ -340,12 +341,10 @@ describe('<CourseUnit />', () => {
});
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
const {
getByTitle, getByText, queryByRole, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(async () => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -356,10 +355,10 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id,
});
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument();
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
const dialog = getByRole('dialog');
const dialog = screen.getByRole('dialog');
expect(dialog).toBeInTheDocument();
// Find the Cancel and Delete buttons within the iframe by their specific classes
@@ -372,7 +371,7 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id,
});
expect(getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toBeInTheDocument();
userEvent.click(deleteButton);
});
@@ -393,14 +392,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
});
axiosMock
@@ -431,28 +430,28 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
);
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
@@ -460,9 +459,7 @@ describe('<CourseUnit />', () => {
});
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
const {
getByTitle, getByRole, getByText, queryByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
id: courseVerticalChildrenMock.children[0].block_id,
@@ -482,8 +479,14 @@ describe('<CourseUnit />', () => {
const updatedCourseVerticalChildren = [
...courseVerticalChildrenMock.children,
{
...courseVerticalChildrenMock.children[0],
name: 'New Cloned XBlock',
block_id: '1234567890',
block_type: 'drag-and-drop-v2',
user_partition_info: {
selectable_partitions: [],
selected_partition_index: -1,
selected_groups_label: '',
},
},
];
@@ -495,9 +498,9 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -526,14 +529,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
});
axiosMock
@@ -542,7 +545,7 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -550,21 +553,21 @@ describe('<CourseUnit />', () => {
);
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
@@ -574,19 +577,19 @@ describe('<CourseUnit />', () => {
it('handles CourseUnit header action buttons', async () => {
const { open } = window;
window.open = jest.fn();
const { getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
const {
draft_preview_link: draftPreviewLink,
published_preview_link: publishedPreviewLink,
} = courseSectionVerticalMock;
await waitFor(() => {
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
userEvent.click(viewLiveButton);
expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
userEvent.click(previewButton);
expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
@@ -596,12 +599,7 @@ describe('<CourseUnit />', () => {
});
it('checks courseUnit title changing when edit query is successfully', async () => {
const {
findByText,
queryByRole,
getByRole,
getByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
let editTitleButton = null;
let titleEditField = null;
const newDisplayName = `${unitDisplayName} new`;
@@ -637,7 +635,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
const unitHeaderTitle = screen.getByTestId('unit-header-title');
editTitleButton = within(unitHeaderTitle)
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
titleEditField = within(unitHeaderTitle)
@@ -645,7 +643,7 @@ describe('<CourseUnit />', () => {
});
expect(titleEditField).not.toBeInTheDocument();
userEvent.click(editTitleButton);
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
await userEvent.clear(titleEditField);
await userEvent.type(titleEditField, newDisplayName);
@@ -653,9 +651,10 @@ describe('<CourseUnit />', () => {
expect(titleEditField).toHaveValue(newDisplayName);
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
expect(titleEditField).not.toBeInTheDocument();
expect(await findByText(newDisplayName)).toBeInTheDocument();
expect(await screen.findByText(newDisplayName)).toBeInTheDocument();
});
it('doesn\'t handle creating xblock and displays an error message', async () => {
@@ -679,10 +678,10 @@ describe('<CourseUnit />', () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
const { getByText, getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
@@ -702,7 +701,7 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
const problemButton = getByRole('button', {
const problemButton = screen.getByRole('button', {
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
hidden: true,
});
@@ -711,7 +710,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
expect(getByRole('heading', {
expect(screen.getByRole('heading', {
name: new RegExp(`${addComponentMessages.blockEditorModalTitle.defaultMessage}`, 'i'),
})).toBeInTheDocument();
});
@@ -723,28 +722,28 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
});
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
const { getByRole, getAllByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -754,7 +753,7 @@ describe('<CourseUnit />', () => {
]);
await waitFor(async () => {
units = getAllByTestId('course-unit-btn');
units = screen.getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length);
});
@@ -771,8 +770,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
units = getAllByTestId('course-unit-btn');
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
units = screen.getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -784,7 +783,7 @@ describe('<CourseUnit />', () => {
});
it('the sequence unit is updated after changing the unit header', async () => {
const { getAllByTestId, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
@@ -816,7 +815,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const unitHeaderTitle = getByTestId('unit-header-title');
const unitHeaderTitle = screen.getByTestId('unit-header-title');
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
userEvent.click(editTitleButton);
@@ -828,19 +827,21 @@ describe('<CourseUnit />', () => {
await userEvent.tab();
await waitFor(async () => {
const units = getAllByTestId('course-unit-btn');
const units = screen.getAllByTestId('course-unit-btn');
expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
});
});
it('handles creating Video xblock and showing editor modal', async () => {
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => {
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true });
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
@@ -861,15 +862,89 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
});
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
userEvent.click(videoButton);
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
waffleSpy.mockRestore();
});
it('handles creating Video xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
.onPost(getXBlockBaseApiUrl(blockId), {
publish: PUBLISH_TYPES.makePublic,
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = getByRole('button', {
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
@@ -890,45 +965,45 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
});
it('renders course unit details for a draft with unpublished changes', async () => {
const { getByText } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(getByText(
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
@@ -936,14 +1011,14 @@ describe('<CourseUnit />', () => {
});
it('renders course unit details in the sidebar', async () => {
const { getByText } = render(<RootWrapper />);
render(<RootWrapper />);
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
await waitFor(() => {
expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(courseUnitLocationId)).toBeInTheDocument();
expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
});
});
@@ -981,13 +1056,13 @@ describe('<CourseUnit />', () => {
});
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
const { getByRole, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let draftUnpublishedChangesHeading;
let visibilityCheckbox;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1024,7 +1099,7 @@ describe('<CourseUnit />', () => {
userEvent.click(visibilityCheckbox);
const modalNotification = getByRole('dialog');
const modalNotification = screen.getByRole('dialog');
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
@@ -1056,12 +1131,12 @@ describe('<CourseUnit />', () => {
});
it('should publish course unit after click on the "Publish" button', async () => {
const { getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let publishBtn;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
expect(publishBtn).toBeInTheDocument();
@@ -1095,12 +1170,12 @@ describe('<CourseUnit />', () => {
});
it('should discard changes after click on the "Discard changes" button', async () => {
const { getByTestId, getByRole } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let discardChangesBtn;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1110,7 +1185,7 @@ describe('<CourseUnit />', () => {
userEvent.click(discardChangesBtn);
const modalNotification = getByRole('dialog');
const modalNotification = screen.getByRole('dialog');
expect(modalNotification).toBeInTheDocument();
expect(within(modalNotification)
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
@@ -1147,7 +1222,7 @@ describe('<CourseUnit />', () => {
});
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
const { getByRole, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
let courseUnitSidebar;
let sidebarVisibilityCheckbox;
let modalVisibilityCheckbox;
@@ -1155,16 +1230,16 @@ describe('<CourseUnit />', () => {
let restrictAccessSelect;
await waitFor(() => {
courseUnitSidebar = getByTestId('course-unit-sidebar');
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
sidebarVisibilityCheckbox = within(courseUnitSidebar)
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
expect(sidebarVisibilityCheckbox).not.toBeChecked();
const headerConfigureBtn = getByRole('button', { name: /settings/i });
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i });
expect(headerConfigureBtn).toBeInTheDocument();
userEvent.click(headerConfigureBtn);
configureModal = getByTestId('configure-modal');
configureModal = screen.getByTestId('configure-modal');
restrictAccessSelect = within(configureModal)
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
expect(within(configureModal)
@@ -1220,8 +1295,8 @@ describe('<CourseUnit />', () => {
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
});
const { getByText } = render(<RootWrapper />);
await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
render(<RootWrapper />);
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); });
});
it('hides the Tags sidebar when not enabled', async () => {
@@ -1229,15 +1304,13 @@ describe('<CourseUnit />', () => {
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
});
const { queryByText } = render(<RootWrapper />);
await waitFor(() => { expect(queryByText('Unit tags')).not.toBeInTheDocument(); });
render(<RootWrapper />);
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); });
});
describe('Copy paste functionality', () => {
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
const {
getAllByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1249,8 +1322,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
@@ -1261,7 +1334,7 @@ describe('<CourseUnit />', () => {
]);
await waitFor(() => {
units = getAllByTestId('course-unit-btn');
units = screen.getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length);
});
@@ -1277,7 +1350,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
units = getAllByTestId('course-unit-btn');
units = screen.getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -1288,7 +1361,7 @@ describe('<CourseUnit />', () => {
});
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
const { getByRole, getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.copyXBlock, {
id: courseVerticalChildrenMock.children[0].block_id,
@@ -1307,11 +1380,11 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1347,7 +1420,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1357,9 +1430,7 @@ describe('<CourseUnit />', () => {
});
it('displays a notification about new files after pasting a component', async () => {
const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1371,8 +1442,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1391,7 +1462,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const newFilesAlert = getByTestId('has-new-files-alert');
const newFilesAlert = screen.getByTestId('has-new-files-alert');
expect(within(newFilesAlert)
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
@@ -1405,13 +1476,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
expect(queryByTestId('has-new-files-alert')).toBeNull();
expect(screen.queryByTestId('has-new-files-alert')).toBeNull();
});
it('displays a notification about conflicting errors after pasting a component', async () => {
const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1423,8 +1492,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1445,7 +1514,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
const conflictingErrorsAlert = screen.getByTestId('has-conflicting-errors-alert');
expect(within(conflictingErrorsAlert)
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1459,13 +1528,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull();
});
it('displays a notification about error files after pasting a component', async () => {
const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
@@ -1477,8 +1544,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1499,7 +1566,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const errorFilesAlert = getByTestId('has-error-files-alert');
const errorFilesAlert = screen.getByTestId('has-error-files-alert');
expect(within(errorFilesAlert)
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1508,11 +1575,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
expect(queryByTestId('has-error-files')).toBeNull();
expect(screen.queryByTestId('has-error-files')).toBeNull();
});
it('should hide the "Paste component" block if canPasteComponent is false', async () => {
const { queryByText, queryByRole } = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId))
@@ -1523,10 +1590,10 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
expect(queryByRole('button', {
expect(screen.queryByRole('button', {
name: messages.pasteButtonText.defaultMessage,
})).not.toBeInTheDocument();
expect(queryByText(
expect(screen.queryByText(
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
)).not.toBeInTheDocument();
});
@@ -1560,9 +1627,7 @@ describe('<CourseUnit />', () => {
});
it('should display "Move Modal" on receive trigger message', async () => {
const {
getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
await screen.findByText(unitDisplayName);
@@ -1576,15 +1641,12 @@ describe('<CourseUnit />', () => {
await screen.findByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
);
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
});
it('should navigates to xBlock current unit', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
await screen.findByText(unitDisplayName);
@@ -1600,7 +1662,7 @@ describe('<CourseUnit />', () => {
);
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
const currentSectionItemBtn = screen.getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1608,7 +1670,7 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
const currentSubsection = currentSection.child_info.children[0];
const currentSubsectionItemBtn = getByRole('button', {
const currentSubsectionItemBtn = screen.getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1616,7 +1678,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const currentComponentLocationText = getByText(
const currentComponentLocationText = screen.getByText(
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
);
expect(currentComponentLocationText).toBeInTheDocument();
@@ -1624,9 +1686,7 @@ describe('<CourseUnit />', () => {
});
it('should allow move operation and handles it successfully', async () => {
const {
getByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
@@ -1650,7 +1710,7 @@ describe('<CourseUnit />', () => {
);
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
const currentSectionItemBtn = screen.getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1658,7 +1718,7 @@ describe('<CourseUnit />', () => {
const currentSubsection = currentSection.child_info.children[1];
await waitFor(() => {
const currentSubsectionItemBtn = getByRole('button', {
const currentSubsectionItemBtn = screen.getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1667,14 +1727,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
const currentUnit = currentSubsection.child_info.children[0];
const currentUnitItemBtn = getByRole('button', {
const currentUnitItemBtn = screen.getByRole('button', {
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentUnitItemBtn).toBeInTheDocument();
userEvent.click(currentUnitItemBtn);
});
const moveModalBtn = getByRole('button', {
const moveModalBtn = screen.getByRole('button', {
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
});
expect(moveModalBtn).toBeInTheDocument();
@@ -1688,10 +1748,7 @@ describe('<CourseUnit />', () => {
});
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
const {
queryByRole,
getByText,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
@@ -1708,18 +1765,18 @@ describe('<CourseUnit />', () => {
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
const dismissButton = queryByRole('button', {
const dismissButton = screen.queryByRole('button', {
name: /dismiss/i, hidden: true,
});
const undoButton = queryByRole('button', {
const undoButton = screen.queryByRole('button', {
name: messages.undoMoveButton.defaultMessage, hidden: true,
});
const newLocationButton = queryByRole('button', {
const newLocationButton = screen.queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
expect(undoButton).toBeInTheDocument();
expect(newLocationButton).toBeInTheDocument();
@@ -1727,9 +1784,9 @@ describe('<CourseUnit />', () => {
userEvent.click(undoButton);
await waitFor(() => {
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
});
expect(getByText(
expect(screen.getByText(
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
@@ -1738,9 +1795,7 @@ describe('<CourseUnit />', () => {
});
it('should navigate to new location by button click', async () => {
const {
queryByRole,
} = render(<RootWrapper />);
render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
@@ -1755,7 +1810,7 @@ describe('<CourseUnit />', () => {
callbackFn: requestData.callbackFn,
}), store.dispatch);
const newLocationButton = queryByRole('button', {
const newLocationButton = screen.queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
userEvent.click(newLocationButton);
@@ -1768,16 +1823,14 @@ describe('<CourseUnit />', () => {
describe('XBlock restrict access', () => {
it('opens xblock restrict access modal successfully', async () => {
const {
getByTitle, getByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const usageId = courseVerticalChildrenMock.children[0].block_id;
expect(iframe).toBeInTheDocument();
@@ -1787,7 +1840,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const configureModal = getByTestId('configure-modal');
const configureModal = screen.getByTestId('configure-modal');
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
@@ -1796,12 +1849,10 @@ describe('<CourseUnit />', () => {
});
it('closes xblock restrict access modal when cancel button is clicked', async () => {
const {
getByTitle, queryByTestId, getByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
usageId: courseVerticalChildrenMock.children[0].block_id,
@@ -1809,7 +1860,7 @@ describe('<CourseUnit />', () => {
});
await waitFor(() => {
const configureModal = getByTestId('configure-modal');
const configureModal = screen.getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument();
userEvent.click(within(configureModal).getByRole('button', {
name: configureModalMessages.cancelButton.defaultMessage,
@@ -1817,7 +1868,7 @@ describe('<CourseUnit />', () => {
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
});
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
});
it('handles submit xblock restrict access data when save button is clicked', async () => {
@@ -1828,15 +1879,13 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
const {
getByTitle, getByRole, getByTestId, queryByTestId,
} = render(<RootWrapper />);
render(<RootWrapper />);
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
});
@@ -1846,13 +1895,13 @@ describe('<CourseUnit />', () => {
});
});
const configureModal = await waitFor(() => getByTestId('configure-modal'));
const configureModal = await waitFor(() => screen.getByTestId('configure-modal'));
expect(configureModal).toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
const restrictAccessSelect = getByRole('combobox', {
const restrictAccessSelect = screen.getByRole('combobox', {
name: configureModalMessages.restrictAccessTo.defaultMessage,
});
@@ -1882,17 +1931,17 @@ describe('<CourseUnit />', () => {
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
});
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
});
});
const checkLegacyEditModalOnEditMessage = async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
render(<RootWrapper />);
await waitFor(() => {
const editButton = getByTestId('header-edit-button');
const editButton = screen.getByTestId('header-edit-button');
expect(editButton).toBeInTheDocument();
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
userEvent.click(editButton);
});
@@ -2079,46 +2128,65 @@ describe('<CourseUnit />', () => {
});
it('should render split test content page correctly', async () => {
const {
getByText,
getByRole,
queryByRole,
getByTestId,
queryByText,
} = render(<RootWrapper />);
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';
waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
const unitHeaderTitle = screen.getByTestId('unit-header-title');
expect(screen.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(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.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(screen.queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.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();
expect(screen.queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(screen.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 },
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage
.replaceAll('{bold_tag}', ''),
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'link',
name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
},
];
sidebarContent.forEach(({ query, type, name }) => {
@@ -2126,7 +2194,7 @@ describe('<CourseUnit />', () => {
});
expect(
queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
).toHaveAttribute('href', helpLinkUrl);
});
});
@@ -2139,7 +2207,7 @@ describe('<CourseUnit />', () => {
});
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
const { getByTitle } = render(<RootWrapper />);
render(<RootWrapper />);
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
@@ -2166,7 +2234,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.currentXBlockId, {
id: targetBlockId,

View File

@@ -8,6 +8,7 @@ import {
} from '@openedx/paragon';
import { getCourseSectionVertical } from '../data/selectors';
import { getWaffleFlags } from '../../data/selectors';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn';
@@ -16,6 +17,7 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useEventListener } from '../../generic/hooks';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
const AddComponent = ({
@@ -32,6 +34,7 @@ const AddComponent = ({
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState(null);
@@ -41,6 +44,7 @@ const AddComponent = ({
const [selectedComponents, setSelectedComponents] = useState([]);
const [usageId, setUsageId] = useState(null);
const { sendMessageToIframe } = useIframe();
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
const receiveMessage = useCallback(({ data: { type, payload } }) => {
if (type === messageTypes.showMultipleComponentPicker) {
@@ -61,8 +65,9 @@ const AddComponent = ({
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, sendMessageToIframe]);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
const handleLibraryV2Selection = useCallback((selection) => {
handleCreateNewCourseXBlock({
@@ -80,7 +85,6 @@ const AddComponent = ({
case COMPONENT_TYPES.dragAndDrop:
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break;
case COMPONENT_TYPES.video:
case COMPONENT_TYPES.problem:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
setCourseId(courseKey);
@@ -89,6 +93,21 @@ const AddComponent = ({
showXBlockEditorModal();
});
break;
case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock(
{ type, parentLocator: blockId },
/* istanbul ignore next */ ({ courseKey, locator }) => {
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
if (useVideoGalleryFlow) {
showVideoSelectorModal();
} else {
showXBlockEditorModal();
}
},
);
break;
// TODO: The library functional will be a bit different of current legacy (CMS)
// behaviour and this ticket is on hold (blocked by other development team).
case COMPONENT_TYPES.library:
@@ -215,6 +234,24 @@ const AddComponent = ({
onChangeComponentSelection={setSelectedComponents}
/>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.videoPickerModalTitle)}
isOpen={isVideoSelectorModalOpen}
onClose={closeVideoSelectorModal}
isOverflowVisible={false}
size="xl"
>
<div className="selector-page">
<VideoSelectorPage
blockId={newBlockId}
courseId={courseId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onCancel={closeVideoSelectorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.blockEditorModalTitle)}
isOpen={isXBlockEditorModalOpen}

View File

@@ -4,7 +4,7 @@ export type UseMessageHandlersTypes = {
setIframeOffset: (height: number) => void;
handleDeleteXBlock: (usageId: string) => void;
handleScrollToXBlock: (scrollOffset: number) => void;
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
handleDuplicateXBlock: (usageId: string) => void;
handleEditXBlock: (blockType: string, usageId: string) => void;
handleManageXBlockAccess: (usageId: string) => void;
handleShowLegacyEditXBlockModal: (id: string) => void;

View File

@@ -37,7 +37,7 @@ export const useMessageHandlers = ({
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId),
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
[messageTypes.toggleCourseXBlockDropdown]: ({

View File

@@ -4,8 +4,7 @@ import {
} from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import {
hideProcessingNotification,
@@ -14,9 +13,9 @@ import {
import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import ModalIframe from '../../generic/modal-iframe';
import { getWaffleFlags } from '../../data/selectors';
import { IFRAME_FEATURE_POLICY } from '../../constants';
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
import supportedEditors from '../../editors/supportedEditors';
import { useIframe } from '../../generic/hooks/context/hooks';
import {
fetchCourseSectionVerticalData,
@@ -36,6 +35,7 @@ import messages from './messages';
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
import { useIframeContent } from '../../generic/hooks/useIframeContent';
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
@@ -43,12 +43,13 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
}) => {
const intl = useIntl();
const dispatch = useDispatch();
const navigate = useNavigate();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState<string>('');
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
const [newBlockId, setNewBlockId] = useState<string>('');
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
const [iframeOffset, setIframeOffset] = useState(0);
@@ -71,24 +72,25 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, sendMessageToIframe]);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
const handleEditXBlock = useCallback((type: string, id: string) => {
setBlockType(type);
setNewBlockId(id);
showXBlockEditorModal();
}, [showXBlockEditorModal]);
if (type === 'video' && useVideoGalleryFlow) {
showVideoSelectorModal();
} else {
showXBlockEditorModal();
}
}, [showVideoSelectorModal, showXBlockEditorModal]);
const handleDuplicateXBlock = useCallback(
(type: string, usageId: string) => {
(usageId: string) => {
unitXBlockActions.handleDuplicate(usageId);
if (supportedEditors[type]) {
// istanbul ignore next
handleEditXBlock(type, usageId);
}
},
[unitXBlockActions, courseId, navigate],
[unitXBlockActions, courseId],
);
const handleDeleteXBlock = (usageId: string) => {
@@ -198,6 +200,24 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
close={closeDeleteModal}
onDeleteSubmit={onDeleteSubmit}
/>
<StandardModal
title={intl.formatMessage(messages.videoPickerModalTitle)}
isOpen={isVideoSelectorModalOpen}
onClose={closeVideoSelectorModal}
isOverflowVisible={false}
size="xl"
>
<div className="selector-page">
<VideoSelectorPage
blockId={newBlockId}
courseId={courseId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onCancel={closeVideoSelectorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.blockEditorModalTitle)}
isOpen={isXBlockEditorModalOpen}

View File

@@ -26,6 +26,7 @@ const slice = createSlice({
useNewCertificatesPage: true,
useNewTextbooksPage: true,
useNewGroupConfigurationsPage: true,
useVideoGalleryFlow: false,
},
},
reducers: {

View File

@@ -9,6 +9,8 @@ const VideoSelector = ({
learningContextId,
lmsEndpointUrl,
studioEndpointUrl,
returnFunction,
onCancel,
}) => {
const dispatch = useDispatch();
const loading = hooks.useInitializeApp({
@@ -26,7 +28,7 @@ const VideoSelector = ({
return null;
}
return (
<VideoGallery />
<VideoGallery returnFunction={returnFunction} onCancel={onCancel} />
);
};
@@ -35,6 +37,8 @@ VideoSelector.propTypes = {
learningContextId: PropTypes.string.isRequired,
lmsEndpointUrl: PropTypes.string.isRequired,
studioEndpointUrl: PropTypes.string.isRequired,
returnFunction: PropTypes.func,
onCancel: PropTypes.func,
};
export default VideoSelector;

View File

@@ -10,6 +10,8 @@ const VideoSelectorPage = ({
courseId,
lmsEndpointUrl,
studioEndpointUrl,
returnFunction,
onCancel,
}) => (
<Provider store={store}>
<ErrorBoundary
@@ -24,6 +26,8 @@ const VideoSelectorPage = ({
learningContextId: courseId,
lmsEndpointUrl,
studioEndpointUrl,
returnFunction,
onCancel,
}}
/>
</ErrorBoundary>
@@ -42,6 +46,8 @@ VideoSelectorPage.propTypes = {
courseId: PropTypes.string,
lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string,
returnFunction: PropTypes.func,
onCancel: PropTypes.func,
};
export default VideoSelectorPage;

View File

@@ -86,6 +86,7 @@ export const filterList = ({
export const useVideoListProps = ({
searchSortProps,
videos,
returnFunction,
}) => {
const [highlighted, setHighlighted] = React.useState(null);
const [
@@ -128,7 +129,10 @@ export const useVideoListProps = ({
},
selectBtnProps: {
onClick: () => {
if (highlighted) {
/* istanbul ignore next */
if (returnFunction) {
returnFunction()();
} else if (highlighted) {
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
} else {
setShowSelectVideoError(true);
@@ -138,10 +142,15 @@ export const useVideoListProps = ({
};
};
export const useVideoUploadHandler = ({ replace }) => {
export const useVideoUploadHandler = ({ replace, uploadHandler }) => {
const learningContextId = useSelector(selectors.app.learningContextId);
const blockId = useSelector(selectors.app.blockId);
const path = `/course/${learningContextId}/editor/video_upload/${blockId}`;
if (uploadHandler) {
return () => {
uploadHandler();
};
}
if (replace) {
return () => window.location.replace(path);
}
@@ -191,11 +200,12 @@ export const getstatusBadgeVariant = ({ status }) => {
export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status);
export const useVideoProps = ({ videos }) => {
export const useVideoProps = ({ videos, uploadHandler, returnFunction }) => {
const searchSortProps = useSearchAndSortProps();
const videoList = useVideoListProps({
searchSortProps,
videos,
returnFunction,
});
const {
galleryError,
@@ -203,7 +213,7 @@ export const useVideoProps = ({ videos }) => {
inputError,
selectBtnProps,
} = videoList;
const fileInput = { click: useVideoUploadHandler({ replace: false }) };
const fileInput = { click: useVideoUploadHandler({ replace: false, uploadHandler }) };
return {
galleryError,

View File

@@ -1,5 +1,10 @@
import React, { useEffect } from 'react';
import { Image } from '@openedx/paragon';
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Image, useToggle, StandardModal,
} from '@openedx/paragon';
import { useSearchParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { selectors } from '../../data/redux';
import * as hooks from './hooks';
@@ -8,8 +13,11 @@ import { acceptedImgKeys } from './utils';
import messages from './messages';
import { RequestKeys } from '../../data/constants/requests';
import videoThumbnail from '../../data/images/videoThumbnail.svg';
import VideoUploadEditor from '../VideoUploadEditor';
import VideoEditor from '../VideoEditor';
const VideoGallery = () => {
const VideoGallery = ({ returnFunction, onCancel }) => {
const intl = useIntl();
const rawVideos = useSelector(selectors.app.videos);
const isLoaded = useSelector(
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
@@ -21,14 +29,27 @@ const VideoGallery = () => {
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
);
const videos = hooks.buildVideos({ rawVideos });
const handleVideoUpload = hooks.useVideoUploadHandler({ replace: true });
const [isVideoUploadModalOpen, showVideoUploadModal, closeVideoUploadModal] = useToggle();
const [isVideoEditorModalOpen, showVideoEditorModal, closeVideoEditorModal] = useToggle();
const setSearchParams = useSearchParams()[1];
useEffect(() => {
// If no videos exists redirects to the video upload screen
// If no videos exists opens to the video upload modal
if (isLoaded && videos.length === 0) {
handleVideoUpload();
showVideoUploadModal();
}
}, [isLoaded]);
const onVideoUpload = useCallback((videoUrl) => {
closeVideoUploadModal();
showVideoEditorModal();
setSearchParams({ selectedVideoUrl: videoUrl });
}, [closeVideoUploadModal, showVideoEditorModal, setSearchParams]);
const uploadHandler = useCallback(() => {
showVideoUploadModal();
});
const {
galleryError,
inputError,
@@ -36,7 +57,7 @@ const VideoGallery = () => {
galleryProps,
searchSortProps,
selectBtnProps,
} = hooks.useVideoProps({ videos });
} = hooks.useVideoProps({ videos, uploadHandler, returnFunction });
const handleCancel = hooks.useCancelHandler();
const modalMessages = {
@@ -60,8 +81,8 @@ const VideoGallery = () => {
<SelectionModal
{...{
isOpen: true,
close: handleCancel,
size: 'fullscreen',
close: onCancel || handleCancel,
size: 'xl',
isFullscreenScroll: false,
galleryError,
inputError,
@@ -79,10 +100,34 @@ const VideoGallery = () => {
isFetchError,
}}
/>
<StandardModal
title={intl.formatMessage(messages.videoUploadModalTitle)}
isOpen={isVideoUploadModalOpen}
onClose={closeVideoUploadModal}
isOverflowVisible={false}
size="xl"
hasCloseButton={false}
>
<div className="editor-page">
<VideoUploadEditor
onUpload={onVideoUpload}
onClose={closeVideoUploadModal}
/>
</div>
</StandardModal>
{isVideoEditorModalOpen && (
<VideoEditor
onClose={closeVideoEditorModal}
returnFunction={returnFunction}
/>
)}
</div>
);
};
VideoGallery.propTypes = {};
VideoGallery.propTypes = {
onCancel: PropTypes.func,
returnFunction: PropTypes.func,
};
export default VideoGallery;

View File

@@ -6,6 +6,8 @@ import React from 'react';
import {
act, fireEvent, render, screen,
} from '@testing-library/react';
import * as reactRouterDom from 'react-router-dom';
import * as reduxThunks from '../../data/redux';
import VideoGallery from './index';
@@ -120,11 +122,10 @@ describe('VideoGallery', () => {
expect(screen.getByText(video.client_video_id)).toBeInTheDocument()
));
});
it('navigates to video upload page when there are no videos', async () => {
expect(window.location.replace).not.toHaveBeenCalled();
it('renders video upload modal when there are no videos', async () => {
updateState({ videos: [] });
await renderComponent();
expect(window.location.replace).toHaveBeenCalled();
expect(screen.getByRole('heading', { name: /upload or embed a new video/i })).toBeInTheDocument();
});
it.each([
[/newest/i, [2, 1, 3]],
@@ -191,5 +192,36 @@ describe('VideoGallery', () => {
expect(screen.queryByText('client_id_1')).not.toBeInTheDocument();
expect(screen.queryByText('client_id_3')).not.toBeInTheDocument();
});
it('calls onVideoUpload correctly when a video is uploaded', async () => {
// Mock useSearchParams
const setSearchParams = jest.fn();
jest.spyOn(reactRouterDom, 'useSearchParams').mockReturnValue([{}, setSearchParams]);
// Mock the uploadVideo thunk to immediately call postUploadRedirect
jest.spyOn(reduxThunks.thunkActions.video, 'uploadVideo').mockImplementation(
({ postUploadRedirect }) => () => {
if (postUploadRedirect) {
postUploadRedirect('http://test.video/url.mp4');
}
return { type: 'MOCK_UPLOAD_VIDEO' };
},
);
await renderComponent();
// Open the upload modal by clicking the button
const openModalButton = screen.getByRole('button', { name: /upload or embed a new video/i });
fireEvent.click(openModalButton);
// Wait for the input to appear in the modal
const urlInput = await screen.findByPlaceholderText('Paste your video ID or URL');
fireEvent.change(urlInput, { target: { value: 'http://test.video/url.mp4' } });
const submitButton = screen.getByRole('button', { name: /submit/i });
fireEvent.click(submitButton);
expect(setSearchParams).toHaveBeenCalledWith({ selectedVideoUrl: 'http://test.video/url.mp4' });
});
});
});

View File

@@ -21,7 +21,16 @@ const messages = {
defaultMessage: 'Upload or embed a new video',
description: 'Label for upload button',
},
videoUploadModalTitle: {
id: 'authoring.selectvideomodal.upload.title',
defaultMessage: 'Upload or embed a new video',
description: 'Label for upload modal',
},
videoEditorModalTitle: {
id: 'authoring.selectvideomodal.edit.title',
defaultMessage: 'Edit selected video',
description: 'Label for editor modal',
},
// Sort Dropdown
sortByDateNewest: {
id: 'authoring.selectvideomodal.sort.datenewest.label',

View File

@@ -10,9 +10,9 @@ import { thunkActions } from '../../data/redux';
import * as hooks from './hooks';
import messages from './messages';
const URLUploader = () => {
const URLUploader = ({ onUpload }) => {
const [textInputValue, setTextInputValue] = React.useState('');
const onURLUpload = hooks.onVideoUpload('selectedVideoUrl');
const onURLUpload = hooks.onVideoUpload('selectedVideoUrl', onUpload);
const intl = useIntl();
return (
<div className="d-flex flex-column">
@@ -58,16 +58,16 @@ const URLUploader = () => {
);
};
export const VideoUploader = ({ setLoading }) => {
export const VideoUploader = ({ setLoading, onUpload, onClose }) => {
const dispatch = useDispatch();
const intl = useIntl();
const goBack = hooks.useHistoryGoBack();
const goBack = onClose || hooks.useHistoryGoBack();
const handleProcessUpload = ({ fileData }) => {
dispatch(thunkActions.video.uploadVideo({
supportedFiles: [fileData],
setLoadSpinner: setLoading,
postUploadRedirect: hooks.onVideoUpload('selectedVideoId'),
postUploadRedirect: hooks.onVideoUpload('selectedVideoId', onUpload),
}));
};
@@ -85,14 +85,20 @@ export const VideoUploader = ({ setLoading }) => {
<Dropzone
accept={{ 'video/*': ['.mp4', '.mov'] }}
onProcessUpload={handleProcessUpload}
inputComponent={<URLUploader />}
inputComponent={<URLUploader onUpload={onUpload} />}
/>
</div>
);
};
URLUploader.propTypes = {
onUpload: PropTypes.func,
};
VideoUploader.propTypes = {
setLoading: PropTypes.func.isRequired,
onUpload: PropTypes.func,
onClose: PropTypes.func,
};
export default VideoUploader;

View File

@@ -11,15 +11,20 @@ export const {
navigateTo,
} = appHooks;
export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl') => {
export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl', onUpload = null) => {
const learningContextId = selectors.app.learningContextId(storeState);
const blockId = selectors.app.blockId(storeState);
if (onUpload) {
return (videoUrl) => {
onUpload(videoUrl, learningContextId, blockId);
};
}
return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?${uploadType}=${videoUrl}`);
};
export const onVideoUpload = (uploadType) => {
export const onVideoUpload = (uploadType, onUpload) => {
const storeState = store.getState();
return module.postUploadRedirect(storeState, uploadType);
return module.postUploadRedirect(storeState, uploadType, onUpload);
};
export const useUploadVideo = async ({

View File

@@ -1,17 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Spinner } from '@openedx/paragon';
import './index.scss';
import messages from './messages';
import { VideoUploader } from './VideoUploader';
const VideoUploadEditor = () => {
const VideoUploadEditor = ({ onUpload, onClose }) => {
const [loading, setLoading] = React.useState(false);
const intl = useIntl();
return (!loading) ? (
<div className="d-flex marked-area flex-column p-3">
<VideoUploader setLoading={setLoading} />
<VideoUploader onUpload={onUpload} onClose={onClose} setLoading={setLoading} />
</div>
) : (
<div style={{
@@ -30,4 +31,9 @@ const VideoUploadEditor = () => {
);
};
VideoUploadEditor.propTypes = {
onUpload: PropTypes.func,
onClose: PropTypes.func,
};
export default VideoUploadEditor;