Skip to content

Commit a7645af

Browse files
authored
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
1 parent 7379e73 commit a7645af

File tree

6 files changed

+122
-29
lines changed

6 files changed

+122
-29
lines changed

src/library-authoring/EmptyStates.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useContext } from 'react';
2+
import { useParams } from 'react-router';
23
import { FormattedMessage } from '@edx/frontend-platform/i18n';
34
import {
45
Button, Stack,
@@ -7,16 +8,22 @@ import { Add } from '@openedx/paragon/icons';
78
import { ClearFiltersButton } from '../search-manager';
89
import messages from './messages';
910
import { LibraryContext } from './common/context';
11+
import { useContentLibrary } from './data/apiHooks';
1012

1113
export const NoComponents = () => {
1214
const { openAddContentSidebar } = useContext(LibraryContext);
15+
const { libraryId } = useParams();
16+
const { data: libraryData } = useContentLibrary(libraryId);
17+
const canEditLibrary = libraryData?.canEditLibrary ?? false;
1318

1419
return (
1520
<Stack direction="horizontal" gap={3} className="mt-6 justify-content-center">
1621
<FormattedMessage {...messages.noComponents} />
17-
<Button iconBefore={Add} onClick={() => openAddContentSidebar()}>
18-
<FormattedMessage {...messages.addComponent} />
19-
</Button>
22+
{canEditLibrary && (
23+
<Button iconBefore={Add} onClick={() => openAddContentSidebar()}>
24+
<FormattedMessage {...messages.addComponent} />
25+
</Button>
26+
)}
2027
</Stack>
2128
);
2229
};

src/library-authoring/LibraryAuthoringPage.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,23 @@ describe('<LibraryAuthoringPage />', () => {
235235
expect(getByText('You have not added any content to this library yet.')).toBeInTheDocument();
236236
});
237237

238+
it('show library without components without permission', async () => {
239+
const data = {
240+
...libraryData,
241+
canEditLibrary: false,
242+
};
243+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
244+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, data);
245+
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
246+
247+
render(<RootWrapper />);
248+
249+
expect(await screen.findByText('Content library')).toBeInTheDocument();
250+
251+
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
252+
expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument();
253+
});
254+
238255
it('show new content button', async () => {
239256
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
240257
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
@@ -245,6 +262,21 @@ describe('<LibraryAuthoringPage />', () => {
245262
expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
246263
});
247264

265+
it('read only state of library', async () => {
266+
const data = {
267+
...libraryData,
268+
canEditLibrary: false,
269+
};
270+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
271+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, data);
272+
273+
render(<RootWrapper />);
274+
expect(await screen.findByRole('heading')).toBeInTheDocument();
275+
expect(screen.queryByRole('button', { name: /new/i })).not.toBeInTheDocument();
276+
277+
expect(screen.getByText('Read Only')).toBeInTheDocument();
278+
});
279+
248280
it('show library without search results', async () => {
249281
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
250282
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

src/library-authoring/LibraryAuthoringPage.tsx

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import React, { useContext } from 'react';
22
import { StudioFooter } from '@edx/frontend-component-footer';
33
import { useIntl } from '@edx/frontend-platform/i18n';
44
import {
5+
Badge,
56
Button,
67
Col,
78
Container,
89
Icon,
910
IconButton,
1011
Row,
12+
Stack,
1113
Tab,
1214
Tabs,
1315
} from '@openedx/paragon';
@@ -42,18 +44,53 @@ enum TabList {
4244
collections = 'collections',
4345
}
4446

45-
const SubHeaderTitle = ({ title }: { title: string }) => {
47+
interface HeaderActionsProps {
48+
canEditLibrary: boolean;
49+
}
50+
51+
const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
52+
const intl = useIntl();
53+
const {
54+
openAddContentSidebar,
55+
} = useContext(LibraryContext);
56+
57+
if (!canEditLibrary) {
58+
return null;
59+
}
60+
61+
return (
62+
<Button
63+
iconBefore={Add}
64+
variant="primary rounded-0"
65+
onClick={() => openAddContentSidebar()}
66+
disabled={!canEditLibrary}
67+
>
68+
{intl.formatMessage(messages.newContentButton)}
69+
</Button>
70+
);
71+
};
72+
73+
const SubHeaderTitle = ({ title, canEditLibrary }: { title: string, canEditLibrary: boolean }) => {
4674
const intl = useIntl();
4775
return (
48-
<>
49-
{title}
50-
<IconButton
51-
src={InfoOutline}
52-
iconAs={Icon}
53-
alt={intl.formatMessage(messages.headingInfoAlt)}
54-
className="mr-2"
55-
/>
56-
</>
76+
<Stack direction="vertical">
77+
<Stack direction="horizontal">
78+
{title}
79+
<IconButton
80+
src={InfoOutline}
81+
iconAs={Icon}
82+
alt={intl.formatMessage(messages.headingInfoAlt)}
83+
className="mr-2"
84+
/>
85+
</Stack>
86+
{ !canEditLibrary && (
87+
<div>
88+
<Badge variant="primary" style={{ fontSize: '50%' }}>
89+
{intl.formatMessage(messages.readOnlyBadge)}
90+
</Badge>
91+
</div>
92+
)}
93+
</Stack>
5794
);
5895
};
5996

@@ -67,7 +104,7 @@ const LibraryAuthoringPage = () => {
67104

68105
const currentPath = location.pathname.split('/').pop();
69106
const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home;
70-
const { sidebarBodyComponent, openAddContentSidebar } = useContext(LibraryContext);
107+
const { sidebarBodyComponent } = useContext(LibraryContext);
71108

72109
const [searchParams] = useSearchParams();
73110

@@ -102,18 +139,9 @@ const LibraryAuthoringPage = () => {
102139
>
103140
<Container size="xl" className="p-4 mt-3">
104141
<SubHeader
105-
title={<SubHeaderTitle title={libraryData.title} />}
142+
title={<SubHeaderTitle title={libraryData.title} canEditLibrary={libraryData.canEditLibrary} />}
106143
subtitle={intl.formatMessage(messages.headingSubtitle)}
107-
headerActions={[
108-
<Button
109-
iconBefore={Add}
110-
variant="primary rounded-0"
111-
onClick={openAddContentSidebar}
112-
disabled={!libraryData.canEditLibrary}
113-
>
114-
{intl.formatMessage(messages.newContentButton)}
115-
</Button>,
116-
]}
144+
headerActions={<HeaderActions canEditLibrary={libraryData.canEditLibrary} />}
117145
/>
118146
<SearchKeywordsField className="w-50" />
119147
<div className="d-flex mt-3 align-items-center">

src/library-authoring/components/LibraryComponents.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
2121
const mockUseLibraryBlockTypes = jest.fn();
2222
const mockFetchNextPage = jest.fn();
2323
const mockUseSearchContext = jest.fn();
24+
const mockUseContentLibrary = jest.fn();
2425

2526
const data = {
2627
totalHits: 1,
@@ -75,6 +76,7 @@ const blockTypeData = {
7576

7677
jest.mock('../data/apiHooks', () => ({
7778
useLibraryBlockTypes: () => mockUseLibraryBlockTypes(),
79+
useContentLibrary: () => mockUseContentLibrary(),
7880
}));
7981

8082
jest.mock('../../search-manager', () => ({
@@ -128,9 +130,31 @@ describe('<LibraryComponents />', () => {
128130
...data,
129131
totalHits: 0,
130132
});
133+
mockUseContentLibrary.mockReturnValue({
134+
data: {
135+
canEditLibrary: true,
136+
},
137+
});
138+
139+
render(<RootWrapper />);
140+
expect(await screen.findByText(/you have not added any content to this library yet\./i));
141+
expect(screen.getByRole('button', { name: /add component/i })).toBeInTheDocument();
142+
});
143+
144+
it('should render empty state without add content button', async () => {
145+
mockUseSearchContext.mockReturnValue({
146+
...data,
147+
totalHits: 0,
148+
});
149+
mockUseContentLibrary.mockReturnValue({
150+
data: {
151+
canEditLibrary: false,
152+
},
153+
});
131154

132155
render(<RootWrapper />);
133156
expect(await screen.findByText(/you have not added any content to this library yet\./i));
157+
expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument();
134158
});
135159

136160
it('should render components in full variant', async () => {

src/library-authoring/components/LibraryComponents.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ type LibraryComponentsProps = {
1919
* - 'full': Show all components with Infinite scroll pagination.
2020
* - 'preview': Show first 4 components without pagination.
2121
*/
22-
const LibraryComponents = ({
23-
libraryId,
24-
variant,
25-
}: LibraryComponentsProps) => {
22+
const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => {
2623
const {
2724
hits,
2825
totalHits: componentCount,

src/library-authoring/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ const messages = defineMessages({
100100
defaultMessage: 'Close',
101101
description: 'Alt text of close button',
102102
},
103+
readOnlyBadge: {
104+
id: 'course-authoring.library-authoring.badge.read-only',
105+
defaultMessage: 'Read Only',
106+
description: 'Text in badge when the user has read only access',
107+
},
103108
});
104109

105110
export default messages;

0 commit comments

Comments
 (0)