feat: library unit page skeleton [FC-0083] (#1779)

* View a unit page, which has its own URL
* Components appear within a unit as full previews. Their top bar shows type icon and title on the left, and draft status (if any), tag count, overflow menu, and drag handle on the right.
* Components have an overflow menu within a unit
* Components can be selected within a unit
* When components are selected, the standard component sidebar appears. The preview tab is hidden, since component previews are visible in the main content area.
* Components within a unit full-page view have hover and selected states
* Unit sidebar preview.
* Frontend implementation Drag-n-drop components to reorder them in unit.
This commit is contained in:
Navin Karkera
2025-04-11 18:50:40 +00:00
committed by GitHub
parent 01365d080e
commit a43027b328
56 changed files with 1206 additions and 519 deletions

View File

@@ -59,7 +59,7 @@ import configureModalMessages from '../generic/configure-modal/messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
import addComponentMessages from './add-component/messages';
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
import { IframeProvider } from './context/iFrameContext';
import { IframeProvider } from '../generic/hooks/context/iFrameContext';
import moveModalMessages from './move-modal/messages';
import xblockContainerIframeMessages from './xblock-container-iframe/messages';
import headerNavigationsMessages from './header-navigations/messages';

View File

@@ -14,7 +14,7 @@ import AddComponentButton from './add-component-btn';
import messages from './messages';
import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants';
import { useIframe } from '../context/hooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useEventListener } from '../../generic/hooks';
const AddComponent = ({

View File

@@ -18,7 +18,7 @@ import { courseSectionVerticalMock } from '../__mocks__';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import AddComponent from './AddComponent';
import messages from './messages';
import { IframeProvider } from '../context/iFrameContext';
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
import { messageTypes } from '../constants';
let store;
@@ -52,7 +52,7 @@ jest.mock('../../library-authoring/component-picker', () => ({
}));
const mockSendMessageToIframe = jest.fn();
jest.mock('../context/hooks', () => ({
jest.mock('../../generic/hooks/context/hooks', () => ({
useIframe: () => ({
sendMessageToIframe: mockSendMessageToIframe,
}),

View File

@@ -39,17 +39,7 @@ export const getXBlockSupportMessages = (intl) => ({
},
});
export const stateKeys = {
iframeHeight: 'iframeHeight',
hasLoaded: 'hasLoaded',
showError: 'showError',
windowTopOffset: 'windowTopOffset',
};
export const messageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
completeXBlockMoving: 'completeXBlockMoving',

View File

@@ -1,23 +0,0 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { useIframe } from './hooks';
import { IframeProvider } from './iFrameContext';
describe('useIframe hook', () => {
it('throws an error when used outside of IframeProvider', () => {
expect(() => { renderHook(() => useIframe()); }).toThrow('useIframe must be used within an IframeProvider');
});
it('returns context value when used inside IframeProvider', () => {
const wrapper = ({ children }: { children: ReactNode }) => (
<IframeProvider>
{children}
</IframeProvider>
);
const { result } = renderHook(() => useIframe(), { wrapper });
expect(result.current).toHaveProperty('setIframeRef');
expect(result.current).toHaveProperty('sendMessageToIframe');
});
});

View File

@@ -1,11 +0,0 @@
import { useContext } from 'react';
import { IframeContext, IframeContextType } from './iFrameContext';
export const useIframe = (): IframeContextType => {
const context = useContext(IframeContext);
if (!context) {
throw new Error('useIframe must be used within an IframeProvider');
}
return context;
};

View File

@@ -1,43 +0,0 @@
import React, {
createContext, MutableRefObject, useRef, useCallback, useMemo, ReactNode,
} from 'react';
import { logError } from '@edx/frontend-platform/logging';
export interface IframeContextType {
setIframeRef: (ref: MutableRefObject<HTMLIFrameElement | null>) => void;
sendMessageToIframe: (messageType: string, payload: unknown, consumerWindow?: Window | null) => void;
}
export const IframeContext = createContext<IframeContextType | undefined>(undefined);
export const IframeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const setIframeRef = useCallback((ref: MutableRefObject<HTMLIFrameElement | null>) => {
iframeRef.current = ref.current;
}, []);
const sendMessageToIframe = useCallback((messageType: string, payload: any, consumerWindow?: Window | null) => {
const iframeWindow = iframeRef?.current?.contentWindow;
const targetWindow = consumerWindow || iframeWindow;
if (targetWindow) {
try {
targetWindow.postMessage({ type: messageType, payload }, '*');
} catch (error) {
logError('Failed to send message to iframe:', error);
}
} else {
logError('Iframe is not accessible or loaded yet.');
}
}, [iframeRef]);
const value = useMemo(() => ({
setIframeRef,
sendMessageToIframe,
}), [setIframeRef, sendMessageToIframe]);
return (
<IframeContext.Provider value={value}>
{children}
</IframeContext.Provider>
);
};

View File

@@ -9,7 +9,7 @@ import { camelCaseObject } from '@edx/frontend-platform/utils';
import { RequestStatus } from '../data/constants';
import { useClipboard } from '../generic/clipboard';
import { useEventListener } from '../generic/hooks';
import { COURSE_BLOCK_NAMES } from '../constants';
import { COURSE_BLOCK_NAMES, iframeMessageTypes } from '../constants';
import { messageTypes, PUBLISH_TYPES } from './constants';
import {
createNewCourseXBlock,
@@ -41,7 +41,7 @@ import {
updateMovedXBlockParams,
updateQueryPendingStatus,
} from './data/slice';
import { useIframe } from './context/hooks';
import { useIframe } from '../generic/hooks/context/hooks';
export const useCourseUnit = ({ courseId, blockId }) => {
const dispatch = useDispatch();
@@ -313,7 +313,7 @@ export const useScrollToLastPosition = (storageKey = 'createXBlockLastYPosition'
}, [storageKey]);
const handleMessage = useCallback((event) => {
if (event.data?.type === messageTypes.resize) {
if (event.data?.type === iframeMessageTypes.resize) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react';
import { useScrollToLastPosition, useLayoutGrid } from './hooks';
import { messageTypes } from './constants';
import { iframeMessageTypes } from '../constants';
jest.useFakeTimers();
@@ -108,7 +108,7 @@ describe('useScrollToLastPosition', () => {
const { unmount } = renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
jest.advanceTimersByTime(1000);
});
@@ -136,8 +136,8 @@ describe('useScrollToLastPosition', () => {
renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
});
expect(clearTimeoutSpy).toHaveBeenCalled();
@@ -150,9 +150,9 @@ describe('useScrollToLastPosition', () => {
renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
jest.advanceTimersByTime(500);
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
});
expect(window.scrollTo).not.toHaveBeenCalled();
@@ -164,7 +164,7 @@ describe('useScrollToLastPosition', () => {
renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
jest.advanceTimersByTime(1000);
});

View File

@@ -1,2 +1 @@
export { default as CourseUnit } from './CourseUnit';
export { IframeProvider } from './context/iFrameContext';

View File

@@ -11,7 +11,7 @@ import { RequestStatus } from '../../data/constants';
import { useEventListener } from '../../generic/hooks';
import { getCourseOutlineInfo, getCourseOutlineInfoLoadingStatus } from '../data/selectors';
import { getCourseOutlineInfoQuery, patchUnitItemQuery } from '../data/thunk';
import { useIframe } from '../context/hooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import { messageTypes } from '../constants';
import { CATEGORIES, MOVE_DIRECTIONS } from './constants';
import {

View File

@@ -11,7 +11,7 @@ import { getCourseOutlineInfoUrl } from '../data/api';
import { courseOutlineInfoMock } from '../__mocks__';
import { executeThunk } from '../../utils';
import { getCourseOutlineInfoQuery } from '../data/thunk';
import { IframeProvider } from '../context/iFrameContext';
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
import { IXBlock } from './interfaces';
import MoveModal from './index';
import messages from './messages';

View File

@@ -10,7 +10,6 @@ import {
import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.';
import { messageTypes } from '../constants';
import { IframeProvider } from '../context/iFrameContext';
import { libraryBlockChangesUrl } from '../data/api';
import { ToastActionData } from '../../generic/toast-context';
import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api';
@@ -25,15 +24,15 @@ const defaultEventData: LibraryChangesMessageData = {
};
const mockSendMessageToIframe = jest.fn();
jest.mock('../context/hooks', () => ({
jest.mock('../../generic/hooks/context/hooks', () => ({
useIframe: () => ({
iframeRef: { current: { contentWindow: {} as HTMLIFrameElement } },
setIframeRef: () => {},
sendMessageToIframe: mockSendMessageToIframe,
}),
}));
const render = (eventData?: LibraryChangesMessageData) => {
baseRender(<IframePreviewLibraryXBlockChanges />, {
extraWrapper: ({ children }) => <IframeProvider>{ children }</IframeProvider>,
});
baseRender(<IframePreviewLibraryXBlockChanges />);
const message = {
data: {
type: messageTypes.showXBlockLibraryChangesPreview,

View File

@@ -8,7 +8,7 @@ import { useEventListener } from '../../generic/hooks';
import { messageTypes } from '../constants';
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
import { useIframe } from '../context/hooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import DeleteModal from '../../generic/delete-modal/DeleteModal';
import messages from './messages';
import { ToastContext } from '../../generic/toast-context';

View File

@@ -4,7 +4,7 @@ import { useToggle } from '@openedx/paragon';
import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import useCourseUnitData from './hooks';
import { useIframe } from '../context/hooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import { editCourseUnitVisibilityAndData } from '../data/thunk';
import { SidebarBody, SidebarFooter, SidebarHeader } from './components';
import { PUBLISH_TYPES, messageTypes } from '../constants';

View File

@@ -1,5 +1 @@
export { useIframeMessages } from './useIframeMessages';
export { useIframeContent } from './useIframeContent';
export { useMessageHandlers } from './useMessageHandlers';
export { useIFrameBehavior } from './useIFrameBehavior';
export { useLoadBearingHook } from './useLoadBearingHook';

View File

@@ -1,16 +1,14 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { useKeyedState } from '@edx/react-unit-test-utils';
import { initializeMockApp } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Provider } from 'react-redux';
import { stateKeys, messageTypes } from '../../../constants';
import { messageTypes } from '../../../constants';
import { mockBroadcastChannel } from '../../../../generic/data/api.mock';
import initializeStore from '../../../../store';
import { useLoadBearingHook, useIFrameBehavior, useMessageHandlers } from '..';
import { useMessageHandlers } from '..';
jest.useFakeTimers();
@@ -24,171 +22,6 @@ jest.mock('@edx/frontend-platform/logging', () => ({
mockBroadcastChannel();
describe('useIFrameBehavior', () => {
const id = 'test-id';
const iframeUrl = 'http://example.com';
const setIframeHeight = jest.fn();
const setHasLoaded = jest.fn();
const setShowError = jest.fn();
const setWindowTopOffset = jest.fn();
beforeEach(() => {
(useKeyedState as jest.Mock).mockImplementation((key, initialValue) => {
switch (key) {
case stateKeys.iframeHeight:
return [0, setIframeHeight];
case stateKeys.hasLoaded:
return [false, setHasLoaded];
case stateKeys.showError:
return [false, setShowError];
case stateKeys.windowTopOffset:
return [null, setWindowTopOffset];
default:
return [initialValue, jest.fn()];
}
});
window.scrollTo = jest.fn((x: number | ScrollToOptions, y?: number): void => {
const scrollY = typeof x === 'number' ? y : (x as ScrollToOptions).top || 0;
Object.defineProperty(window, 'scrollY', { value: scrollY, writable: true });
}) as typeof window.scrollTo;
});
it('initializes state correctly', () => {
const { result } = renderHook(() => useIFrameBehavior({ id, iframeUrl }));
expect(result.current.iframeHeight).toBe(0);
expect(result.current.showError).toBe(false);
expect(result.current.hasLoaded).toBe(false);
});
it('scrolls to previous position on video fullscreen exit', () => {
const mockWindowTopOffset = 100;
(useKeyedState as jest.Mock).mockImplementation((key) => {
if (key === stateKeys.windowTopOffset) {
return [mockWindowTopOffset, setWindowTopOffset];
}
return [null, jest.fn()];
});
renderHook(() => useIFrameBehavior({ id, iframeUrl }));
const message = {
data: {
type: messageTypes.videoFullScreen,
payload: { open: false },
},
};
act(() => {
window.dispatchEvent(new MessageEvent('message', message));
});
expect(window.scrollTo).toHaveBeenCalledWith(0, mockWindowTopOffset);
});
it('handles resize message correctly', () => {
renderHook(() => useIFrameBehavior({ id, iframeUrl }));
const message = {
data: {
type: messageTypes.resize,
payload: { height: 500 },
},
};
act(() => {
window.dispatchEvent(new MessageEvent('message', message));
});
expect(setIframeHeight).toHaveBeenCalledWith(500);
expect(setHasLoaded).toHaveBeenCalledWith(true);
});
it('handles videoFullScreen message correctly', () => {
renderHook(() => useIFrameBehavior({ id, iframeUrl }));
const message = {
data: {
type: messageTypes.videoFullScreen,
payload: { open: true },
},
};
act(() => {
window.dispatchEvent(new MessageEvent('message', message));
});
expect(setWindowTopOffset).toHaveBeenCalledWith(window.scrollY);
});
it('handles offset message correctly', () => {
document.body.innerHTML = '<div id="unit-iframe" style="position: absolute; top: 50px;"></div>';
renderHook(() => useIFrameBehavior({ id, iframeUrl }));
const message = {
data: { offset: 100 },
};
act(() => {
window.dispatchEvent(new MessageEvent('message', message));
});
expect(window.scrollY).toBe(100 + (document.getElementById('unit-iframe') as HTMLElement).offsetTop);
});
it('handles iframe load error correctly', () => {
const { result } = renderHook(() => useIFrameBehavior({ id, iframeUrl }));
act(() => {
result.current.handleIFrameLoad();
});
expect(setShowError).toHaveBeenCalledWith(true);
expect(logError).toHaveBeenCalledWith('Unit iframe failed to load. Server possibly returned 4xx or 5xx response.', {
iframeUrl,
});
});
it('resets state when iframeUrl changes', () => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { rerender } = renderHook(({ id, iframeUrl }) => useIFrameBehavior({ id, iframeUrl }), {
initialProps: { id, iframeUrl },
});
rerender({ id, iframeUrl: 'http://new-url.com' });
expect(setIframeHeight).toHaveBeenCalledWith(0);
expect(setHasLoaded).toHaveBeenCalledWith(false);
});
});
describe('useLoadBearingHook', () => {
const setValue = jest.fn();
beforeEach(() => {
jest.spyOn(React, 'useState').mockReturnValue([0, setValue]);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('updates state when id changes', () => {
const { rerender } = renderHook(({ id }) => useLoadBearingHook(id), {
initialProps: { id: 'initial-id' },
});
setValue.mockClear();
rerender({ id: 'new-id' });
expect(setValue).toHaveBeenCalledWith(expect.any(Function));
expect(setValue.mock.calls);
});
});
describe('useMessageHandlers', () => {
let handlers;
let result;

View File

@@ -18,16 +18,3 @@ export type UseMessageHandlersTypes = {
};
export type MessageHandlersTypes = Record<string, (payload: any) => void>;
export interface UseIFrameBehaviorTypes {
id: string;
iframeUrl: string;
onLoaded?: boolean;
}
export interface UseIFrameBehaviorReturnTypes {
iframeHeight: number;
handleIFrameLoad: () => void;
showError: boolean;
hasLoaded: boolean;
}

View File

@@ -1,94 +0,0 @@
import { useCallback, useEffect } from 'react';
import { logError } from '@edx/frontend-platform/logging';
// eslint-disable-next-line import/no-extraneous-dependencies
import { useKeyedState } from '@edx/react-unit-test-utils';
import { useEventListener } from '../../../generic/hooks';
import { stateKeys, messageTypes } from '../../constants';
import { useLoadBearingHook } from './useLoadBearingHook';
import { UseIFrameBehaviorTypes, UseIFrameBehaviorReturnTypes } from './types';
/**
* Custom hook to manage iframe behavior.
*
* @param {Object} params - The parameters for the hook.
* @param {string} params.id - The unique identifier for the iframe.
* @param {string} params.iframeUrl - The URL of the iframe.
* @param {boolean} [params.onLoaded=true] - Flag to indicate if the iframe has loaded.
* @returns {Object} The state and handlers for the iframe.
* @returns {number} return.iframeHeight - The height of the iframe.
* @returns {Function} return.handleIFrameLoad - The handler for iframe load event.
* @returns {boolean} return.showError - Flag to indicate if there was an error loading the iframe.
* @returns {boolean} return.hasLoaded - Flag to indicate if the iframe has loaded.
*/
export const useIFrameBehavior = ({
id,
iframeUrl,
onLoaded = true,
}: UseIFrameBehaviorTypes): UseIFrameBehaviorReturnTypes => {
// Do not remove this hook. See function description.
useLoadBearingHook(id);
const [iframeHeight, setIframeHeight] = useKeyedState<number>(stateKeys.iframeHeight, 0);
const [hasLoaded, setHasLoaded] = useKeyedState<boolean>(stateKeys.hasLoaded, false);
const [showError, setShowError] = useKeyedState<boolean>(stateKeys.showError, false);
const [windowTopOffset, setWindowTopOffset] = useKeyedState<number | null>(stateKeys.windowTopOffset, null);
const receiveMessage = useCallback(({ data }: MessageEvent) => {
const { payload, type } = data;
if (type === messageTypes.resize) {
setIframeHeight(payload.height);
if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true);
}
} else if (type === messageTypes.videoFullScreen) {
// We observe exit from the video xblock fullscreen mode
// and scroll to the previously saved scroll position
if (!payload.open && windowTopOffset !== null) {
window.scrollTo(0, Number(windowTopOffset));
}
// We listen for this message from LMS to know when we need to
// save or reset scroll position on toggle video xblock fullscreen mode
setWindowTopOffset(payload.open ? window.scrollY : null);
} else if (data.offset) {
// We listen for this message from LMS to know when the page needs to
// be scrolled to another location on the page.
window.scrollTo(0, data.offset + document.getElementById('unit-iframe')!.offsetTop);
}
}, [
id,
onLoaded,
hasLoaded,
setHasLoaded,
iframeHeight,
setIframeHeight,
windowTopOffset,
setWindowTopOffset,
]);
useEventListener('message', receiveMessage);
const handleIFrameLoad = () => {
if (!hasLoaded) {
setShowError(true);
logError('Unit iframe failed to load. Server possibly returned 4xx or 5xx response.', {
iframeUrl,
});
}
};
useEffect(() => {
setIframeHeight(0);
setHasLoaded(false);
}, [iframeUrl]);
return {
iframeHeight,
handleIFrameLoad,
showError,
hasLoaded,
};
};

View File

@@ -1,20 +0,0 @@
import { useEffect, RefObject } from 'react';
/**
* Hook for managing iframe content and providing utilities to interact with the iframe.
*
* @param {React.RefObject<HTMLIFrameElement>} iframeRef - A React ref for the iframe element.
* @param {(ref: React.RefObject<HTMLIFrameElement>) => void} setIframeRef -
* A function to associate the iframeRef with the parent context.
*
* @returns {Object} - An object containing utility functions.
* @returns {() => void}
*/
export const useIframeContent = (
iframeRef: RefObject<HTMLIFrameElement>,
setIframeRef: (ref: RefObject<HTMLIFrameElement>) => void,
): void => {
useEffect(() => {
setIframeRef(iframeRef);
}, [setIframeRef, iframeRef]);
};

View File

@@ -1,20 +0,0 @@
import { useEffect } from 'react';
/**
* Hook for managing and handling messages received by the iframe.
*
* @param {Record<string, (payload: any) => void>} messageHandlers -
* A mapping of message types to their corresponding handler functions.
*/
export const useIframeMessages = (messageHandlers: Record<string, (payload: any) => void>) => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const { type, payload } = event.data || {};
if (type in messageHandlers) {
messageHandlers[type](payload);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [messageHandlers]);
};

View File

@@ -1,33 +0,0 @@
import { useLayoutEffect, useState } from 'react';
/**
* We discovered an error in Firefox where - upon iframe load - React would cease to call any
* useEffect hooks until the user interacts with the page again. This is particularly confusing
* when navigating between sequences, as the UI partially updates leaving the user in a nebulous
* state.
*
* We were able to solve this error by using a layout effect to update some component state, which
* executes synchronously on render. Somehow this forces React to continue it's lifecycle
* immediately, rather than waiting for user interaction. This layout effect could be anywhere in
* the parent tree, as far as we can tell - we chose to add a conspicuously 'load bearing' (that's
* a joke) one here so it wouldn't be accidentally removed elsewhere.
*
* If we remove this hook when one of these happens:
* 1. React figures out that there's an issue here and fixes a bug.
* 2. We cease to use an iframe for unit rendering.
* 3. Firefox figures out that there's an issue in their iframe loading and fixes a bug.
* 4. We stop supporting Firefox.
* 5. An enterprising engineer decides to create a repo that reproduces the problem, submits it to
* Firefox/React for review, and they kindly help us figure out what in the world is happening
* so we can fix it.
*
* This hook depends on the unit id just to make sure it re-evaluates whenever the ID changes. If
* we change whether or not the Unit component is re-mounted when the unit ID changes, this may
* become important, as this hook will otherwise only evaluate the useLayoutEffect once.
*/
export const useLoadBearingHook = (id: string): void => {
const setValue = useState(0)[1];
useLayoutEffect(() => {
setValue(currentValue => currentValue + 1);
}, [id]);
};

View File

@@ -1,5 +1,5 @@
import {
useRef, FC, useEffect, useState, useMemo, useCallback,
FC, useEffect, useState, useMemo, useCallback,
} from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle, Sheet } from '@openedx/paragon';
@@ -16,7 +16,7 @@ import ModalIframe from '../../generic/modal-iframe';
import { IFRAME_FEATURE_POLICY } from '../../constants';
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
import supportedEditors from '../../editors/supportedEditors';
import { useIframe } from '../context/hooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import {
fetchCourseSectionVerticalData,
fetchCourseVerticalChildrenData,
@@ -25,9 +25,6 @@ import {
import { messageTypes } from '../constants';
import {
useMessageHandlers,
useIframeContent,
useIframeMessages,
useIFrameBehavior,
} from './hooks';
import {
XBlockContainerIframeProps,
@@ -35,12 +32,14 @@ import {
} from './types';
import { formatAccessManagedXBlockData, getIframeUrl, getLegacyEditModalUrl } from './utils';
import messages from './messages';
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
import { useIframeContent } from '../../generic/hooks/useIframeContent';
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
}) => {
const intl = useIntl();
const iframeRef = useRef<HTMLIFrameElement>(null);
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -56,8 +55,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
const iframeUrl = useMemo(() => getIframeUrl(blockId), [blockId]);
const legacyEditModalUrl = useMemo(() => getLegacyEditModalUrl(configureXBlockId), [configureXBlockId]);
const { setIframeRef, sendMessageToIframe } = useIframe();
const { iframeHeight } = useIFrameBehavior({ id: blockId, iframeUrl });
const { iframeRef, setIframeRef, sendMessageToIframe } = useIframe();
const { iframeHeight } = useIframeBehavior({ id: blockId, iframeUrl, iframeRef });
useIframeContent(iframeRef, setIframeRef);