Skip to content

Commit ce4c27e

Browse files
committed
feat: adds sharable URLs for library components/collections
* 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.
1 parent a6465cb commit ce4c27e

16 files changed

+283
-98
lines changed

src/library-authoring/LibraryAuthoringPage.tsx

+34-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import { Helmet } from 'react-helmet';
33
import classNames from 'classnames';
44
import { StudioFooter } from '@edx/frontend-component-footer';
@@ -16,12 +16,7 @@ import {
1616
Tabs,
1717
} from '@openedx/paragon';
1818
import { Add, ArrowBack, InfoOutline } from '@openedx/paragon/icons';
19-
import {
20-
Link,
21-
useLocation,
22-
useNavigate,
23-
useSearchParams,
24-
} from 'react-router-dom';
19+
import { Link } from 'react-router-dom';
2520

2621
import Loading from '../generic/Loading';
2722
import SubHeader from '../generic/sub-header/SubHeader';
@@ -36,11 +31,12 @@ import {
3631
SearchKeywordsField,
3732
SearchSortWidget,
3833
} from '../search-manager';
39-
import LibraryContent, { ContentType } from './LibraryContent';
34+
import LibraryContent from './LibraryContent';
4035
import { LibrarySidebar } from './library-sidebar';
4136
import { useComponentPickerContext } from './common/context/ComponentPickerContext';
4237
import { useLibraryContext } from './common/context/LibraryContext';
4338
import { SidebarBodyComponentId, useSidebarContext } from './common/context/SidebarContext';
39+
import { ContentType, useLibraryRoutes } from './routes';
4440

4541
import messages from './messages';
4642

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

