diff --git a/src/library-authoring/EmptyStates.tsx b/src/library-authoring/EmptyStates.tsx index a968bcaa0..e8e03cb4a 100644 --- a/src/library-authoring/EmptyStates.tsx +++ b/src/library-authoring/EmptyStates.tsx @@ -1,4 +1,5 @@ import React, { useContext } from 'react'; +import { useParams } from 'react-router'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button, Stack, @@ -7,16 +8,22 @@ import { Add } from '@openedx/paragon/icons'; import { ClearFiltersButton } from '../search-manager'; import messages from './messages'; import { LibraryContext } from './common/context'; +import { useContentLibrary } from './data/apiHooks'; export const NoComponents = () => { const { openAddContentSidebar } = useContext(LibraryContext); + const { libraryId } = useParams(); + const { data: libraryData } = useContentLibrary(libraryId); + const canEditLibrary = libraryData?.canEditLibrary ?? false; return ( - + {canEditLibrary && ( + + )} ); }; diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index fa425a54a..182a24577 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -235,6 +235,23 @@ describe('', () => { expect(getByText('You have not added any content to this library yet.')).toBeInTheDocument(); }); + it('show library without components without permission', async () => { + const data = { + ...libraryData, + canEditLibrary: false, + }; + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, data); + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + render(); + + expect(await screen.findByText('Content library')).toBeInTheDocument(); + + expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument(); + }); + it('show new content button', async () => { mockUseParams.mockReturnValue({ libraryId: libraryData.id }); axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); @@ -245,6 +262,21 @@ describe('', () => { expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument(); }); + it('read only state of library', async () => { + const data = { + ...libraryData, + canEditLibrary: false, + }; + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, data); + + render(); + expect(await screen.findByRole('heading')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /new/i })).not.toBeInTheDocument(); + + expect(screen.getByText('Read Only')).toBeInTheDocument(); + }); + it('show library without search results', async () => { mockUseParams.mockReturnValue({ libraryId: libraryData.id }); axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 02e7d9326..ff940aca0 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -2,12 +2,14 @@ import React, { useContext } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { useIntl } from '@edx/frontend-platform/i18n'; import { + Badge, Button, Col, Container, Icon, IconButton, Row, + Stack, Tab, Tabs, } from '@openedx/paragon'; @@ -42,18 +44,53 @@ enum TabList { collections = 'collections', } -const SubHeaderTitle = ({ title }: { title: string }) => { +interface HeaderActionsProps { + canEditLibrary: boolean; +} + +const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => { + const intl = useIntl(); + const { + openAddContentSidebar, + } = useContext(LibraryContext); + + if (!canEditLibrary) { + return null; + } + + return ( + + ); +}; + +const SubHeaderTitle = ({ title, canEditLibrary }: { title: string, canEditLibrary: boolean }) => { const intl = useIntl(); return ( - <> - {title} - - + + + {title} + + + { !canEditLibrary && ( +
+ + {intl.formatMessage(messages.readOnlyBadge)} + +
+ )} +
); }; @@ -67,7 +104,7 @@ const LibraryAuthoringPage = () => { const currentPath = location.pathname.split('/').pop(); const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home; - const { sidebarBodyComponent, openAddContentSidebar } = useContext(LibraryContext); + const { sidebarBodyComponent } = useContext(LibraryContext); const [searchParams] = useSearchParams(); @@ -102,18 +139,9 @@ const LibraryAuthoringPage = () => { > } + title={} subtitle={intl.formatMessage(messages.headingSubtitle)} - headerActions={[ - , - ]} + headerActions={} />
diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx index a68309812..c21036b30 100644 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ b/src/library-authoring/components/LibraryComponents.test.tsx @@ -21,6 +21,7 @@ const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; const mockUseLibraryBlockTypes = jest.fn(); const mockFetchNextPage = jest.fn(); const mockUseSearchContext = jest.fn(); +const mockUseContentLibrary = jest.fn(); const data = { totalHits: 1, @@ -75,6 +76,7 @@ const blockTypeData = { jest.mock('../data/apiHooks', () => ({ useLibraryBlockTypes: () => mockUseLibraryBlockTypes(), + useContentLibrary: () => mockUseContentLibrary(), })); jest.mock('../../search-manager', () => ({ @@ -128,9 +130,31 @@ describe('', () => { ...data, totalHits: 0, }); + mockUseContentLibrary.mockReturnValue({ + data: { + canEditLibrary: true, + }, + }); render(); expect(await screen.findByText(/you have not added any content to this library yet\./i)); + expect(screen.getByRole('button', { name: /add component/i })).toBeInTheDocument(); + }); + + it('should render empty state without add content button', async () => { + mockUseSearchContext.mockReturnValue({ + ...data, + totalHits: 0, + }); + mockUseContentLibrary.mockReturnValue({ + data: { + canEditLibrary: false, + }, + }); + + render(); + expect(await screen.findByText(/you have not added any content to this library yet\./i)); + expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument(); }); it('should render components in full variant', async () => { diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 0e4d97872..24140abac 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -19,10 +19,7 @@ type LibraryComponentsProps = { * - 'full': Show all components with Infinite scroll pagination. * - 'preview': Show first 4 components without pagination. */ -const LibraryComponents = ({ - libraryId, - variant, -}: LibraryComponentsProps) => { +const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => { const { hits, totalHits: componentCount, diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index eb9a9f21f..38332b543 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -100,6 +100,11 @@ const messages = defineMessages({ defaultMessage: 'Close', description: 'Alt text of close button', }, + readOnlyBadge: { + id: 'course-authoring.library-authoring.badge.read-only', + defaultMessage: 'Read Only', + description: 'Text in badge when the user has read only access', + }, }); export default messages;