Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add AdvancedEditors with an iframe [FC-0076] #1568

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/editors/AdvancedEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getConfig } from '@edx/frontend-platform';

import {
render,
initializeMocks,
} from '../testUtils';
import AdvancedEditor from './AdvancedEditor';

jest.mock('./containers/EditorContainer', () => ({
EditorModalWrapper: jest.fn(() => (<div>Advanced Editor Iframe</div>)),
}));

describe('AdvancedEditor', () => {
beforeEach(() => {
initializeMocks();
});

it('should call onClose when receiving "cancel-clicked" message', () => {
const onCloseMock = jest.fn();

render(<AdvancedEditor usageKey="test" onClose={onCloseMock} />);

const messageEvent = new MessageEvent('message', {
data: 'cancel-clicked',
origin: getConfig().STUDIO_BASE_URL,
});

window.dispatchEvent(messageEvent);

expect(onCloseMock).toHaveBeenCalled();
});

it('should call onClose when receiving "save-clicked" message', () => {
const onCloseMock = jest.fn();

render(<AdvancedEditor usageKey="test" onClose={onCloseMock} />);

const messageEvent = new MessageEvent('message', {
data: 'save-clicked',
origin: getConfig().STUDIO_BASE_URL,
});

window.dispatchEvent(messageEvent);

expect(onCloseMock).toHaveBeenCalled();
});

it('should not call onClose if the message is from an invalid origin', () => {
const onCloseMock = jest.fn();

render(<AdvancedEditor usageKey="test" onClose={onCloseMock} />);

const messageEvent = new MessageEvent('message', {
data: 'cancel-clicked',
origin: 'https://invalid-origin.com',
});

window.dispatchEvent(messageEvent);

expect(onCloseMock).not.toHaveBeenCalled();
});
});
41 changes: 41 additions & 0 deletions src/editors/AdvancedEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';

import { LibraryBlock } from '../library-authoring/LibraryBlock';
import { EditorModalWrapper } from './containers/EditorContainer';

interface AdvancedEditorProps {
usageKey: string,
onClose: Function | null,
}

const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => {
useEffect(() => {
const handleIframeMessage = (event) => {
if (event.origin !== getConfig().STUDIO_BASE_URL) {
return;
}

if (onClose && (event.data === 'cancel-clicked' || event.data === 'save-clicked')) {
onClose();
}
};

window.addEventListener('message', handleIframeMessage);

return () => {
window.removeEventListener('message', handleIframeMessage);
};
}, []);

return (
<EditorModalWrapper onClose={onClose as () => void}>
<LibraryBlock
usageKey={usageKey}
view="studio_view"
/>
</EditorModalWrapper>
);
};

export default AdvancedEditor;
17 changes: 12 additions & 5 deletions src/editors/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// <EditorPage> as its parent, so they are tested together in EditorPage.test.tsx
import React from 'react';
import { useDispatch } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import messages from './messages';
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 {
blockType: string;
Expand Down Expand Up @@ -43,9 +42,17 @@ const Editor: React.FC<Props> = ({
const { fullScreen } = useEditorContext();

const EditorComponent = supportedEditors[blockType];
const innerEditor = (EditorComponent !== undefined)
? <EditorComponent {...{ onClose, returnFunction }} />
: <FormattedMessage {...messages.couldNotFindEditor} />;

if (EditorComponent === undefined && blockId) {
return (
<AdvancedEditor
usageKey={blockId}
onClose={onClose}
/>
);
}

const innerEditor = <EditorComponent {...{ onClose, returnFunction }} />;

if (fullScreen) {
return (
Expand Down
9 changes: 5 additions & 4 deletions src/editors/EditorPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jest.spyOn(editorCmsApi, 'fetchByUnitId').mockImplementation(async () => ({
}],
},
}));
jest.mock('../library-authoring/LibraryBlock', () => ({
LibraryBlock: jest.fn(() => (<div>Advanced Editor Iframe</div>)),
}));

const defaultPropsHtml = {
blockId: 'block-v1:Org+TS100+24+type@html+block@123456html',
Expand Down Expand Up @@ -79,9 +82,7 @@ describe('EditorPage', () => {
expect(modalElement.classList).not.toContain('pgn__modal-xl');
});

test('it shows an error message if there is no corresponding editor', async () => {
// We can edit 'html', 'problem', and 'video' blocks.
// But if we try to edit some other type, say 'fake', we should get an error:
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: '' } }
));
Expand All @@ -93,6 +94,6 @@ describe('EditorPage', () => {
};
render(<EditorPage {...defaultPropsFake} />);

expect(await screen.findByText('Error: Could Not find Editor')).toBeInTheDocument();
expect(await screen.findByText('Advanced Editor Iframe')).toBeInTheDocument();
});
});
5 changes: 0 additions & 5 deletions src/editors/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({

couldNotFindEditor: {
id: 'authoring.editorpage.selecteditor.error',
defaultMessage: 'Error: Could Not find Editor',
description: 'Error Message Dispayed When An unsopported Editor is desired in V2',
},
dropVideoFileHere: {
defaultMessage: 'Drag and drop video here or click to upload',
id: 'VideoUploadEditor.dropVideoFileHere',
Expand Down
12 changes: 10 additions & 2 deletions src/library-authoring/LibraryBlock/LibraryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface LibraryBlockProps {
onBlockNotification?: (event: { eventType: string; [key: string]: any }) => void;
usageKey: string;
version?: VersionSpec;
view?: string;
}
/**
* React component that displays an XBlock in a sandboxed IFrame.
Expand All @@ -20,7 +21,12 @@ interface LibraryBlockProps {
* cannot access things like the user's cookies, nor can it make GET/POST
* requests as the user. However, it is allowed to call any XBlock handlers.
*/
export const LibraryBlock = ({ onBlockNotification, usageKey, version }: LibraryBlockProps) => {
export const LibraryBlock = ({
onBlockNotification,
usageKey,
version,
view,
}: LibraryBlockProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iFrameHeight, setIFrameHeight] = useState(50);
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
Expand Down Expand Up @@ -71,6 +77,8 @@ export const LibraryBlock = ({ onBlockNotification, usageKey, version }: Library

const queryStr = version ? `?version=${version}` : '';

const xblockView = view ?? 'student_view';

return (
<div style={{
height: `${iFrameHeight}vh`,
Expand All @@ -83,7 +91,7 @@ export const LibraryBlock = ({ onBlockNotification, usageKey, version }: Library
<iframe
ref={iframeRef}
title={intl.formatMessage(messages.iframeTitle)}
src={`${studioBaseUrl}/xblocks/v2/${usageKey}/embed/student_view/${queryStr}`}
src={`${studioBaseUrl}/xblocks/v2/${usageKey}/embed/${xblockView}/${queryStr}`}
data-testid="block-preview"
style={{
width: '100%',
Expand Down
4 changes: 1 addition & 3 deletions src/library-authoring/components/ComponentEditorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ export function canEditComponent(usageKey: string): boolean {
return false;
}

// Which XBlock/component types are supported by the 'editors' built in to this repo?
const mfeEditorTypes = ['html', 'problem', 'video'];
return mfeEditorTypes.includes(blockType);
return getConfig().LIBRARY_SUPPORTED_BLOCKS.includes(blockType);
}

export const ComponentEditorModal: React.FC<Record<never, never>> = () => {
Expand Down
Loading