Skip to content

Commit f6b46c4

Browse files
committed
feat: store sidebar tab and additional action in query string
Related changes: * adds "manage-team" sidebar action to trigger LibraryInfo's "Manage Team" modal * moves useStateWithUrlSearchParam up to hooks.ts so it can be used by search-manager and library-authoring * splits sidebarCurrentTab and additionalAction out of SidebarComponentInfo -- they're now managed independently as url parameters. This also simplifies setSidebarComponentInfo: we can simply set the new id + key, and so there's no need to merge the ...prev state and new state. * shortens some sidebar property names: sidebarCurrentTab => sidebarTab sidebarAdditionalAction => sidebarAction * test: Tab changes now trigger a navigate() call, which invalidates the within(sidebar) element in the tests. So using screen instead.
1 parent fc2f467 commit f6b46c4

File tree

10 files changed

+153
-122
lines changed

10 files changed

+153
-122
lines changed

src/hooks.ts

+49-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { useEffect, useState } from 'react';
1+
import {
2+
type Dispatch,
3+
type SetStateAction,
4+
useCallback,
5+
useEffect,
6+
useState,
7+
} from 'react';
28
import { history } from '@edx/frontend-platform';
3-
import { useLocation } from 'react-router-dom';
9+
import { useLocation, useSearchParams } from 'react-router-dom';
410

