From c80b2fd47c9e112497746b36862a50368c001f95 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 11 Dec 2024 21:16:20 -0500 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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/8] 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(); } }; From 44c471fd07385101f315c7ef718aabed5e3afa7a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 10 Jan 2025 12:23:44 -0500 Subject: [PATCH 6/8] feat: Handle save-end and error events in AdvancedEditor --- src/editors/AdvancedEditor.test.tsx | 27 ++++++++++++++++++++------- src/editors/AdvancedEditor.tsx | 12 ++++++++++-- src/editors/messages.ts | 6 +++++- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx index 32ab90945e..e18fd45924 100644 --- a/src/editors/AdvancedEditor.test.tsx +++ b/src/editors/AdvancedEditor.test.tsx @@ -3,12 +3,14 @@ import { getConfig } from '@edx/frontend-platform'; import { render, initializeMocks, + waitFor, } from '../testUtils'; import AdvancedEditor from './AdvancedEditor'; jest.mock('./containers/EditorContainer', () => ({ EditorModalWrapper: jest.fn(() => (
Advanced Editor Iframe
)), })); +const onCloseMock = jest.fn(); describe('AdvancedEditor', () => { beforeEach(() => { @@ -16,8 +18,6 @@ describe('AdvancedEditor', () => { }); it('should call onClose when receiving "cancel-clicked" message', () => { - const onCloseMock = jest.fn(); - render(); const messageEvent = new MessageEvent('message', { @@ -31,12 +31,10 @@ describe('AdvancedEditor', () => { }); it('should call onClose when receiving "save-clicked" message', () => { - const onCloseMock = jest.fn(); - render(); const messageEvent = new MessageEvent('message', { - data: 'save-clicked', + data: 'save-end', origin: getConfig().STUDIO_BASE_URL, }); @@ -45,9 +43,24 @@ describe('AdvancedEditor', () => { expect(onCloseMock).toHaveBeenCalled(); }); - it('should not call onClose if the message is from an invalid origin', () => { - const onCloseMock = jest.fn(); + it('should call showToast when receiving "error" message', async () => { + const { mockShowToast } = initializeMocks(); + + render(); + const messageEvent = new MessageEvent('message', { + data: 'error', + origin: getConfig().STUDIO_BASE_URL, + }); + + window.dispatchEvent(messageEvent); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalled(); + }); + }); + + it('should not call onClose if the message is from an invalid origin', () => { render(); const messageEvent = new MessageEvent('message', { diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx index a044e6ad9d..81398b2f0a 100644 --- a/src/editors/AdvancedEditor.tsx +++ b/src/editors/AdvancedEditor.tsx @@ -1,8 +1,11 @@ -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { getConfig } from '@edx/frontend-platform'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { LibraryBlock } from '../library-authoring/LibraryBlock'; import { EditorModalWrapper } from './containers/EditorContainer'; +import { ToastContext } from '../generic/toast-context'; +import messages from './messages'; interface AdvancedEditorProps { usageKey: string, @@ -10,14 +13,19 @@ interface AdvancedEditorProps { } const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => { + const intl = useIntl(); + const { showToast } = React.useContext(ToastContext); + useEffect(() => { const handleIframeMessage = (event) => { if (event.origin !== getConfig().STUDIO_BASE_URL) { return; } - if (onClose && (event.data === 'cancel-clicked' || event.data === 'save-clicked')) { + if (onClose && (event.data === 'cancel-clicked' || event.data === 'save-end')) { onClose(); + } else if (event.data === 'error') { + showToast(intl.formatMessage(messages.advancedEditorGenericError)); } }; diff --git a/src/editors/messages.ts b/src/editors/messages.ts index 1ac257514e..69cf26d681 100644 --- a/src/editors/messages.ts +++ b/src/editors/messages.ts @@ -1,7 +1,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - dropVideoFileHere: { defaultMessage: 'Drag and drop video here or click to upload', id: 'VideoUploadEditor.dropVideoFileHere', @@ -32,6 +31,11 @@ const messages = defineMessages({ defaultMessage: 'View in Library', description: 'Link text for opening library block in another tab.', }, + advancedEditorGenericError: { + id: 'authoring.advancedEditor.error.generic', + defaultMessage: 'An unexpected error occurred in the editor', + description: 'Generic error message shown when an error occurs in the Advanced Editor.', + }, }); export default messages; From 10549b5ec1b3f1f9c813e95996ac0fc32c7a1809 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 14 Jan 2025 13:32:56 -0500 Subject: [PATCH 7/8] refactor: Update code to catch xblock-event from edx-platform --- src/editors/AdvancedEditor.test.tsx | 23 +++++++++++++++++++---- src/editors/AdvancedEditor.tsx | 14 ++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx index e18fd45924..d0eac4c875 100644 --- a/src/editors/AdvancedEditor.test.tsx +++ b/src/editors/AdvancedEditor.test.tsx @@ -21,7 +21,10 @@ describe('AdvancedEditor', () => { render(); const messageEvent = new MessageEvent('message', { - data: 'cancel-clicked', + data: { + type: 'xblock-event', + eventName: 'cancel-clicked', + }, origin: getConfig().STUDIO_BASE_URL, }); @@ -34,7 +37,13 @@ describe('AdvancedEditor', () => { render(); const messageEvent = new MessageEvent('message', { - data: 'save-end', + data: { + type: 'xblock-event', + eventName: 'save', + data: { + state: 'end', + }, + }, origin: getConfig().STUDIO_BASE_URL, }); @@ -49,7 +58,10 @@ describe('AdvancedEditor', () => { render(); const messageEvent = new MessageEvent('message', { - data: 'error', + data: { + type: 'xblock-event', + eventName: 'error', + }, origin: getConfig().STUDIO_BASE_URL, }); @@ -64,7 +76,10 @@ describe('AdvancedEditor', () => { render(); const messageEvent = new MessageEvent('message', { - data: 'cancel-clicked', + data: { + type: 'xblock-event', + eventName: 'cancel-clicked', + }, origin: 'https://invalid-origin.com', }); diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx index 81398b2f0a..4c87bc012c 100644 --- a/src/editors/AdvancedEditor.tsx +++ b/src/editors/AdvancedEditor.tsx @@ -22,10 +22,16 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => { return; } - if (onClose && (event.data === 'cancel-clicked' || event.data === 'save-end')) { - onClose(); - } else if (event.data === 'error') { - showToast(intl.formatMessage(messages.advancedEditorGenericError)); + if (event.data.type === 'xblock-event') { + const { eventName, data } = event.data; + + if (onClose && (eventName === 'cancel-clicked' + || (eventName === 'save' && data.state === 'end')) + ) { + onClose(); + } else if (eventName === 'error') { + showToast(intl.formatMessage(messages.advancedEditorGenericError)); + } } }; From 9ef366dec8a2b7267cec0cf9842c28f7eb19733e Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 14 Jan 2025 14:46:14 -0500 Subject: [PATCH 8/8] refactor: Use cancel event of xblock-event --- src/editors/AdvancedEditor.test.tsx | 4 ++-- src/editors/AdvancedEditor.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editors/AdvancedEditor.test.tsx b/src/editors/AdvancedEditor.test.tsx index d0eac4c875..fcbd4a8860 100644 --- a/src/editors/AdvancedEditor.test.tsx +++ b/src/editors/AdvancedEditor.test.tsx @@ -23,7 +23,7 @@ describe('AdvancedEditor', () => { const messageEvent = new MessageEvent('message', { data: { type: 'xblock-event', - eventName: 'cancel-clicked', + eventName: 'cancel', }, origin: getConfig().STUDIO_BASE_URL, }); @@ -78,7 +78,7 @@ describe('AdvancedEditor', () => { const messageEvent = new MessageEvent('message', { data: { type: 'xblock-event', - eventName: 'cancel-clicked', + eventName: 'cancel', }, origin: 'https://invalid-origin.com', }); diff --git a/src/editors/AdvancedEditor.tsx b/src/editors/AdvancedEditor.tsx index 4c87bc012c..17e6c82b05 100644 --- a/src/editors/AdvancedEditor.tsx +++ b/src/editors/AdvancedEditor.tsx @@ -25,7 +25,7 @@ const AdvancedEditor = ({ usageKey, onClose }: AdvancedEditorProps) => { if (event.data.type === 'xblock-event') { const { eventName, data } = event.data; - if (onClose && (eventName === 'cancel-clicked' + if (onClose && (eventName === 'cancel' || (eventName === 'save' && data.state === 'end')) ) { onClose();