Skip to content

Commit 591a02e

Browse files
chore: update with master
2 parents f90bbb2 + 95ac098 commit 591a02e

29 files changed

+1429
-229
lines changed

package-lock.json

Lines changed: 96 additions & 150 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/generic/clipboard/hooks/useCopyToClipboard.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
// @ts-check
12
import { useEffect, useState } from 'react';
2-
import { useSelector } from 'react-redux';
3+
import { useDispatch, useSelector } from 'react-redux';
34

5+
import { getClipboard } from '../../data/api';
6+
import { updateClipboardData } from '../../data/slice';
47
import { CLIPBOARD_STATUS, STRUCTURAL_XBLOCK_TYPES, STUDIO_CLIPBOARD_CHANNEL } from '../../../constants';
58
import { getClipboardData } from '../../data/selectors';
69

@@ -14,6 +17,7 @@ import { getClipboardData } from '../../data/selectors';
1417
* @property {Object} sharedClipboardData - The shared clipboard data object.
1518
*/
1619
const useCopyToClipboard = (canEdit = true) => {
20+
const dispatch = useDispatch();
1721
const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL));
1822
const [showPasteUnit, setShowPasteUnit] = useState(false);
1923
const [showPasteXBlock, setShowPasteXBlock] = useState(false);
@@ -30,6 +34,22 @@ const useCopyToClipboard = (canEdit = true) => {
3034
setShowPasteUnit(!!isPasteableUnit);
3135
};
3236