511
export const useScrollToHashElement = ({ isLoading }: { isLoading: boolean }) => {
612
const [elementWithHash, setElementWithHash] = useState<string | null>(null);
@@ -77,3 +83,44 @@ export const useLoadOnScroll = (
7783
return () => { };
7884
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
7985
};
86+
87+
/**
88+
* Hook which stores state variables in the URL search parameters.
89+
*
90+
* It wraps useState with functions that get/set a query string
91+
* search parameter when returning/setting the state variable.
92+
*
93+
* @param defaultValue: Type
94+
* Returned when no valid value is found in the url search parameter.
95+
* @param paramName: name of the url search parameter to store this value in.
96+
* @param fromString: returns the Type equivalent of the given string value,
97+
* or undefined if the value is invalid.
98+
* @param toString: returns the string equivalent of the given Type value.
99+
* Return defaultValue to clear the url search paramName.
100+
*/
101+
export function useStateWithUrlSearchParam<Type>(
102+
defaultValue: Type,
103+
paramName: string,
104+
fromString: (value: string | null) => Type | undefined,
105+
toString: (value: Type) => string | undefined,
106+
): [value: Type, setter: Dispatch<SetStateAction<Type>>] {
107+
const [searchParams, setSearchParams] = useSearchParams();
108+
const returnValue: Type = fromString(searchParams.get(paramName)) ?? defaultValue;
109+
// Function to update the url search parameter
110+
const returnSetter: Dispatch<SetStateAction<Type>> = useCallback((value: Type) => {
111+
setSearchParams((prevParams) => {
112+
const paramValue: string = toString(value) ?? '';
113+
const newSearchParams = new URLSearchParams(prevParams);
114+
// If using the default paramValue, remove it from the search params.
115+
if (paramValue === defaultValue) {
116+
newSearchParams.delete(paramName);
117+
} else {
118+
newSearchParams.set(paramName, paramValue);
119+
}
120+
return newSearchParams;
121+
}, { replace: true });
122+
}, [setSearchParams]);
123+
124+
// Return the computed value and wrapped set state function
125+
return [returnValue, returnSetter];
126+
}

src/library-authoring/LibraryAuthoringPage.test.tsx

+4-8
Original file line numberDiff line numberDiff line change
@@ -431,27 +431,23 @@ describe('<LibraryAuthoringPage />', () => {
431431
// Click on the first collection
432432
fireEvent.click((await screen.findByText('Collection 1')));
433433

434-
const sidebar = screen.getByTestId('library-sidebar');
435-
436-
const { getByRole } = within(sidebar);
437-
438434
// Click on the Details tab
439-
fireEvent.click(getByRole('tab', { name: 'Details' }));
435+
fireEvent.click(screen.getByRole('tab', { name: 'Details' }));
440436

441437
// Change to a component
442438
fireEvent.click((await screen.findAllByText('Introduction to Testing'))[0]);
443439

444440
// Check that the Details tab is still selected
445-
expect(getByRole('tab', { name: 'Details' })).toHaveAttribute('aria-selected', 'true');
441+
expect(screen.getByRole('tab', { name: 'Details' })).toHaveAttribute('aria-selected', 'true');
446442

447443
// Click on the Previews tab
448-
fireEvent.click(getByRole('tab', { name: 'Preview' }));
444+
fireEvent.click(screen.getByRole('tab', { name: 'Preview' }));
449445

450446
// Switch back to the collection
451447
fireEvent.click((await screen.findByText('Collection 1')));
452448

453449
// The Manage (default) tab should be selected because the collection does not have a Preview tab
454-
expect(getByRole('tab', { name: 'Manage' })).toHaveAttribute('aria-selected', 'true');
450+
expect(screen.getByRole('tab', { name: 'Manage' })).toHaveAttribute('aria-selected', 'true');
455451
});
456452

457453
it('can filter by capa problem type', async () => {

src/library-authoring/collections/CollectionInfo.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ const CollectionInfo = () => {
2727

2828
const { componentPickerMode } = useComponentPickerContext();
2929
const { libraryId, setCollectionId } = useLibraryContext();
30-
const { sidebarComponentInfo, setSidebarCurrentTab } = useSidebarContext();
30+
const { sidebarComponentInfo, sidebarTab, setSidebarTab } = useSidebarContext();
3131

3232
const tab: CollectionInfoTab = (
33-
sidebarComponentInfo?.currentTab && isCollectionInfoTab(sidebarComponentInfo.currentTab)
34-
) ? sidebarComponentInfo?.currentTab : COLLECTION_INFO_TABS.Manage;
33+
sidebarTab && isCollectionInfoTab(sidebarTab)
34+
) ? sidebarTab : COLLECTION_INFO_TABS.Manage;
3535

3636
const collectionId = sidebarComponentInfo?.id;
3737
// istanbul ignore if: this should never happen
@@ -70,7 +70,7 @@ const CollectionInfo = () => {
7070
className="my-3 d-flex justify-content-around"
7171
defaultActiveKey={COMPONENT_INFO_TABS.Manage}
7272
activeKey={tab}
73-
onSelect={setSidebarCurrentTab}
73+
onSelect={setSidebarTab}
7474
>
7575
<Tab eventKey={COMPONENT_INFO_TABS.Manage} title={intl.formatMessage(messages.manageTabTitle)}>
7676
<ContentTagsDrawer

src/library-authoring/common/context/SidebarContext.tsx

+54-38
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useMemo,
66
useState,
77
} from 'react';
8+
import { useStateWithUrlSearchParam } from '../../../hooks';
89

910
export enum SidebarBodyComponentId {
1011
AddContent = 'add-content',
@@ -32,29 +33,36 @@ export const isComponentInfoTab = (tab: string): tab is ComponentInfoTab => (
3233
Object.values<string>(COMPONENT_INFO_TABS).includes(tab)
3334
);
3435

36+
type SidebarInfoTab = ComponentInfoTab | CollectionInfoTab;
37+
const toSidebarInfoTab = (tab: string): SidebarInfoTab | undefined => (
38+
isComponentInfoTab(tab) || isCollectionInfoTab(tab)
39+
? tab : undefined
40+
);
41+
3542
export interface SidebarComponentInfo {
3643
type: SidebarBodyComponentId;
3744
id: string;
38-
/** Additional action on Sidebar display */
39-
additionalAction?: SidebarAdditionalActions;
40-
/** Current tab in the sidebar */
41-
currentTab?: CollectionInfoTab | ComponentInfoTab;
4245
}
4346

44-
export enum SidebarAdditionalActions {
47+
export enum SidebarActions {
4548
JumpToAddCollections = 'jump-to-add-collections',
49+
ManageTeam = 'manage-team',
50+
None = '',
4651
}
4752

4853
export type SidebarContextData = {
4954
closeLibrarySidebar: () => void;
5055
openAddContentSidebar: () => void;
5156
openInfoSidebar: (componentId?: string, collectionId?: string) => void;
5257
openLibrarySidebar: () => void;
53-
openCollectionInfoSidebar: (collectionId: string, additionalAction?: SidebarAdditionalActions) => void;
54-
openComponentInfoSidebar: (usageKey: string, additionalAction?: SidebarAdditionalActions) => void;
58+
openCollectionInfoSidebar: (collectionId: string) => void;
59+
openComponentInfoSidebar: (usageKey: string) => void;
5560
sidebarComponentInfo?: SidebarComponentInfo;
56-
resetSidebarAdditionalActions: () => void;
57-
setSidebarCurrentTab: (tab: CollectionInfoTab | ComponentInfoTab) => void;
61+
sidebarAction: SidebarActions;
62+
setSidebarAction: (action: SidebarActions) => void;
63+
resetSidebarAction: () => void;
64+
sidebarTab: SidebarInfoTab;
65+
setSidebarTab: (tab: SidebarInfoTab) => void;
5866
};
5967

6068
/**
@@ -82,12 +90,22 @@ export const SidebarProvider = ({
8290
initialSidebarComponentInfo,
8391
);
8492

85-
/** Helper function to consume additional action once performed.
86-
Required to redo the action.
87-
*/
88-
const resetSidebarAdditionalActions = useCallback(() => {
89-
setSidebarComponentInfo((prev) => (prev && { ...prev, additionalAction: undefined }));
90-
}, []);
93+
const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam<SidebarInfoTab>(
94+
COMPONENT_INFO_TABS.Preview,
95+
'st',
96+
(value: string) => toSidebarInfoTab(value),
97+
(value: SidebarInfoTab) => value.toString(),
98+
);
99+
100+
const [sidebarAction, setSidebarAction] = useStateWithUrlSearchParam<SidebarActions>(
101+
SidebarActions.None,
102+
'sa',
103+
(value: string) => Object.values(SidebarActions).find((enumValue) => value === enumValue),
104+
(value: SidebarActions) => value.toString(),
105+
);
106+
const resetSidebarAction = useCallback(() => {
107+
setSidebarAction(SidebarActions.None);
108+
}, [setSidebarAction]);
91109

92110
const closeLibrarySidebar = useCallback(() => {
93111
setSidebarComponentInfo(undefined);
@@ -99,25 +117,18 @@ export const SidebarProvider = ({
99117
setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.Info });
100118
}, []);
101119

102-
const openComponentInfoSidebar = useCallback((usageKey: string, additionalAction?: SidebarAdditionalActions) => {
103-
setSidebarComponentInfo((prev) => ({
104-
...prev,
120+
const openComponentInfoSidebar = useCallback((usageKey: string) => {
121+
setSidebarComponentInfo({
105122
id: usageKey,
106123
type: SidebarBodyComponentId.ComponentInfo,
107-
additionalAction,
108-
}));
124+
});
109125
}, []);
110126

111-
const openCollectionInfoSidebar = useCallback((
112-
newCollectionId: string,
113-
additionalAction?: SidebarAdditionalActions,
114-
) => {
115-
setSidebarComponentInfo((prev) => ({
116-
...prev,
127+
const openCollectionInfoSidebar = useCallback((newCollectionId: string) => {
128+
setSidebarComponentInfo({
117129
id: newCollectionId,
118130
type: SidebarBodyComponentId.CollectionInfo,
119-
additionalAction,
120-
}));
131+
});
121132
}, []);
122133

123134
const openInfoSidebar = useCallback((componentId?: string, collectionId?: string) => {
@@ -130,10 +141,6 @@ export const SidebarProvider = ({
130141
}
131142
}, []);
132143

133-
const setSidebarCurrentTab = useCallback((tab: CollectionInfoTab | ComponentInfoTab) => {
134-
setSidebarComponentInfo((prev) => (prev && { ...prev, currentTab: tab }));
135-
}, []);
136-
137144
const context = useMemo<SidebarContextData>(() => {
138145
const contextValue = {
139146
closeLibrarySidebar,
@@ -143,8 +150,11 @@ export const SidebarProvider = ({
143150
openComponentInfoSidebar,
144151
sidebarComponentInfo,
145152
openCollectionInfoSidebar,
146-
resetSidebarAdditionalActions,
147-
setSidebarCurrentTab,
153+
sidebarAction,
154+
setSidebarAction,
155+
resetSidebarAction,
156+
sidebarTab,
157+
setSidebarTab,
148158
};
149159

150160
return contextValue;
@@ -156,8 +166,11 @@ export const SidebarProvider = ({
156166
openComponentInfoSidebar,
157167
sidebarComponentInfo,
158168
openCollectionInfoSidebar,
159-
resetSidebarAdditionalActions,
160-
setSidebarCurrentTab,
169+
sidebarAction,
170+
setSidebarAction,
171+
resetSidebarAction,
172+
sidebarTab,
173+
setSidebarTab,
161174
]);
162175

163176
return (
@@ -178,8 +191,11 @@ export function useSidebarContext(): SidebarContextData {
178191
openLibrarySidebar: () => {},
179192
openComponentInfoSidebar: () => {},
180193
openCollectionInfoSidebar: () => {},
181-
resetSidebarAdditionalActions: () => {},
182-
setSidebarCurrentTab: () => {},
194+
sidebarAction: SidebarActions.None,
195+
setSidebarAction: () => {},
196+
resetSidebarAction: () => {},
197+
sidebarTab: COMPONENT_INFO_TABS.Preview,
198+
setSidebarTab: () => {},
183199
sidebarComponentInfo: undefined,
184200
};
185201
}

src/library-authoring/component-info/ComponentInfo.tsx

+15-15
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useLibraryContext } from '../common/context/LibraryContext';
1616
import {
1717
type ComponentInfoTab,
1818
COMPONENT_INFO_TABS,
19-
SidebarAdditionalActions,
19+
SidebarActions,
2020
isComponentInfoTab,
2121
useSidebarContext,
2222
} from '../common/context/SidebarContext';
@@ -101,27 +101,27 @@ const ComponentInfo = () => {
101101
const intl = useIntl();
102102

103103
const { readOnly, openComponentEditor } = useLibraryContext();
104-
const { setSidebarCurrentTab, sidebarComponentInfo, resetSidebarAdditionalActions } = useSidebarContext();
104+
const {
105+
sidebarTab,
106+
setSidebarTab,
107+
sidebarComponentInfo,
108+
sidebarAction,
109+
} = useSidebarContext();
105110

106-
const jumpToCollections = sidebarComponentInfo?.additionalAction === SidebarAdditionalActions.JumpToAddCollections;
111+
const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections;
107112

108113
const tab: ComponentInfoTab = (
109-
sidebarComponentInfo?.currentTab && isComponentInfoTab(sidebarComponentInfo.currentTab)
110-
) ? sidebarComponentInfo?.currentTab : COMPONENT_INFO_TABS.Preview;
114+
isComponentInfoTab(sidebarTab)
115+
? sidebarTab
116+
: COMPONENT_INFO_TABS.Preview
117+
);
111118

112119
useEffect(() => {
113120
// Show Manage tab if JumpToAddCollections action is set in sidebarComponentInfo
114121
if (jumpToCollections) {
115-
setSidebarCurrentTab(COMPONENT_INFO_TABS.Manage);
116-
}
117-
}, [jumpToCollections]);
118-
119-
useEffect(() => {
120-
// This is required to redo actions.
121-
if (tab !== COMPONENT_INFO_TABS.Manage) {
122-
resetSidebarAdditionalActions();
122+
setSidebarTab(COMPONENT_INFO_TABS.Manage);
123123
}
124-
}, [tab]);
124+
}, [jumpToCollections, setSidebarTab]);
125125

126126
const usageKey = sidebarComponentInfo?.id;
127127
// istanbul ignore if: this should never happen
@@ -169,7 +169,7 @@ const ComponentInfo = () => {
169169
className="my-3 d-flex justify-content-around"
170170
defaultActiveKey={COMPONENT_INFO_TABS.Preview}
171171
activeKey={tab}
172-
onSelect={setSidebarCurrentTab}
172+
onSelect={setSidebarTab}
173173
>
174174
<Tab eventKey={COMPONENT_INFO_TABS.Preview} title={intl.formatMessage(messages.previewTabTitle)}>
175175
<ComponentPreview />

src/library-authoring/component-info/ComponentManagement.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@openedx/paragon/icons';
88

99
import { useLibraryContext } from '../common/context/LibraryContext';
10-
import { SidebarAdditionalActions, useSidebarContext } from '../common/context/SidebarContext';
10+
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
1111
import { useLibraryBlockMetadata } from '../data/apiHooks';
1212
import StatusWidget from '../generic/status-widget';
1313
import messages from './messages';
@@ -18,8 +18,8 @@ import ManageCollections from './ManageCollections';
1818
const ComponentManagement = () => {
1919
const intl = useIntl();
2020
const { readOnly, isLoadingLibraryData } = useLibraryContext();
21-
const { sidebarComponentInfo, resetSidebarAdditionalActions } = useSidebarContext();
22-
const jumpToCollections = sidebarComponentInfo?.additionalAction === SidebarAdditionalActions.JumpToAddCollections;
21+
const { sidebarComponentInfo, sidebarAction, resetSidebarAction } = useSidebarContext();
22+
const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections;
2323
const [tagsCollapseIsOpen, setTagsCollapseOpen] = React.useState(!jumpToCollections);
2424
const [collectionsCollapseIsOpen, setCollectionsCollapseOpen] = React.useState(true);
2525

@@ -33,7 +33,7 @@ const ComponentManagement = () => {
3333
useEffect(() => {
3434
// This is required to redo actions.
3535
if (tagsCollapseIsOpen || !collectionsCollapseIsOpen) {
36-
resetSidebarAdditionalActions();
36+
resetSidebarAction();
3737
}
3838
}, [tagsCollapseIsOpen, collectionsCollapseIsOpen]);
3939

0 commit comments

Comments
 (0)