feat: display editors as modals (#1838)
This commit is contained in:
committed by
GitHub
parent
b30a1c8c5e
commit
dbb1a996e1
@@ -671,8 +671,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handle creating Problem xblock and navigate to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
it('handle creating Problem xblock and showing editor modal', async () => {
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
@@ -701,11 +700,16 @@ describe('<CourseUnit />', () => {
|
||||
await waitFor(() => {
|
||||
const problemButton = getByRole('button', {
|
||||
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
userEvent.click(problemButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('heading', {
|
||||
name: new RegExp(`${addComponentMessages.blockEditorModalTitle.defaultMessage}`, 'i'),
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -735,44 +739,6 @@ describe('<CourseUnit />', () => {
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
|
||||
const { getByText, getByRole } = render(<RootWrapper />);
|
||||
const xblockType = 'text';
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
|
||||
window.scrollTo(0, 250);
|
||||
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
|
||||
|
||||
await waitFor(() => {
|
||||
const textButton = screen.getByRole('button', { name: /Text/i });
|
||||
|
||||
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
userEvent.click(textButton);
|
||||
|
||||
const addXBlockDialog = getByRole('dialog');
|
||||
expect(addXBlockDialog).toBeInTheDocument();
|
||||
|
||||
expect(getByText(
|
||||
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
|
||||
)).toBeInTheDocument();
|
||||
|
||||
const textRadio = screen.getByRole('radio', { name: /Text/i });
|
||||
userEvent.click(textRadio);
|
||||
expect(textRadio).toBeChecked();
|
||||
|
||||
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
|
||||
expect(selectBtn).toBeInTheDocument();
|
||||
|
||||
userEvent.click(selectBtn);
|
||||
});
|
||||
|
||||
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
|
||||
});
|
||||
|
||||
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
|
||||
const { getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
let units = null;
|
||||
@@ -863,8 +829,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles creating Video xblock and navigates to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
it('handles creating Video xblock and showing editor modal', async () => {
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
@@ -902,13 +867,18 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
const videoButton = getByRole('button', {
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
userEvent.click(videoButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
|
||||
});
|
||||
|
||||
/** TODO -- fix this test.
|
||||
await waitFor(() => {
|
||||
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
|
||||
});
|
||||
*/
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, StandardModal, useToggle,
|
||||
@@ -16,6 +16,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 EditorPage from '../../editors/EditorPage';
|
||||
|
||||
const AddComponent = ({
|
||||
parentLocator,
|
||||
@@ -24,7 +25,6 @@ const AddComponent = ({
|
||||
addComponentTemplateData,
|
||||
handleCreateNewCourseXBlock,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
|
||||
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
||||
@@ -32,6 +32,11 @@ const AddComponent = ({
|
||||
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
|
||||
const blockId = addComponentTemplateData.parentLocator || parentLocator;
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
|
||||
|
||||
const [blockType, setBlockType] = useState(null);
|
||||
const [courseId, setCourseId] = useState(null);
|
||||
const [newBlockId, setNewBlockId] = useState(null);
|
||||
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
|
||||
const [selectedComponents, setSelectedComponents] = useState([]);
|
||||
const [usageId, setUsageId] = useState(null);
|
||||
@@ -54,6 +59,11 @@ const AddComponent = ({
|
||||
closeSelectLibraryContentModal();
|
||||
}, [selectedComponents]);
|
||||
|
||||
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
|
||||
closeXBlockEditorModal();
|
||||
sendMessageToIframe(messageTypes.refreshXBlock, null);
|
||||
}, [closeXBlockEditorModal, sendMessageToIframe]);
|
||||
|
||||
const handleLibraryV2Selection = useCallback((selection) => {
|
||||
handleCreateNewCourseXBlock({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
@@ -70,11 +80,13 @@ const AddComponent = ({
|
||||
case COMPONENT_TYPES.dragAndDrop:
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.problem:
|
||||
case COMPONENT_TYPES.video:
|
||||
case COMPONENT_TYPES.problem:
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
||||
setCourseId(courseKey);
|
||||
setBlockType(type);
|
||||
setNewBlockId(locator);
|
||||
showXBlockEditorModal();
|
||||
});
|
||||
break;
|
||||
// TODO: The library functional will be a bit different of current legacy (CMS)
|
||||
@@ -99,9 +111,11 @@ const AddComponent = ({
|
||||
type,
|
||||
boilerplate: moduleName,
|
||||
parentLocator: blockId,
|
||||
}, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/html/${locator}`);
|
||||
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
|
||||
setCourseId(courseKey);
|
||||
setBlockType(type);
|
||||
setNewBlockId(locator);
|
||||
showXBlockEditorModal();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -201,6 +215,25 @@ const AddComponent = ({
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
<StandardModal
|
||||
title={intl.formatMessage(messages.blockEditorModalTitle)}
|
||||
isOpen={isXBlockEditorModalOpen}
|
||||
onClose={closeXBlockEditorModal}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
>
|
||||
<div className="editor-page">
|
||||
<EditorPage
|
||||
courseId={courseId}
|
||||
blockType={blockType}
|
||||
blockId={newBlockId}
|
||||
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||
onClose={closeXBlockEditorModal}
|
||||
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
|
||||
/>
|
||||
</div>
|
||||
</StandardModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Add selected components',
|
||||
description: 'Problem bank component add button text.',
|
||||
},
|
||||
videoPickerModalTitle: {
|
||||
id: 'course-authoring.course-unit.modal.video-title.text',
|
||||
defaultMessage: 'Select video',
|
||||
description: 'Video picker modal title.',
|
||||
},
|
||||
blockEditorModalTitle: {
|
||||
id: 'course-authoring.course-unit.modal.block-editor-title.text',
|
||||
defaultMessage: 'Edit component',
|
||||
description: 'Block editor modal title.',
|
||||
},
|
||||
modalContainerTitle: {
|
||||
id: 'course-authoring.course-unit.modal.container.title',
|
||||
defaultMessage: 'Add {componentTitle} component',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type UseMessageHandlersTypes = {
|
||||
courseId: string;
|
||||
navigate: (path: string) => void;
|
||||
dispatch: (action: any) => void;
|
||||
setIframeOffset: (height: number) => void;
|
||||
handleDeleteXBlock: (usageId: string) => void;
|
||||
handleScrollToXBlock: (scrollOffset: number) => void;
|
||||
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
|
||||
handleEditXBlock: (blockType: string, usageId: string) => void;
|
||||
handleManageXBlockAccess: (usageId: string) => void;
|
||||
handleShowLegacyEditXBlockModal: (id: string) => void;
|
||||
handleCloseLegacyEditorXBlockModal: () => void;
|
||||
@@ -14,7 +14,6 @@ export type UseMessageHandlersTypes = {
|
||||
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>;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
|
||||
*/
|
||||
export const useMessageHandlers = ({
|
||||
courseId,
|
||||
navigate,
|
||||
dispatch,
|
||||
setIframeOffset,
|
||||
handleDeleteXBlock,
|
||||
@@ -30,14 +29,14 @@ export const useMessageHandlers = ({
|
||||
handleOpenManageTagsModal,
|
||||
handleShowProcessingNotification,
|
||||
handleHideProcessingNotification,
|
||||
handleRedirectToXBlockEditPage,
|
||||
handleEditXBlock,
|
||||
}: UseMessageHandlersTypes): MessageHandlersTypes => {
|
||||
const { copyToClipboard } = useClipboard();
|
||||
|
||||
return useMemo(() => ({
|
||||
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
|
||||
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
|
||||
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
|
||||
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
|
||||
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
|
||||
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
|
||||
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
|
||||
@@ -52,9 +51,14 @@ export const useMessageHandlers = ({
|
||||
[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.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification(
|
||||
NOTIFICATION_MESSAGES.copying,
|
||||
),
|
||||
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
|
||||
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
|
||||
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock(
|
||||
payload.type,
|
||||
payload.locator,
|
||||
),
|
||||
}), [
|
||||
courseId,
|
||||
handleDeleteXBlock,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FC, useEffect, useState, useMemo, useCallback,
|
||||
} from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useToggle, Sheet } from '@openedx/paragon';
|
||||
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -35,6 +36,7 @@ import messages from './messages';
|
||||
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
|
||||
import { useIframeContent } from '../../generic/hooks/useIframeContent';
|
||||
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
|
||||
import EditorPage from '../../editors/EditorPage';
|
||||
|
||||
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
|
||||
@@ -45,6 +47,9 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
|
||||
const [blockType, setBlockType] = useState<string>('');
|
||||
const [newBlockId, setNewBlockId] = useState<string>('');
|
||||
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
|
||||
const [iframeOffset, setIframeOffset] = useState(0);
|
||||
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
|
||||
@@ -64,11 +69,23 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
setIframeRef(iframeRef);
|
||||
}, [setIframeRef]);
|
||||
|
||||
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
|
||||
closeXBlockEditorModal();
|
||||
sendMessageToIframe(messageTypes.refreshXBlock, null);
|
||||
}, [closeXBlockEditorModal, sendMessageToIframe]);
|
||||
|
||||
const handleEditXBlock = useCallback((type: string, id: string) => {
|
||||
setBlockType(type);
|
||||
setNewBlockId(id);
|
||||
showXBlockEditorModal();
|
||||
}, [showXBlockEditorModal]);
|
||||
|
||||
const handleDuplicateXBlock = useCallback(
|
||||
(blockType: string, usageId: string) => {
|
||||
(type: string, usageId: string) => {
|
||||
unitXBlockActions.handleDuplicate(usageId);
|
||||
if (supportedEditors[blockType]) {
|
||||
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
|
||||
if (supportedEditors[type]) {
|
||||
// istanbul ignore next
|
||||
handleEditXBlock(type, usageId);
|
||||
}
|
||||
},
|
||||
[unitXBlockActions, courseId, navigate],
|
||||
@@ -147,13 +164,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
dispatch(hideProcessingNotification());
|
||||
};
|
||||
|
||||
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
|
||||
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
|
||||
};
|
||||
|
||||
const messageHandlers = useMessageHandlers({
|
||||
courseId,
|
||||
navigate,
|
||||
dispatch,
|
||||
setIframeOffset,
|
||||
handleDeleteXBlock,
|
||||
@@ -167,7 +179,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
handleOpenManageTagsModal,
|
||||
handleShowProcessingNotification,
|
||||
handleHideProcessingNotification,
|
||||
handleRedirectToXBlockEditPage,
|
||||
handleEditXBlock,
|
||||
});
|
||||
|
||||
useIframeMessages(messageHandlers);
|
||||
@@ -186,6 +198,25 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
close={closeDeleteModal}
|
||||
onDeleteSubmit={onDeleteSubmit}
|
||||
/>
|
||||
<StandardModal
|
||||
title={intl.formatMessage(messages.blockEditorModalTitle)}
|
||||
isOpen={isXBlockEditorModalOpen}
|
||||
onClose={closeXBlockEditorModal}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
>
|
||||
<div className="editor-page">
|
||||
<EditorPage
|
||||
courseId={courseId}
|
||||
blockType={blockType}
|
||||
blockId={newBlockId}
|
||||
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||
onClose={closeXBlockEditorModal}
|
||||
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
|
||||
/>
|
||||
</div>
|
||||
</StandardModal>
|
||||
{Object.keys(accessManagedXBlockData).length ? (
|
||||
<ConfigureModal
|
||||
isXBlockComponent
|
||||
|
||||
@@ -15,6 +15,15 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-unit.xblock.iframe.label',
|
||||
defaultMessage: '{xblockCount} xBlocks inside the frame',
|
||||
},
|
||||
videoPickerModalTitle: {
|
||||
id: 'course-authoring.course-unit.xblock.video-editor.title',
|
||||
defaultMessage: 'Select video',
|
||||
},
|
||||
blockEditorModalTitle: {
|
||||
id: 'course-authoring.course-unit.xblock.editor.title',
|
||||
defaultMessage: 'Edit component',
|
||||
description: 'Block editor modal title.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as hooks from './hooks';
|
||||
|
||||
import supportedEditors from './supportedEditors';
|
||||
import type { EditorComponent } from './EditorComponent';
|
||||
import { useEditorContext } from './EditorContext';
|
||||
import AdvancedEditor from './AdvancedEditor';
|
||||
|
||||
export interface Props extends EditorComponent {
|
||||
@@ -17,7 +16,6 @@ export interface Props extends EditorComponent {
|
||||
learningContextId: string | null;
|
||||
lmsEndpointUrl: string | null;
|
||||
studioEndpointUrl: string | null;
|
||||
fullScreen?: boolean; // eslint-disable-line react/no-unused-prop-types
|
||||
}
|
||||
|
||||
const Editor: React.FC<Props> = ({
|
||||
@@ -42,7 +40,6 @@ const Editor: React.FC<Props> = ({
|
||||
studioEndpointUrl,
|
||||
},
|
||||
});
|
||||
const { fullScreen } = useEditorContext();
|
||||
|
||||
const EditorComponent = supportedEditors[blockType];
|
||||
|
||||
@@ -60,24 +57,7 @@ const Editor: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const innerEditor = <EditorComponent {...{ onClose, returnFunction }} />;
|
||||
|
||||
if (fullScreen) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="pgn__modal-fullscreen h-100"
|
||||
role="dialog"
|
||||
aria-label={blockType}
|
||||
>
|
||||
{innerEditor}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return innerEditor;
|
||||
return <EditorComponent {...{ onClose, returnFunction }} />;
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
@@ -7,14 +7,6 @@ import React from 'react';
|
||||
*/
|
||||
export interface EditorContext {
|
||||
learningContextId: string;
|
||||
/**
|
||||
* When editing components in the libraries part of the Authoring MFE, we show
|
||||
* the editors in a modal (fullScreen = false). This is the preferred approach
|
||||
* so that authors can see context behind the modal.
|
||||
* However, when making edits from the legacy course view, we display the
|
||||
* editors in a fullscreen view. This approach is deprecated.
|
||||
*/
|
||||
fullScreen: boolean;
|
||||
}
|
||||
|
||||
const context = React.createContext<EditorContext | undefined>(undefined);
|
||||
@@ -32,7 +24,6 @@ export function useEditorContext() {
|
||||
export const EditorContextProvider: React.FC<{
|
||||
children: React.ReactNode,
|
||||
learningContextId: string;
|
||||
fullScreen: boolean;
|
||||
}> = ({ children, ...contextData }) => {
|
||||
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
|
||||
return <context.Provider value={ctx}>{children}</context.Provider>;
|
||||
|
||||
@@ -37,7 +37,6 @@ const defaultPropsHtml = {
|
||||
lmsEndpointUrl: 'http://lms.test.none/',
|
||||
studioEndpointUrl: 'http://cms.test.none/',
|
||||
onClose: jest.fn(),
|
||||
fullScreen: false,
|
||||
};
|
||||
const fieldsHtml = {
|
||||
displayName: 'Introduction to Testing',
|
||||
@@ -66,22 +65,6 @@ describe('EditorPage', () => {
|
||||
expect(modalElement.classList).not.toContain('pgn__modal-fullscreen');
|
||||
});
|
||||
|
||||
test('it can display the Text (html) editor as a full page (when coming from the legacy UI)', async () => {
|
||||
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
|
||||
{ status: 200, data: snakeCaseObject(fieldsHtml) }
|
||||
));
|
||||
|
||||
render(<EditorPage {...defaultPropsHtml} fullScreen />);
|
||||
|
||||
// Then the editor should open
|
||||
expect(await screen.findByRole('heading', { name: /Introduction to Testing/ })).toBeInTheDocument();
|
||||
|
||||
const modalElement = screen.getByRole('dialog');
|
||||
expect(modalElement.classList).toContain('pgn__modal-fullscreen');
|
||||
expect(modalElement.classList).not.toContain('pgn__modal');
|
||||
expect(modalElement.classList).not.toContain('pgn__modal-xl');
|
||||
});
|
||||
|
||||
test('it shows the Advanced Editor if there is no corresponding editor', async () => {
|
||||
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line
|
||||
{ status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } }
|
||||
|
||||
@@ -14,7 +14,6 @@ interface Props extends EditorComponent {
|
||||
isMarkdownEditorEnabledForCourse?: boolean;
|
||||
lmsEndpointUrl?: string;
|
||||
studioEndpointUrl?: string;
|
||||
fullScreen?: boolean;
|
||||
children?: never;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ const EditorPage: React.FC<Props> = ({
|
||||
studioEndpointUrl = null,
|
||||
onClose = null,
|
||||
returnFunction = null,
|
||||
fullScreen = true,
|
||||
}) => (
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
@@ -40,7 +38,7 @@ const EditorPage: React.FC<Props> = ({
|
||||
studioEndpointUrl,
|
||||
}}
|
||||
>
|
||||
<EditorContextProvider fullScreen={fullScreen} learningContextId={courseId}>
|
||||
<EditorContextProvider learningContextId={courseId}>
|
||||
<Editor
|
||||
{...{
|
||||
onClose,
|
||||
|
||||
@@ -32,7 +32,6 @@ const defaultPropsHtml = {
|
||||
lmsEndpointUrl: 'http://lms.test.none/',
|
||||
studioEndpointUrl: 'http://cms.test.none/',
|
||||
onClose: jest.fn(),
|
||||
fullScreen: false,
|
||||
};
|
||||
const fieldsHtml = {
|
||||
displayName: 'Introduction to Testing',
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Close } from '@openedx/paragon/icons';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { EditorComponent } from '../../EditorComponent';
|
||||
import { useEditorContext } from '../../EditorContext';
|
||||
import TitleHeader from './components/TitleHeader';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -30,37 +29,18 @@ interface WrapperProps {
|
||||
}
|
||||
|
||||
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
|
||||
const { fullScreen } = useEditorContext();
|
||||
const intl = useIntl();
|
||||
if (fullScreen) {
|
||||
return (
|
||||
<div
|
||||
className="editor-container d-flex flex-column position-relative zindex-0"
|
||||
style={{ minHeight: '100%' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(messages.modalTitle);
|
||||
return (
|
||||
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => {
|
||||
const { fullScreen } = useEditorContext();
|
||||
return <ModalDialog.Body className={fullScreen ? 'pb-6' : 'pb-0'}>{ children }</ModalDialog.Body>;
|
||||
};
|
||||
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => <ModalDialog.Body className="pb-0">{ children }</ModalDialog.Body>;
|
||||
|
||||
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => {
|
||||
const { fullScreen } = useEditorContext();
|
||||
if (fullScreen) {
|
||||
return <div className="editor-footer fixed-bottom">{children}</div>;
|
||||
}
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{ children }</>;
|
||||
};
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{ children }</>;
|
||||
|
||||
interface Props extends EditorComponent {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('SelectTypeModal', () => {
|
||||
jest.spyOn(hooks, 'onSelect').mockImplementation(mockSelect);
|
||||
// This is a new-style test, unlike most of the old snapshot-based editor tests.
|
||||
render(
|
||||
<EditorContextProvider fullScreen={false} learningContextId="course-v1:Org+COURSE+RUN">
|
||||
<EditorContextProvider learningContextId="course-v1:Org+COURSE+RUN">
|
||||
<Provider store={editorStore}>
|
||||
<SelectTypeModal onClose={mockClose} />
|
||||
</Provider>
|
||||
|
||||
@@ -18,6 +18,7 @@ exports[`VideoEditor snapshots renders as expected with default behavior 1`] = `
|
||||
"useSelector": [MockFunction],
|
||||
}
|
||||
}
|
||||
onClose={[MockFunction props.onClose]}
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
|
||||
@@ -7,7 +7,9 @@ import VideoSettingsModal from './VideoSettingsModal';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
|
||||
interface Props {
|
||||
onReturn?: (() => void);
|
||||
isLibrary: boolean;
|
||||
onClose?: (() => void) | null;
|
||||
}
|
||||
|
||||
export const {
|
||||
@@ -27,13 +29,15 @@ export const hooks = {
|
||||
|
||||
const VideoEditorModal: React.FC<Props> = ({
|
||||
isLibrary,
|
||||
onClose,
|
||||
onReturn,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const selectedVideoId = searchParams.get('selectedVideoId');
|
||||
const selectedVideoUrl = searchParams.get('selectedVideoUrl');
|
||||
const onReturn = hooks.useReturnToGallery();
|
||||
const onSettingsReturn = onReturn || hooks.useReturnToGallery();
|
||||
const isLoaded = useSelector(
|
||||
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
);
|
||||
@@ -44,8 +48,9 @@ const VideoEditorModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<VideoSettingsModal {...{
|
||||
onReturn,
|
||||
onReturn: onSettingsReturn,
|
||||
isLibrary,
|
||||
onClose,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -20,11 +20,13 @@ import messages from '../../messages';
|
||||
interface Props {
|
||||
onReturn: () => void;
|
||||
isLibrary: boolean;
|
||||
onClose?: (() => void) | null;
|
||||
}
|
||||
|
||||
const VideoSettingsModal: React.FC<Props> = ({
|
||||
onReturn,
|
||||
isLibrary,
|
||||
onClose,
|
||||
}) => (
|
||||
<>
|
||||
{!isLibrary && (
|
||||
@@ -32,7 +34,7 @@ const VideoSettingsModal: React.FC<Props> = ({
|
||||
variant="link"
|
||||
className="text-primary-500"
|
||||
size="sm"
|
||||
onClick={onReturn}
|
||||
onClick={onClose || onReturn}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
marginLeft: '3px',
|
||||
|
||||
@@ -39,7 +39,7 @@ const VideoEditor: React.FC<EditorComponent> = ({
|
||||
>
|
||||
{(isCreateWorkflow || studioViewFinished) ? (
|
||||
<div className="video-editor">
|
||||
<VideoEditorModal {...{ isLibrary }} />
|
||||
<VideoEditorModal {...{ isLibrary, onClose, returnFunction }} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
|
||||
@@ -41,7 +41,6 @@ export const ComponentEditorModal: React.FC<Record<never, never>> = () => {
|
||||
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||
onClose={onClose}
|
||||
returnFunction={() => onClose}
|
||||
fullScreen={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user