37+
// Called on initial render to fetch and populate the initial clipboard data in redux state.
38+
// Without this, the initial clipboard data redux state is always null.
39+
useEffect(() => {
40+
const fetchInitialClipboardData = async () => {
41+
try {
42+
const userClipboard = await getClipboard();
43+
dispatch(updateClipboardData(userClipboard));
44+
} catch (error) {
45+
// eslint-disable-next-line no-console
46+
console.error(`Failed to fetch initial clipboard data: ${error}`);
47+
}
48+
};
49+
50+
fetchInitialClipboardData();
51+
}, [dispatch]);
52+
3353
useEffect(() => {
3454
// Handle updates to clipboard data
3555
if (canEdit) {

src/index.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
@import "export-page/CourseExportPage";
1919
@import "import-page/CourseImportPage";
2020
@import "taxonomy";
21+
@import "library-authoring";
2122
@import "files-and-videos";
2223
@import "content-tags-drawer";
2324
@import "course-outline/CourseOutline";
@@ -30,7 +31,6 @@
3031
@import "search-manager";
3132
@import "certificates/scss/Certificates";
3233
@import "group-configurations/GroupConfigurations";
33-
@import "library-authoring";
3434

3535
// To apply the glow effect to the selected Section/Subsection, in the Course Outline
3636
div.row:has(> div > div.highlight) {

src/library-authoring/LibraryAuthoringPage.test.tsx

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,27 @@ const libraryData: ContentLibrary = {
8383
numBlocks: 2,
8484
version: 0,
8585
lastPublished: null,
86+
lastDraftCreated: '2024-07-22',
87+
publishedBy: 'staff',
88+
lastDraftCreatedBy: 'staff',
8689
allowLti: false,
8790
allowPublicLearning: false,
8891
allowPublicRead: false,
8992
hasUnpublishedChanges: true,
9093
hasUnpublishedDeletes: false,
9194
canEditLibrary: true,
9295
license: '',
96+
created: '2024-06-26',
97+
updated: '2024-07-20',
9398
};
9499

100+
const clipboardBroadcastChannelMock = {
101+
postMessage: jest.fn(),
102+
close: jest.fn(),
103+
};
104+
105+
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
106+
95107
const RootWrapper = () => (
96108
<AppProvider store={store}>
97109
<IntlProvider locale="en" messages={{}}>
@@ -177,23 +189,23 @@ describe('<LibraryAuthoringPage />', () => {
177189
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
178190

179191
const {
180-
getByRole, getByText, getAllByText, queryByText,
192+
getByRole, getByText, queryByText, findByText, findAllByText,
181193
} = render(<RootWrapper />);
182194

183195
// Ensure the search endpoint is called:
184196
// Call 1: To fetch searchable/filterable/sortable library data
185197
// Call 2: To fetch the recently modified components only
186198
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
187199

188-
expect(getByText('Content library')).toBeInTheDocument();
189-
expect(getByText(libraryData.title)).toBeInTheDocument();
200+
expect(await findByText('Content library')).toBeInTheDocument();
201+
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
190202

191203
expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
192204

193205
expect(getByText('Recently Modified')).toBeInTheDocument();
194206
expect(getByText('Collections (0)')).toBeInTheDocument();
195207
expect(getByText('Components (6)')).toBeInTheDocument();
196-
expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument();
208+
expect((await findAllByText('Test HTML Block'))[0]).toBeInTheDocument();
197209

198210
// Navigate to the components tab
199211
fireEvent.click(getByRole('tab', { name: 'Components' }));
@@ -222,10 +234,10 @@ describe('<LibraryAuthoringPage />', () => {
222234
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
223235
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
224236

225-
const { findByText, getByText } = render(<RootWrapper />);
237+
const { findByText, getByText, findAllByText } = render(<RootWrapper />);
226238

227239
expect(await findByText('Content library')).toBeInTheDocument();
228-
expect(await findByText(libraryData.title)).toBeInTheDocument();
240+
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
229241

230242
// Ensure the search endpoint is called:
231243
// Call 1: To fetch searchable/filterable/sortable library data
@@ -282,10 +294,15 @@ describe('<LibraryAuthoringPage />', () => {
282294
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
283295
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
284296

285-
const { findByText, getByRole, getByText } = render(<RootWrapper />);
297+
const {
298+
findByText,
299+
getByRole,
300+
getByText,
301+
findAllByText,
302+
} = render(<RootWrapper />);
286303

287304
expect(await findByText('Content library')).toBeInTheDocument();
288-
expect(await findByText(libraryData.title)).toBeInTheDocument();
305+
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
289306

290307
// Ensure the search endpoint is called:
291308
// Call 1: To fetch searchable/filterable/sortable library data
@@ -329,12 +346,54 @@ describe('<LibraryAuthoringPage />', () => {
329346
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
330347
});
331348

349+
it('should open Library Info by default', async () => {
350+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
351+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
352+
353+
render(<RootWrapper />);
354+
355+
expect(await screen.findByText('Content library')).toBeInTheDocument();
356+
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
357+
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();
358+
359+
expect(screen.getByText('Draft')).toBeInTheDocument();
360+
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
361+
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
362+
expect(screen.getByText('staff')).toBeInTheDocument();
363+
expect(screen.getByText(libraryData.org)).toBeInTheDocument();
364+
expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
365+
expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
366+
});
367+
368+
it('should close and open Library Info', async () => {
369+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
370+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
371+
372+
render(<RootWrapper />);
373+
374+
expect(await screen.findByText('Content library')).toBeInTheDocument();
375+
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
376+
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();
377+
378+
const closeButton = screen.getByRole('button', { name: /close/i });
379+
fireEvent.click(closeButton);
380+
381+
expect(screen.queryByText('Draft')).not.toBeInTheDocument();
382+
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();
383+
384+
const libraryInfoButton = screen.getByRole('button', { name: /library info/i });
385+
fireEvent.click(libraryInfoButton);
386+
387+
expect(screen.getByText('Draft')).toBeInTheDocument();
388+
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
389+
});
390+
332391
it('show the "View All" button when viewing library with many components', async () => {
333392
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
334393
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
335394

336395
const {
337-
getByRole, getByText, queryByText, getAllByText,
396+
getByRole, getByText, queryByText, getAllByText, findAllByText,
338397
} = render(<RootWrapper />);
339398

340399
// Ensure the search endpoint is called:
@@ -343,7 +402,7 @@ describe('<LibraryAuthoringPage />', () => {
343402
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
344403

345404
expect(getByText('Content library')).toBeInTheDocument();
346-
expect(getByText(libraryData.title)).toBeInTheDocument();
405+
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
347406

348407
await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
349408
expect(getByText('Collections (0)')).toBeInTheDocument();
@@ -376,7 +435,7 @@ describe('<LibraryAuthoringPage />', () => {
376435
fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true });
377436

378437
const {
379-
getByText, queryByText, getAllByText,
438+
getByText, queryByText, getAllByText, findAllByText,
380439
} = render(<RootWrapper />);
381440

382441
// Ensure the search endpoint is called:
@@ -385,7 +444,7 @@ describe('<LibraryAuthoringPage />', () => {
385444
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
386445

387446
expect(getByText('Content library')).toBeInTheDocument();
388-
expect(getByText(libraryData.title)).toBeInTheDocument();
447+
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
389448

390449
await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
391450
expect(getByText('Collections (0)')).toBeInTheDocument();

src/library-authoring/LibraryAuthoringPage.tsx

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import React, { useContext } from 'react';
1+
import React, { useContext, useEffect } from 'react';
22
import { StudioFooter } from '@edx/frontend-component-footer';
33
import { useIntl } from '@edx/frontend-platform/i18n';
44
import {
55
Badge,
66
Button,
77
Col,
88
Container,
9-
Icon,
10-
IconButton,
119
Row,
1210
Stack,
1311
Tab,
@@ -52,37 +50,40 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
5250
const intl = useIntl();
5351
const {
5452
openAddContentSidebar,
53+
openInfoSidebar,
5554
} = useContext(LibraryContext);
5655

5756
if (!canEditLibrary) {
5857
return null;
5958
}
6059

6160
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>
61+
<>
62+
<Button
63+
iconBefore={InfoOutline}
64+
variant="outline-primary rounded-0"
65+
onClick={openInfoSidebar}
66+
>
67+
{intl.formatMessage(messages.libraryInfoButton)}
68+
</Button>
69+
<Button
70+
iconBefore={Add}
71+
variant="primary rounded-0"
72+
onClick={openAddContentSidebar}
73+
disabled={!canEditLibrary}
74+
>
75+
{intl.formatMessage(messages.newContentButton)}
76+
</Button>
77+
</>
7078
);
7179
};
7280

7381
const SubHeaderTitle = ({ title, canEditLibrary }: { title: string, canEditLibrary: boolean }) => {
7482
const intl = useIntl();
83+
7584
return (
7685
<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+
{title}
8687
{ !canEditLibrary && (
8788
<div>
8889
<Badge variant="primary" style={{ fontSize: '50%' }}>
@@ -104,7 +105,14 @@ const LibraryAuthoringPage = () => {
104105

105106
const currentPath = location.pathname.split('/').pop();
106107
const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home;
107-
const { sidebarBodyComponent } = useContext(LibraryContext);
108+
const {
109+
sidebarBodyComponent,
110+
openInfoSidebar,
111+
} = useContext(LibraryContext);
112+
113+
useEffect(() => {
114+
openInfoSidebar();
115+
}, []);
108116

109117
const [searchParams] = useSearchParams();
110118

@@ -190,8 +198,8 @@ const LibraryAuthoringPage = () => {
190198
<StudioFooter />
191199
</Col>
192200
{ sidebarBodyComponent !== null && (
193-
<Col xs={6} md={4} className="box-shadow-left-1">
194-
<LibrarySidebar />
201+
<Col xs={3} md={3} className="box-shadow-left-1">
202+
<LibrarySidebar library={libraryData} />
195203
</Col>
196204
)}
197205
</Row>

0 commit comments

Comments
 (0)