Skip to content

Commit 4d5c583

Browse files
feat: moving allow public read switch from admin console (#2942)
1 parent 4d7d91b commit 4d5c583

File tree

6 files changed

+221
-1
lines changed

6 files changed

+221
-1
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import userEvent from '@testing-library/user-event';
2+
import { initializeMocks, render, screen } from '@src/testUtils';
3+
import PublicReadToggle from './PublicReadToggle';
4+
import messages from './messages';
5+
6+
jest.mock('../data/apiHooks', () => ({
7+
useContentLibrary: jest.fn(),
8+
useUpdateLibraryMetadata: jest.fn(),
9+
}));
10+
11+
const mockUseContentLibrary = require('../data/apiHooks').useContentLibrary;
12+
const mockUseUpdateLibraryMetadata = require('../data/apiHooks').useUpdateLibraryMetadata;
13+
14+
let mockShowToast;
15+
16+
describe('PublicReadToggle', () => {
17+
beforeEach(() => {
18+
const mocks = initializeMocks();
19+
mockShowToast = mocks.mockShowToast;
20+
});
21+
22+
it('renders toggle when allowPublicRead is true and canEditToggle is true', () => {
23+
mockUseContentLibrary.mockReturnValue({ data: { allowPublicRead: true } });
24+
mockUseUpdateLibraryMetadata.mockReturnValue({ mutateAsync: jest.fn(), isPending: false });
25+
26+
render(
27+
<PublicReadToggle libraryId="lib1" canEditToggle />,
28+
);
29+
expect(screen.getByText(messages.publicReadToggleLabel.defaultMessage)).toBeInTheDocument();
30+
expect(screen.getByText(messages.publicReadToggleSubtext.defaultMessage)).toBeInTheDocument();
31+
});
32+
33+
it('toggle is disabled when canEditToggle is false', () => {
34+
mockUseContentLibrary.mockReturnValue({ data: { allowPublicRead: true } });
35+
mockUseUpdateLibraryMetadata.mockReturnValue({ mutateAsync: jest.fn(), isPending: false });
36+
37+
render(
38+
<PublicReadToggle libraryId="lib1" canEditToggle={false} />,
39+
);
40+
expect(screen.getByRole('switch')).toBeDisabled();
41+
});
42+
43+
it('calls updateLibrary when toggle is changed', async () => {
44+
const user = userEvent.setup();
45+
const mockMutateAsync = jest.fn().mockImplementation(() => Promise.resolve());
46+
mockUseContentLibrary.mockReturnValue({ data: { allowPublicRead: false } });
47+
mockUseUpdateLibraryMetadata.mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false });
48+
49+
render(
50+
<PublicReadToggle libraryId="lib1" canEditToggle />,
51+
);
52+
await user.click(screen.getByRole('switch'));
53+
expect(mockMutateAsync).toHaveBeenCalledWith(
54+
{
55+
id: 'lib1',
56+
allow_public_read: true,
57+
},
58+
);
59+
});
60+
61+
it('shows error toast when updateLibrary fails', async () => {
62+
const user = userEvent.setup();
63+
const mockMutateAsync = jest.fn();
64+
65+
const error = {
66+
customAttributes: {
67+
httpErrorStatus: 500,
68+
},
69+
};
70+
71+
mockMutateAsync.mockImplementation((_, options) => {
72+
if (options?.onError) {
73+
options.onError(error);
74+
}
75+
return Promise.reject(error);
76+
});
77+
78+
mockUseContentLibrary.mockReturnValue({ data: { allowPublicRead: false } });
79+
mockUseUpdateLibraryMetadata.mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false });
80+
81+
render(
82+
<PublicReadToggle libraryId="lib1" canEditToggle />,
83+
);
84+
85+
await user.click(screen.getByRole('switch'));
86+
87+
expect(mockMutateAsync).toHaveBeenCalledWith(
88+
{
89+
id: 'lib1',
90+
allow_public_read: true,
91+
},
92+
);
93+
94+
expect(mockShowToast).toHaveBeenCalledWith(messages.publicReadToggleDefaultError.defaultMessage);
95+
});
96+
97+
it('shows error toast when updateLibrary promise is rejected', async () => {
98+
const user = userEvent.setup();
99+
const mockMutateAsync = jest.fn().mockRejectedValue(new Error('Network error'));
100+
101+
mockUseContentLibrary.mockReturnValue({ data: { allowPublicRead: false } });
102+
mockUseUpdateLibraryMetadata.mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false });
103+
104+
render(
105+
<PublicReadToggle libraryId="lib1" canEditToggle />,
106+
);
107+
108+
await user.click(screen.getByRole('switch'));
109+
expect(mockMutateAsync).toHaveBeenCalledWith(
110+
{
111+
id: 'lib1',
112+
allow_public_read: true,
113+
},
114+
);
115+
expect(mockShowToast).toHaveBeenCalledWith(messages.publicReadToggleDefaultError.defaultMessage);
116+
});
117+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { Form } from '@openedx/paragon';
3+
import { ToastContext } from '@src/generic/toast-context';
4+
import { useContext } from 'react';
5+
import messages from './messages';
6+
import { useContentLibrary, useUpdateLibraryMetadata } from '../data/apiHooks';
7+
8+
type PublicReadToggleProps = {
9+
libraryId: string;
10+
canEditToggle: boolean;
11+
};
12+
13+
const PublicReadToggle = ({ libraryId, canEditToggle }: PublicReadToggleProps) => {
14+
const { formatMessage } = useIntl();
15+
const { data: library } = useContentLibrary(libraryId);
16+
const { mutateAsync: updateLibrary, isPending } = useUpdateLibraryMetadata();
17+
const { showToast } = useContext(ToastContext);
18+
19+
const onChangeToggle = async () => {
20+
await updateLibrary({
21+
id: libraryId,
22+
allow_public_read: !library?.allowPublicRead,
23+
}).catch(() => {
24+
showToast(formatMessage(messages.publicReadToggleDefaultError));
25+
});
26+
};
27+
28+
return (
29+
<Form.Switch
30+
checked={library?.allowPublicRead}
31+
disabled={!canEditToggle || isPending}
32+
onChange={onChangeToggle}
33+
helperText={
34+
<span>{formatMessage(messages.publicReadToggleSubtext)}</span>
35+
}
36+
>
37+
{formatMessage(messages.publicReadToggleLabel)}
38+
</Form.Switch>
39+
);
40+
};
41+
42+
export default PublicReadToggle;

