Skip to content

Commit

Permalink
feat: adds sharable URLs for library components/collections
Browse files Browse the repository at this point in the history
* Restructures LibraryLayout so that LibraryContext can useParams() to
  initialize its componentId/collectionId instead of having to parse
  complicated route strings.

  Initialization-from-URL can be disabled for the content pickers by
  passing skipUrlUpdate to the LibraryContext -- which is needed by the
  component picker.

* Clicking/selecting a ComponentCard/CollectionCard navigates to an
  appropriate component/collection route given the current page.

* Adds useLibraryRoutes() hook so components can easily navigate to the
  best available route without having to know the route strings or
  maintain search params.

* Moves ContentType declaration to the new routes.ts to avoid circular
  imports.

* Renames openInfoSidebar to openLibrarySidebar, so that openInfoSidebar
  can be used to open the best sidebar for a given
  library/component/collection.
  • Loading branch information
pomegranited committed Dec 19, 2024
1 parent a6465cb commit 18b4e2d
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 95 deletions.
51 changes: 29 additions & 22 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,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';
Expand All @@ -36,11 +31,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';

Expand All @@ -51,7 +47,7 @@ const HeaderActions = () => {

const {
openAddContentSidebar,
openInfoSidebar,
openLibrarySidebar,
closeLibrarySidebar,
sidebarComponentInfo,
} = useSidebarContext();
Expand All @@ -60,11 +56,15 @@ const HeaderActions = () => {

const infoSidebarIsOpen = sidebarComponentInfo?.type === SidebarBodyComponentId.Info;

const { navigateTo } = useLibraryRoutes();
const handleOnClickInfoSidebar = () => {
// Reset URL to library home
navigateTo();

if (infoSidebarIsOpen) {
closeLibrarySidebar();
} else {
openInfoSidebar();
openLibrarySidebar();
}
};

Expand Down Expand Up @@ -124,8 +124,6 @@ interface LibraryAuthoringPageProps {

const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPageProps) => {
const intl = useIntl();
const location = useLocation();
const navigate = useNavigate();

const {
isLoadingPage: isLoadingStudioHome,
Expand All @@ -139,29 +137,41 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
libraryData,
isLoadingLibraryData,
showOnlyPublished,
componentId,
collectionId,
} = useLibraryContext();
const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext();

const { insideCollections, insideComponents, navigateTo } = useLibraryRoutes();

// The activeKey determines the currently selected tab.
const [activeKey, setActiveKey] = useState<ContentType>(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] || ContentType.home);
} else {
setActiveKey(contentType);
}
}, []);

useEffect(() => {
if (!componentPickerMode) {
openInfoSidebar();
openInfoSidebar(componentId, collectionId);
}
}, []);

const [searchParams] = useSearchParams();

if (isLoadingLibraryData) {
return <Loading />;
}
Expand All @@ -181,10 +191,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
const handleTabChange = (key: ContentType) => {
setActiveKey(key);
if (!componentPickerMode) {
navigate({
pathname: key,
search: searchParams.toString(),
});
navigateTo({ contentType: key });
}
};

Expand Down
7 changes: 1 addition & 6 deletions src/library-authoring/LibraryContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
60 changes: 38 additions & 22 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,43 +18,57 @@ 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) => (
<LibraryProvider
/** We need to pass the collectionId as key to the LibraryProvider to force a re-render
* when we navigate to a collection page. */
key={collectionId}
/** We need to pass the pathname as key to the LibraryProvider to force a
* re-render when we navigate to a new path or page. */
key={location.pathname}
libraryId={libraryId}
collectionId={collectionId}
/** 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 >
* Sidebar > AddContentContainer > ComponentPicker */
componentPicker={ComponentPicker}
>
<SidebarProvider>
<Routes>
<Route
path="collection/:collectionId"
element={<LibraryCollectionPage />}
/>
<Route
path="*"
element={<LibraryAuthoringPage />}
/>
</Routes>
<CreateCollectionModal />
<ComponentEditorModal />
<>
{childPage}
<CreateCollectionModal />
<ComponentEditorModal />
</>
</SidebarProvider>
</LibraryProvider>
), [location.pathname]);

return (
<Routes>
<Route
path={ROUTES.COMPONENTS}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COLLECTIONS}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COMPONENT}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COLLECTION}
element={context(<LibraryCollectionPage />)}
/>
<Route
path={ROUTES.HOME}
element={context(<LibraryAuthoringPage />)}
/>
</Routes>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<AddContentContainer />, {
path: '/library/:libraryId/*',
path: '/library/:libraryId/:collectionId?',
params,
extraWrapper: ({ children }) => (
<LibraryProvider
libraryId={libraryId}
collectionId={collectionId}
>
{ children }
<ComponentEditorModal />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const render = () => baseRender(<PickLibraryContentModal isOpen onClose={onClose
extraWrapper: ({ children }) => (
<LibraryProvider
libraryId={libraryId}
collectionId="collectionId"
componentPicker={ComponentPicker}
>
{children}
Expand Down
27 changes: 12 additions & 15 deletions src/library-authoring/collections/CollectionInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,43 +16,41 @@ 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';
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 (
<Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
14 changes: 10 additions & 4 deletions src/library-authoring/collections/LibraryCollectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
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';
Expand Down Expand Up @@ -46,6 +47,7 @@ const HeaderActions = () => {
openCollectionInfoSidebar,
sidebarComponentInfo,
} = useSidebarContext();
const { navigateTo } = useLibraryRoutes();

// istanbul ignore if: this should never happen
if (!collectionId) {
Expand All @@ -56,6 +58,10 @@ const HeaderActions = () => {
&& sidebarComponentInfo?.id === collectionId;

const handleOnClickInfoSidebar = () => {
if (!componentPickerMode) {
navigateTo({ collectionId });
}

if (infoSidebarIsOpen) {
closeLibrarySidebar();
} else {
Expand Down Expand Up @@ -102,8 +108,8 @@ const LibraryCollectionPage = () => {
}

const { componentPickerMode } = useComponentPickerContext();
const { showOnlyPublished, setCollectionId } = useLibraryContext();
const { sidebarComponentInfo, openCollectionInfoSidebar } = useSidebarContext();
const { showOnlyPublished, setCollectionId, componentId } = useLibraryContext();
const { sidebarComponentInfo, openInfoSidebar } = useSidebarContext();

const {
data: collectionData,
Expand All @@ -113,8 +119,8 @@ const LibraryCollectionPage = () => {
} = useCollection(libraryId, collectionId);

useEffect(() => {
openCollectionInfoSidebar(collectionId);
}, [collectionData]);
openInfoSidebar(componentId, collectionId);
}, []);

const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId);

Expand Down
Loading

0 comments on commit 18b4e2d

Please sign in to comment.