Skip to content

Commit 5fad981

Browse files
committed
feat: adds sharable URLs for library components/collections
* Restructure LibraryLayout so that LibraryContext can (optionally) useParams() to initialize its componentId/collectionId instead of having to parse route strings. This behavior can be disabled for the content pickers by passing initializeFromUrl={false} to the LibraryContext. * Add useLibraryRoutes() hook so components can easily navigate to the best available route without having to know the route strings or maintain search params. Also moved ContentType declaration to the new routes.ts to avoid circular imports. * Clicking/selecting a ComponentCard/CollectionCard navigates to an appropriate component/collection route given the current page. * Rename openInfoSidebar to openLibrarySidebar, so that openInfoSidebar can be used to open the best sidebar for a given library/component/collection.
1 parent a4728e1 commit 5fad981

16 files changed

+280
-103
lines changed

src/library-authoring/LibraryAuthoringPage.tsx

+30-28
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,7 @@ import {
1515
Tabs,
1616
} from '@openedx/paragon';
1717
import { Add, ArrowBack, InfoOutline } from '@openedx/paragon/icons';
18-
import {
19-
Link,
20-
useLocation,
21-
useNavigate,
22-
useSearchParams,
23-
} from 'react-router-dom';
18+
import { Link } from 'react-router-dom';
2419

2520
import Loading from '../generic/Loading';
2621
import SubHeader from '../generic/sub-header/SubHeader';
@@ -35,11 +30,12 @@ import {
3530
SearchKeywordsField,
3631
SearchSortWidget,
3732
} from '../search-manager';
38-
import LibraryContent, { ContentType } from './LibraryContent';
33+
import LibraryContent from './LibraryContent';
3934
import { LibrarySidebar } from './library-sidebar';
4035
import { useComponentPickerContext } from './common/context/ComponentPickerContext';
4136
import { useLibraryContext } from './common/context/LibraryContext';
4237
import { SidebarBodyComponentId, useSidebarContext } from './common/context/SidebarContext';
38+
import { ContentType, useLibraryRoutes } from './routes';
4339

4440
import messages from './messages';
4541

@@ -50,7 +46,7 @@ const HeaderActions = () => {
5046

5147
const {
5248
openAddContentSidebar,
53-
openInfoSidebar,
49+
openLibrarySidebar,
5450
closeLibrarySidebar,
5551
sidebarComponentInfo,
5652
} = useSidebarContext();
@@ -61,11 +57,15 @@ const HeaderActions = () => {
6157
sidebarComponentInfo?.type === SidebarBodyComponentId.Info
6258
);
6359

60+
const { navigateTo } = useLibraryRoutes();
6461
const handleOnClickInfoSidebar = () => {
62+
// Reset URL to library home
63+
navigateTo();
64+
6565
if (infoSidebarIsOpen()) {
6666
closeLibrarySidebar();
6767
} else {
68-
openInfoSidebar();
68+
openLibrarySidebar();
6969
}
7070
};
7171

@@ -125,8 +125,6 @@ interface LibraryAuthoringPageProps {
125125

126126
const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPageProps) => {
127127
const intl = useIntl();
128-
const location = useLocation();
129-
const navigate = useNavigate();
130128

131129
const {
132130
isLoadingPage: isLoadingStudioHome,
@@ -140,29 +138,41 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
140138
libraryData,
141139
isLoadingLibraryData,
142140
showOnlyPublished,
141+
componentId,
142+
collectionId,
143143
} = useLibraryContext();
144144
const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext();
145145

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

148160
useEffect(() => {
149-
const currentPath = location.pathname.split('/').pop();
161+
const contentType = getActiveKey();
150162

151-
if (componentPickerMode || currentPath === libraryId || currentPath === '') {
163+
if (componentPickerMode) {
152164
setActiveKey(ContentType.home);
153-
} else if (currentPath && currentPath in ContentType) {
154-
setActiveKey(ContentType[currentPath]);
165+
} else {
166+
setActiveKey(contentType);
155167
}
156168
}, []);
157169

158170
useEffect(() => {
159171
if (!componentPickerMode) {
160-
openInfoSidebar();
172+
openInfoSidebar(componentId, collectionId);
161173
}
162174
}, []);
163175

164-
const [searchParams] = useSearchParams();
165-
166176
if (isLoadingLibraryData) {
167177
return <Loading />;
168178
}
@@ -175,22 +185,14 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
175185
);
176186
}
177187

178-
// istanbul ignore if: this should never happen
179-
if (activeKey === undefined) {
180-
return <NotFoundAlert />;
181-
}
182-
183188
if (!libraryData) {
184189
return <NotFoundAlert />;
185190
}
186191

187192
const handleTabChange = (key: ContentType) => {
188193
setActiveKey(key);
189194
if (!componentPickerMode) {
190-
navigate({
191-
pathname: key,
192-
search: searchParams.toString(),
193-
});
195+
navigateTo({ contentType: key });
194196
}
195197
};
196198

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();

0 commit comments

Comments
 (0)