src/library-authoring/components/messages.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,5 +246,20 @@ const messages = defineMessages({
246246
defaultMessage: 'Remove',
247247
description: 'Button to confirm removal of a container from its parent',
248248
},
249+
publicReadToggleLabel: {
250+
id: 'course-authoring.library-authoring.public.read.toggle.label',
251+
defaultMessage: 'Allow public read',
252+
description: 'Library label toggle to allow public read',
253+
},
254+
publicReadToggleSubtext: {
255+
id: 'course-authoring.library-authoring.public.read.toggle.subtext',
256+
defaultMessage: 'Allows reuse of library content in courses.',
257+
description: 'Library description toggle to allow public read',
258+
},
259+
publicReadToggleDefaultError: {
260+
id: 'course-authoring.library-authoring.public.read.toggle.default.error.message',
261+
defaultMessage: 'Something went wrong on our end. Please try again later.',
262+
description: 'Public read toggle default error message',
263+
},
249264
});
250265
export default messages;

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('<LibraryInfo />', () => {
4545
mockShowToast = mocks.mockShowToast;
4646
validateUserPermissionsMock = mocks.validateUserPermissionsMock;
4747

48-
validateUserPermissionsMock.mockResolvedValue({ canPublish: true });
48+
validateUserPermissionsMock.mockResolvedValue({ canPublish: true, canManageTeam: true });
4949
});
5050