5248
const {
5349
openAddContentSidebar,
54-
openInfoSidebar,
50+
openLibrarySidebar,
5551
closeLibrarySidebar,
5652
sidebarComponentInfo,
5753
} = useSidebarContext();
@@ -60,13 +56,19 @@ const HeaderActions = () => {
6056

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

63-
const handleOnClickInfoSidebar = () => {
59+
const { navigateTo } = useLibraryRoutes();
60+
const handleOnClickInfoSidebar = useCallback(() => {
6461
if (infoSidebarIsOpen) {
6562
closeLibrarySidebar();
6663
} else {
67-
openInfoSidebar();
64+
openLibrarySidebar();
6865
}
69-
};
66+
67+
if (!componentPickerMode) {
68+
// Reset URL to library home
69+
navigateTo();
70+
}
71+
}, [navigateTo, sidebarComponentInfo, closeLibrarySidebar, openLibrarySidebar]);
7072

7173
return (
7274
<div className="header-actions">
@@ -124,8 +126,6 @@ interface LibraryAuthoringPageProps {
124126

125127
const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPageProps) => {
126128
const intl = useIntl();
127-
const location = useLocation();
128-
const navigate = useNavigate();
129129

130130
const {
131131
isLoadingPage: isLoadingStudioHome,
@@ -139,29 +139,41 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
139139
libraryData,
140140
isLoadingLibraryData,
141141
showOnlyPublished,
142+
componentId,
143+
collectionId,
142144
} = useLibraryContext();
143145
const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext();
144146

147+
const { insideCollections, insideComponents, navigateTo } = useLibraryRoutes();
148+
149+
// The activeKey determines the currently selected tab.
145150
const [activeKey, setActiveKey] = useState<ContentType>(ContentType.home);
151+
const getActiveKey = () => {
152+
if (insideCollections) {
153+
return ContentType.collections;
154+
}
155+
if (insideComponents) {
156+
return ContentType.components;
157+
}
158+
return ContentType.home;
159+
};
146160

147161
useEffect(() => {
148-
const currentPath = location.pathname.split('/').pop();
162+
const contentType = getActiveKey();
149163

150-
if (componentPickerMode || currentPath === libraryId || currentPath === '') {
164+
if (componentPickerMode) {
151165
setActiveKey(ContentType.home);
152-
} else if (currentPath && currentPath in ContentType) {
153-
setActiveKey(ContentType[currentPath] || ContentType.home);
166+
} else {
167+
setActiveKey(contentType);
154168
}
155169
}, []);
156170

157171
useEffect(() => {
158172
if (!componentPickerMode) {
159-
openInfoSidebar();
173+
openInfoSidebar(componentId, collectionId);
160174
}
161175
}, []);
162176

163-
const [searchParams] = useSearchParams();
164-
165177
if (isLoadingLibraryData) {
166178
return <Loading />;
167179
}
@@ -181,10 +193,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
181193
const handleTabChange = (key: ContentType) => {
182194
setActiveKey(key);
183195
if (!componentPickerMode) {
184-
navigate({
185-
pathname: key,
186-
search: searchParams.toString(),
187-
});
196+
navigateTo({ contentType: key });
188197
}
189198
};
190199

src/library-authoring/LibraryContent.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@ import { useLibraryContext } from './common/context/LibraryContext';
66
import { useSidebarContext } from './common/context/SidebarContext';
77
import CollectionCard from './components/CollectionCard';
88
import ComponentCard from './components/ComponentCard';
9+
import { ContentType } from './routes';
910
import { useLoadOnScroll } from '../hooks';
1011
import messages from './collections/messages';
1112

12-
export enum ContentType {
13-
home = '',
14-
components = 'components',
15-
collections = 'collections',
16-
}
17-
1813
/**
1914
* Library Content to show content grid
2015
*

src/library-authoring/LibraryLayout.tsx

+38-22
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { useCallback } from 'react';
12
import {
23
Route,
34
Routes,
45
useParams,
5-
useMatch,
6+
useLocation,
67
} from 'react-router-dom';
78

9+
import { ROUTES } from './routes';
810
import LibraryAuthoringPage from './LibraryAuthoringPage';
911
import { LibraryProvider } from './common/context/LibraryContext';
1012
import { SidebarProvider } from './common/context/SidebarContext';
@@ -16,43 +18,57 @@ import { ComponentEditorModal } from './components/ComponentEditorModal';
1618
const LibraryLayout = () => {
1719
const { libraryId } = useParams();
1820

19-
const match = useMatch('/library/:libraryId/collection/:collectionId');
20-
21-
const collectionId = match?.params.collectionId;
22-
2321
if (libraryId === undefined) {
2422
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
2523
throw new Error('Error: route is missing libraryId.');
2624
}
2725

28-
return (
26+
const location = useLocation();
27+
const context = useCallback((childPage) => (
2928
<LibraryProvider
30-
/** We need to pass the collectionId as key to the LibraryProvider to force a re-render
31-
* when we navigate to a collection page. */
32-
key={collectionId}
29+
/** We need to pass the pathname as key to the LibraryProvider to force a
30+
* re-render when we navigate to a new path or page. */
31+
key={location.pathname}
3332
libraryId={libraryId}
34-
collectionId={collectionId}
3533
/** The component picker modal to use. We need to pass it as a reference instead of
3634
* directly importing it to avoid the import cycle:
3735
* ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage >
3836
* Sidebar > AddContentContainer > ComponentPicker */
3937
componentPicker={ComponentPicker}
4038
>
4139
<SidebarProvider>
42-
<Routes>
43-
<Route
44-
path="collection/:collectionId"
45-
element={<LibraryCollectionPage />}
46-
/>
47-
<Route
48-
path="*"
49-
element={<LibraryAuthoringPage />}
50-
/>
51-
</Routes>
52-
<CreateCollectionModal />
53-
<ComponentEditorModal />
40+
<>
41+
{childPage}
42+
<CreateCollectionModal />
43+
<ComponentEditorModal />
44+
</>
5445
</SidebarProvider>
5546
</LibraryProvider>
47+
), [location.pathname]);
48+
49+
return (
50+
<Routes>
51+
<Route
52+
path={ROUTES.COMPONENTS}
53+
element={context(<LibraryAuthoringPage />)}
54+
/>
55+
<Route
56+
path={ROUTES.COLLECTIONS}
57+
element={context(<LibraryAuthoringPage />)}
58+
/>
59+
<Route
60+
path={ROUTES.COMPONENT}
61+
element={context(<LibraryAuthoringPage />)}
62+
/>
63+
<Route
64+
path={ROUTES.COLLECTION}
65+
element={context(<LibraryCollectionPage />)}
66+
/>
67+
<Route
68+
path={ROUTES.HOME}
69+
element={context(<LibraryAuthoringPage />)}
70+
/>
71+
</Routes>
5672
);
5773
};
5874

src/library-authoring/add-content/AddContentContainer.test.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,13 @@ jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCs
2525

2626
const { libraryId } = mockContentLibrary;
2727
const render = (collectionId?: string) => {
28-
const params: { libraryId: string, collectionId?: string } = { libraryId };
29-
if (collectionId) {
30-
params.collectionId = collectionId;
31-
}
28+
const params: { libraryId: string, collectionId?: string } = { libraryId, collectionId };
3229
return baseRender(<AddContentContainer />, {
33-
path: '/library/:libraryId/*',
30+
path: '/library/:libraryId/:collectionId?',
3431
params,
3532
extraWrapper: ({ children }) => (
3633
<LibraryProvider
3734
libraryId={libraryId}
38-
collectionId={collectionId}
3935
>
4036
{ children }
4137
<ComponentEditorModal />

src/library-authoring/add-content/PickLibraryContentModal.test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ const render = () => baseRender(<PickLibraryContentModal isOpen onClose={onClose
3434
extraWrapper: ({ children }) => (
3535
<LibraryProvider
3636
libraryId={libraryId}
37-
collectionId="collectionId"
3837
componentPicker={ComponentPicker}
3938
>
4039
{children}

src/library-authoring/collections/CollectionInfo.tsx

+12-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
Tabs,
77
} from '@openedx/paragon';
88
import { useCallback } from 'react';
9-
import { useNavigate, useMatch } from 'react-router-dom';
109

1110
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
1211
import { useLibraryContext } from '../common/context/LibraryContext';
@@ -17,43 +16,41 @@ import {
1716
isCollectionInfoTab,
1817
useSidebarContext,
1918
} from '../common/context/SidebarContext';
19+
import { useLibraryRoutes } from '../routes';
2020
import { ContentTagsDrawer } from '../../content-tags-drawer';
2121
import { buildCollectionUsageKey } from '../../generic/key-utils';
2222
import CollectionDetails from './CollectionDetails';
2323
import messages from './messages';
2424

2525
const CollectionInfo = () => {
2626
const intl = useIntl();
27-
const navigate = useNavigate();
2827

2928
const { componentPickerMode } = useComponentPickerContext();
30-
const { libraryId, collectionId, setCollectionId } = useLibraryContext();
29+
const { libraryId, setCollectionId } = useLibraryContext();
3130
const { sidebarComponentInfo, setSidebarCurrentTab } = useSidebarContext();
3231

3332
const tab: CollectionInfoTab = (
3433
sidebarComponentInfo?.currentTab && isCollectionInfoTab(sidebarComponentInfo.currentTab)
3534
) ? sidebarComponentInfo?.currentTab : COLLECTION_INFO_TABS.Manage;
3635

37-
const sidebarCollectionId = sidebarComponentInfo?.id;
36+
const collectionId = sidebarComponentInfo?.id;
3837
// istanbul ignore if: this should never happen
39-
if (!sidebarCollectionId) {
40-
throw new Error('sidebarCollectionId is required');
38+
if (!collectionId) {
39+
throw new Error('collectionId is required');
4140
}
4241

43-
const url = `/library/${libraryId}/collection/${sidebarCollectionId}`;
44-
const urlMatch = useMatch(url);
42+
const collectionUsageKey = buildCollectionUsageKey(libraryId, collectionId);
4543

46-
const showOpenCollectionButton = !urlMatch && collectionId !== sidebarCollectionId;
47-
48-
const collectionUsageKey = buildCollectionUsageKey(libraryId, sidebarCollectionId);
44+
const { insideCollection, navigateTo } = useLibraryRoutes();
45+
const showOpenCollectionButton = !insideCollection || componentPickerMode;
4946

5047
const handleOpenCollection = useCallback(() => {
51-
if (!componentPickerMode) {
52-
navigate(url);
48+
if (componentPickerMode) {
49+
setCollectionId(collectionId);
5350
} else {
54-
setCollectionId(sidebarCollectionId);
51+
navigateTo({ collectionId });
5552
}
56-
}, [componentPickerMode, url]);
53+
}, [componentPickerMode, navigateTo]);
5754

5855
return (
5956
<Stack>

src/library-authoring/collections/LibraryCollectionComponents.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { NoComponents, NoSearchResults } from '../EmptyStates';
33
import { useSearchContext } from '../../search-manager';
44
import messages from './messages';
55
import { useSidebarContext } from '../common/context/SidebarContext';
6-
import LibraryContent, { ContentType } from '../LibraryContent';
6+
import LibraryContent from '../LibraryContent';
7+
import { ContentType } from '../routes';
78

89
const LibraryCollectionComponents = () => {
910
const { totalHits: componentCount, isFiltered } = useSearchContext();

src/library-authoring/collections/LibraryCollectionPage.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import classNames from 'classnames';
1313
import { Helmet } from 'react-helmet';
1414
import { Link } from 'react-router-dom';
1515

16+
import { useLibraryRoutes } from '../routes';
1617
import Loading from '../../generic/Loading';
1718
import ErrorAlert from '../../generic/alert-error';
1819
import SubHeader from '../../generic/sub-header/SubHeader';
@@ -46,6 +47,7 @@ const HeaderActions = () => {
4647
openCollectionInfoSidebar,
4748
sidebarComponentInfo,
4849
} = useSidebarContext();
50+
const { navigateTo } = useLibraryRoutes();
4951

5052
// istanbul ignore if: this should never happen
5153
if (!collectionId) {
@@ -61,6 +63,10 @@ const HeaderActions = () => {
6163
} else {
6264
openCollectionInfoSidebar(collectionId);
6365
}
66+
67+
if (!componentPickerMode) {
68+
navigateTo({ collectionId });
69+
}
6470
};
6571

6672
return (
@@ -102,8 +108,8 @@ const LibraryCollectionPage = () => {
102108
}
103109

104110
const { componentPickerMode } = useComponentPickerContext();
105-
const { showOnlyPublished, setCollectionId } = useLibraryContext();
106-
const { sidebarComponentInfo, openCollectionInfoSidebar } = useSidebarContext();
111+
const { showOnlyPublished, setCollectionId, componentId } = useLibraryContext();
112+
const { sidebarComponentInfo, openInfoSidebar } = useSidebarContext();
107113

108114
const {
109115
data: collectionData,
@@ -113,8 +119,8 @@ const LibraryCollectionPage = () => {
113119
} = useCollection(libraryId, collectionId);
114120

115121
useEffect(() => {
116-
openCollectionInfoSidebar(collectionId);
117-
}, [collectionData]);
122+
openInfoSidebar(componentId, collectionId);
123+
}, []);
118124

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

0 commit comments

Comments
 (0)