feat(authz): [FC-0099] create toggle to make a library public read (#25)

* refactor: modify actions to add splitter

* feat: add API function to update the library metadata

* feat: create a toggle component to make the library public read

* refactor: update behaviour depending on user permissions

* feat: add success toast

* refactor: use stack to display the actions

* style: fix typo

* style: add mx-5 to divider to avoid wide growth

* chore: update paragon to work with useMediaQuery
This commit is contained in:
Diana Olarte
2025-10-30 07:31:30 +11:00
committed by GitHub
parent 8dc3139ef9
commit 9fef2704bc
12 changed files with 388 additions and 88 deletions

68
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@edx/frontend-platform": "^8.3.0",
"@edx/openedx-atlas": "^0.7.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@openedx/paragon": "^23.15.1",
"@tanstack/react-query": "5.89.0",
"lodash.debounce": "^4.0.8",
"react": "^18.3.1",
@@ -6971,9 +6971,9 @@
}
},
"node_modules/@openedx/paragon": {
"version": "23.14.2",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.14.2.tgz",
"integrity": "sha512-mBsoH9nwt4VGkoE9y33BrSJsjTzWlKjooWGXeJng4LdFNnBy7bhtEvRENQ9/0L0/trWhEMZffAMP7h9HBfg5EQ==",
"version": "23.15.1",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.15.1.tgz",
"integrity": "sha512-uqbKE5pfLLdEaTltd27nyyV/enjOjPkkINES/LRBZXwRgGWhQh+vH2xA+iXigwvGGeWdzuxnJ0lXyfiUR/R7Ig==",
"license": "Apache-2.0",
"workspaces": [
"example",
@@ -6985,7 +6985,7 @@
"dependencies": {
"@popperjs/core": "^2.11.4",
"@tokens-studio/sd-transforms": "^1.2.4",
"axios": "^0.27.2",
"axios": "^0.30.2",
"bootstrap": "^4.6.2",
"chalk": "^4.1.2",
"child_process": "^1.0.2",
@@ -6994,7 +6994,7 @@
"cli-progress": "^3.12.0",
"commander": "^9.4.1",
"email-prop-type": "^3.0.0",
"file-selector": "^0.6.0",
"file-selector": "^0.10.0",
"glob": "^8.0.3",
"inquirer": "^8.2.5",
"js-toml": "^1.0.0",
@@ -7019,11 +7019,11 @@
"react-loading-skeleton": "^3.1.0",
"react-popper": "^2.2.5",
"react-proptype-conditional-require": "^1.0.4",
"react-responsive": "^8.2.0",
"react-responsive": "^10.0.0",
"react-table": "^7.7.0",
"react-transition-group": "^4.4.2",
"sass": "^1.58.3",
"style-dictionary": "^4.3.2",
"style-dictionary": "^4.4.0",
"tabbable": "^5.3.3",
"uncontrollable": "^7.2.1",
"uuid": "^9.0.0"
@@ -7038,13 +7038,14 @@
}
},
"node_modules/@openedx/paragon/node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz",
"integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
"follow-redirects": "^1.15.4",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/@openedx/paragon/node_modules/brace-expansion": {
@@ -7085,6 +7086,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@openedx/paragon/node_modules/matchmediaquery": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz",
"integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==",
"license": "MIT",
"dependencies": {
"css-mediaquery": "^0.1.2"
}
},
"node_modules/@openedx/paragon/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@@ -7125,6 +7135,30 @@
"postcss": "^8.4"
}
},
"node_modules/@openedx/paragon/node_modules/react-responsive": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.1.tgz",
"integrity": "sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==",
"license": "MIT",
"dependencies": {
"hyphenate-style-name": "^1.0.0",
"matchmediaquery": "^0.4.2",
"prop-types": "^15.6.1",
"shallow-equal": "^3.1.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@openedx/paragon/node_modules/shallow-equal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
"integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
"license": "MIT"
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
@@ -14489,12 +14523,12 @@
}
},
"node_modules/file-selector": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.10.0.tgz",
"integrity": "sha512-iXLQxZTDe9qtBDkpaU4msOWNbh/4JxYSux7BsVxgt+0HBCpj9qPUFjD3SDBPLCJDoU3MsJh1i+CseQ/9488F/A==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"

