From ede76c4583f0d960449c60f2d1e5f61143c752a9 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 22 Oct 2025 10:43:30 +0530 Subject: [PATCH 1/6] feat: library settings menu --- src/header/Header.tsx | 20 +++++++--- src/header/{hooks.jsx => hooks.tsx} | 37 +++++++++++++++---- src/header/messages.js | 5 +++ src/library-authoring/LibraryLayout.tsx | 2 + .../common/context/SidebarContext.tsx | 9 ++++- .../library-info/LibraryInfo.tsx | 8 +--- .../library-team/LibraryTeamModal.tsx | 20 +++++----- src/library-authoring/routes.ts | 3 +- src/studio-home/data/slice.ts | 1 + 9 files changed, 71 insertions(+), 34 deletions(-) rename src/header/{hooks.jsx => hooks.tsx} (81%) diff --git a/src/header/Header.tsx b/src/header/Header.tsx index b9f7210fcb..4a6f1eaae4 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -6,7 +6,7 @@ import { type Container, useToggle } from '@openedx/paragon'; import { useWaffleFlags } from '../data/apiHooks'; import { SearchModal } from '../search-modal'; import { - useContentMenuItems, useLibraryToolsMenuItems, useSettingMenuItems, useToolsMenuItems, + useContentMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems, useSettingMenuItems, useToolsMenuItems, } from './hooks'; import messages from './messages'; @@ -43,6 +43,7 @@ const Header = ({ const settingMenuItems = useSettingMenuItems(contextId); const toolsMenuItems = useToolsMenuItems(contextId); const libraryToolsMenuItems = useLibraryToolsMenuItems(contextId); + const libraryToolsSettingsItems = useLibrarySettingsMenuItems(); const mainMenuDropdowns = !isLibrary ? [ { id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`, @@ -59,11 +60,18 @@ const Header = ({ buttonTitle: intl.formatMessage(messages['header.links.tools']), items: toolsMenuItems, }, - ] : [{ - id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, - buttonTitle: intl.formatMessage(messages['header.links.tools']), - items: libraryToolsMenuItems, - }]; + ] : [ + { + id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`, + buttonTitle: intl.formatMessage(messages['header.links.settings']), + items: libraryToolsSettingsItems, + }, + { + id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, + buttonTitle: intl.formatMessage(messages['header.links.tools']), + items: libraryToolsMenuItems, + }, + ]; const getOutlineLink = () => { if (isLibrary) { diff --git a/src/header/hooks.jsx b/src/header/hooks.tsx similarity index 81% rename from src/header/hooks.jsx rename to src/header/hooks.tsx index 80389e3a72..07cb1a12d3 100644 --- a/src/header/hooks.jsx +++ b/src/header/hooks.tsx @@ -3,13 +3,14 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useSelector } from 'react-redux'; import { Badge } from '@openedx/paragon'; -import { getPagePath } from '../utils'; -import { useWaffleFlags } from '../data/apiHooks'; -import { getStudioHomeData } from '../studio-home/data/selectors'; +import { getPagePath } from '@src/utils'; +import { useWaffleFlags } from '@src/data/apiHooks'; +import { getStudioHomeData } from '@src/studio-home/data/selectors'; import messages from './messages'; -import courseOptimizerMessages from '../optimizer-page/messages'; +import courseOptimizerMessages from '@src/optimizer-page/messages'; +import { LibQueryParamKeys, SidebarActions } from '@src/library-authoring/common/context/SidebarContext'; -export const useContentMenuItems = courseId => { +export const useContentMenuItems = (courseId: string) => { const intl = useIntl(); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const waffleFlags = useWaffleFlags(); @@ -50,7 +51,7 @@ export const useContentMenuItems = courseId => { return items; }; -export const useSettingMenuItems = courseId => { +export const useSettingMenuItems = (courseId: string) => { const intl = useIntl(); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const { canAccessAdvancedSettings } = useSelector(getStudioHomeData); @@ -89,7 +90,7 @@ export const useSettingMenuItems = courseId => { return items; }; -export const useToolsMenuItems = (courseId) => { +export const useToolsMenuItems = (courseId: string) => { const intl = useIntl(); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const waffleFlags = useWaffleFlags(); @@ -127,7 +128,7 @@ export const useToolsMenuItems = (courseId) => { return items; }; -export const useLibraryToolsMenuItems = itemId => { +export const useLibraryToolsMenuItems = (itemId: string) => { const intl = useIntl(); const items = [ @@ -139,3 +140,23 @@ export const useLibraryToolsMenuItems = itemId => { return items; }; + +export const useLibrarySettingsMenuItems = () => { + const intl = useIntl(); + + const openTeamAccessModalUrl = () => { + const url = new URL(window.location.href); + // Set ?sa=manage-team in url which in turn opens team access modal + url.searchParams.set(LibQueryParamKeys.SidebarActions, SidebarActions.ManageTeam); + return url.toString(); + } + + const items = [ + { + title: intl.formatMessage(messages['header.menu.teamAccess']), + href: openTeamAccessModalUrl(), + }, + ]; + + return items; +}; diff --git a/src/header/messages.js b/src/header/messages.js index b755b9dc5d..4d0f4404bb 100644 --- a/src/header/messages.js +++ b/src/header/messages.js @@ -106,6 +106,11 @@ const messages = defineMessages({ defaultMessage: 'Backup to local archive', description: 'Link to Studio Backup Library page', }, + 'header.menu.teamAccess': { + id: 'header.links.teamAccess', + defaultMessage: 'Team Access', + description: 'Menu item to open team access popup', + }, 'header.links.optimizer': { id: 'header.links.optimizer', defaultMessage: 'Course Optimizer', diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index 350bd5c39c..d51a25c0a8 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -18,6 +18,7 @@ import { CreateContainerModal } from './create-container'; import { ROUTES } from './routes'; import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections'; import { LibraryUnitPage } from './units'; +import { LibraryTeamModal } from './library-team'; const LibraryLayoutWrapper: React.FC = ({ children }) => { const { @@ -48,6 +49,7 @@ const LibraryLayoutWrapper: React.FC = ({ children }) = + ); diff --git a/src/library-authoring/common/context/SidebarContext.tsx b/src/library-authoring/common/context/SidebarContext.tsx index fea6d9c353..918f52cf5d 100644 --- a/src/library-authoring/common/context/SidebarContext.tsx +++ b/src/library-authoring/common/context/SidebarContext.tsx @@ -81,6 +81,11 @@ export enum SidebarActions { None = '', } +export enum LibQueryParamKeys { + SidebarActions = 'sa', + SidebarTab = 'st', +} + export type SidebarContextData = { closeLibrarySidebar: () => void; openAddContentSidebar: () => void; @@ -129,14 +134,14 @@ export const SidebarProvider = ({ const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam( defaultTab.component, - 'st', + LibQueryParamKeys.SidebarTab, (value: string) => toSidebarInfoTab(value), (value: SidebarInfoTab) => value.toString(), ); const [sidebarAction, setSidebarAction] = useStateWithUrlSearchParam( SidebarActions.None, - 'sa', + LibQueryParamKeys.SidebarActions, (value: string) => Object.values(SidebarActions).find((enumValue) => value === enumValue), (value: SidebarActions) => value.toString(), ); diff --git a/src/library-authoring/library-info/LibraryInfo.tsx b/src/library-authoring/library-info/LibraryInfo.tsx index 1a44937d8f..562e511e72 100644 --- a/src/library-authoring/library-info/LibraryInfo.tsx +++ b/src/library-authoring/library-info/LibraryInfo.tsx @@ -5,15 +5,13 @@ import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import LibraryPublishStatus from './LibraryPublishStatus'; -import { LibraryTeamModal } from '../library-team'; import { useLibraryContext } from '../common/context/LibraryContext'; import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; const LibraryInfo = () => { const intl = useIntl(); const { libraryId, libraryData, readOnly } = useLibraryContext(); - const { sidebarAction, setSidebarAction, resetSidebarAction } = useSidebarContext(); - const isLibraryTeamModalOpen = (sidebarAction === SidebarActions.ManageTeam); + const { setSidebarAction } = useSidebarContext(); const adminConsoleUrl = getConfig().ADMIN_CONSOLE_URL; // always show link to admin console MFE if it is being used @@ -25,9 +23,6 @@ const LibraryInfo = () => { const openLibraryTeamModal = useCallback(() => { setSidebarAction(SidebarActions.ManageTeam); }, [setSidebarAction]); - const closeLibraryTeamModal = useCallback(() => { - resetSidebarAction(); - }, [resetSidebarAction]); return ( @@ -81,7 +76,6 @@ const LibraryInfo = () => { - {isLibraryTeamModalOpen && } ); }; diff --git a/src/library-authoring/library-team/LibraryTeamModal.tsx b/src/library-authoring/library-team/LibraryTeamModal.tsx index e7a9707969..f3f66324f3 100644 --- a/src/library-authoring/library-team/LibraryTeamModal.tsx +++ b/src/library-authoring/library-team/LibraryTeamModal.tsx @@ -1,24 +1,24 @@ -import React from 'react'; - import { StandardModal } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import LibraryTeam from './LibraryTeam'; import messages from './messages'; +import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; +import { useCallback } from 'react'; -interface LibraryTeamModalProps { - onClose: () => void; -} - -export const LibraryTeamModal: React.FC = ({ - onClose, -}) => { +export const LibraryTeamModal = () => { const intl = useIntl(); + const { sidebarAction, resetSidebarAction } = useSidebarContext(); + // Open the library team modal only when Manage Team sidebar action is set + const isOpen = (sidebarAction === SidebarActions.ManageTeam); + const onClose = useCallback(() => { + resetSidebarAction(); + }, [resetSidebarAction]); // Show Library Team modal in full screen return ( { } // Also remove the `sa` (sidebar action) search param if it exists. - searchParams.delete('sa'); + searchParams.delete(LibQueryParamKeys.SidebarActions); const newPath = generatePath(BASE_ROUTE + route, routeParams); // Prevent unnecessary navigation if the path is the same. diff --git a/src/studio-home/data/slice.ts b/src/studio-home/data/slice.ts index eeb9297ff6..8bed445b8d 100644 --- a/src/studio-home/data/slice.ts +++ b/src/studio-home/data/slice.ts @@ -63,6 +63,7 @@ const slice = createSlice({ studioShortName?: string; techSupportEmail?: string; userIsActive?: boolean; + canAccessAdvancedSettings?: boolean; }, studioHomeCoursesRequestParams: studioHomeCoursesRequestParamsDefault, }, From 8f03bdce374dc0607d290656c451f73374d77e65 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 22 Oct 2025 17:07:53 +0530 Subject: [PATCH 2/6] test: add tests --- src/header/{hooks.test.js => hooks.test.ts} | 30 ++++++++++++++++----- src/header/hooks.tsx | 4 +++ src/header/{index.js => index.ts} | 0 src/header/{messages.js => messages.ts} | 5 ++++ 4 files changed, 33 insertions(+), 6 deletions(-) rename src/header/{hooks.test.js => hooks.test.ts} (82%) rename src/header/{index.js => index.ts} (100%) rename src/header/{messages.js => messages.ts} (97%) diff --git a/src/header/hooks.test.js b/src/header/hooks.test.ts similarity index 82% rename from src/header/hooks.test.js rename to src/header/hooks.test.ts index 176c0905a1..57bd47cbb0 100644 --- a/src/header/hooks.test.js +++ b/src/header/hooks.test.ts @@ -2,7 +2,7 @@ import { useSelector } from 'react-redux'; import { getConfig, setConfig } from '@edx/frontend-platform'; import { renderHook } from '@testing-library/react'; import messages from './messages'; -import { useContentMenuItems, useToolsMenuItems, useSettingMenuItems } from './hooks'; +import { useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems } from './hooks'; import { mockWaffleFlags } from '../data/apiHooks.mock'; jest.mock('@edx/frontend-platform/i18n', () => ({ @@ -28,7 +28,7 @@ jest.mock('react-redux', () => ({ describe('header utils', () => { describe('getContentMenuItems', () => { it('when video upload page enabled should include Video Uploads option', () => { - useSelector.mockReturnValue({ + jest.mocked(useSelector).mockReturnValue({ librariesV2Enabled: false, }); setConfig({ @@ -39,7 +39,7 @@ describe('header utils', () => { expect(actualItems).toHaveLength(5); }); it('when video upload page disabled should not include Video Uploads option', () => { - useSelector.mockReturnValue({ + jest.mocked(useSelector).mockReturnValue({ librariesV2Enabled: false, }); setConfig({ @@ -50,7 +50,7 @@ describe('header utils', () => { expect(actualItems).toHaveLength(4); }); it('adds course libraries link to content menu when libraries v2 is enabled', () => { - useSelector.mockReturnValue({ + jest.mocked(useSelector).mockReturnValue({ librariesV2Enabled: true, }); const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current; @@ -60,7 +60,7 @@ describe('header utils', () => { describe('getSettingsMenuitems', () => { beforeEach(() => { - useSelector.mockReturnValue({ + jest.mocked(useSelector).mockReturnValue({ canAccessAdvancedSettings: true, }); }); @@ -86,7 +86,7 @@ describe('header utils', () => { expect(actualItemsTitle).toContain('Advanced Settings'); }); it('when user has no access to advanced settings should not include advanced settings option', () => { - useSelector.mockReturnValue({ canAccessAdvancedSettings: false }); + jest.mocked(useSelector).mockReturnValue({ canAccessAdvancedSettings: false }); const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123')).result.current.map((item) => item.title); expect(actualItemsTitle).not.toContain('Advanced Settings'); }); @@ -137,4 +137,22 @@ describe('header utils', () => { expect(actualItemsTitle).not.toContain(messages['header.links.optimizer'].defaultMessage); }); }); + + describe('useLibrarySettingsMenuItems', () => { + it('should contain team access url', () => { + const items = renderHook(() => useLibrarySettingsMenuItems()).result.current; + expect(items).toContainEqual({ title: 'Team Access', href: 'http://localhost/?sa=manage-team' }); + }); + }); + + describe('useLibraryToolsMenuItems', () => { + it('should contain backup and import url', () => { + const items = renderHook(() => useLibraryToolsMenuItems('course-123')).result.current; + expect(items).toContainEqual({ + href: '/library/course-123/backup', + title: 'Backup to local archive' + }); + expect(items).toContainEqual({ href: '/library/course-123/import', title: 'Import' }); + }); + }); }); diff --git a/src/header/hooks.tsx b/src/header/hooks.tsx index 07cb1a12d3..8c3151df10 100644 --- a/src/header/hooks.tsx +++ b/src/header/hooks.tsx @@ -136,6 +136,10 @@ export const useLibraryToolsMenuItems = (itemId: string) => { href: `/library/${itemId}/backup`, title: intl.formatMessage(messages['header.links.exportLibrary']), }, + { + href: `/library/${itemId}/import`, + title: intl.formatMessage(messages['header.links.lib.import']), + }, ]; return items; diff --git a/src/header/index.js b/src/header/index.ts similarity index 100% rename from src/header/index.js rename to src/header/index.ts diff --git a/src/header/messages.js b/src/header/messages.ts similarity index 97% rename from src/header/messages.js rename to src/header/messages.ts index 4d0f4404bb..d1bb89a7b4 100644 --- a/src/header/messages.js +++ b/src/header/messages.ts @@ -96,6 +96,11 @@ const messages = defineMessages({ defaultMessage: 'Import', description: 'Link to Studio Import page', }, + 'header.links.lib.import': { + id: 'header.links.lib.import', + defaultMessage: 'Import', + description: 'Link to Course Import page in library', + }, 'header.links.exportCourse': { id: 'header.links.exportCourse', defaultMessage: 'Export Course', From a7b478f40e118881b486a68bf1b97f98b456b4f6 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 22 Oct 2025 17:12:39 +0530 Subject: [PATCH 3/6] fix: lint issues --- src/header/hooks.test.ts | 6 ++++-- src/header/hooks.tsx | 7 ++++--- src/library-authoring/common/context/SidebarContext.tsx | 9 ++------- src/library-authoring/library-team/LibraryTeamModal.tsx | 4 ++-- src/library-authoring/routes.ts | 6 +++++- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/header/hooks.test.ts b/src/header/hooks.test.ts index 57bd47cbb0..7dd5e0525a 100644 --- a/src/header/hooks.test.ts +++ b/src/header/hooks.test.ts @@ -2,7 +2,9 @@ import { useSelector } from 'react-redux'; import { getConfig, setConfig } from '@edx/frontend-platform'; import { renderHook } from '@testing-library/react'; import messages from './messages'; -import { useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems } from './hooks'; +import { + useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems, +} from './hooks'; import { mockWaffleFlags } from '../data/apiHooks.mock'; jest.mock('@edx/frontend-platform/i18n', () => ({ @@ -150,7 +152,7 @@ describe('header utils', () => { const items = renderHook(() => useLibraryToolsMenuItems('course-123')).result.current; expect(items).toContainEqual({ href: '/library/course-123/backup', - title: 'Backup to local archive' + title: 'Backup to local archive', }); expect(items).toContainEqual({ href: '/library/course-123/import', title: 'Import' }); }); diff --git a/src/header/hooks.tsx b/src/header/hooks.tsx index 8c3151df10..5d9d11972e 100644 --- a/src/header/hooks.tsx +++ b/src/header/hooks.tsx @@ -6,9 +6,10 @@ import { Badge } from '@openedx/paragon'; import { getPagePath } from '@src/utils'; import { useWaffleFlags } from '@src/data/apiHooks'; import { getStudioHomeData } from '@src/studio-home/data/selectors'; -import messages from './messages'; import courseOptimizerMessages from '@src/optimizer-page/messages'; -import { LibQueryParamKeys, SidebarActions } from '@src/library-authoring/common/context/SidebarContext'; +import { SidebarActions } from '@src/library-authoring/common/context/SidebarContext'; +import { LibQueryParamKeys } from '@src/library-authoring/routes'; +import messages from './messages'; export const useContentMenuItems = (courseId: string) => { const intl = useIntl(); @@ -153,7 +154,7 @@ export const useLibrarySettingsMenuItems = () => { // Set ?sa=manage-team in url which in turn opens team access modal url.searchParams.set(LibQueryParamKeys.SidebarActions, SidebarActions.ManageTeam); return url.toString(); - } + }; const items = [ { diff --git a/src/library-authoring/common/context/SidebarContext.tsx b/src/library-authoring/common/context/SidebarContext.tsx index 918f52cf5d..a3eb59b1ad 100644 --- a/src/library-authoring/common/context/SidebarContext.tsx +++ b/src/library-authoring/common/context/SidebarContext.tsx @@ -7,10 +7,10 @@ import { useState, } from 'react'; import { useParams } from 'react-router-dom'; -import { useStateWithUrlSearchParam } from '../../../hooks'; +import { useStateWithUrlSearchParam } from '@src/hooks'; +import { LibQueryParamKeys, useLibraryRoutes } from '@src/library-authoring/routes'; import { useComponentPickerContext } from './ComponentPickerContext'; import { useLibraryContext } from './LibraryContext'; -import { useLibraryRoutes } from '../../routes'; export enum SidebarBodyItemId { AddContent = 'add-content', @@ -81,11 +81,6 @@ export enum SidebarActions { None = '', } -export enum LibQueryParamKeys { - SidebarActions = 'sa', - SidebarTab = 'st', -} - export type SidebarContextData = { closeLibrarySidebar: () => void; openAddContentSidebar: () => void; diff --git a/src/library-authoring/library-team/LibraryTeamModal.tsx b/src/library-authoring/library-team/LibraryTeamModal.tsx index f3f66324f3..87eab3a2a3 100644 --- a/src/library-authoring/library-team/LibraryTeamModal.tsx +++ b/src/library-authoring/library-team/LibraryTeamModal.tsx @@ -1,10 +1,10 @@ import { StandardModal } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { useCallback } from 'react'; +import { SidebarActions, useSidebarContext } from '@src/library-authoring/common/context/SidebarContext'; import LibraryTeam from './LibraryTeam'; import messages from './messages'; -import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; -import { useCallback } from 'react'; export const LibraryTeamModal = () => { const intl = useIntl(); diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index a1b94e12a5..75a912755d 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -11,10 +11,14 @@ import { useSearchParams, } from 'react-router-dom'; import { ContainerType, getBlockType } from '../generic/key-utils'; -import { LibQueryParamKeys } from './common/context/SidebarContext'; export const BASE_ROUTE = '/library/:libraryId'; +export enum LibQueryParamKeys { + SidebarActions = 'sa', + SidebarTab = 'st', +} + export const ROUTES = { // LibraryAuthoringPage routes: // * Components tab, with an optionally selected component in the sidebar. From b1311907fdc38325fd13abbff82e10e56a0456e0 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 23 Oct 2025 11:16:35 +0530 Subject: [PATCH 4/6] fix: tests --- src/header/hooks.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/header/hooks.tsx b/src/header/hooks.tsx index 5d9d11972e..7e0e8a1026 100644 --- a/src/header/hooks.tsx +++ b/src/header/hooks.tsx @@ -150,6 +150,9 @@ export const useLibrarySettingsMenuItems = () => { const intl = useIntl(); const openTeamAccessModalUrl = () => { + if (!window.location.href) { + return null; + } const url = new URL(window.location.href); // Set ?sa=manage-team in url which in turn opens team access modal url.searchParams.set(LibQueryParamKeys.SidebarActions, SidebarActions.ManageTeam); From 7a516d697bb3d41a474b7d6ff83ff09dd6b318e9 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 3 Nov 2025 12:12:28 +0530 Subject: [PATCH 5/6] refactor: point to admin console if url set --- src/header/Header.tsx | 23 +++++++---- src/header/hooks.test.ts | 2 +- src/header/hooks.tsx | 39 +++++++++++++------ .../backup-restore/LibraryBackupPage.tsx | 3 +- .../collections/LibraryCollectionPage.tsx | 2 + .../LibrarySectionPage.tsx | 3 +- .../LibrarySubsectionPage.tsx | 3 +- .../units/LibraryUnitPage.tsx | 6 +-- 8 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/header/Header.tsx b/src/header/Header.tsx index 4a6f1eaae4..d756b236c7 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -20,6 +20,7 @@ interface HeaderProps { isHiddenMainMenu?: boolean, isLibrary?: boolean, containerProps?: ContainerPropsType, + readOnly?: boolean, } const Header = ({ @@ -30,6 +31,7 @@ const Header = ({ isHiddenMainMenu = false, isLibrary = false, containerProps = {}, + readOnly = false, }: HeaderProps) => { const intl = useIntl(); const waffleFlags = useWaffleFlags(); @@ -43,8 +45,8 @@ const Header = ({ const settingMenuItems = useSettingMenuItems(contextId); const toolsMenuItems = useToolsMenuItems(contextId); const libraryToolsMenuItems = useLibraryToolsMenuItems(contextId); - const libraryToolsSettingsItems = useLibrarySettingsMenuItems(); - const mainMenuDropdowns = !isLibrary ? [ + const libraryToolsSettingsItems = useLibrarySettingsMenuItems(contextId, readOnly); + let mainMenuDropdowns = !isLibrary ? [ { id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.content']), @@ -61,11 +63,6 @@ const Header = ({ items: toolsMenuItems, }, ] : [ - { - id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`, - buttonTitle: intl.formatMessage(messages['header.links.settings']), - items: libraryToolsSettingsItems, - }, { id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, buttonTitle: intl.formatMessage(messages['header.links.tools']), @@ -73,6 +70,18 @@ const Header = ({ }, ]; + // Include settings menu only if user is allowed to see them. + if (isLibrary && libraryToolsSettingsItems.length > 0) { + mainMenuDropdowns = [ + { + id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`, + buttonTitle: intl.formatMessage(messages['header.links.settings']), + items: libraryToolsSettingsItems, + }, + ...mainMenuDropdowns, + ]; + } + const getOutlineLink = () => { if (isLibrary) { return `/library/${contextId}`; diff --git a/src/header/hooks.test.ts b/src/header/hooks.test.ts index 7dd5e0525a..c06d383e0a 100644 --- a/src/header/hooks.test.ts +++ b/src/header/hooks.test.ts @@ -142,7 +142,7 @@ describe('header utils', () => { describe('useLibrarySettingsMenuItems', () => { it('should contain team access url', () => { - const items = renderHook(() => useLibrarySettingsMenuItems()).result.current; + const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current; expect(items).toContainEqual({ title: 'Team Access', href: 'http://localhost/?sa=manage-team' }); }); }); diff --git a/src/header/hooks.tsx b/src/header/hooks.tsx index 7e0e8a1026..d009e14834 100644 --- a/src/header/hooks.tsx +++ b/src/header/hooks.tsx @@ -146,25 +146,40 @@ export const useLibraryToolsMenuItems = (itemId: string) => { return items; }; -export const useLibrarySettingsMenuItems = () => { +export const useLibrarySettingsMenuItems = (itemId: string, readOnly: boolean) => { const intl = useIntl(); const openTeamAccessModalUrl = () => { - if (!window.location.href) { - return null; + const adminConsoleUrl = getConfig().ADMIN_CONSOLE_URL; + // always show link to admin console MFE if it is being used + const shouldShowAdminConsoleLink = !!adminConsoleUrl; + + // if the admin console MFE isn't being used, show team modal button for non–read-only users + const shouldShowTeamModalButton = !adminConsoleUrl && !readOnly; + if (shouldShowTeamModalButton) { + if (!window.location.href) { + return null; + } + const url = new URL(window.location.href); + // Set ?sa=manage-team in url which in turn opens team access modal + url.searchParams.set(LibQueryParamKeys.SidebarActions, SidebarActions.ManageTeam); + return url.toString(); } - const url = new URL(window.location.href); - // Set ?sa=manage-team in url which in turn opens team access modal - url.searchParams.set(LibQueryParamKeys.SidebarActions, SidebarActions.ManageTeam); - return url.toString(); + if (shouldShowAdminConsoleLink) { + return `${adminConsoleUrl}/authz/libraries/${itemId}`; + } + return null; }; - const items = [ - { + const items: { title: string; href: string }[] = []; + + const teamAccessUrl = openTeamAccessModalUrl(); + if (teamAccessUrl) { + items.push({ title: intl.formatMessage(messages['header.menu.teamAccess']), - href: openTeamAccessModalUrl(), - }, - ]; + href: teamAccessUrl, + }); + } return items; }; diff --git a/src/library-authoring/backup-restore/LibraryBackupPage.tsx b/src/library-authoring/backup-restore/LibraryBackupPage.tsx index d54a76563a..9cd51f0bf9 100644 --- a/src/library-authoring/backup-restore/LibraryBackupPage.tsx +++ b/src/library-authoring/backup-restore/LibraryBackupPage.tsx @@ -24,7 +24,7 @@ import { useContentLibrary } from '@src/library-authoring/data/apiHooks'; export const LibraryBackupPage = () => { const intl = useIntl(); - const { libraryId } = useLibraryContext(); + const { libraryId, readOnly } = useLibraryContext(); const [taskId, setTaskId] = useState(''); const [isMutationInProgress, setIsMutationInProgress] = useState(false); const timeoutRef = useRef(null); @@ -144,6 +144,7 @@ export const LibraryBackupPage = () => { title={libraryData.title} org={libraryData.org} contextId={libraryId} + readOnly={readOnly} isLibrary containerProps={{ size: undefined, diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 2b54615bd7..05a209a63a 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -107,6 +107,7 @@ const LibraryCollectionPage = () => { showOnlyPublished, extraFilter: contextExtraFilter, setCollectionId, + readOnly, } = useLibraryContext(); const { sidebarItemInfo } = useSidebarContext(); @@ -194,6 +195,7 @@ const LibraryCollectionPage = () => { title={libraryData.title} org={libraryData.org} contextId={libraryId} + readOnly={readOnly} isLibrary containerProps={{ size: undefined, diff --git a/src/library-authoring/section-subsections/LibrarySectionPage.tsx b/src/library-authoring/section-subsections/LibrarySectionPage.tsx index 63055a2c21..40035ca2e3 100644 --- a/src/library-authoring/section-subsections/LibrarySectionPage.tsx +++ b/src/library-authoring/section-subsections/LibrarySectionPage.tsx @@ -20,7 +20,7 @@ import { ContainerEditableTitle, FooterActions, HeaderActions } from '../contain /** Full library section page */ export const LibrarySectionPage = () => { const intl = useIntl(); - const { libraryId, containerId } = useLibraryContext(); + const { libraryId, containerId, readOnly } = useLibraryContext(); const { sidebarItemInfo, } = useSidebarContext(); @@ -84,6 +84,7 @@ export const LibrarySectionPage = () => { org={libraryData.org} contextId={libraryData.id} isLibrary + readOnly={readOnly} containerProps={{ size: undefined, }} diff --git a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx index e99509f013..64116a6cc2 100644 --- a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx +++ b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx @@ -22,7 +22,7 @@ import { ContainerEditableTitle, FooterActions, HeaderActions } from '../contain /** Full library subsection page */ export const LibrarySubsectionPage = () => { const intl = useIntl(); - const { libraryId, containerId } = useLibraryContext(); + const { libraryId, containerId, readOnly } = useLibraryContext(); const { sidebarItemInfo } = useSidebarContext(); const { data: libraryData, isPending: isLibPending } = useContentLibrary(libraryId); @@ -64,6 +64,7 @@ export const LibrarySubsectionPage = () => { title={libraryData.title} org={libraryData.org} contextId={libraryData.id} + readOnly={readOnly} isLibrary containerProps={{ size: undefined, diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx index f8fd6ea16e..9d447ef547 100644 --- a/src/library-authoring/units/LibraryUnitPage.tsx +++ b/src/library-authoring/units/LibraryUnitPage.tsx @@ -23,10 +23,7 @@ import { ContainerEditableTitle, FooterActions, HeaderActions } from '../contain export const LibraryUnitPage = () => { const intl = useIntl(); - const { - libraryId, - containerId, - } = useLibraryContext(); + const { libraryId, containerId, readOnly } = useLibraryContext(); // istanbul ignore if: this should never happen if (!containerId) { @@ -71,6 +68,7 @@ export const LibraryUnitPage = () => { org={libraryData.org} contextId={libraryId} isLibrary + readOnly={readOnly} containerProps={{ size: undefined, }} From 573e86454997eff9797e5df0beebf59f4b868553 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 3 Nov 2025 12:17:41 +0530 Subject: [PATCH 6/6] test: add tests --- src/header/Header.tsx | 12 +++++------- src/header/hooks.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/header/Header.tsx b/src/header/Header.tsx index d756b236c7..56768db2b3 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -62,13 +62,11 @@ const Header = ({ buttonTitle: intl.formatMessage(messages['header.links.tools']), items: toolsMenuItems, }, - ] : [ - { - id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, - buttonTitle: intl.formatMessage(messages['header.links.tools']), - items: libraryToolsMenuItems, - }, - ]; + ] : [{ + id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, + buttonTitle: intl.formatMessage(messages['header.links.tools']), + items: libraryToolsMenuItems, + }]; // Include settings menu only if user is allowed to see them. if (isLibrary && libraryToolsSettingsItems.length > 0) { diff --git a/src/header/hooks.test.ts b/src/header/hooks.test.ts index c06d383e0a..28fc53c249 100644 --- a/src/header/hooks.test.ts +++ b/src/header/hooks.test.ts @@ -145,6 +145,28 @@ describe('header utils', () => { const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current; expect(items).toContainEqual({ title: 'Team Access', href: 'http://localhost/?sa=manage-team' }); }); + it('should contain admin console url if set', () => { + setConfig({ + ...getConfig(), + ADMIN_CONSOLE_URL: 'http://admin-console.com', + }); + const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current; + expect(items).toContainEqual({ + title: 'Team Access', + href: 'http://admin-console.com/authz/libraries/library-123', + }); + }); + it('should contain admin console url if set and readOnly is true', () => { + setConfig({ + ...getConfig(), + ADMIN_CONSOLE_URL: 'http://admin-console.com', + }); + const items = renderHook(() => useLibrarySettingsMenuItems('library-123', true)).result.current; + expect(items).toContainEqual({ + title: 'Team Access', + href: 'http://admin-console.com/authz/libraries/library-123', + }); + }); }); describe('useLibraryToolsMenuItems', () => {