fix: UI fixes for read-only libraries etc. (#1198)
* fix: Hide open Create content buttons without permissions * feat: Read only badge on library Home * refactor: library authoring to get canEditLibrary from useContentLibrary * style: Typo on the code
This commit is contained in:
@@ -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 (
|
||||
<Stack direction="horizontal" gap={3} className="mt-6 justify-content-center">
|
||||
<FormattedMessage {...messages.noComponents} />
|
||||
<Button iconBefore={Add} onClick={() => openAddContentSidebar()}>
|
||||
<FormattedMessage {...messages.addComponent} />
|
||||
</Button>
|
||||
{canEditLibrary && (
|
||||
<Button iconBefore={Add} onClick={() => openAddContentSidebar()}>
|
||||
<FormattedMessage {...messages.addComponent} />
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -235,6 +235,23 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
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(<RootWrapper />);
|
||||
|
||||
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('<LibraryAuthoringPage />', () => {
|
||||
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(<RootWrapper />);
|
||||
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);
|
||||
|
||||
@@ -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 (
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="primary rounded-0"
|
||||
onClick={() => openAddContentSidebar()}
|
||||
disabled={!canEditLibrary}
|
||||
>
|
||||
{intl.formatMessage(messages.newContentButton)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const SubHeaderTitle = ({ title, canEditLibrary }: { title: string, canEditLibrary: boolean }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
{title}
|
||||
<IconButton
|
||||
src={InfoOutline}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.headingInfoAlt)}
|
||||
className="mr-2"
|
||||
/>
|
||||
</>
|
||||
<Stack direction="vertical">
|
||||
<Stack direction="horizontal">
|
||||
{title}
|
||||
<IconButton
|
||||
src={InfoOutline}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.headingInfoAlt)}
|
||||
className="mr-2"
|
||||
/>
|
||||
</Stack>
|
||||
{ !canEditLibrary && (
|
||||
<div>
|
||||
<Badge variant="primary" style={{ fontSize: '50%' }}>
|
||||
{intl.formatMessage(messages.readOnlyBadge)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
>
|
||||
<Container size="xl" className="p-4 mt-3">
|
||||
<SubHeader
|
||||
title={<SubHeaderTitle title={libraryData.title} />}
|
||||
title={<SubHeaderTitle title={libraryData.title} canEditLibrary={libraryData.canEditLibrary} />}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
headerActions={[
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="primary rounded-0"
|
||||
onClick={openAddContentSidebar}
|
||||
disabled={!libraryData.canEditLibrary}
|
||||
>
|
||||
{intl.formatMessage(messages.newContentButton)}
|
||||
</Button>,
|
||||
]}
|
||||
headerActions={<HeaderActions canEditLibrary={libraryData.canEditLibrary} />}
|
||||
/>
|
||||
<SearchKeywordsField className="w-50" />
|
||||
<div className="d-flex mt-3 align-items-center">
|
||||
|
||||
@@ -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('<LibraryComponents />', () => {
|
||||
...data,
|
||||
totalHits: 0,
|
||||
});
|
||||
mockUseContentLibrary.mockReturnValue({
|
||||
data: {
|
||||
canEditLibrary: true,
|
||||
},
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
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(<RootWrapper />);
|
||||
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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user