View File

@@ -41,7 +41,7 @@
"@edx/frontend-platform": "^8.3.0",
"@edx/openedx-atlas": "^0.7.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@openedx/paragon": "^23.15.1",
"@tanstack/react-query": "5.89.0",
"lodash.debounce": "^4.0.8",
"react": "^18.3.1",

View File

@@ -1,7 +1,12 @@
import { ComponentType, isValidElement, ReactNode } from 'react';
import {
ComponentType, isValidElement, ReactNode, Fragment,
} from 'react';
import { Link } from 'react-router-dom';
import {
Breadcrumb, Col, Container, Row, Button, Badge,
Stack,
useMediaQuery,
breakpoints,
} from '@openedx/paragon';
interface BreadcrumbLink {
@@ -23,46 +28,57 @@ export interface AuthZTitleProps {
actions?: (Action | ReactNode)[];
}
export const ActionButton = ({ label, icon, onClick }: Action) => (
<Button
iconBefore={icon}
onClick={onClick}
>
{label}
</Button>
);
const AuthZTitle = ({
activeLabel, navLinks = [], pageTitle, pageSubtitle, actions = [],
}: AuthZTitleProps) => (
<Container className="p-5 bg-light-100">
<Breadcrumb
linkAs={Link}
links={navLinks}
activeLabel={activeLabel}
/>
<Row className="mt-4">
<Col xs={12} md={8} className="mb-4">
<h1 className="text-primary">{pageTitle}</h1>
{typeof pageSubtitle === 'string'
? <h3><Badge className="py-2 px-3 font-weight-normal" variant="light">{pageSubtitle}</Badge></h3>
: pageSubtitle}
</Col>
<Col xs={12} md={4}>
<div className="d-flex justify-content-md-end">
{
actions.map((action) => {
if (isValidElement(action)) {
return action;
}
const { label, icon, onClick } = action as Action;
}: AuthZTitleProps) => {
const isDesktop = useMediaQuery({ minWidth: breakpoints.large.minWidth });
return (
<Container className="p-5 bg-light-100">
<Breadcrumb
linkAs={Link}
links={navLinks}
activeLabel={activeLabel}
/>
<Row className="mt-4">
<Col xs={12} md={7} className="mb-4">
<h1 className="text-primary">{pageTitle}</h1>
{typeof pageSubtitle === 'string'
? <h3><Badge className="py-2 px-3 font-weight-normal" variant="light">{pageSubtitle}</Badge></h3>
: pageSubtitle}
</Col>
<Col xs={12} md={5}>
<Stack className="justify-content-end" direction={isDesktop ? 'horizontal' : 'vertical'}>
{
actions.map((action, index) => {
const content = isValidElement(action)
? action
: <ActionButton {...action as Action} />;
const key = isValidElement(action)
? action.key
: (action as Action).label;
return (
<Button
key={`authz-header-action-${label}`}
iconBefore={icon}
onClick={onClick}
>
{label}
</Button>
<Fragment key={`authz-header-action-${key}`}>
{content}
{(index === actions.length - 1) ? null
: (<hr className="mx-lg-5" />)}
</Fragment>
);
})
}
</div>
</Col>
</Row>
</Container>
);
</Stack>
</Col>
</Row>
</Container>
);
};
export default AuthZTitle;

View File

@@ -1,6 +1,6 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { LibraryMetadata, TeamMember } from '@src/types';
import { camelCaseObject } from '@edx/frontend-platform';
import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform';
import { getApiUrl, getStudioApiUrl } from '@src/data/utils';
export interface QuerySettings {
@@ -85,6 +85,7 @@ export const getLibrary = async (libraryId: string): Promise<LibraryMetadata> =>
org: data.org,
title: data.title,
slug: data.slug,
allowPublicRead: data.allow_public_read,
};
};
@@ -107,3 +108,17 @@ export const revokeUserRoles = async (
const res = await getAuthenticatedHttpClient().delete(url.toString());
return camelCaseObject(res.data);
};
export const updateLibrary = async (libraryId, updatedData): Promise<LibraryMetadata> => {
const { data } = await getAuthenticatedHttpClient().patch(
getStudioApiUrl(`/api/libraries/v2/${libraryId}/`),
snakeCaseObject(updatedData),
);
return {
id: data.id,
org: data.org,
title: data.title,
slug: data.slug,
allowPublicRead: data.allow_public_read,
};
};

View File

@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
useLibrary, usePermissionsByRole, useTeamMembers, useAssignTeamMembersRole, useRevokeUserRoles,
useUpdateLibrary,
} from './hooks';
jest.mock('@edx/frontend-platform/auth', () => ({
@@ -340,3 +341,80 @@ describe('useRevokeUserRoles', () => {
expect(calledUrl.searchParams.get('scope')).toBe(revokeRoleData.scope);
});
});
describe('useUpdateLibrary', () => {
const queryKeyTest = ['org.openedx.frontend.app.adminConsole', 'authz', 'library', 'lib:123'];
beforeEach(() => {
jest.clearAllMocks();
});
it('calls updateLibrary with correct params and updates cache', async () => {
const mockData = { id: 'lib:123', title: 'Library Test' };
getAuthenticatedHttpClient.mockReturnValue({
patch: jest.fn().mockResolvedValue({ data: mockData }),
});
const { result } = renderHook(() => useUpdateLibrary(), { wrapper: createWrapper() });
await act(async () => {
await result.current.mutateAsync({
libraryId: 'lib:123',
updatedData: { title: 'Library Test' },
});
});
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
});
it('sets query data on success', async () => {
const mockData = { id: 'lib:123', title: 'Updated Library' };
getAuthenticatedHttpClient.mockReturnValue({
patch: jest.fn().mockResolvedValue({ data: mockData }),
});
const queryClient = new QueryClient();
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useUpdateLibrary(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
libraryId: 'lib:123',
updatedData: { title: 'Updated Library' },
});
});
// verify cache updated with the returned data
expect(queryClient.getQueryData(queryKeyTest)).toEqual(mockData);
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
});
it('invalidates query on settled', async () => {
const mockData = { id: 'lib:123', title: 'Final Title' };
getAuthenticatedHttpClient.mockReturnValue({
patch: jest.fn().mockResolvedValue({ data: mockData }),
});
const queryClient = new QueryClient();
const invalidateSpy = jest.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useUpdateLibrary(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
libraryId: 'lib:123',
updatedData: { title: 'Final Title' },
});
});
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: queryKeyTest,
});
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
});
});

