From c80b2fd47c9e112497746b36862a50368c001f95 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 11 Dec 2024 21:16:20 -0500 Subject: [PATCH 1/5] feat: Add AdvancedEditors with an iframe --- src/editors/AdvancedEditor.tsx | 18 ++++++++++++++++++ src/editors/Editor.tsx | 17 ++++++++++++----- src/editors/messages.ts | 5 ----- .../LibraryBlock/LibraryBlock.tsx | 12 ++++++++++-- .../components/ComponentEditorModal.tsx | 4 +--- 5 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 src/editors/AdvancedEditor.tsx diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx new file mode 100644 index 0000000000..e081a443d4 --- /dev/null +++ b/src/editors/AdvancedEditor.tsx @@ -0,0 +1,18 @@ +import { LibraryBlock } from '../library-authoring/LibraryBlock'; +import { EditorModalWrapper } from './containers/EditorContainer'; + +interface AdvancedEditorProps { + usageKey: string, + onClose: Function | null, +} + +const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => ( + void}> + + +); + +export default AdvancedEditor; diff --git a/src/editors/Editor.tsx b/src/editors/Editor.tsx index 8e52448242..33f24c9f39 100644 --- a/src/editors/Editor.tsx +++ b/src/editors/Editor.tsx @@ -2,14 +2,13 @@ // 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; @@ -43,9 +42,17 @@ const Editor: React.FC = ({ const { fullScreen } = useEditorContext(); const EditorComponent = supportedEditors[blockType]; - const innerEditor = (EditorComponent !== undefined) - ? - : ; + + if (EditorComponent === undefined && blockId) { + return ( + + ); + } + + const innerEditor = ; if (fullScreen) { return ( diff --git a/src/editors/messages.ts b/src/editors/messages.ts index 4b1134a270..1ac257514e 100644 --- a/src/editors/messages.ts +++ b/src/editors/messages.ts @@ -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', diff --git a/src/library-authoring/LibraryBlock/LibraryBlock.tsx b/src/library-authoring/LibraryBlock/LibraryBlock.tsx index 4397090edd..e635f1f8ba 100644 --- a/src/library-authoring/LibraryBlock/LibraryBlock.tsx +++ b/src/library-authoring/LibraryBlock/LibraryBlock.tsx @@ -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. @@ -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(null); const [iFrameHeight, setIFrameHeight] = useState(50); const studioBaseUrl = getConfig().STUDIO_BASE_URL; @@ -71,6 +77,8 @@ export const LibraryBlock = ({ onBlockNotification, usageKey, version }: Library const queryStr = version ? `?version=${version}` : ''; + const xblockView = view ?? 'student_view'; + return (
> = () => { From fcc14458dd339ed1a1ed11c16ebc014784b2ad4d Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 12 Dec 2024 17:02:59 -0500 Subject: [PATCH 2/5] test: Update EditorPage.test to test Advanced Editors --- src/editors/EditorPage.test.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/editors/EditorPage.test.tsx b/src/editors/EditorPage.test.tsx index 4ae9817d3b..66d7ffac95 100644 --- a/src/editors/EditorPage.test.tsx +++ b/src/editors/EditorPage.test.tsx @@ -26,6 +26,9 @@ jest.spyOn(editorCmsApi, 'fetchByUnitId').mockImplementation(async () => ({ }], }, })); +jest.mock('../library-authoring/LibraryBlock', () => ({ + LibraryBlock: jest.fn(() => (
Advanced Editor Iframe
)), +})); const defaultPropsHtml = { blockId: 'block-v1:Org+TS100+24+type@html+block@123456html', @@ -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: '' } } )); @@ -93,6 +94,6 @@ describe('EditorPage', () => { }; render(); - expect(await screen.findByText('Error: Could Not find Editor')).toBeInTheDocument(); + expect(await screen.findByText('Advanced Editor Iframe')).toBeInTheDocument(); }); }); From c0f84ab9e99af62abd5339ba5f935f46ea62f5c1 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 13 Dec 2024 14:37:44 -0500 Subject: [PATCH 3/5] feat: Intercept cancel button event to close Advanced Editor --- src/editors/AdvancedEditor.test.tsx | 50 +++++++++++++++++++++++++++++ src/editors/AdvancedEditor.tsx | 39 +++++++++++++++++----- 2 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/editors/AdvancedEditor.test.tsx diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx new file mode 100644 index 0000000000..d070e8129e --- /dev/null +++ b/src/editors/AdvancedEditor.test.tsx @@ -0,0 +1,50 @@ +import { getConfig } from '@edx/frontend-platform'; + +import { + render, + initializeMocks, + waitFor, +} from '../testUtils'; +import AdvancedEditor from './AdvancedEditor'; + +jest.mock('../library-authoring/LibraryBlock', () => ({ + LibraryBlock: jest.fn(() => (
Advanced Editor Iframe
)), +})); + +describe('AdvancedEditor', () => { + beforeEach(() => { + initializeMocks(); + }); + + it('should call onClose when receiving "cancel-clicked" message', () => { + const onCloseMock = jest.fn(); + + render(); + + const messageEvent = new MessageEvent('message', { + data: 'cancel-clicked', + origin: getConfig().STUDIO_BASE_URL, + }); + + window.dispatchEvent(messageEvent); + + waitFor(() => { + expect(onCloseMock).toHaveBeenCalled(); + }); + }); + + it('should not call onClose if the message is from an invalid origin', () => { + const onCloseMock = jest.fn(); + + render(); + + const messageEvent = new MessageEvent('message', { + data: 'cancel-clicked', + origin: 'https://invalid-origin.com', + }); + + window.dispatchEvent(messageEvent); + + expect(onCloseMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx index e081a443d4..60f7a6547a 100644 --- a/src/editors/AdvancedEditor.tsx +++ b/src/editors/AdvancedEditor.tsx @@ -1,3 +1,6 @@ +import { useEffect } from 'react'; +import { getConfig } from '@edx/frontend-platform'; + import { LibraryBlock } from '../library-authoring/LibraryBlock'; import { EditorModalWrapper } from './containers/EditorContainer'; @@ -6,13 +9,33 @@ interface AdvancedEditorProps { onClose: Function | null, } -const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => ( - void}> - - -); +const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => { + useEffect(() => { + const handleIframeMessage = (event) => { + if (event.origin !== getConfig().STUDIO_BASE_URL) { + return; + } + + if (event.data === 'cancel-clicked' && onClose) { + onClose(); + } + }; + + window.addEventListener('message', handleIframeMessage); + + return () => { + window.removeEventListener('message', handleIframeMessage); + }; + }, []); + + return ( + void}> + + + ); +}; export default AdvancedEditor; From 14f311d0403926c1f15b71a4e87787092aefd136 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 16 Dec 2024 11:19:11 -0500 Subject: [PATCH 4/5] style: Nits on the code test --- src/editors/AdvancedEditor.test.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx index d070e8129e..741ae9cfbb 100644 --- a/src/editors/AdvancedEditor.test.tsx +++ b/src/editors/AdvancedEditor.test.tsx @@ -3,12 +3,11 @@ import { getConfig } from '@edx/frontend-platform'; import { render, initializeMocks, - waitFor, } from '../testUtils'; import AdvancedEditor from './AdvancedEditor'; -jest.mock('../library-authoring/LibraryBlock', () => ({ - LibraryBlock: jest.fn(() => (
Advanced Editor Iframe
)), +jest.mock('./containers/EditorContainer', () => ({ + EditorModalWrapper: jest.fn(() => (
Advanced Editor Iframe
)), })); describe('AdvancedEditor', () => { @@ -28,9 +27,7 @@ describe('AdvancedEditor', () => { window.dispatchEvent(messageEvent); - waitFor(() => { - expect(onCloseMock).toHaveBeenCalled(); - }); + expect(onCloseMock).toHaveBeenCalled(); }); it('should not call onClose if the message is from an invalid origin', () => { From 091957d4e8138e2b2c25b98a5d712a0f410a67a5 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 16 Dec 2024 13:28:16 -0500 Subject: [PATCH 5/5] feat: save-cliked added --- src/editors/AdvancedEditor.test.tsx | 15 +++++++++++++++ src/editors/AdvancedEditor.tsx | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx index 741ae9cfbb..32ab90945e 100644 --- a/src/editors/AdvancedEditor.test.tsx +++ b/src/editors/AdvancedEditor.test.tsx @@ -30,6 +30,21 @@ describe('AdvancedEditor', () => { expect(onCloseMock).toHaveBeenCalled(); }); + it('should call onClose when receiving "save-clicked" message', () => { + const onCloseMock = jest.fn(); + + render(); + + 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(); diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx index 60f7a6547a..a044e6ad9d 100644 --- a/src/editors/AdvancedEditor.tsx +++ b/src/editors/AdvancedEditor.tsx @@ -16,7 +16,7 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => { return; } - if (event.data === 'cancel-clicked' && onClose) { + if (onClose && (event.data === 'cancel-clicked' || event.data === 'save-clicked')) { onClose(); } };