5151
afterEach(() => {
@@ -285,4 +285,29 @@ describe('<LibraryInfo />', () => {
285285
expect(manageTeam).toBeInTheDocument();
286286
expect(manageTeam).toHaveAttribute('href', `${ADMIN_CONSOLE_URL}/authz/libraries/${libraryData.id}`);
287287
});
288+
289+
it('renders settings section title', () => {
290+
render();
291+
expect(screen.getByText('Settings')).toBeInTheDocument();
292+
});
293+
294+
it('renders PublicReadToggle when user can manage team', async () => {
295+
render();
296+
const allowSwitch = await screen.findByRole('switch', { name: /allow public read/i });
297+
expect(allowSwitch).toBeInTheDocument();
298+
await waitFor(() => {
299+
expect(allowSwitch).toBeEnabled();
300+
});
301+
});
302+
303+
it('renders PublicReadToggle in disabled mode when user can not manage team', async () => {
304+
validateUserPermissionsMock.mockResolvedValue({ canPublish: true, canManageTeam: false });
305+
306+
render();
307+
const allowSwitch = await screen.findByRole('switch', { name: /allow public read/i });
308+
expect(allowSwitch).toBeInTheDocument();
309+
await waitFor(() => {
310+
expect(allowSwitch).toBeDisabled();
311+
});
312+
});
288313
});

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,24 @@ import { Button, Hyperlink, Stack } from '@openedx/paragon';
33
import { getConfig } from '@edx/frontend-platform';
44
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
55

6+
import { CONTENT_LIBRARY_PERMISSIONS } from '@src/authz/constants';
7+
import { useUserPermissions } from '@src/authz/data/apiHooks';
68
import messages from './messages';
79
import LibraryPublishStatus from './LibraryPublishStatus';
810
import { useLibraryContext } from '../common/context/LibraryContext';
911
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
12+
import PublicReadToggle from '../components/PublicReadToggle';
1013

1114
const LibraryInfo = () => {
1215
const intl = useIntl();
1316
const { libraryId, libraryData, readOnly } = useLibraryContext();
1417
const { setSidebarAction } = useSidebarContext();
18+
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
19+
canManageTeam: {
20+
action: CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM,
21+
scope: libraryId,
22+
},
23+
}, typeof libraryId !== 'undefined');
1524
const adminConsoleUrl = getConfig().ADMIN_CONSOLE_URL;
1625

1726
// always show link to admin console MFE if it is being used
@@ -28,6 +37,13 @@ const LibraryInfo = () => {
2837
<Stack direction="vertical" gap={2.5}>
2938
<LibraryPublishStatus />
3039
<Stack gap={3} direction="vertical">
40+
<span className="font-weight-bold">
41+
{intl.formatMessage(messages.settingsSectionTitle)}
42+
</span>
43+
<PublicReadToggle
44+
libraryId={libraryId}
45+
canEditToggle={(!isLoadingUserPermissions && userPermissions?.canManageTeam) || false}
46+
/>
3147
<span className="font-weight-bold">
3248
{intl.formatMessage(messages.organizationSectionTitle)}
3349
</span>

src/library-authoring/library-info/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const messages = defineMessages({
1111
defaultMessage: 'Organization',
1212
description: 'Title for Organization section in Library info sidebar.',
1313
},
14+
settingsSectionTitle: {
15+
id: 'course-authoring.library-authoring.sidebar.info.settings.title',
16+
defaultMessage: 'Settings',
17+
description: 'Title for Settings section in Library info sidebar.',
18+
},
1419
libraryTeamButtonTitle: {
1520
id: 'course-authoring.library-authoring.sidebar.info.library-team.button.title',
1621
defaultMessage: 'Library Team',

0 commit comments

Comments
 (0)