diff --git a/src/library-authoring/common/context/SidebarContext.tsx b/src/library-authoring/common/context/SidebarContext.tsx index e05db180b0..19b284aa1e 100644 --- a/src/library-authoring/common/context/SidebarContext.tsx +++ b/src/library-authoring/common/context/SidebarContext.tsx @@ -10,6 +10,7 @@ import { useParams } from 'react-router-dom'; import { useStateWithUrlSearchParam } from '../../../hooks'; import { useComponentPickerContext } from './ComponentPickerContext'; import { useLibraryContext } from './LibraryContext'; +import { useRunOnNextRender } from '@src/utils'; export enum SidebarBodyItemId { AddContent = 'add-content', @@ -124,19 +125,22 @@ export const SidebarProvider = ({ const [defaultTab, setDefaultTab] = useState(DEFAULT_TAB); const [hiddenTabs, setHiddenTabs] = useState>([]); - const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam( + const [sidebarTab, setSidebarTabInternal] = useStateWithUrlSearchParam( defaultTab.component, 'st', (value: string) => toSidebarInfoTab(value), (value: SidebarInfoTab) => value.toString(), ); + const setSidebarTab = useRunOnNextRender((tab: SidebarInfoTab) => setSidebarTabInternal(tab)); - const [sidebarAction, setSidebarAction] = useStateWithUrlSearchParam( + const [sidebarAction, setSidebarActionInternal] = useStateWithUrlSearchParam( SidebarActions.None, 'sa', (value: string) => Object.values(SidebarActions).find((enumValue) => value === enumValue), (value: SidebarActions) => value.toString(), ); + const setSidebarAction = useRunOnNextRender((action: SidebarActions) => setSidebarActionInternal(action)); + const resetSidebarAction = useCallback(() => { setSidebarAction(SidebarActions.None); }, [setSidebarAction]); diff --git a/src/library-authoring/components/ComponentMenu.tsx b/src/library-authoring/components/ComponentMenu.tsx index c1777c7cb2..26c229c694 100644 --- a/src/library-authoring/components/ComponentMenu.tsx +++ b/src/library-authoring/components/ComponentMenu.tsx @@ -21,7 +21,6 @@ import { canEditComponent } from './ComponentEditorModal'; import ComponentDeleter from './ComponentDeleter'; import messages from './messages'; import { useLibraryRoutes } from '../routes'; -import { useRunOnNextRender } from '../../utils'; export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const intl = useIntl(); @@ -95,21 +94,9 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { openComponentEditor(usageKey); }, [usageKey, navigateTo]); - const scheduleJumpToCollection = useRunOnNextRender(() => { - // TODO: Ugly hack to make sure sidebar shows add to collection section - // This needs to run after all changes to url takes place to avoid conflicts. - setTimeout(() => setSidebarAction(SidebarActions.JumpToManageCollections), 250); - }); - const showManageCollections = useCallback(() => { - navigateTo({ selectedItemId: usageKey }); - scheduleJumpToCollection(); - }, [ - scheduleJumpToCollection, - openComponentInfoSidebar, - usageKey, - navigateTo, - ]); + navigateTo({ selectedItemId: usageKey }, () => setSidebarAction(SidebarActions.JumpToManageCollections)); + }, [usageKey, setSidebarAction, navigateTo]); return ( diff --git a/src/library-authoring/components/ContainerCard.tsx b/src/library-authoring/components/ContainerCard.tsx index 3468842b5d..90a682669f 100644 --- a/src/library-authoring/components/ContainerCard.tsx +++ b/src/library-authoring/components/ContainerCard.tsx @@ -23,7 +23,6 @@ import AddComponentWidget from './AddComponentWidget'; import BaseCard from './BaseCard'; import messages from './messages'; import ContainerDeleter from './ContainerDeleter'; -import { useRunOnNextRender } from '../../utils'; type ContainerMenuProps = { containerKey: string; @@ -56,16 +55,9 @@ export const ContainerMenu = ({ containerKey, displayName } : ContainerMenuProps }); }; - const scheduleJumpToCollection = useRunOnNextRender(() => { - // TODO: Ugly hack to make sure sidebar shows add to collection section - // This needs to run after all changes to url takes place to avoid conflicts. - setTimeout(() => setSidebarAction(SidebarActions.JumpToManageCollections)); - }); - const showManageCollections = useCallback(() => { - navigateTo({ selectedItemId: containerKey }); - scheduleJumpToCollection(); - }, [scheduleJumpToCollection, navigateTo, containerKey]); + navigateTo({ selectedItemId: containerKey }, () => setSidebarAction(SidebarActions.JumpToManageCollections)); + }, [navigateTo, containerKey]); const openContainer = useCallback(() => { navigateTo({ containerId: containerKey }); diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index e7760c6fb9..71ed6aff1f 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -1,7 +1,13 @@ /** * Constants and utility hook for the Library Authoring routes. */ -import { useCallback, useMemo } from 'react'; +import { + useCallback, + useMemo, + useState, + useRef, + useEffect, +} from 'react'; import { generatePath, matchPath, @@ -9,6 +15,7 @@ import { useLocation, useNavigate, useSearchParams, + useNavigation, type PathMatch, } from 'react-router-dom'; import { ContainerType, getBlockType } from '../generic/key-utils'; @@ -76,7 +83,7 @@ export type LibraryRoutesData = { * This function can be mutated if there are changes in the current route, so always include * it in the dependencies array if used on a `useCallback`. */ - navigateTo: (dict?: NavigateToData) => void; + navigateTo: (dict?: NavigateToData, callback?: () => void) => void; }; export const useLibraryRoutes = (): LibraryRoutesData => { @@ -84,6 +91,9 @@ export const useLibraryRoutes = (): LibraryRoutesData => { const params = useParams(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); + const navigation = useNavigation(); + const [hasNavigated, setHasNavigated] = useState(false); + const navigationCallbackRef = useRef<() => void>(); const insideCollection = matchPath(BASE_ROUTE + ROUTES.COLLECTION, pathname); const insideCollections = matchPath(BASE_ROUTE + ROUTES.COLLECTIONS, pathname); @@ -119,7 +129,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { collectionId, containerId, contentType, - }: NavigateToData = {}) => { + }: NavigateToData = {}, callback?: () => void) => { const routeParams = { ...params, // Overwrite the params with the provided values. @@ -237,6 +247,8 @@ export const useLibraryRoutes = (): LibraryRoutesData => { pathname: newPath, search: searchParams.toString(), }); + navigationCallbackRef.current = callback; + setHasNavigated(true); } }, [ navigate, @@ -245,6 +257,14 @@ export const useLibraryRoutes = (): LibraryRoutesData => { pathname, ]); + useEffect(() => { + if (hasNavigated && navigation.state === 'idle') { + navigationCallbackRef.current?.(); + navigationCallbackRef.current = undefined; + setHasNavigated(false); + } + }, [navigation.state, hasNavigated]); + return useMemo(() => ({ navigateTo, insideCollection, diff --git a/src/library-authoring/section-subsections/LibraryContainerChildren.tsx b/src/library-authoring/section-subsections/LibraryContainerChildren.tsx index c0b2010ec5..745818f5bd 100644 --- a/src/library-authoring/section-subsections/LibraryContainerChildren.tsx +++ b/src/library-authoring/section-subsections/LibraryContainerChildren.tsx @@ -25,7 +25,6 @@ import TagCount from '../../generic/tag-count'; import { ContainerMenu } from '../components/ContainerCard'; import { useLibraryRoutes } from '../routes'; import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; -import { useRunOnNextRender } from '../../utils'; interface LibraryContainerChildrenProps { containerKey: string; @@ -60,17 +59,9 @@ const ContainerRow = ({ containerKey, container, readOnly }: ContainerRowProps) } }; - /* istanbul ignore next */ - const scheduleJumpToTags = useRunOnNextRender(() => { - // TODO: Ugly hack to make sure sidebar shows manage tags section - // This needs to run after all changes to url takes place to avoid conflicts. - setTimeout(() => setSidebarAction(SidebarActions.JumpToManageTags), 250); - }); - - const jumpToManageTags = () => { - navigateTo({ selectedItemId: container.originalId }); - scheduleJumpToTags(); - }; + const jumpToManageTags = useCallback(() => { + navigateTo({ selectedItemId: container.originalId }, () => setSidebarAction(SidebarActions.JumpToManageTags)) + }, [navigateTo, container.originalId, setSidebarAction]); return ( <> diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx index 0dd1072b7f..7369556c86 100644 --- a/src/library-authoring/units/LibraryUnitBlocks.tsx +++ b/src/library-authoring/units/LibraryUnitBlocks.tsx @@ -31,7 +31,6 @@ import messages from './messages'; import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; import { ToastContext } from '../../generic/toast-context'; import { canEditComponent } from '../components/ComponentEditorModal'; -import { useRunOnNextRender } from '../../utils'; /** Components that need large min height in preview */ const LARGE_COMPONENTS = [ @@ -75,17 +74,9 @@ const BlockHeader = ({ block, readOnly }: ComponentBlockProps) => { } }; - /* istanbul ignore next */ - const scheduleJumpToTags = useRunOnNextRender(() => { - // TODO: Ugly hack to make sure sidebar shows manage tags section - // This needs to run after all changes to url takes place to avoid conflicts. - setTimeout(() => setSidebarAction(SidebarActions.JumpToManageTags), 250); - }); - /* istanbul ignore next */ const jumpToManageTags = () => { - navigateTo({ selectedItemId: block.originalId }); - scheduleJumpToTags(); + navigateTo({ selectedItemId: block.originalId }, () => setSidebarAction(SidebarActions.JumpToManageTags)); }; return ( diff --git a/src/utils.js b/src/utils.js index d763a6246f..9107149991 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,10 @@ -import { useState, useContext, useEffect } from 'react'; +import { + useState, + useContext, + useEffect, + useRef, + useCallback, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useMediaQuery } from 'react-responsive'; import * as Yup from 'yup'; @@ -308,6 +314,7 @@ export const getFileSizeToClosestByte = (fileSize) => { */ export const useRunOnNextRender = (callback) => { const [scheduled, setScheduled] = useState(false); + const argsRef = useRef(); useEffect(() => { if (!scheduled) { @@ -315,8 +322,13 @@ export const useRunOnNextRender = (callback) => { } setScheduled(false); - callback(); + callback(argsRef.current); }, [scheduled]); - return () => setScheduled(true); + const trigger = useCallback((...args) => { + argsRef.current = args; + setScheduled(true); + }, []); + + return trigger; };