View File

@@ -6,6 +6,7 @@ import { LibraryMetadata } from '@src/types';
import {
assignTeamMembersRole, AssignTeamMembersRoleRequest, getLibrary, getPermissionsByRole, getTeamMembers,
GetTeamMembersResponse, PermissionsByRole, QuerySettings, revokeUserRoles, RevokeUserRolesRequest,
updateLibrary,
} from './api';
const authzQueryKeys = {
@@ -106,3 +107,28 @@ export const useRevokeUserRoles = () => {
},
});
};
/**
* React Query hook to update the library metadata.
*
* @example
* const { mutate: updateLibrary } = useUpdateLibrary();
* updateLibrary({ libraryId: 'lib:123', updatedData: { title: 'Library Test' }});
*/
export const useUpdateLibrary = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ libraryId, updatedData }: {
libraryId: string;
updatedData: Partial<LibraryMetadata>
}) => updateLibrary(libraryId, updatedData),
onSuccess: (data) => {
queryClient.setQueryData(authzQueryKeys.library(data.id), data);
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: authzQueryKeys.library(variables.libraryId) });
},
});
};

View File

@@ -1,9 +1,26 @@
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
.authz-libraries {
--height-action-divider: 30px;
.pgn__breadcrumb li:first-child a {
color: var(--pgn-color-breadcrumb-active);
text-decoration: none;
}
hr {
border-top: var(--pgn-size-border-width) solid var(--pgn-color-border);
width: 100%;
}
@media (--pgn-size-breakpoint-min-width-lg) {
hr {
border-right: var(--pgn-size-border-width) solid var(--pgn-color-border);
height: var(--height-action-divider);
width: 0;
}
}
.tab-content {
background-color: var(--pgn-color-light-200);
}
@@ -48,4 +65,4 @@
// Move toast to the right
left: auto;
right: var(--pgn-spacing-toast-container-gutter-lg);
}
}

View File

