Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -20,6 +20,7 @@ interface HeaderProps {
isHiddenMainMenu?: boolean,
isLibrary?: boolean,
containerProps?: ContainerPropsType,
readOnly?: boolean,
}

const Header = ({
Expand All @@ -30,6 +31,7 @@ const Header = ({
isHiddenMainMenu = false,
isLibrary = false,
containerProps = {},
readOnly = false,
}: HeaderProps) => {
const intl = useIntl();
const waffleFlags = useWaffleFlags();
Expand All @@ -43,7 +45,8 @@ const Header = ({
const settingMenuItems = useSettingMenuItems(contextId);
const toolsMenuItems = useToolsMenuItems(contextId);
const libraryToolsMenuItems = useLibraryToolsMenuItems(contextId);
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']),
Expand All @@ -65,6 +68,18 @@ const Header = ({
items: libraryToolsMenuItems,
}];

// 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}`;
Expand Down
54 changes: 48 additions & 6 deletions src/header/hooks.test.js → src/header/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from './hooks';
import {
useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems,
} from './hooks';
import { mockWaffleFlags } from '../data/apiHooks.mock';

jest.mock('@edx/frontend-platform/i18n', () => ({
Expand All @@ -28,7 +30,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({
Expand All @@ -39,7 +41,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({
Expand All @@ -50,7 +52,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;
Expand All @@ -60,7 +62,7 @@ describe('header utils', () => {

describe('getSettingsMenuitems', () => {
beforeEach(() => {
useSelector.mockReturnValue({
jest.mocked(useSelector).mockReturnValue({
canAccessAdvancedSettings: true,
});
});
Expand All @@ -86,7 +88,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');
});
Expand Down Expand Up @@ -137,4 +139,44 @@ describe('header utils', () => {
expect(actualItemsTitle).not.toContain(messages['header.links.optimizer'].defaultMessage);
});
});

describe('useLibrarySettingsMenuItems', () => {
it('should contain team access url', () => {
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', () => {
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' });
});
});
});
60 changes: 52 additions & 8 deletions src/header/hooks.jsx → src/header/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ 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 courseOptimizerMessages from '@src/optimizer-page/messages';
import { SidebarActions } from '@src/library-authoring/common/context/SidebarContext';
import { LibQueryParamKeys } from '@src/library-authoring/routes';
import messages from './messages';
import courseOptimizerMessages from '../optimizer-page/messages';

export const useContentMenuItems = courseId => {
export const useContentMenuItems = (courseId: string) => {
const intl = useIntl();
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
const waffleFlags = useWaffleFlags();
Expand Down Expand Up @@ -50,7 +52,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);
Expand Down Expand Up @@ -89,7 +91,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();
Expand Down Expand Up @@ -127,15 +129,57 @@ export const useToolsMenuItems = (courseId) => {
return items;
};

export const useLibraryToolsMenuItems = itemId => {
export const useLibraryToolsMenuItems = (itemId: string) => {
const intl = useIntl();

const items = [
{
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;
};

export const useLibrarySettingsMenuItems = (itemId: string, readOnly: boolean) => {
const intl = useIntl();

const openTeamAccessModalUrl = () => {
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();
}
if (shouldShowAdminConsoleLink) {
return `${adminConsoleUrl}/authz/libraries/${itemId}`;
}
return null;
};

const items: { title: string; href: string }[] = [];

const teamAccessUrl = openTeamAccessModalUrl();
if (teamAccessUrl) {
items.push({
title: intl.formatMessage(messages['header.menu.teamAccess']),
href: teamAccessUrl,
});
}

return items;
};
File renamed without changes.
10 changes: 10 additions & 0 deletions src/header/messages.js → src/header/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -106,6 +111,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',
Expand Down
2 changes: 2 additions & 0 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<React.PropsWithChildren> = ({ children }) => {
const {
Expand Down Expand Up @@ -48,6 +49,7 @@ const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) =
<CreateCollectionModal />
<CreateContainerModal />
<ComponentEditorModal />
<LibraryTeamModal />
</SidebarProvider>
</LibraryProvider>
);
Expand Down
3 changes: 2 additions & 1 deletion src/library-authoring/backup-restore/LibraryBackupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>('');
const [isMutationInProgress, setIsMutationInProgress] = useState<boolean>(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
Expand Down Expand Up @@ -144,6 +144,7 @@ export const LibraryBackupPage = () => {
title={libraryData.title}
org={libraryData.org}
contextId={libraryId}
readOnly={readOnly}
isLibrary
containerProps={{
size: undefined,
Expand Down
2 changes: 2 additions & 0 deletions src/library-authoring/collections/LibraryCollectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const LibraryCollectionPage = () => {
showOnlyPublished,
extraFilter: contextExtraFilter,
setCollectionId,
readOnly,
} = useLibraryContext();
const { sidebarItemInfo } = useSidebarContext();

Expand Down Expand Up @@ -194,6 +195,7 @@ const LibraryCollectionPage = () => {
title={libraryData.title}
org={libraryData.org}
contextId={libraryId}
readOnly={readOnly}
isLibrary
containerProps={{
size: undefined,
Expand Down
8 changes: 4 additions & 4 deletions src/library-authoring/common/context/SidebarContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -129,14 +129,14 @@ export const SidebarProvider = ({

const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam<SidebarInfoTab>(
defaultTab.component,
'st',
LibQueryParamKeys.SidebarTab,
(value: string) => toSidebarInfoTab(value),
(value: SidebarInfoTab) => value.toString(),
);

const [sidebarAction, setSidebarAction] = useStateWithUrlSearchParam<SidebarActions>(
SidebarActions.None,
'sa',
LibQueryParamKeys.SidebarActions,
(value: string) => Object.values(SidebarActions).find((enumValue) => value === enumValue),
(value: SidebarActions) => value.toString(),
);
Expand Down
8 changes: 1 addition & 7 deletions src/library-authoring/library-info/LibraryInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,9 +23,6 @@ const LibraryInfo = () => {
const openLibraryTeamModal = useCallback(() => {
setSidebarAction(SidebarActions.ManageTeam);
}, [setSidebarAction]);
const closeLibraryTeamModal = useCallback(() => {
resetSidebarAction();
}, [resetSidebarAction]);

return (
<Stack direction="vertical" gap={2.5}>
Expand Down Expand Up @@ -81,7 +76,6 @@ const LibraryInfo = () => {
</span>
</Stack>
</Stack>
{isLibraryTeamModalOpen && <LibraryTeamModal onClose={closeLibraryTeamModal} />}
</Stack>
);
};
Expand Down
Loading