From 9fef2704bc6e5d1ef3484672003b536e821255ca Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Thu, 30 Oct 2025 07:31:30 +1100 Subject: [PATCH] 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 --- package-lock.json | 68 ++++++++--- package.json | 2 +- src/authz-module/components/AuthZTitle.tsx | 88 ++++++++------ src/authz-module/data/api.ts | 17 ++- src/authz-module/data/hooks.test.tsx | 78 +++++++++++++ src/authz-module/data/hooks.ts | 26 +++++ src/authz-module/index.scss | 19 ++- .../LibrariesTeamManager.test.tsx | 110 +++++++++++++----- .../LibrariesTeamManager.tsx | 7 +- .../components/PublicReadToggle.tsx | 45 +++++++ .../libraries-manager/components/messages.ts | 15 +++ src/types.ts | 1 + 12 files changed, 388 insertions(+), 88 deletions(-) create mode 100644 src/authz-module/libraries-manager/components/PublicReadToggle.tsx diff --git a/package-lock.json b/package-lock.json index c2ceb41..52f45b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 9aec8d8..18ebedb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/authz-module/components/AuthZTitle.tsx b/src/authz-module/components/AuthZTitle.tsx index 19c6c1a..916a708 100644 --- a/src/authz-module/components/AuthZTitle.tsx +++ b/src/authz-module/components/AuthZTitle.tsx @@ -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) => ( + +); + const AuthZTitle = ({ activeLabel, navLinks = [], pageTitle, pageSubtitle, actions = [], -}: AuthZTitleProps) => ( - - - - -

{pageTitle}

- {typeof pageSubtitle === 'string' - ?

{pageSubtitle}

- : pageSubtitle} - - -
- { - actions.map((action) => { - if (isValidElement(action)) { - return action; - } - - const { label, icon, onClick } = action as Action; +}: AuthZTitleProps) => { + const isDesktop = useMediaQuery({ minWidth: breakpoints.large.minWidth }); + return ( + + + + +

{pageTitle}

+ {typeof pageSubtitle === 'string' + ?

{pageSubtitle}

+ : pageSubtitle} + + + + { + actions.map((action, index) => { + const content = isValidElement(action) + ? action + : ; + const key = isValidElement(action) + ? action.key + : (action as Action).label; return ( - + + {content} + {(index === actions.length - 1) ? null + : (
)} +
); }) } -
- -
-
-); + + + + + ); +}; export default AuthZTitle; diff --git a/src/authz-module/data/api.ts b/src/authz-module/data/api.ts index 6fe08a8..2ff3724 100644 --- a/src/authz-module/data/api.ts +++ b/src/authz-module/data/api.ts @@ -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 => 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 => { + 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, + }; +}; diff --git a/src/authz-module/data/hooks.test.tsx b/src/authz-module/data/hooks.test.tsx index d55c7e0..0c269ad 100644 --- a/src/authz-module/data/hooks.test.tsx +++ b/src/authz-module/data/hooks.test.tsx @@ -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 }) => ( + {children} + ); + + 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 }) => ( + {children} + ); + + 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(); + }); +}); diff --git a/src/authz-module/data/hooks.ts b/src/authz-module/data/hooks.ts index c47ec60..3483550 100644 --- a/src/authz-module/data/hooks.ts +++ b/src/authz-module/data/hooks.ts @@ -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 + }) => updateLibrary(libraryId, updatedData), + onSuccess: (data) => { + queryClient.setQueryData(authzQueryKeys.library(data.id), data); + }, + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ queryKey: authzQueryKeys.library(variables.libraryId) }); + }, + }); +}; diff --git a/src/authz-module/index.scss b/src/authz-module/index.scss index 96eb1a6..f7be36b 100644 --- a/src/authz-module/index.scss +++ b/src/authz-module/index.scss @@ -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); -} +} \ No newline at end of file diff --git a/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx b/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx index 9e5ffa4..75eb88d 100644 --- a/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx +++ b/src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx @@ -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(); 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(); - + 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(); + 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(); + 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(); + }); }); diff --git a/src/authz-module/libraries-manager/LibrariesTeamManager.tsx b/src/authz-module/libraries-manager/LibrariesTeamManager.tsx index 11dcd2d..2c0b082 100644 --- a/src/authz-module/libraries-manager/LibrariesTeamManager.tsx +++ b/src/authz-module/libraries-manager/LibrariesTeamManager.tsx @@ -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 - ? [] - : [] + [, + ...(canManageTeam ? [] : []), + ] } > { + 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 ( + {intl.formatMessage(messages['libraries.authz.public.read.toggle.subtext'])} + } + > + {intl.formatMessage(messages['libraries.authz.public.read.toggle.label'])} + + ); +}; + +export default PublicReadToggle; diff --git a/src/authz-module/libraries-manager/components/messages.ts b/src/authz-module/libraries-manager/components/messages.ts index b69444f..1281ede 100644 --- a/src/authz-module/libraries-manager/components/messages.ts +++ b/src/authz-module/libraries-manager/components/messages.ts @@ -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; diff --git a/src/types.ts b/src/types.ts index 4e9fef5..5def79c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export interface LibraryMetadata { org: string; title: string; slug: string; + allowPublicRead: boolean; } export interface RoleMetadata {