@@ -2,9 +2,10 @@ import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWrapper } from '@src/setupTest';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import { useLibrary } from '@src/authz-module/data/hooks';
import { useLibrary, useUpdateLibrary } from '@src/authz-module/data/hooks';
import { useLibraryAuthZ } from './context';
import LibrariesTeamManager from './LibrariesTeamManager';
import { ToastManagerProvider } from './ToastManagerContext';
jest.mock('./context', () => {
const actual = jest.requireActual('./context');
@@ -18,6 +19,7 @@ const mockedUseLibraryAuthZ = useLibraryAuthZ as jest.Mock;
jest.mock('@src/authz-module/data/hooks', () => ({
useLibrary: jest.fn(),
useUpdateLibrary: jest.fn(),
}));
jest.mock('./components/TeamTable', () => ({
@@ -45,45 +47,56 @@ jest.mock('../components/RoleCard', () => ({
),
}));
const renderTeamManager = () => renderWrapper(<ToastManagerProvider><LibrariesTeamManager /></ToastManagerProvider>);
describe('LibrariesTeamManager', () => {
const libraryData = {
id: 'lib-001',
title: 'Test Library',
org: 'Test Org',
allowPublicRead: false,
};
const mutate = jest.fn();
const libraryAuthZContext = {
libraryId: libraryData.id,
libraryName: libraryData.title,
libraryOrg: libraryData.org,
username: 'mockuser',
roles: [
{
name: 'Instructor',
description: 'Can manage content.',
userCount: 3,
permissions: ['view', 'edit'],
},
],
permissions: [
{ key: 'view_library', label: 'view', resource: 'library' },
{ key: 'edit_library', label: 'edit', resource: 'library' },
],
resources: [{ key: 'library', label: 'Library' }],
canManageTeam: true,
};
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
username: 'admin',
},
});
mockedUseLibraryAuthZ.mockReturnValue({
libraryId: 'lib-001',
libraryName: 'Mock Library',
libraryOrg: 'MockOrg',
username: 'mockuser',
roles: [
{
name: 'Instructor',
description: 'Can manage content.',
userCount: 3,
permissions: ['view', 'edit'],
},
],
permissions: [
{ key: 'view_library', label: 'view', resource: 'library' },
{ key: 'edit_library', label: 'edit', resource: 'library' },
],
resources: [{ key: 'library', label: 'Library' }],
canManageTeam: true,
});
jest.resetAllMocks();
mockedUseLibraryAuthZ.mockReturnValue(libraryAuthZContext);
(useLibrary as jest.Mock).mockReturnValue({
data: {
title: 'Test Library',
org: 'Test Org',
},
data: libraryData,
});
(useUpdateLibrary as jest.Mock).mockReturnValue({
mutate,
isPending: false,
});
});
it('renders tabs and layout content correctly', () => {
renderWrapper(<LibrariesTeamManager />);
renderTeamManager();
// Tabs
expect(screen.getByRole('tab', { name: /Team Members/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /Roles/i })).toBeInTheDocument();
@@ -103,7 +116,7 @@ describe('LibrariesTeamManager', () => {
it('renders role cards when "Roles" tab is selected', async () => {
const user = userEvent.setup();
renderWrapper(<LibrariesTeamManager />);
renderTeamManager();
// Click on "Roles" tab
const rolesTab = await screen.findByRole('tab', { name: /roles/i });
@@ -120,7 +133,7 @@ describe('LibrariesTeamManager', () => {
it('renders role matrix when "Permissions" tab is selected', async () => {
const user = userEvent.setup();
renderWrapper(<LibrariesTeamManager />);
renderTeamManager();
// Click on "Permissions" tab
const permissionsTab = await screen.findByRole('tab', { name: /permissions/i });
@@ -134,4 +147,43 @@ describe('LibrariesTeamManager', () => {
expect(matrixScope.getByText('edit')).toBeInTheDocument();
expect(matrixScope.getByText('view')).toBeInTheDocument();
});
it('renders allow public library read toggle and change the value by user interaction', async () => {
const user = userEvent.setup();
renderTeamManager();
const readPublicToggle = await screen.findByRole('switch', { name: /Allow public read/i });
await user.click(readPublicToggle);
expect(mutate).toHaveBeenCalledWith(
{
libraryId: 'lib-001',
updatedData: { allowPublicRead: !libraryData.allowPublicRead },
},
expect.objectContaining({
onSuccess: expect.any(Function),
}),
);
});
it('should not render the toggle if the user can not manage team and the Library Public Read is disabled', () => {
(useLibrary as jest.Mock).mockReturnValue({ data: { ...libraryData, allowPublicRead: false } });
(useLibraryAuthZ as jest.Mock).mockReturnValue({ ...libraryAuthZContext, canManageTeam: false });
renderTeamManager();
expect(screen.queryByRole('switch', { name: /Allow public read/i })).not.toBeInTheDocument();
});
it('should render the toggle as disabled if the user can not manage team but the Library Public Read is enabled', async () => {
(useLibrary as jest.Mock).mockReturnValue({ data: { ...libraryData, allowPublicRead: true } });
(useLibraryAuthZ as jest.Mock).mockReturnValue({ ...libraryAuthZContext, canManageTeam: false });
renderTeamManager();
const readPublicToggle = await screen.findByRole('switch', { name: /Allow public read/i });
expect(readPublicToggle).toBeInTheDocument();
expect(readPublicToggle).toBeDisabled();
});
});

View File

@@ -14,6 +14,7 @@ import { AddNewTeamMemberTrigger } from './components/AddNewTeamMemberModal';
import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils';
import messages from './messages';
import PublicReadToggle from './components/PublicReadToggle';
const LibrariesTeamManager = () => {
const intl = useIntl();
@@ -46,9 +47,9 @@ const LibrariesTeamManager = () => {
pageTitle={pageTitle}
pageSubtitle={libraryId}
actions={
canManageTeam
? [<AddNewTeamMemberTrigger libraryId={libraryId} />]
: []
[<PublicReadToggle libraryId={libraryId} canEditToggle={canManageTeam} key="allow-public-read" />,
...(canManageTeam ? [<AddNewTeamMemberTrigger libraryId={libraryId} key="add-new-member" />] : []),
]
}
>
<Tabs

View File

@@ -0,0 +1,45 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import { useLibrary, useUpdateLibrary } from '@src/authz-module/data/hooks';
import { useToastManager } from '../ToastManagerContext';
import messages from './messages';
type PublicReadToggleProps = {
libraryId: string;
canEditToggle: boolean;
};
const PublicReadToggle = ({ libraryId, canEditToggle }: PublicReadToggleProps) => {
const intl = useIntl();
const { data: library } = useLibrary(libraryId);
const { mutate: updateLibrary, isPending } = useUpdateLibrary();
const { handleShowToast } = useToastManager();
const onChangeToggle = () => updateLibrary({
libraryId,
updatedData: { allowPublicRead: !library.allowPublicRead },
}, {
onSuccess: () => {
handleShowToast(intl.formatMessage(messages['libraries.authz.public.read.toggle.success']));
},
});
if (!library.allowPublicRead && !canEditToggle) {
return null;
}
return (
<Form.Switch
checked={library.allowPublicRead}
disabled={!canEditToggle || isPending}
onChange={onChangeToggle}
helperText={
<span>{intl.formatMessage(messages['libraries.authz.public.read.toggle.subtext'])}</span>
}
>
{intl.formatMessage(messages['libraries.authz.public.read.toggle.label'])}
</Form.Switch>
);
};
export default PublicReadToggle;

View File

@@ -61,6 +61,21 @@ const messages = defineMessages({
defaultMessage: 'Remove',
description: 'Libraries AuthZ remove button title',
},
'libraries.authz.public.read.toggle.label': {
id: 'libraries.authz.public.read.toggle.label',
defaultMessage: 'Allow public read',
description: 'Library label toggle to allow public read',
},
'libraries.authz.public.read.toggle.subtext': {
id: 'libraries.authz.public.read.toggle.subtext',
defaultMessage: 'Allows reuse of library content in courses.',
description: 'Library description toggle to allow public read',
},
'libraries.authz.public.read.toggle.success': {
id: 'libraries.authz.public.read.toggle.success',
defaultMessage: 'The library setting has been updated successfully.',
description: 'Success message for allow public read',
},
});
export default messages;

View File

@@ -21,6 +21,7 @@ export interface LibraryMetadata {
org: string;
title: string;
slug: string;
allowPublicRead: boolean;
}
export interface RoleMetadata {