diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 01fc146b6..f80bd1dcd 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -15,12 +15,7 @@ import { Tabs, } from '@openedx/paragon'; import { Add, ArrowBack, InfoOutline } from '@openedx/paragon/icons'; -import { - Link, - useLocation, - useNavigate, - useSearchParams, -} from 'react-router-dom'; +import { Link } from 'react-router-dom'; import Loading from '../generic/Loading'; import SubHeader from '../generic/sub-header/SubHeader'; @@ -35,11 +30,12 @@ import { SearchKeywordsField, SearchSortWidget, } from '../search-manager'; -import LibraryContent, { ContentType } from './LibraryContent'; +import LibraryContent from './LibraryContent'; import { LibrarySidebar } from './library-sidebar'; import { useComponentPickerContext } from './common/context/ComponentPickerContext'; import { useLibraryContext } from './common/context/LibraryContext'; import { SidebarBodyComponentId, useSidebarContext } from './common/context/SidebarContext'; +import { ContentType, useLibraryRoutes } from './routes'; import messages from './messages'; @@ -50,7 +46,7 @@ const HeaderActions = () => { const { openAddContentSidebar, - openInfoSidebar, + openLibrarySidebar, closeLibrarySidebar, sidebarComponentInfo, } = useSidebarContext(); @@ -61,11 +57,15 @@ const HeaderActions = () => { sidebarComponentInfo?.type === SidebarBodyComponentId.Info ); + const { navigateTo } = useLibraryRoutes(); const handleOnClickInfoSidebar = () => { + // Reset URL to library home + navigateTo(); + if (infoSidebarIsOpen()) { closeLibrarySidebar(); } else { - openInfoSidebar(); + openLibrarySidebar(); } }; @@ -125,8 +125,6 @@ interface LibraryAuthoringPageProps { const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPageProps) => { const intl = useIntl(); - const location = useLocation(); - const navigate = useNavigate(); const { isLoadingPage: isLoadingStudioHome, @@ -140,29 +138,41 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage libraryData, isLoadingLibraryData, showOnlyPublished, + componentId, + collectionId, } = useLibraryContext(); const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext(); - const [activeKey, setActiveKey] = useState(ContentType.home); + const { insideCollections, insideComponents, navigateTo } = useLibraryRoutes(); + + // The activeKey determines the currently selected tab. + const [activeKey, setActiveKey] = useState(ContentType.home); + const getActiveKey = () => { + if (insideCollections) { + return ContentType.collections; + } + if (insideComponents) { + return ContentType.components; + } + return ContentType.home; + }; useEffect(() => { - const currentPath = location.pathname.split('/').pop(); + const contentType = getActiveKey(); - if (componentPickerMode || currentPath === libraryId || currentPath === '') { + if (componentPickerMode) { setActiveKey(ContentType.home); - } else if (currentPath && currentPath in ContentType) { - setActiveKey(ContentType[currentPath]); + } else { + setActiveKey(contentType); } }, []); useEffect(() => { if (!componentPickerMode) { - openInfoSidebar(); + openInfoSidebar(componentId, collectionId); } }, []); - const [searchParams] = useSearchParams(); - if (isLoadingLibraryData) { return ; } @@ -175,11 +185,6 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage ); } - // istanbul ignore if: this should never happen - if (activeKey === undefined) { - return ; - } - if (!libraryData) { return ; } @@ -187,10 +192,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage const handleTabChange = (key: ContentType) => { setActiveKey(key); if (!componentPickerMode) { - navigate({ - pathname: key, - search: searchParams.toString(), - }); + navigateTo({ contentType: key }); } }; diff --git a/src/library-authoring/LibraryContent.tsx b/src/library-authoring/LibraryContent.tsx index 5eb050520..1913994b2 100644 --- a/src/library-authoring/LibraryContent.tsx +++ b/src/library-authoring/LibraryContent.tsx @@ -6,15 +6,10 @@ import { useLibraryContext } from './common/context/LibraryContext'; import { useSidebarContext } from './common/context/SidebarContext'; import CollectionCard from './components/CollectionCard'; import ComponentCard from './components/ComponentCard'; +import { ContentType } from './routes'; import { useLoadOnScroll } from '../hooks'; import messages from './collections/messages'; -export enum ContentType { - home = '', - components = 'components', - collections = 'collections', -} - /** * Library Content to show content grid * diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index 610a28eac..c093af7ad 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -1,10 +1,12 @@ +import { useCallback } from 'react'; import { Route, Routes, useParams, - useMatch, + useLocation, } from 'react-router-dom'; +import { ROUTES } from './routes'; import LibraryAuthoringPage from './LibraryAuthoringPage'; import { LibraryProvider } from './common/context/LibraryContext'; import { SidebarProvider } from './common/context/SidebarContext'; @@ -16,22 +18,18 @@ import { ComponentEditorModal } from './components/ComponentEditorModal'; const LibraryLayout = () => { const { libraryId } = useParams(); - const match = useMatch('/library/:libraryId/collection/:collectionId'); - - const collectionId = match?.params.collectionId; - if (libraryId === undefined) { // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. throw new Error('Error: route is missing libraryId.'); } - return ( + const location = useLocation(); + const context = useCallback((childPage) => ( LibraryAuthoringPage/LibraryCollectionPage > @@ -39,20 +37,38 @@ const LibraryLayout = () => { componentPicker={ComponentPicker} > - - } - /> - } - /> - - - + <> + {childPage} + + + + ), [location.pathname]); + + return ( + + )} + /> + )} + /> + )} + /> + )} + /> + )} + /> + ); }; diff --git a/src/library-authoring/add-content/AddContentContainer.test.tsx b/src/library-authoring/add-content/AddContentContainer.test.tsx index 2f233629c..229948c39 100644 --- a/src/library-authoring/add-content/AddContentContainer.test.tsx +++ b/src/library-authoring/add-content/AddContentContainer.test.tsx @@ -25,17 +25,13 @@ jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCs const { libraryId } = mockContentLibrary; const render = (collectionId?: string) => { - const params: { libraryId: string, collectionId?: string } = { libraryId }; - if (collectionId) { - params.collectionId = collectionId; - } + const params: { libraryId: string, collectionId?: string } = { libraryId, collectionId }; return baseRender(, { - path: '/library/:libraryId/*', + path: '/library/:libraryId/:collectionId?', params, extraWrapper: ({ children }) => ( { children } diff --git a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx index f5d3606c5..80efc8cb3 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx @@ -34,7 +34,6 @@ const render = () => baseRender( ( {children} diff --git a/src/library-authoring/collections/CollectionInfo.tsx b/src/library-authoring/collections/CollectionInfo.tsx index 4c370f26e..10b254135 100644 --- a/src/library-authoring/collections/CollectionInfo.tsx +++ b/src/library-authoring/collections/CollectionInfo.tsx @@ -6,7 +6,6 @@ import { Tabs, } from '@openedx/paragon'; import { useCallback } from 'react'; -import { useNavigate, useMatch } from 'react-router-dom'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; @@ -17,6 +16,7 @@ import { isCollectionInfoTab, useSidebarContext, } from '../common/context/SidebarContext'; +import { useLibraryRoutes } from '../routes'; import { ContentTagsDrawer } from '../../content-tags-drawer'; import { buildCollectionUsageKey } from '../../generic/key-utils'; import CollectionDetails from './CollectionDetails'; @@ -24,36 +24,33 @@ import messages from './messages'; const CollectionInfo = () => { const intl = useIntl(); - const navigate = useNavigate(); const { componentPickerMode } = useComponentPickerContext(); - const { libraryId, collectionId, setCollectionId } = useLibraryContext(); + const { libraryId, setCollectionId } = useLibraryContext(); const { sidebarComponentInfo, setSidebarCurrentTab } = useSidebarContext(); const tab: CollectionInfoTab = ( sidebarComponentInfo?.currentTab && isCollectionInfoTab(sidebarComponentInfo.currentTab) ) ? sidebarComponentInfo?.currentTab : COLLECTION_INFO_TABS.Manage; - const sidebarCollectionId = sidebarComponentInfo?.id; + const collectionId = sidebarComponentInfo?.id; // istanbul ignore if: this should never happen - if (!sidebarCollectionId) { - throw new Error('sidebarCollectionId is required'); + if (!collectionId) { + throw new Error('collectionId is required'); } - const url = `/library/${libraryId}/collection/${sidebarCollectionId}`; - const urlMatch = useMatch(url); + const collectionUsageKey = buildCollectionUsageKey(libraryId, collectionId); - const showOpenCollectionButton = !urlMatch && collectionId !== sidebarCollectionId; - - const collectionUsageKey = buildCollectionUsageKey(libraryId, sidebarCollectionId); + const { insideCollection, navigateTo } = useLibraryRoutes(); + const showOpenCollectionButton = !insideCollection || componentPickerMode; const handleOpenCollection = useCallback(() => { - if (!componentPickerMode) { - navigate(url); + if (componentPickerMode) { + setCollectionId(collectionId); } else { - setCollectionId(sidebarCollectionId); + navigateTo({ collectionId }); } - }, [componentPickerMode, url]); + }, [componentPickerMode, navigateTo]); return ( diff --git a/src/library-authoring/collections/LibraryCollectionComponents.tsx b/src/library-authoring/collections/LibraryCollectionComponents.tsx index e0338dd11..6fcd79aa5 100644 --- a/src/library-authoring/collections/LibraryCollectionComponents.tsx +++ b/src/library-authoring/collections/LibraryCollectionComponents.tsx @@ -3,7 +3,8 @@ import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useSearchContext } from '../../search-manager'; import messages from './messages'; import { useSidebarContext } from '../common/context/SidebarContext'; -import LibraryContent, { ContentType } from '../LibraryContent'; +import LibraryContent from '../LibraryContent'; +import { ContentType } from '../routes'; const LibraryCollectionComponents = () => { const { totalHits: componentCount, isFiltered } = useSearchContext(); diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index e59d0decb..ac5277e1c 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -13,6 +13,7 @@ import { import { Add, ArrowBack, InfoOutline } from '@openedx/paragon/icons'; import { Link } from 'react-router-dom'; +import { useLibraryRoutes } from '../routes'; import Loading from '../../generic/Loading'; import ErrorAlert from '../../generic/alert-error'; import SubHeader from '../../generic/sub-header/SubHeader'; @@ -105,8 +106,8 @@ const LibraryCollectionPage = () => { } const { componentPickerMode } = useComponentPickerContext(); - const { showOnlyPublished, setCollectionId } = useLibraryContext(); - const { sidebarComponentInfo, openCollectionInfoSidebar } = useSidebarContext(); + const { showOnlyPublished, setCollectionId, componentId } = useLibraryContext(); + const { sidebarComponentInfo, openCollectionInfoSidebar, openInfoSidebar } = useSidebarContext(); const { data: collectionData, @@ -115,9 +116,15 @@ const LibraryCollectionPage = () => { error, } = useCollection(libraryId, collectionId); - useEffect(() => { + const { navigateTo } = useLibraryRoutes(); + const openCollection = useCallback(() => { openCollectionInfoSidebar(collectionId); - }, [collectionData]); + navigateTo({ collectionId }); + }, [navigateTo, openCollectionInfoSidebar]); + + useEffect(() => { + openInfoSidebar(componentId, collectionId); + }, []); const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); @@ -198,7 +205,7 @@ const LibraryCollectionPage = () => { title={( openCollectionInfoSidebar(collectionId)} + infoClickHandler={openCollection} /> )} breadcrumbs={breadcumbs} diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 9612a9285..5fc23796d 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -6,6 +6,7 @@ import { useMemo, useState, } from 'react'; +import { useParams } from 'react-router-dom'; import type { ComponentPicker } from '../../component-picker'; import type { ContentLibrary } from '../../data/api'; @@ -25,6 +26,8 @@ export type LibraryContextData = { isLoadingLibraryData: boolean; collectionId: string | undefined; setCollectionId: (collectionId?: string) => void; + componentId: string | undefined; + setComponentId: (componentId?: string) => void; // Only show published components showOnlyPublished: boolean; // "Create New Collection" modal @@ -53,9 +56,10 @@ const LibraryContext = createContext(undefined); type LibraryProviderProps = { children?: React.ReactNode; libraryId: string; - /** The initial collection ID to show */ - collectionId?: string; showOnlyPublished?: boolean; + // If set, will initialize the current collection and/or component from the current URL + initializeFromUrl?: boolean; + /** The component picker modal to use. We need to pass it as a reference instead of * directly importing it to avoid the import cycle: * ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > @@ -69,11 +73,10 @@ type LibraryProviderProps = { export const LibraryProvider = ({ children, libraryId, - collectionId: collectionIdProp, showOnlyPublished = false, + initializeFromUrl = true, componentPicker, }: LibraryProviderProps) => { - const [collectionId, setCollectionId] = useState(collectionIdProp); const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false); const [componentBeingEdited, setComponentBeingEdited] = useState(); const closeComponentEditor = useCallback(() => { @@ -94,12 +97,23 @@ export const LibraryProvider = ({ const readOnly = !!componentPickerMode || !libraryData?.canEditLibrary; + // Parse the initial collectionId and/or componentId from the current URL params + const params = useParams(); + const [componentId, setComponentId] = useState( + initializeFromUrl ? params.componentId : undefined, + ); + const [collectionId, setCollectionId] = useState( + initializeFromUrl ? params.collectionId : undefined, + ); + const context = useMemo(() => { const contextValue = { libraryId, libraryData, collectionId, setCollectionId, + componentId, + setComponentId, readOnly, isLoadingLibraryData, showOnlyPublished, @@ -115,9 +129,11 @@ export const LibraryProvider = ({ return contextValue; }, [ libraryId, + libraryData, collectionId, setCollectionId, - libraryData, + componentId, + setComponentId, readOnly, isLoadingLibraryData, showOnlyPublished, diff --git a/src/library-authoring/common/context/SidebarContext.tsx b/src/library-authoring/common/context/SidebarContext.tsx index d9ba68d69..3f5408830 100644 --- a/src/library-authoring/common/context/SidebarContext.tsx +++ b/src/library-authoring/common/context/SidebarContext.tsx @@ -48,7 +48,8 @@ export enum SidebarAdditionalActions { export type SidebarContextData = { closeLibrarySidebar: () => void; openAddContentSidebar: () => void; - openInfoSidebar: () => void; + openInfoSidebar: (componentId?: string, collectionId?: string) => void; + openLibrarySidebar: () => void; openCollectionInfoSidebar: (collectionId: string, additionalAction?: SidebarAdditionalActions) => void; openComponentInfoSidebar: (usageKey: string, additionalAction?: SidebarAdditionalActions) => void; sidebarComponentInfo?: SidebarComponentInfo; @@ -71,7 +72,7 @@ type SidebarProviderProps = { }; /** - * React component to provide `LibraryContext` + * React component to provide `SidebarContext` */ export const SidebarProvider = ({ children, @@ -81,7 +82,7 @@ export const SidebarProvider = ({ initialSidebarComponentInfo, ); - /** Helper function to consume addtional action once performed. + /** Helper function to consume additional action once performed. Required to redo the action. */ const resetSidebarAdditionalActions = useCallback(() => { @@ -94,7 +95,7 @@ export const SidebarProvider = ({ const openAddContentSidebar = useCallback(() => { setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.AddContent }); }, []); - const openInfoSidebar = useCallback(() => { + const openLibrarySidebar = useCallback(() => { setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.Info }); }, []); @@ -119,6 +120,16 @@ export const SidebarProvider = ({ })); }, []); + const openInfoSidebar = useCallback((componentId?: string, collectionId?: string) => { + if (componentId) { + openComponentInfoSidebar(componentId); + } else if (collectionId) { + openCollectionInfoSidebar(collectionId); + } else { + openLibrarySidebar(); + } + }, []); + const setSidebarCurrentTab = useCallback((tab: CollectionInfoTab | ComponentInfoTab) => { setSidebarComponentInfo((prev) => (prev && { ...prev, currentTab: tab })); }, []); @@ -128,6 +139,7 @@ export const SidebarProvider = ({ closeLibrarySidebar, openAddContentSidebar, openInfoSidebar, + openLibrarySidebar, openComponentInfoSidebar, sidebarComponentInfo, openCollectionInfoSidebar, @@ -140,6 +152,7 @@ export const SidebarProvider = ({ closeLibrarySidebar, openAddContentSidebar, openInfoSidebar, + openLibrarySidebar, openComponentInfoSidebar, sidebarComponentInfo, openCollectionInfoSidebar, @@ -162,6 +175,7 @@ export function useSidebarContext(): SidebarContextData { closeLibrarySidebar: () => {}, openAddContentSidebar: () => {}, openInfoSidebar: () => {}, + openLibrarySidebar: () => {}, openComponentInfoSidebar: () => {}, openCollectionInfoSidebar: () => {}, resetSidebarAdditionalActions: () => {}, diff --git a/src/library-authoring/component-picker/ComponentPicker.tsx b/src/library-authoring/component-picker/ComponentPicker.tsx index 115c081fe..67d47d78b 100644 --- a/src/library-authoring/component-picker/ComponentPicker.tsx +++ b/src/library-authoring/component-picker/ComponentPicker.tsx @@ -105,6 +105,7 @@ export const ComponentPicker: React.FC = ({ { calcShowOnlyPublished diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx index 3b5aa748c..00ccc0016 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -16,7 +16,7 @@ type BaseComponentCardProps = { numChildren?: number, tags: ContentHitTags, actions: React.ReactNode, - openInfoSidebar: () => void + onSelect: () => void }; const BaseComponentCard = ({ @@ -26,7 +26,7 @@ const BaseComponentCard = ({ numChildren, tags, actions, - openInfoSidebar, + onSelect, } : BaseComponentCardProps) => { const tagCount = useMemo(() => { if (!tags) { @@ -42,10 +42,10 @@ const BaseComponentCard = ({ { if (['Enter', ' '].includes(e.key)) { - openInfoSidebar(); + onSelect(); } }} > diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index 6935bc12c..aa5f9d07e 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -14,6 +14,7 @@ import { type CollectionHit } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; import { useSidebarContext } from '../common/context/SidebarContext'; +import { useLibraryRoutes } from '../routes'; import BaseComponentCard from './BaseComponentCard'; import { ToastContext } from '../../generic/toast-context'; import { useDeleteCollection, useRestoreCollection } from '../data/apiHooks'; @@ -112,6 +113,7 @@ const CollectionCard = ({ collectionHit } : CollectionCardProps) => { const { type: componentType, + blockId: collectionId, formatted, tags, numChildren, @@ -124,6 +126,14 @@ const CollectionCard = ({ collectionHit } : CollectionCardProps) => { const { displayName = '', description = '' } = formatted; + const { navigateTo } = useLibraryRoutes(); + const openCollection = useCallback(() => { + if (!componentPickerMode) { + navigateTo({ collectionId }); + } + openCollectionInfoSidebar(collectionId); + }, [collectionId, navigateTo, openCollectionInfoSidebar]); + return ( { )} - openInfoSidebar={() => openCollectionInfoSidebar(collectionHit.blockId)} + onSelect={openCollection} /> ); }; diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 813255b97..9e8f594a2 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react'; +import { useCallback, useContext, useState } from 'react'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, @@ -23,6 +23,8 @@ import { useComponentPickerContext } from '../common/context/ComponentPickerCont import { useLibraryContext } from '../common/context/LibraryContext'; import { SidebarAdditionalActions, useSidebarContext } from '../common/context/SidebarContext'; import { useRemoveComponentsFromCollection } from '../data/apiHooks'; +import { useLibraryRoutes } from '../routes'; + import BaseComponentCard from './BaseComponentCard'; import { canEditComponent } from './ComponentEditorModal'; import messages from './messages'; @@ -200,6 +202,14 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { showOnlyPublished ? formatted.published?.displayName : formatted.displayName ) ?? ''; + const { navigateTo } = useLibraryRoutes(); + const openComponent = useCallback(() => { + if (!componentPickerMode) { + navigateTo({ componentId: usageKey }); + } + openComponentInfoSidebar(usageKey); + }, [usageKey, navigateTo, openComponentInfoSidebar]); + return ( { )} )} - openInfoSidebar={() => openComponentInfoSidebar(usageKey)} + onSelect={openComponent} /> ); }; diff --git a/src/library-authoring/library-info/LibraryInfoHeader.tsx b/src/library-authoring/library-info/LibraryInfoHeader.tsx index 95d2f5fd2..cafeccf26 100644 --- a/src/library-authoring/library-info/LibraryInfoHeader.tsx +++ b/src/library-authoring/library-info/LibraryInfoHeader.tsx @@ -43,7 +43,7 @@ const LibraryInfoHeader = () => { setIsActive(true); }; - const hanldeOnKeyDown = (event) => { + const handleOnKeyDown = (event) => { if (event.key === 'Enter') { handleSaveTitle(event); } else if (event.key === 'Escape') { @@ -63,7 +63,7 @@ const LibraryInfoHeader = () => { aria-label="Title input" defaultValue={library.title} onBlur={handleSaveTitle} - onKeyDown={hanldeOnKeyDown} + onKeyDown={handleOnKeyDown} /> ) : ( diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts new file mode 100644 index 000000000..59d081179 --- /dev/null +++ b/src/library-authoring/routes.ts @@ -0,0 +1,113 @@ +/** + * Constants and utility hook for the Library Authoring routes. + */ +import { useCallback } from 'react'; +import { + generatePath, + matchPath, + useParams, + useLocation, + useNavigate, + useSearchParams, + type PathMatch, +} from 'react-router-dom'; + +export const BASE_ROUTE = '/library/:libraryId'; + +export const ROUTES = { + COMPONENTS: '/components/:componentId?', + COLLECTIONS: '/collections/:collectionId?', + COMPONENT: '/component/:componentId', + COLLECTION: '/collection/:collectionId/:componentId?', + HOME: '/:collectionId?', +}; + +export enum ContentType { + home = '', + components = 'components', + collections = 'collections', +} + +export type NavigateToData = { + componentId?: string, + collectionId?: string, + contentType?: ContentType, +}; + +export type LibraryRoutesData = { + insideCollection: PathMatch | null; + insideCollections: PathMatch | null; + insideComponents: PathMatch | null; + + // Navigate using the best route from the current location for the given parameters. + navigateTo: (dict?: NavigateToData) => void; +}; + +export const useLibraryRoutes = (): LibraryRoutesData => { + const { pathname } = useLocation(); + const params = useParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const insideCollection = matchPath(BASE_ROUTE + ROUTES.COLLECTION, pathname); + const insideCollections = matchPath(BASE_ROUTE + ROUTES.COLLECTIONS, pathname); + const insideComponents = matchPath(BASE_ROUTE + ROUTES.COMPONENTS, pathname); + + const navigateTo = useCallback(({ + componentId, + collectionId, + contentType, + }: NavigateToData = {}) => { + const routeParams = { + ...params, + componentId, + // Overwrite the current collectionId param only if one is specified + ...(collectionId && { collectionId }), + }; + let route; + + // contentType overrides the current route + if (contentType === ContentType.components) { + route = ROUTES.COMPONENTS; + } else if (contentType === ContentType.collections) { + route = ROUTES.COLLECTIONS; + } else if (contentType === ContentType.home) { + route = ROUTES.HOME; + } else if (insideCollections) { + route = ( + (collectionId && collectionId === params.collectionId) + // Open the previously-selected collection + ? ROUTES.COLLECTION + // Otherwise just preview the collection, if specified + : ROUTES.COLLECTIONS + ); + } else if (insideCollection) { + route = ROUTES.COLLECTION; + } else if (insideComponents) { + route = ROUTES.COMPONENTS; + } else if (componentId) { + route = ROUTES.COMPONENT; + } else { + route = ( + (collectionId && collectionId === params.collectionId) + // Open the previously-selected collection + ? ROUTES.COLLECTION + // Otherwise just preview the collection, if specified + : ROUTES.HOME + ); + } + + const newPath = generatePath(BASE_ROUTE + route, routeParams); + navigate({ + pathname: newPath, + search: searchParams.toString(), + }); + }, [navigate, params, searchParams, pathname]); + + return { + navigateTo, + insideCollection, + insideCollections, + insideComponents, + }; +};