feat: [FC-0070] Manage Tags interoperation (#1454)
Added interaction between MFE and Legacy tagging functionality for xblocks on the Course unit page.
This commit is contained in:
@@ -126,6 +126,7 @@ export const useContentData = (contentId) => (
|
||||
*/
|
||||
export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
const queryClient = useQueryClient();
|
||||
const unitIframe = window.frames['xblock-iframe'];
|
||||
|
||||
return useMutation({
|
||||
/**
|
||||
@@ -160,7 +161,8 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
onSuccess: /* istanbul ignore next */ () => {
|
||||
/* istanbul ignore next */
|
||||
if (window.top != null) {
|
||||
// This send messages to the parent page if the drawer is called from a iframe.
|
||||
// Sends messages to the parent page if the drawer was opened
|
||||
// from an iframe or the unit iframe within the course.
|
||||
// Is used on Studio to update tags data and counts.
|
||||
// In the future, when the Course Outline Page and Unit Page are integrated into this MFE,
|
||||
// they should just use React Query to load the tag counts, and React Query will automatically
|
||||
@@ -169,26 +171,32 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
|
||||
// Sends content tags.
|
||||
getContentTaxonomyTagsData(contentId).then((data) => {
|
||||
const contentData = {
|
||||
contentId,
|
||||
...data,
|
||||
const contentData = { contentId, ...data };
|
||||
|
||||
const message = {
|
||||
type: 'authoring.events.tags.updated',
|
||||
data: contentData,
|
||||
};
|
||||
window.top?.postMessage(
|
||||
{ type: 'authoring.events.tags.updated', data: contentData },
|
||||
getConfig().STUDIO_BASE_URL,
|
||||
);
|
||||
|
||||
const targetOrigin = getConfig().STUDIO_BASE_URL;
|
||||
|
||||
unitIframe?.postMessage(message, targetOrigin);
|
||||
window.top?.postMessage(message, targetOrigin);
|
||||
});
|
||||
|
||||
// Sends tags count.
|
||||
getContentTaxonomyTagsCount(contentId).then((data) => {
|
||||
const contentData = {
|
||||
contentId,
|
||||
count: data,
|
||||
getContentTaxonomyTagsCount(contentId).then((count) => {
|
||||
const contentData = { contentId, count };
|
||||
|
||||
const message = {
|
||||
type: 'authoring.events.tags.count.updated',
|
||||
data: contentData,
|
||||
};
|
||||
window.top?.postMessage(
|
||||
{ type: 'authoring.events.tags.count.updated', data: contentData },
|
||||
getConfig().STUDIO_BASE_URL,
|
||||
);
|
||||
|
||||
const targetOrigin = getConfig().STUDIO_BASE_URL;
|
||||
|
||||
unitIframe?.postMessage(message, targetOrigin);
|
||||
window.top?.postMessage(message, targetOrigin);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||
import HeaderNavigations from './header-navigations/HeaderNavigations';
|
||||
import Sequence from './course-sequence';
|
||||
import Sidebar from './sidebar';
|
||||
import { useCourseUnit, useLayoutGrid } from './hooks';
|
||||
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
||||
import messages from './messages';
|
||||
import PublishControls from './sidebar/PublishControls';
|
||||
import LocationInfo from './sidebar/LocationInfo';
|
||||
@@ -79,6 +79,8 @@ const CourseUnit = ({ courseId }) => {
|
||||
document.title = getPageHeadTitle('', unitTitle);
|
||||
}, [unitTitle]);
|
||||
|
||||
useScrollToLastPosition();
|
||||
|
||||
const {
|
||||
isShow: isShowProcessingNotification,
|
||||
title: processingNotificationTitle,
|
||||
|
||||
@@ -54,6 +54,7 @@ import sidebarMessages from './sidebar/messages';
|
||||
import { extractCourseUnitId } from './sidebar/utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
|
||||
import tagsDrawerMessages from '../content-tags-drawer/messages';
|
||||
import { getClipboardUrl } from '../generic/data/api';
|
||||
import configureModalMessages from '../generic/configure-modal/messages';
|
||||
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
|
||||
@@ -92,6 +93,9 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(({ queryKey }) => {
|
||||
const taxonomyApiHooksModule = jest.requireActual('../taxonomy/data/apiHooks');
|
||||
const actualQueryKeys = taxonomyApiHooksModule.taxonomyQueryKeys;
|
||||
|
||||
if (queryKey[0] === 'contentTaxonomyTags') {
|
||||
return {
|
||||
data: {
|
||||
@@ -105,6 +109,14 @@ jest.mock('@tanstack/react-query', () => ({
|
||||
isSuccess: true,
|
||||
};
|
||||
}
|
||||
if (actualQueryKeys.all.includes(queryKey[0])) {
|
||||
return {
|
||||
data: {
|
||||
results: [],
|
||||
},
|
||||
isSuccess: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: {},
|
||||
isSuccess: true,
|
||||
@@ -261,6 +273,19 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the xBlocks iframe and opens the tags drawer on postMessage event', async () => {
|
||||
const { getByTitle, getByText } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
});
|
||||
|
||||
simulatePostMessageEvent(messageTypes.openManageTags, { contentId: blockId });
|
||||
|
||||
expect(getByText(tagsDrawerMessages.headerSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
||||
const { getByTitle, queryByTitle } = render(<RootWrapper />);
|
||||
|
||||
@@ -750,6 +775,44 @@ 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;
|
||||
|
||||
@@ -61,7 +61,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
case COMPONENT_TYPES.problem:
|
||||
case COMPONENT_TYPES.video:
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('modalEditLastYPosition', window.scrollY);
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
||||
});
|
||||
break;
|
||||
@@ -92,6 +92,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
boilerplate: moduleName,
|
||||
parentLocator: blockId,
|
||||
}, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/html/${locator}`);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -75,4 +75,5 @@ export const messageTypes = {
|
||||
completeXBlockEditing: 'completeXBlockEditing',
|
||||
studioAjaxError: 'studioAjaxError',
|
||||
refreshPositions: 'refreshPositions',
|
||||
openManageTags: 'openManageTags',
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useContext } from 'react';
|
||||
|
||||
import { IframeContext, IframeContextType } from './iFrameContext';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useIframe = (): IframeContextType => {
|
||||
const context = useContext(IframeContext);
|
||||
if (!context) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
@@ -259,3 +261,58 @@ export const useLayoutGrid = (unitCategory, isUnitLibraryType) => (
|
||||
return isUnitLibraryType ? layouts.fullWidth : layouts.default;
|
||||
}, [unitCategory])
|
||||
);
|
||||
|
||||
/**
|
||||
* Custom hook that restores the scroll position from `localStorage` after a page reload.
|
||||
* It listens for a `plugin.resize` message event and scrolls the window to the saved position
|
||||
* after a 1-second delay, provided no new resize messages are received during that time.
|
||||
*
|
||||
* @param {string} [storageKey='createXBlockLastYPosition'] -
|
||||
* The key used to store the last scroll position in `localStorage`.
|
||||
*/
|
||||
export const useScrollToLastPosition = (storageKey = 'createXBlockLastYPosition') => {
|
||||
const timeoutRef = useRef(null);
|
||||
const [hasLastPosition, setHasLastPosition] = useState(() => !!localStorage.getItem(storageKey));
|
||||
|
||||
const scrollToLastPosition = useCallback(() => {
|
||||
const lastYPosition = localStorage.getItem(storageKey);
|
||||
if (!lastYPosition) {
|
||||
setHasLastPosition(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const yPosition = parseInt(lastYPosition, 10);
|
||||
if (!Number.isNaN(yPosition)) {
|
||||
window.scrollTo({ top: yPosition, behavior: 'smooth' });
|
||||
localStorage.removeItem(storageKey);
|
||||
setHasLastPosition(false);
|
||||
}
|
||||
}, [storageKey]);
|
||||
|
||||
const handleMessage = useCallback((event) => {
|
||||
if (event.data?.type === messageTypes.resize) {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(scrollToLastPosition, 1000);
|
||||
}
|
||||
}, [scrollToLastPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasLastPosition) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [hasLastPosition, handleMessage]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
181
src/course-unit/hooks.test.jsx
Normal file
181
src/course-unit/hooks.test.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useScrollToLastPosition, useLayoutGrid } from './hooks';
|
||||
import { messageTypes } from './constants';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('useLayoutGrid', () => {
|
||||
it('returns fullWidth layout when isUnitLibraryType is true', () => {
|
||||
const { result } = renderHook(() => useLayoutGrid('someCategory', true));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lg: [{ span: 12 }, { span: 0 }],
|
||||
md: [{ span: 12 }, { span: 0 }],
|
||||
sm: [{ span: 12 }, { span: 0 }],
|
||||
xs: [{ span: 12 }, { span: 0 }],
|
||||
xl: [{ span: 12 }, { span: 0 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default layout when isUnitLibraryType is false', () => {
|
||||
const { result } = renderHook(() => useLayoutGrid('someCategory', false));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
lg: [{ span: 8 }, { span: 4 }],
|
||||
md: [{ span: 8 }, { span: 4 }],
|
||||
sm: [{ span: 8 }, { span: 3 }],
|
||||
xs: [{ span: 9 }, { span: 3 }],
|
||||
xl: [{ span: 9 }, { span: 3 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not recompute layout if unitCategory remains the same', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ unitCategory, isUnitLibraryType }) => useLayoutGrid(unitCategory, isUnitLibraryType),
|
||||
{ initialProps: { unitCategory: 'category1', isUnitLibraryType: false } },
|
||||
);
|
||||
|
||||
const firstResult = result.current;
|
||||
|
||||
rerender({ unitCategory: 'category1', isUnitLibraryType: false });
|
||||
|
||||
expect(result.current).toBe(firstResult);
|
||||
});
|
||||
|
||||
it('recomputes layout when unitCategory changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ unitCategory, isUnitLibraryType }) => useLayoutGrid(unitCategory, isUnitLibraryType),
|
||||
{ initialProps: { unitCategory: 'category1', isUnitLibraryType: false } },
|
||||
);
|
||||
|
||||
const firstResult = result.current;
|
||||
|
||||
rerender({ unitCategory: 'category2', isUnitLibraryType: false });
|
||||
|
||||
expect(result.current).not.toBe(firstResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useScrollToLastPosition', () => {
|
||||
const storageKey = 'createXBlockLastYPosition';
|
||||
let scrollToSpy;
|
||||
let clearTimeoutSpy;
|
||||
let setTimeoutSpy;
|
||||
let setStateSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
||||
clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
||||
setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
||||
setStateSpy = jest.spyOn(React, 'useState');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
scrollToSpy.mockRestore();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
setTimeoutSpy.mockRestore();
|
||||
setStateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not add event listener if no lastYPosition is in localStorage', () => {
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
|
||||
renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
expect(addEventListenerSpy).not.toHaveBeenCalledWith('message', expect.any(Function));
|
||||
|
||||
addEventListenerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should add event listener if lastYPosition exists in localStorage', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
|
||||
renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
||||
|
||||
addEventListenerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should scroll to saved position on message event', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
const { unmount } = renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(window.scrollTo).toHaveBeenCalledWith({ top: 500, behavior: 'smooth' });
|
||||
expect(localStorage.getItem(storageKey)).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should clear timeout and remove event listener on unmount', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
unmount();
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should clear previous timeout before setting a new one on multiple message events', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
|
||||
});
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
expect(setTimeoutSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not scroll if multiple message events prevent execution of setTimeout callback', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
|
||||
jest.advanceTimersByTime(500);
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
|
||||
});
|
||||
|
||||
expect(window.scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should scroll only after the final timeout', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(window.scrollTo).toHaveBeenCalledWith({ top: 500, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
it('should NOT set hasLastPosition to false if lastYPosition exists and is valid', () => {
|
||||
localStorage.setItem(storageKey, '500');
|
||||
|
||||
renderHook(() => useScrollToLastPosition(storageKey));
|
||||
|
||||
expect(setStateSpy).not.toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ export type UseMessageHandlersTypes = {
|
||||
handleCloseLegacyEditorXBlockModal: () => void;
|
||||
handleSaveEditedXBlockData: () => void;
|
||||
handleFinishXBlockDragging: () => void;
|
||||
handleOpenManageTagsModal: (id: string) => void;
|
||||
};
|
||||
|
||||
export type MessageHandlersTypes = Record<string, (payload: any) => void>;
|
||||
|
||||
@@ -26,6 +26,7 @@ export const useMessageHandlers = ({
|
||||
handleCloseLegacyEditorXBlockModal,
|
||||
handleSaveEditedXBlockData,
|
||||
handleFinishXBlockDragging,
|
||||
handleOpenManageTagsModal,
|
||||
}: UseMessageHandlersTypes): MessageHandlersTypes => useMemo(() => ({
|
||||
[messageTypes.copyXBlock]: ({ usageId }) => dispatch(copyToClipboard(usageId)),
|
||||
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
|
||||
@@ -41,6 +42,7 @@ export const useMessageHandlers = ({
|
||||
[messageTypes.saveEditedXBlockData]: handleSaveEditedXBlockData,
|
||||
[messageTypes.studioAjaxError]: ({ error }) => handleResponseErrors(error, dispatch, updateSavingStatus),
|
||||
[messageTypes.refreshPositions]: handleFinishXBlockDragging,
|
||||
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
|
||||
}), [
|
||||
courseId,
|
||||
handleDeleteXBlock,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
useRef, FC, useEffect, useState, useMemo, useCallback,
|
||||
} from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { useToggle, Sheet } from '@openedx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -10,6 +10,7 @@ import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
||||
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 { updateCourseUnitSidebar } from '../data/thunk';
|
||||
@@ -43,6 +44,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
|
||||
const [configureXBlockId, setConfigureXBlockId] = useState<string | null>(null);
|
||||
const [showLegacyEditModal, setShowLegacyEditModal] = useState<boolean>(false);
|
||||
const [isManageTagsOpen, openManageTagsModal, closeManageTagsModal] = useToggle(false);
|
||||
|
||||
const iframeUrl = useMemo(() => getIframeUrl(blockId), [blockId]);
|
||||
const legacyEditModalUrl = useMemo(() => getLegacyEditModalUrl(configureXBlockId), [configureXBlockId]);
|
||||
@@ -120,6 +122,11 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
dispatch(updateCourseUnitSidebar(blockId));
|
||||
};
|
||||
|
||||
const handleOpenManageTagsModal = (id: string) => {
|
||||
setConfigureXBlockId(id);
|
||||
openManageTagsModal();
|
||||
};
|
||||
|
||||
const messageHandlers = useMessageHandlers({
|
||||
courseId,
|
||||
navigate,
|
||||
@@ -133,6 +140,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
handleCloseLegacyEditorXBlockModal,
|
||||
handleSaveEditedXBlockData,
|
||||
handleFinishXBlockDragging,
|
||||
handleOpenManageTagsModal,
|
||||
});
|
||||
|
||||
useIframeMessages(messageHandlers);
|
||||
@@ -167,6 +175,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title={intl.formatMessage(messages.xblockIframeTitle)}
|
||||
name="xblock-iframe"
|
||||
src={iframeUrl}
|
||||
frameBorder="0"
|
||||
allow={IFRAME_FEATURE_POLICY}
|
||||
@@ -177,6 +186,16 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
referrerPolicy="origin"
|
||||
aria-label={intl.formatMessage(messages.xblockIframeLabel, { xblockCount: courseVerticalChildren.length })}
|
||||
/>
|
||||
{configureXBlockId && (
|
||||
<Sheet
|
||||
position="right"
|
||||
show={isManageTagsOpen}
|
||||
onClose={closeManageTagsModal}
|
||||
blocking
|
||||
>
|
||||
<ContentTagsDrawer id={configureXBlockId} onClose={closeManageTagsModal} />
|
||||
</Sheet>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user