Skip to content

Commit 436ac31

Browse files
authored
feat: nav dropdowns in library authoring view (#2556)
Updates navbar in library authoring page to include `Team Access` and `Import` menu options. Clicking on `Team Access` button opens Team management modal. As per this new PR: #2570, if admin console url is set, it should be used instead of team access modal. So updated this PR accordingly.
1 parent 86a7e06 commit 436ac31

File tree

16 files changed

+161
-45
lines changed

16 files changed

+161
-45
lines changed

src/header/Header.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type Container, useToggle } from '@openedx/paragon';
66
import { useWaffleFlags } from '../data/apiHooks';
77
import { SearchModal } from '../search-modal';
88
import {
9-
useContentMenuItems, useLibraryToolsMenuItems, useSettingMenuItems, useToolsMenuItems,
9+
useContentMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems, useSettingMenuItems, useToolsMenuItems,
1010
} from './hooks';
1111
import messages from './messages';
1212

@@ -20,6 +20,7 @@ interface HeaderProps {
2020
isHiddenMainMenu?: boolean,
2121
isLibrary?: boolean,
2222
containerProps?: ContainerPropsType,
23+
readOnly?: boolean,
2324
}
2425

2526
const Header = ({
@@ -30,6 +31,7 @@ const Header = ({
3031
isHiddenMainMenu = false,
3132
isLibrary = false,
3233
containerProps = {},
34+
readOnly = false,
3335
}: HeaderProps) => {
3436
const intl = useIntl();
3537
const waffleFlags = useWaffleFlags();
@@ -43,7 +45,8 @@ const Header = ({
4345
const settingMenuItems = useSettingMenuItems(contextId);
4446
const toolsMenuItems = useToolsMenuItems(contextId);
4547
const libraryToolsMenuItems = useLibraryToolsMenuItems(contextId);
46-
const mainMenuDropdowns = !isLibrary ? [
48+
const libraryToolsSettingsItems = useLibrarySettingsMenuItems(contextId, readOnly);
49+
let mainMenuDropdowns = !isLibrary ? [
4750
{
4851
id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`,
4952
buttonTitle: intl.formatMessage(messages['header.links.content']),
@@ -65,6 +68,18 @@ const Header = ({
6568
items: libraryToolsMenuItems,
6669
}];
6770

71+
// Include settings menu only if user is allowed to see them.
72+
if (isLibrary && libraryToolsSettingsItems.length > 0) {
73+
mainMenuDropdowns = [
74+
{
75+
id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`,
76+
buttonTitle: intl.formatMessage(messages['header.links.settings']),
77+
items: libraryToolsSettingsItems,
78+
},
79+
...mainMenuDropdowns,
80+
];
81+
}
82+
6883
const getOutlineLink = () => {
6984
if (isLibrary) {
7085
return `/library/${contextId}`;
Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { useSelector } from 'react-redux';
22
import { getConfig, setConfig } from '@edx/frontend-platform';
33
import { renderHook } from '@testing-library/react';
44
import messages from './messages';
5-
import { useContentMenuItems, useToolsMenuItems, useSettingMenuItems } from './hooks';
5+
import {
6+
useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems,
7+
} from './hooks';
68
import { mockWaffleFlags } from '../data/apiHooks.mock';
79

810
jest.mock('@edx/frontend-platform/i18n', () => ({
@@ -28,7 +30,7 @@ jest.mock('react-redux', () => ({
2830
describe('header utils', () => {
2931
describe('getContentMenuItems', () => {
3032
it('when video upload page enabled should include Video Uploads option', () => {
31-
useSelector.mockReturnValue({
33+
jest.mocked(useSelector).mockReturnValue({
3234
librariesV2Enabled: false,
3335
});
3436
setConfig({
@@ -39,7 +41,7 @@ describe('header utils', () => {
3941
expect(actualItems).toHaveLength(5);
4042
});
4143
it('when video upload page disabled should not include Video Uploads option', () => {
42-
useSelector.mockReturnValue({
44+
jest.mocked(useSelector).mockReturnValue({
4345
librariesV2Enabled: false,
4446
});
4547
setConfig({
@@ -50,7 +52,7 @@ describe('header utils', () => {
5052
expect(actualItems).toHaveLength(4);
5153
});
5254
it('adds course libraries link to content menu when libraries v2 is enabled', () => {
53-
useSelector.mockReturnValue({
55+
jest.mocked(useSelector).mockReturnValue({
5456
librariesV2Enabled: true,
5557
});
5658
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
@@ -60,7 +62,7 @@ describe('header utils', () => {
6062

6163
describe('getSettingsMenuitems', () => {
6264
beforeEach(() => {
63-
useSelector.mockReturnValue({
65+
jest.mocked(useSelector).mockReturnValue({
6466
canAccessAdvancedSettings: true,
6567
});
6668
});
@@ -86,7 +88,7 @@ describe('header utils', () => {
8688
expect(actualItemsTitle).toContain('Advanced Settings');
8789
});
8890
it('when user has no access to advanced settings should not include advanced settings option', () => {
89-
useSelector.mockReturnValue({ canAccessAdvancedSettings: false });
91+
jest.mocked(useSelector).mockReturnValue({ canAccessAdvancedSettings: false });
9092
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123')).result.current.map((item) => item.title);
9193
expect(actualItemsTitle).not.toContain('Advanced Settings');
9294
});
@@ -137,4 +139,44 @@ describe('header utils', () => {
137139
expect(actualItemsTitle).not.toContain(messages['header.links.optimizer'].defaultMessage);
138140
});
139141
});
142+
143+
describe('useLibrarySettingsMenuItems', () => {
144+
it('should contain team access url', () => {
145+
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current;
146+
expect(items).toContainEqual({ title: 'Team Access', href: 'http://localhost/?sa=manage-team' });
147+
});
148+
it('should contain admin console url if set', () => {
149+
setConfig({
150+
...getConfig(),
151+
ADMIN_CONSOLE_URL: 'http://admin-console.com',
152+
});
153+
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current;
154+
expect(items).toContainEqual({
155+
title: 'Team Access',
156+
href: 'http://admin-console.com/authz/libraries/library-123',
157+
});
158+
});
159+
it('should contain admin console url if set and readOnly is true', () => {
160+
setConfig({
161+
...getConfig(),
162+
ADMIN_CONSOLE_URL: 'http://admin-console.com',
163+
});
164+
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', true)).result.current;
165+
expect(items).toContainEqual({
166+
title: 'Team Access',
167+
href: 'http://admin-console.com/authz/libraries/library-123',
168+
});
169+
});
170+
});
171+
172+
describe('useLibraryToolsMenuItems', () => {
173+
it('should contain backup and import url', () => {
174+
const items = renderHook(() => useLibraryToolsMenuItems('course-123')).result.current;
175+
expect(items).toContainEqual({
176+
href: '/library/course-123/backup',
177+
title: 'Backup to local archive',
178+
});
179+
expect(items).toContainEqual({ href: '/library/course-123/import', title: 'Import' });
180+
});
181+
});
140182
});
Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { useIntl } from '@edx/frontend-platform/i18n';
33
import { useSelector } from 'react-redux';
44
import { Badge } from '@openedx/paragon';
55

6-
import { getPagePath } from '../utils';
7-
import { useWaffleFlags } from '../data/apiHooks';
8-
import { getStudioHomeData } from '../studio-home/data/selectors';
6+
import { getPagePath } from '@src/utils';
7+
import { useWaffleFlags } from '@src/data/apiHooks';
8+
import { getStudioHomeData } from '@src/studio-home/data/selectors';
9+
import courseOptimizerMessages from '@src/optimizer-page/messages';
10+
import { SidebarActions } from '@src/library-authoring/common/context/SidebarContext';
11+
import { LibQueryParamKeys } from '@src/library-authoring/routes';
912
import messages from './messages';
10-
import courseOptimizerMessages from '../optimizer-page/messages';
1113

12-
export const useContentMenuItems = courseId => {
14+
export const useContentMenuItems = (courseId: string) => {
1315
const intl = useIntl();
1416
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
1517
const waffleFlags = useWaffleFlags();
@@ -50,7 +52,7 @@ export const useContentMenuItems = courseId => {
5052
return items;
5153
};
5254

53-
export const useSettingMenuItems = courseId => {
55+
export const useSettingMenuItems = (courseId: string) => {
5456
const intl = useIntl();
5557
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
5658
const { canAccessAdvancedSettings } = useSelector(getStudioHomeData);
@@ -89,7 +91,7 @@ export const useSettingMenuItems = courseId => {
8991
return items;
9092
};
9193

92-
export const useToolsMenuItems = (courseId) => {
94+
export const useToolsMenuItems = (courseId: string) => {
9395
const intl = useIntl();
9496
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
9597
const waffleFlags = useWaffleFlags();
@@ -127,15 +129,57 @@ export const useToolsMenuItems = (courseId) => {
127129
return items;
128130
};
129131

130-
export const useLibraryToolsMenuItems = itemId => {
132+
export const useLibraryToolsMenuItems = (itemId: string) => {
131133
const intl = useIntl();
132134

133135
const items = [
134136
{
135137
href: `/library/${itemId}/backup`,
136138
title: intl.formatMessage(messages['header.links.exportLibrary']),
137139
},
140+
{
141+
href: `/library/${itemId}/import`,
142+
title: intl.formatMessage(messages['header.links.lib.import']),
143+
},
138144
];
139145

140146
return items;
141147
};
148+
149+
export const useLibrarySettingsMenuItems = (itemId: string, readOnly: boolean) => {
150+
const intl = useIntl();
151+
152+
const openTeamAccessModalUrl = () => {
153+
const adminConsoleUrl = getConfig().ADMIN_CONSOLE_URL;
154+
// always show link to admin console MFE if it is being used
155+
const shouldShowAdminConsoleLink = !!adminConsoleUrl;
156+
157+
// if the admin console MFE isn't being used, show team modal button for non–read-only users
158+
const shouldShowTeamModalButton = !adminConsoleUrl && !readOnly;
159+
if (shouldShowTeamModalButton) {
160+
if (!window.location.href) {
161+
return null;
162+
}
163+
const url = new URL(window.location.href);
164+
// Set ?sa=manage-team in url which in turn opens team access modal
165+
url.searchParams.set(LibQueryParamKeys.SidebarActions, SidebarActions.ManageTeam);
166+
return url.toString();
167+
}
168+
if (shouldShowAdminConsoleLink) {
169+
return `${adminConsoleUrl}/authz/libraries/${itemId}`;
170+
}
171+
return null;
172+
};
173+
174+
const items: { title: string; href: string }[] = [];
175+
176+
const teamAccessUrl = openTeamAccessModalUrl();
177+
if (teamAccessUrl) {
178+
items.push({
179+
title: intl.formatMessage(messages['header.menu.teamAccess']),
180+
href: teamAccessUrl,
181+
});
182+
}
183+
184+
return items;
185+
};
File renamed without changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ const messages = defineMessages({
9696
defaultMessage: 'Import',
9797
description: 'Link to Studio Import page',
9898
},
99+
'header.links.lib.import': {
100+
id: 'header.links.lib.import',
101+
defaultMessage: 'Import',
102+
description: 'Link to Course Import page in library',
103+
},
99104
'header.links.exportCourse': {
100105
id: 'header.links.exportCourse',
101106
defaultMessage: 'Export Course',
@@ -106,6 +111,11 @@ const messages = defineMessages({
106111
defaultMessage: 'Backup to local archive',
107112
description: 'Link to Studio Backup Library page',
108113
},
114+
'header.menu.teamAccess': {
115+
id: 'header.links.teamAccess',
116+
defaultMessage: 'Team Access',
117+
description: 'Menu item to open team access popup',
118+
},
109119
'header.links.optimizer': {
110120
id: 'header.links.optimizer',
111121
defaultMessage: 'Course Optimizer',

src/library-authoring/LibraryLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CreateContainerModal } from './create-container';
1818
import { ROUTES } from './routes';
1919
import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections';
2020
import { LibraryUnitPage } from './units';
21+
import { LibraryTeamModal } from './library-team';
2122

2223
const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
2324
const {
@@ -48,6 +49,7 @@ const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) =
4849
<CreateCollectionModal />
4950
<CreateContainerModal />
5051
<ComponentEditorModal />
52+
<LibraryTeamModal />
5153
</SidebarProvider>
5254
</LibraryProvider>
5355
);

src/library-authoring/backup-restore/LibraryBackupPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { useContentLibrary } from '@src/library-authoring/data/apiHooks';
2424

2525
export const LibraryBackupPage = () => {
2626
const intl = useIntl();
27-
const { libraryId } = useLibraryContext();
27+
const { libraryId, readOnly } = useLibraryContext();
2828
const [taskId, setTaskId] = useState<string>('');
2929
const [isMutationInProgress, setIsMutationInProgress] = useState<boolean>(false);
3030
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -144,6 +144,7 @@ export const LibraryBackupPage = () => {
144144
title={libraryData.title}
145145
org={libraryData.org}
146146
contextId={libraryId}
147+
readOnly={readOnly}
147148
isLibrary
148149
containerProps={{
149150
size: undefined,

src/library-authoring/collections/LibraryCollectionPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const LibraryCollectionPage = () => {
107107
showOnlyPublished,
108108
extraFilter: contextExtraFilter,
109109
setCollectionId,
110+
readOnly,
110111
} = useLibraryContext();
111112
const { sidebarItemInfo } = useSidebarContext();
112113

@@ -194,6 +195,7 @@ const LibraryCollectionPage = () => {
194195
title={libraryData.title}
195196
org={libraryData.org}
196197
contextId={libraryId}
198+
readOnly={readOnly}
197199
isLibrary
198200
containerProps={{
199201
size: undefined,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import {
77
useState,
88
} from 'react';
99
import { useParams } from 'react-router-dom';
10-
import { useStateWithUrlSearchParam } from '../../../hooks';
10+
import { useStateWithUrlSearchParam } from '@src/hooks';
11+
import { LibQueryParamKeys, useLibraryRoutes } from '@src/library-authoring/routes';
1112
import { useComponentPickerContext } from './ComponentPickerContext';
1213
import { useLibraryContext } from './LibraryContext';
13-
import { useLibraryRoutes } from '../../routes';
1414

1515
export enum SidebarBodyItemId {
1616
AddContent = 'add-content',
@@ -130,14 +130,14 @@ export const SidebarProvider = ({
130130

131131
const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam<SidebarInfoTab>(
132132
defaultTab.component,
133-
'st',
133+
LibQueryParamKeys.SidebarTab,
134134
(value: string) => toSidebarInfoTab(value),
135135
(value: SidebarInfoTab) => value.toString(),
136136
);
137137

138138
const [sidebarAction, setSidebarAction] = useStateWithUrlSearchParam<SidebarActions>(
139139
SidebarActions.None,
140-
'sa',
140+
LibQueryParamKeys.SidebarActions,
141141
(value: string) => Object.values(SidebarActions).find((enumValue) => value === enumValue),
142142
(value: SidebarActions) => value.toString(),
143143
);

src/library-authoring/library-info/LibraryInfo.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
55

66
import messages from './messages';
77
import LibraryPublishStatus from './LibraryPublishStatus';
8-
import { LibraryTeamModal } from '../library-team';
98
import { useLibraryContext } from '../common/context/LibraryContext';
109
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
1110

1211
const LibraryInfo = () => {
1312
const intl = useIntl();
1413
const { libraryId, libraryData, readOnly } = useLibraryContext();
15-
const { sidebarAction, setSidebarAction, resetSidebarAction } = useSidebarContext();
16-
const isLibraryTeamModalOpen = (sidebarAction === SidebarActions.ManageTeam);
14+
const { setSidebarAction } = useSidebarContext();
1715
const adminConsoleUrl = getConfig().ADMIN_CONSOLE_URL;
1816

1917
// always show link to admin console MFE if it is being used
@@ -25,9 +23,6 @@ const LibraryInfo = () => {
2523
const openLibraryTeamModal = useCallback(() => {
2624
setSidebarAction(SidebarActions.ManageTeam);
2725
}, [setSidebarAction]);
28-
const closeLibraryTeamModal = useCallback(() => {
29-
resetSidebarAction();
30-
}, [resetSidebarAction]);
3126

3227
return (
3328
<Stack direction="vertical" gap={2.5}>
@@ -81,7 +76,6 @@ const LibraryInfo = () => {
8176
</span>
8277
</Stack>
8378
</Stack>
84-
{isLibraryTeamModalOpen && <LibraryTeamModal onClose={closeLibraryTeamModal} />}
8579
</Stack>
8680
);
8781
};

0 commit comments

Comments
 (0)