Skip to content

Commit 84ba64c

Browse files
committed
Merge branch 'master' into chris/FAL-4266-import-from-course-stepper
2 parents 3b32d20 + 6d619b9 commit 84ba64c

40 files changed

Lines changed: 794 additions & 86 deletions

package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"@openedx/paragon": "^23.5.0",
6565
"@redux-devtools/extension": "^3.3.0",
6666
"@reduxjs/toolkit": "1.9.7",
67-
"@tanstack/react-query": "5.90.5",
67+
"@tanstack/react-query": "5.90.6",
6868
"@tinymce/tinymce-react": "^6.0.0",
6969
"classnames": "2.5.1",
7070
"codemirror": "^6.0.0",

src/editors/sharedComponents/TinyMceWidget/hooks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,9 @@ export const editorConfig = ({
457457
valid_elements: '*[*]',
458458
// FIXME: this is passing 'utf-8', which is not a valid entity_encoding value. It should be 'named' etc.
459459
entity_encoding: 'utf-8' as any,
460+
// Protect self-closing <script /> tags from being mangled,
461+
// to preserve backwards compatibility with content that relied on this behavior
462+
protect: [/<script[^>]*\/>/g],
460463
},
461464
};
462465
};

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/legacy-libraries-migration/ConfirmationView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ export const ConfirmationView = ({
7474
{...messages.confirmationViewAlert}
7575
values={{
7676
count: legacyLibraries.length,
77-
libraryName: destination.title,
7877
b: BoldText,
7978
}}
8079
/>

src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,11 @@ describe('<LegacyLibMigrationPage />', () => {
245245
await user.click(nextButton);
246246
const alert = await screen.findByRole('alert');
247247
expect(await within(alert).findByText(
248-
/All content from the 1 legacy library you selected will be migrated to/i,
249-
)).toBeInTheDocument();
250-
expect(await within(alert).findByText(
251-
/test library 1/i,
248+
/All content from the legacy library you selected will be migrated to the Content Library you select/i,
252249
)).toBeInTheDocument();
253250

254251
const backButton = screen.getByRole('button', { name: /back/i });
255-
backButton.click();
252+
await user.click(backButton);
256253

257254
expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
258255
// The selected v2 library remains checked
@@ -357,10 +354,7 @@ describe('<LegacyLibMigrationPage />', () => {
357354
// Should show alert of ConfirmationView
358355
const alert = await screen.findByRole('alert');
359356
expect(await within(alert).findByText(
360-
/All content from the 3 legacy libraries you selected will be migrated to/i,
361-
)).toBeInTheDocument();
362-
expect(await within(alert).findByText(
363-
/test library 1/i,
357+
/All content from the 3 legacy libraries you selected will be migrated to the Content Library you select/i,
364358
)).toBeInTheDocument();
365359
expect(screen.getByText('MBA')).toBeInTheDocument();
366360
expect(screen.getByText('Legacy library 1')).toBeInTheDocument();
@@ -417,11 +411,7 @@ describe('<LegacyLibMigrationPage />', () => {
417411
// Should show alert of ConfirmationView
418412
const alert = await screen.findByRole('alert');
419413
expect(await within(alert).findByText(
420-
/All content from the 3 legacy libraries you selected will be migrated to /i,
421-
{ exact: false },
422-
)).toBeInTheDocument();
423-
expect(await within(alert).findByText(
424-
/test library 1/i,
414+
/All content from the 3 legacy libraries you selected will be migrated to the Content Library you select/i,
425415
{ exact: false },
426416
)).toBeInTheDocument();
427417
expect(screen.getByText('MBA')).toBeInTheDocument();

0 commit comments

Comments
 (0)