From 7c52f85cd981461b46cf5d9dd4faabf18964ccf7 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 26 Mar 2025 12:07:28 +0530 Subject: [PATCH 01/34] feat: library unit page skeleton --- src/library-authoring/LibraryLayout.tsx | 5 + .../collections/LibraryCollectionPage.tsx | 4 +- .../common/context/LibraryContext.tsx | 9 ++ src/library-authoring/data/apiHooks.ts | 14 +- src/library-authoring/routes.ts | 3 + .../units/LibraryUnitPage.tsx | 134 ++++++++++++++++++ src/library-authoring/units/index.tsx | 1 + src/library-authoring/units/messages.ts | 21 +++ 8 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/library-authoring/units/LibraryUnitPage.tsx create mode 100644 src/library-authoring/units/index.tsx create mode 100644 src/library-authoring/units/messages.ts diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index 8c48bae7a6..3c5e2213c7 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -16,6 +16,7 @@ import { CreateUnitModal } from './create-unit'; import LibraryCollectionPage from './collections/LibraryCollectionPage'; import { ComponentPicker } from './component-picker'; import { ComponentEditorModal } from './components/ComponentEditorModal'; +import { LibraryUnitPage } from './units'; const LibraryLayout = () => { const { libraryId } = useParams(); @@ -71,6 +72,10 @@ const LibraryLayout = () => { path={ROUTES.COLLECTION} element={context()} /> + )} + /> ); }; diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 306a64ab5b..90dee503c1 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -139,7 +139,7 @@ const LibraryCollectionPage = () => { return ; } - const breadcumbs = !componentPickerMode ? ( + const breadcrumbs = !componentPickerMode ? ( { > } - breadcrumbs={breadcumbs} + breadcrumbs={breadcrumbs} headerActions={} hideBorder /> diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 9b467e19a7..b0ab37b041 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -28,6 +28,8 @@ export type LibraryContextData = { /** The ID of the current collection/component/unit, on the sidebar OR page */ collectionId: string | undefined; setCollectionId: (collectionId?: string) => void; + unitId: string | undefined; + setUnitId: (unitId?: string) => void; componentId: string | undefined; setComponentId: (componentId?: string) => void; unitId: string | undefined; @@ -125,6 +127,9 @@ export const LibraryProvider = ({ const [unitId, setUnitId] = useState( skipUrlUpdate ? undefined : urlUnitId || (selectedItemIdIsUnit ? urlSelectedItemId : undefined), ); + const [unitId, setUnitId] = useState( + skipUrlUpdate ? undefined : params.unitId, + ); const context = useMemo(() => { const contextValue = { @@ -132,6 +137,8 @@ export const LibraryProvider = ({ libraryData, collectionId, setCollectionId, + unitId, + setUnitId, componentId, setComponentId, unitId, @@ -157,6 +164,8 @@ export const LibraryProvider = ({ libraryData, collectionId, setCollectionId, + unitId, + setUnitId, componentId, setComponentId, unitId, diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 71b1726af3..d51e324d28 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -93,6 +93,11 @@ export const libraryAuthoringQueryKeys = { libraryId, collectionId, ], + container: (libraryId?: string, containerId?: string) => [ + ...libraryAuthoringQueryKeys.all, + libraryId, + containerId, + ], blockTypes: (libraryId?: string) => [ ...libraryAuthoringQueryKeys.all, 'blockTypes', @@ -599,10 +604,11 @@ export const useCreateLibraryContainer = (libraryId: string) => { /** * Get the metadata for a container in a library */ -export const useContainer = (containerId: string) => ( +export const useContainer = (libraryId?: string, containerId?: string) => ( useQuery({ - queryKey: containerQueryKeys.container(containerId), - queryFn: containerId ? () => getContainerMetadata(containerId) : undefined, + enabled: !!libraryId && !!containerId, + queryKey: libraryAuthoringQueryKeys.container(libraryId, containerId), + queryFn: () => getContainerMetadata(containerId!), }) ); @@ -618,7 +624,7 @@ export const useUpdateContainer = (containerId: string) => { // NOTE: We invalidate the library query here because we need to update the library's // container list. queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); - queryClient.invalidateQueries({ queryKey: containerQueryKeys.container(containerId) }); + queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(libraryId, containerId) }); }, }); }; diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index da97e2d9a5..9822f11b9d 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -29,6 +29,9 @@ export const ROUTES = { // LibraryCollectionPage route: // * with a selected collectionId and/or an optionally selected componentId. COLLECTION: '/collection/:collectionId/:componentId?', + // LibraryUnitPage route: + // * with a selected unitId and/or an optionally selected componentId. + UNIT: '/unit/:unitId/:componentId?', }; export enum ContentType { diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx new file mode 100644 index 0000000000..c77b2243fd --- /dev/null +++ b/src/library-authoring/units/LibraryUnitPage.tsx @@ -0,0 +1,134 @@ +import { useIntl } from "@edx/frontend-platform/i18n"; +import { Breadcrumb, Button, Container } from "@openedx/paragon"; +import { Add, InfoOutline, Link } from "@openedx/paragon/icons"; +import { useEffect } from "react"; +import { Helmet } from 'react-helmet'; + +import ErrorAlert from '../../generic/alert-error'; +import Loading from "../../generic/Loading"; +import NotFoundAlert from "../../generic/NotFoundAlert"; +import SubHeader from "../../generic/sub-header/SubHeader"; +import Header from "../../header"; +import { useLibraryContext } from "../common/context/LibraryContext"; +import { useSidebarContext } from "../common/context/SidebarContext"; +import { useContainer, useContentLibrary } from "../data/apiHooks"; +import { LibrarySidebar } from "../library-sidebar"; +import { SubHeaderTitle } from "../LibraryAuthoringPage"; +import messages from "./messages"; + +const HeaderActions = () => { + const intl = useIntl(); + + return ( +
+ + +
+ ); +}; + +export const LibraryUnitPage = () => { + const intl = useIntl(); + + const { + libraryId, + unitId, + componentId, + } = useLibraryContext(); + const { sidebarComponentInfo, openInfoSidebar } = useSidebarContext(); + + useEffect(() => { + openInfoSidebar(componentId, unitId); + }, []); + + if (!unitId || !libraryId) { + // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. + throw new Error('Rendered without unitId or libraryId URL parameter'); + } + + const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); + const { + data: unitData, + isLoading, + isError, + error, + } = useContainer(libraryId, unitId); + + + // Only show loading if unit or library data is not fetched from index yet + if (isLibLoading || isLoading) { + return ; + } + + if (!libraryData || !unitData) { + return ; + } + + if (isError) { + return ; + } + + const breadcrumbs = ( + ` spacer. + { + label: '', + to: '', + }, + ]} + linkAs={Link} + /> + ) + + return ( +
+
+ {libraryData.title} | {process.env.SITE_NAME} +
+ + } + headerActions={} + breadcrumbs={breadcrumbs} + hideBorder + /> + +
+ {!!sidebarComponentInfo?.type && ( +
+ +
+ )} +
+ ) +} diff --git a/src/library-authoring/units/index.tsx b/src/library-authoring/units/index.tsx new file mode 100644 index 0000000000..67242344b7 --- /dev/null +++ b/src/library-authoring/units/index.tsx @@ -0,0 +1 @@ +export { LibraryUnitPage } from "./LibraryUnitPage"; diff --git a/src/library-authoring/units/messages.ts b/src/library-authoring/units/messages.ts new file mode 100644 index 0000000000..671e9d2abe --- /dev/null +++ b/src/library-authoring/units/messages.ts @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + infoButtonText: { + id: 'course-authoring.library-authoring.unit-header.buttons.info', + defaultMessage: 'Unit Info', + description: 'Button text to unit sidebar from unit page', + }, + newContentButton: { + id: 'course-authoring.library-authoring.unit-header.buttons.new-content', + defaultMessage: 'Add Content', + description: 'Text of button to add new content to unit', + }, + breadcrumbsAriaLabel: { + id: 'course-authoring.library-authoring.breadcrumbs.label.text', + defaultMessage: 'Navigation breadcrumbs', + description: 'Aria label for navigation breadcrumbs', + }, +}); + +export default messages; From f843e9f5679a2099ef42ca8f60aa690daae53a04 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 28 Mar 2025 20:34:24 +0530 Subject: [PATCH 02/34] feat: iframe auto height adjust --- src/CourseAuthoringRoutes.jsx | 3 +- src/constants.js | 13 +++ src/course-unit/CourseUnit.test.jsx | 2 +- .../add-component/AddComponent.jsx | 2 +- .../add-component/AddComponent.test.jsx | 2 +- src/course-unit/constants.js | 10 -- src/course-unit/hooks.jsx | 6 +- src/course-unit/index.js | 1 - src/course-unit/move-modal/hooks.tsx | 2 +- src/course-unit/move-modal/moveModal.test.tsx | 2 +- .../preview-changes/index.test.tsx | 2 +- src/course-unit/preview-changes/index.tsx | 2 +- src/course-unit/sidebar/PublishControls.jsx | 2 +- .../xblock-container-iframe/hooks/index.ts | 4 - .../hooks/tests/hooks.test.tsx | 15 +-- .../xblock-container-iframe/hooks/types.ts | 13 --- .../xblock-container-iframe/index.tsx | 8 +- .../hooks}/context/hooks.test.tsx | 0 .../hooks}/context/hooks.tsx | 0 .../hooks}/context/iFrameContext.tsx | 0 .../hooks/useIFrameBehavior.tsx | 22 ++-- .../hooks/useIframeContent.tsx | 0 .../hooks/useIframeMessages.tsx | 0 .../hooks/useLoadBearingHook.tsx | 0 src/generic/types.ts | 12 ++ .../LibraryBlock/LibraryBlock.tsx | 63 +++++------ .../component-info/ComponentPreview.tsx | 5 +- src/library-authoring/data/api.mocks.ts | 4 +- src/library-authoring/data/api.ts | 5 +- src/library-authoring/data/apiHooks.ts | 16 ++- .../units/LibraryUnitPage.tsx | 107 ++++++++++++------ 31 files changed, 185 insertions(+), 138 deletions(-) rename src/{course-unit => generic/hooks}/context/hooks.test.tsx (100%) rename src/{course-unit => generic/hooks}/context/hooks.tsx (100%) rename src/{course-unit => generic/hooks}/context/iFrameContext.tsx (100%) rename src/{course-unit/xblock-container-iframe => generic}/hooks/useIFrameBehavior.tsx (77%) rename src/{course-unit/xblock-container-iframe => generic}/hooks/useIframeContent.tsx (100%) rename src/{course-unit/xblock-container-iframe => generic}/hooks/useIframeMessages.tsx (100%) rename src/{course-unit/xblock-container-iframe => generic}/hooks/useLoadBearingHook.tsx (100%) create mode 100644 src/generic/types.ts diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index dd64c64022..1a8a89cf86 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -17,7 +17,7 @@ import ScheduleAndDetails from './schedule-and-details'; import { GradingSettings } from './grading-settings'; import CourseTeam from './course-team/CourseTeam'; import { CourseUpdates } from './course-updates'; -import { CourseUnit, IframeProvider } from './course-unit'; +import { CourseUnit } from './course-unit'; import { Certificates } from './certificates'; import CourseExportPage from './export-page/CourseExportPage'; import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage'; @@ -26,6 +26,7 @@ import { DECODED_ROUTES } from './constants'; import CourseChecklist from './course-checklist'; import GroupConfigurations from './group-configurations'; import { CourseLibraries } from './course-libraries'; +import { IframeProvider } from "./generic/hooks/context/iFrameContext"; /** * As of this writing, these routes are mounted at a path prefixed with the following: diff --git a/src/constants.js b/src/constants.js index 849247d395..fee7cdc847 100644 --- a/src/constants.js +++ b/src/constants.js @@ -92,3 +92,16 @@ export const REGEX_RULES = { export const IFRAME_FEATURE_POLICY = ( 'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *' ); + +export const iframeStateKeys = { + iframeHeight: 'iframeHeight', + hasLoaded: 'hasLoaded', + showError: 'showError', + windowTopOffset: 'windowTopOffset', +}; + +export const iframeMessageTypes = { + modal: 'plugin.modal', + resize: 'plugin.resize', + videoFullScreen: 'plugin.videoFullScreen', +} diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 43db176b24..d92d5d4b81 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -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'; diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx index be9481bad7..12dbd665d1 100644 --- a/src/course-unit/add-component/AddComponent.jsx +++ b/src/course-unit/add-component/AddComponent.jsx @@ -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 = ({ diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.jsx index bda1ee1ea4..45bb4e6759 100644 --- a/src/course-unit/add-component/AddComponent.test.jsx +++ b/src/course-unit/add-component/AddComponent.test.jsx @@ -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; diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js index da6c742616..dcea2f603b 100644 --- a/src/course-unit/constants.js +++ b/src/course-unit/constants.js @@ -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', diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 638fe0d30b..fc8fe092eb 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -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); } diff --git a/src/course-unit/index.js b/src/course-unit/index.js index e4ace54b03..f31a91ce92 100644 --- a/src/course-unit/index.js +++ b/src/course-unit/index.js @@ -1,2 +1 @@ export { default as CourseUnit } from './CourseUnit'; -export { IframeProvider } from './context/iFrameContext'; diff --git a/src/course-unit/move-modal/hooks.tsx b/src/course-unit/move-modal/hooks.tsx index d21014e3bb..be06cf1b3f 100644 --- a/src/course-unit/move-modal/hooks.tsx +++ b/src/course-unit/move-modal/hooks.tsx @@ -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 { diff --git a/src/course-unit/move-modal/moveModal.test.tsx b/src/course-unit/move-modal/moveModal.test.tsx index 6080a8c42e..cc83995ee6 100644 --- a/src/course-unit/move-modal/moveModal.test.tsx +++ b/src/course-unit/move-modal/moveModal.test.tsx @@ -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'; diff --git a/src/course-unit/preview-changes/index.test.tsx b/src/course-unit/preview-changes/index.test.tsx index ccdb13c7f9..a1584f6dee 100644 --- a/src/course-unit/preview-changes/index.test.tsx +++ b/src/course-unit/preview-changes/index.test.tsx @@ -10,7 +10,7 @@ import { import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'; import { messageTypes } from '../constants'; -import { IframeProvider } from '../context/iFrameContext'; +import { IframeProvider } from '../../generic/hooks/context/iFrameContext'; import { libraryBlockChangesUrl } from '../data/api'; import { ToastActionData } from '../../generic/toast-context'; import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api'; diff --git a/src/course-unit/preview-changes/index.tsx b/src/course-unit/preview-changes/index.tsx index 606910ad02..c26550eb12 100644 --- a/src/course-unit/preview-changes/index.tsx +++ b/src/course-unit/preview-changes/index.tsx @@ -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'; diff --git a/src/course-unit/sidebar/PublishControls.jsx b/src/course-unit/sidebar/PublishControls.jsx index d5076aa838..2a6ea83995 100644 --- a/src/course-unit/sidebar/PublishControls.jsx +++ b/src/course-unit/sidebar/PublishControls.jsx @@ -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'; diff --git a/src/course-unit/xblock-container-iframe/hooks/index.ts b/src/course-unit/xblock-container-iframe/hooks/index.ts index c49993dc1e..2dda3523dc 100644 --- a/src/course-unit/xblock-container-iframe/hooks/index.ts +++ b/src/course-unit/xblock-container-iframe/hooks/index.ts @@ -1,5 +1 @@ -export { useIframeMessages } from './useIframeMessages'; -export { useIframeContent } from './useIframeContent'; export { useMessageHandlers } from './useMessageHandlers'; -export { useIFrameBehavior } from './useIFrameBehavior'; -export { useLoadBearingHook } from './useLoadBearingHook'; diff --git a/src/course-unit/xblock-container-iframe/hooks/tests/hooks.test.tsx b/src/course-unit/xblock-container-iframe/hooks/tests/hooks.test.tsx index b91a7fec1b..951a5d8ff1 100644 --- a/src/course-unit/xblock-container-iframe/hooks/tests/hooks.test.tsx +++ b/src/course-unit/xblock-container-iframe/hooks/tests/hooks.test.tsx @@ -7,10 +7,11 @@ 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 { iframeMessageTypes, iframeStateKeys } from '../../../../constants'; jest.useFakeTimers(); @@ -35,13 +36,13 @@ describe('useIFrameBehavior', () => { beforeEach(() => { (useKeyedState as jest.Mock).mockImplementation((key, initialValue) => { switch (key) { - case stateKeys.iframeHeight: + case iframeStateKeys.iframeHeight: return [0, setIframeHeight]; - case stateKeys.hasLoaded: + case iframeStateKeys.hasLoaded: return [false, setHasLoaded]; - case stateKeys.showError: + case iframeStateKeys.showError: return [false, setShowError]; - case stateKeys.windowTopOffset: + case iframeStateKeys.windowTopOffset: return [null, setWindowTopOffset]; default: return [initialValue, jest.fn()]; @@ -66,7 +67,7 @@ describe('useIFrameBehavior', () => { const mockWindowTopOffset = 100; (useKeyedState as jest.Mock).mockImplementation((key) => { - if (key === stateKeys.windowTopOffset) { + if (key === iframeStateKeys.windowTopOffset) { return [mockWindowTopOffset, setWindowTopOffset]; } return [null, jest.fn()]; @@ -76,7 +77,7 @@ describe('useIFrameBehavior', () => { const message = { data: { - type: messageTypes.videoFullScreen, + type: iframeMessageTypes.videoFullScreen, payload: { open: false }, }, }; diff --git a/src/course-unit/xblock-container-iframe/hooks/types.ts b/src/course-unit/xblock-container-iframe/hooks/types.ts index b43b8502cf..4775673c1c 100644 --- a/src/course-unit/xblock-container-iframe/hooks/types.ts +++ b/src/course-unit/xblock-container-iframe/hooks/types.ts @@ -18,16 +18,3 @@ export type UseMessageHandlersTypes = { }; export type MessageHandlersTypes = Record void>; - -export interface UseIFrameBehaviorTypes { - id: string; - iframeUrl: string; - onLoaded?: boolean; -} - -export interface UseIFrameBehaviorReturnTypes { - iframeHeight: number; - handleIFrameLoad: () => void; - showError: boolean; - hasLoaded: boolean; -} diff --git a/src/course-unit/xblock-container-iframe/index.tsx b/src/course-unit/xblock-container-iframe/index.tsx index 984359d8bf..a3da71453c 100644 --- a/src/course-unit/xblock-container-iframe/index.tsx +++ b/src/course-unit/xblock-container-iframe/index.tsx @@ -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,6 +32,9 @@ 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 = ({ courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType, diff --git a/src/course-unit/context/hooks.test.tsx b/src/generic/hooks/context/hooks.test.tsx similarity index 100% rename from src/course-unit/context/hooks.test.tsx rename to src/generic/hooks/context/hooks.test.tsx diff --git a/src/course-unit/context/hooks.tsx b/src/generic/hooks/context/hooks.tsx similarity index 100% rename from src/course-unit/context/hooks.tsx rename to src/generic/hooks/context/hooks.tsx diff --git a/src/course-unit/context/iFrameContext.tsx b/src/generic/hooks/context/iFrameContext.tsx similarity index 100% rename from src/course-unit/context/iFrameContext.tsx rename to src/generic/hooks/context/iFrameContext.tsx diff --git a/src/course-unit/xblock-container-iframe/hooks/useIFrameBehavior.tsx b/src/generic/hooks/useIFrameBehavior.tsx similarity index 77% rename from src/course-unit/xblock-container-iframe/hooks/useIFrameBehavior.tsx rename to src/generic/hooks/useIFrameBehavior.tsx index 832ac94cd3..3b064477a6 100644 --- a/src/course-unit/xblock-container-iframe/hooks/useIFrameBehavior.tsx +++ b/src/generic/hooks/useIFrameBehavior.tsx @@ -3,10 +3,10 @@ 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'; +import { iframeStateKeys, iframeMessageTypes } from '../../constants'; +import { UseIFrameBehaviorReturnTypes, UseIFrameBehaviorTypes } from '../types'; +import { useEventListener } from './useEventListener'; /** * Custom hook to manage iframe behavior. @@ -29,21 +29,25 @@ export const useIFrameBehavior = ({ // Do not remove this hook. See function description. useLoadBearingHook(id); - const [iframeHeight, setIframeHeight] = useKeyedState(stateKeys.iframeHeight, 0); - const [hasLoaded, setHasLoaded] = useKeyedState(stateKeys.hasLoaded, false); - const [showError, setShowError] = useKeyedState(stateKeys.showError, false); - const [windowTopOffset, setWindowTopOffset] = useKeyedState(stateKeys.windowTopOffset, null); + const [iframeHeight, setIframeHeight] = useKeyedState(iframeStateKeys.iframeHeight, 0); + const [hasLoaded, setHasLoaded] = useKeyedState(iframeStateKeys.hasLoaded, false); + const [showError, setShowError] = useKeyedState(iframeStateKeys.showError, false); + const [windowTopOffset, setWindowTopOffset] = useKeyedState(iframeStateKeys.windowTopOffset, null); const receiveMessage = useCallback(({ data }: MessageEvent) => { const { payload, type } = data; + // __AUTO_GENERATED_PRINT_VAR_START__ + console.log("useIFrameBehavior#(anon) type: ", type); // __AUTO_GENERATED_PRINT_VAR_END__ + // __AUTO_GENERATED_PRINT_VAR_START__ + console.log("useIFrameBehavior#(anon) payload: ", payload); // __AUTO_GENERATED_PRINT_VAR_END__ - if (type === messageTypes.resize) { + if (type === iframeMessageTypes.resize) { setIframeHeight(payload.height); if (!hasLoaded && iframeHeight === 0 && payload.height > 0) { setHasLoaded(true); } - } else if (type === messageTypes.videoFullScreen) { + } else if (type === iframeMessageTypes.videoFullScreen) { // We observe exit from the video xblock fullscreen mode // and scroll to the previously saved scroll position if (!payload.open && windowTopOffset !== null) { diff --git a/src/course-unit/xblock-container-iframe/hooks/useIframeContent.tsx b/src/generic/hooks/useIframeContent.tsx similarity index 100% rename from src/course-unit/xblock-container-iframe/hooks/useIframeContent.tsx rename to src/generic/hooks/useIframeContent.tsx diff --git a/src/course-unit/xblock-container-iframe/hooks/useIframeMessages.tsx b/src/generic/hooks/useIframeMessages.tsx similarity index 100% rename from src/course-unit/xblock-container-iframe/hooks/useIframeMessages.tsx rename to src/generic/hooks/useIframeMessages.tsx diff --git a/src/course-unit/xblock-container-iframe/hooks/useLoadBearingHook.tsx b/src/generic/hooks/useLoadBearingHook.tsx similarity index 100% rename from src/course-unit/xblock-container-iframe/hooks/useLoadBearingHook.tsx rename to src/generic/hooks/useLoadBearingHook.tsx diff --git a/src/generic/types.ts b/src/generic/types.ts new file mode 100644 index 0000000000..fa6fff0fd5 --- /dev/null +++ b/src/generic/types.ts @@ -0,0 +1,12 @@ +export interface UseIFrameBehaviorTypes { + id: string; + iframeUrl: string; + onLoaded?: boolean; +} + +export interface UseIFrameBehaviorReturnTypes { + iframeHeight: number; + handleIFrameLoad: () => void; + showError: boolean; + hasLoaded: boolean; +} diff --git a/src/library-authoring/LibraryBlock/LibraryBlock.tsx b/src/library-authoring/LibraryBlock/LibraryBlock.tsx index b23386c4d5..873c96f5cd 100644 --- a/src/library-authoring/LibraryBlock/LibraryBlock.tsx +++ b/src/library-authoring/LibraryBlock/LibraryBlock.tsx @@ -3,6 +3,10 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import messages from './messages'; +import { IFRAME_FEATURE_POLICY } from '../../constants'; +import { useIFrameBehavior } from '../../generic/hooks/useIFrameBehavior'; +import { useIframe } from '../../generic/hooks/context/hooks'; +import { useIframeContent } from '../../generic/hooks/useIframeContent'; export type VersionSpec = 'published' | 'draft' | number; @@ -28,13 +32,21 @@ export const LibraryBlock = ({ view, }: LibraryBlockProps) => { const iframeRef = useRef(null); + const { setIframeRef } = useIframe(); const xblockView = view ?? 'student_view'; - const defaultiFrameHeight = xblockView === 'studio_view' ? 80 : 50; - const [iFrameHeight, setIFrameHeight] = useState(defaultiFrameHeight); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const intl = useIntl(); + const queryStr = version ? `?version=${version}` : ''; + const iframeUrl = `${studioBaseUrl}/xblocks/v2/${usageKey}/embed/${xblockView}/${queryStr}`; + const { iframeHeight } = useIFrameBehavior({ id: usageKey, iframeUrl }); + + useIframeContent(iframeRef, setIframeRef); + + useEffect(() => { + setIframeRef(iframeRef); + }, [setIframeRef]); /** * Handle any messages we receive from the XBlock Runtime code in the IFrame. @@ -47,10 +59,7 @@ export const LibraryBlock = ({ } const { method, replyKey, ...args } = event.data; - - if (method === 'update_frame_height') { - setIFrameHeight(args.height); - } else if (method?.indexOf('xblock:') === 0) { + if (method?.indexOf('xblock:') === 0) { // This is a notification from the XBlock's frontend via 'runtime.notify(event, args)' if (onBlockNotification) { onBlockNotification({ @@ -70,7 +79,7 @@ export const LibraryBlock = ({ window.addEventListener('message', receivedWindowMessage); if (window.self !== window.top) { // This component is loaded inside an iframe. - setIFrameHeight(86); + // setIFrameHeight(86); } return () => { @@ -78,32 +87,20 @@ export const LibraryBlock = ({ }; }, []); - const queryStr = version ? `?version=${version}` : ''; - return ( -
-