Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Library Home - Paste Content (temp) #56

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 99 additions & 153 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion src/generic/clipboard/hooks/useCopyToClipboard.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// @ts-check
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';

import { getClipboard } from '../../data/api';
import { updateClipboardData } from '../../data/slice';
import { CLIPBOARD_STATUS, STRUCTURAL_XBLOCK_TYPES, STUDIO_CLIPBOARD_CHANNEL } from '../../../constants';
import { getClipboardData } from '../../data/selectors';

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

// Called on initial render to fetch and populate the initial clipboard data in redux state.
// Without this, the initial clipboard data redux state is always null.
useEffect(() => {
const fetchInitialClipboardData = async () => {
try {
const userClipboard = await getClipboard();
dispatch(updateClipboardData(userClipboard));
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Failed to fetch initial clipboard data: ${error}`);
}
};

fetchInitialClipboardData();
}, [dispatch]);

useEffect(() => {
// Handle updates to clipboard data
if (canEdit) {
Expand Down
2 changes: 1 addition & 1 deletion src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@import "export-page/CourseExportPage";
@import "import-page/CourseImportPage";
@import "taxonomy";
@import "library-authoring";
@import "files-and-videos";
@import "content-tags-drawer";
@import "course-outline/CourseOutline";
Expand All @@ -30,7 +31,6 @@
@import "search-manager";
@import "certificates/scss/Certificates";
@import "group-configurations/GroupConfigurations";
@import "library-authoring";

// To apply the glow effect to the selected Section/Subsection, in the Course Outline
div.row:has(> div > div.highlight) {
Expand Down
83 changes: 71 additions & 12 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,27 @@ const libraryData: ContentLibrary = {
numBlocks: 2,
version: 0,
lastPublished: null,
lastDraftCreated: '2024-07-22',
publishedBy: 'staff',
lastDraftCreatedBy: 'staff',
allowLti: false,
allowPublicLearning: false,
allowPublicRead: false,
hasUnpublishedChanges: true,
hasUnpublishedDeletes: false,
canEditLibrary: true,
license: '',
created: '2024-06-26',
updated: '2024-07-20',
};

const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
close: jest.fn(),
};

(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
Expand Down Expand Up @@ -177,23 +189,23 @@ describe('<LibraryAuthoringPage />', () => {
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

const {
getByRole, getByText, getAllByText, queryByText,
getByRole, getByText, queryByText, findByText, findAllByText,
} = render(<RootWrapper />);

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

expect(getByText('Content library')).toBeInTheDocument();
expect(getByText(libraryData.title)).toBeInTheDocument();
expect(await findByText('Content library')).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

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

expect(getByText('Recently Modified')).toBeInTheDocument();
expect(getByText('Collections (0)')).toBeInTheDocument();
expect(getByText('Components (6)')).toBeInTheDocument();
expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument();
expect((await findAllByText('Test HTML Block'))[0]).toBeInTheDocument();

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

const { findByText, getByText } = render(<RootWrapper />);
const { findByText, getByText, findAllByText } = render(<RootWrapper />);

expect(await findByText('Content library')).toBeInTheDocument();
expect(await findByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

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

const { findByText, getByRole, getByText } = render(<RootWrapper />);
const {
findByText,
getByRole,
getByText,
findAllByText,
} = render(<RootWrapper />);

expect(await findByText('Content library')).toBeInTheDocument();
expect(await findByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

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

it('should open Library Info by default', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

render(<RootWrapper />);

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();

expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
expect(screen.getByText('staff')).toBeInTheDocument();
expect(screen.getByText(libraryData.org)).toBeInTheDocument();
expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
});

it('should close and open Library Info', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

render(<RootWrapper />);

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
expect((await screen.findAllByText(libraryData.title))[1]).toBeInTheDocument();

const closeButton = screen.getByRole('button', { name: /close/i });
fireEvent.click(closeButton);

expect(screen.queryByText('Draft')).not.toBeInTheDocument();
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();

const libraryInfoButton = screen.getByRole('button', { name: /library info/i });
fireEvent.click(libraryInfoButton);

expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
});

it('show the "View All" button when viewing library with many components', async () => {
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);

const {
getByRole, getByText, queryByText, getAllByText,
getByRole, getByText, queryByText, getAllByText, findAllByText,
} = render(<RootWrapper />);

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

expect(getByText('Content library')).toBeInTheDocument();
expect(getByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

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

const {
getByText, queryByText, getAllByText,
getByText, queryByText, getAllByText, findAllByText,
} = render(<RootWrapper />);

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

expect(getByText('Content library')).toBeInTheDocument();
expect(getByText(libraryData.title)).toBeInTheDocument();
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();

await waitFor(() => { expect(getByText('Recently Modified')).toBeInTheDocument(); });
expect(getByText('Collections (0)')).toBeInTheDocument();
Expand Down
54 changes: 31 additions & 23 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React, { useContext } from 'react';
import React, { useContext, useEffect } 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,
Expand Down Expand Up @@ -52,37 +50,40 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => {
const intl = useIntl();
const {
openAddContentSidebar,
openInfoSidebar,
} = useContext(LibraryContext);

if (!canEditLibrary) {
return null;
}

return (
<Button
iconBefore={Add}
variant="primary rounded-0"
onClick={() => openAddContentSidebar()}
disabled={!canEditLibrary}
>
{intl.formatMessage(messages.newContentButton)}
</Button>
<>
<Button
iconBefore={InfoOutline}
variant="outline-primary rounded-0"
onClick={openInfoSidebar}
>
{intl.formatMessage(messages.libraryInfoButton)}
</Button>
<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 (
<Stack direction="vertical">
<Stack direction="horizontal">
{title}
<IconButton
src={InfoOutline}
iconAs={Icon}
alt={intl.formatMessage(messages.headingInfoAlt)}
className="mr-2"
/>
</Stack>
{title}
{ !canEditLibrary && (
<div>
<Badge variant="primary" style={{ fontSize: '50%' }}>
Expand All @@ -104,7 +105,14 @@ const LibraryAuthoringPage = () => {

const currentPath = location.pathname.split('/').pop();
const activeKey = (currentPath && currentPath in TabList) ? TabList[currentPath] : TabList.home;
const { sidebarBodyComponent } = useContext(LibraryContext);
const {
sidebarBodyComponent,
openInfoSidebar,
} = useContext(LibraryContext);

useEffect(() => {
openInfoSidebar();
}, []);

const [searchParams] = useSearchParams();

Expand Down Expand Up @@ -190,8 +198,8 @@ const LibraryAuthoringPage = () => {
<StudioFooter />
</Col>
{ sidebarBodyComponent !== null && (
<Col xs={6} md={4} className="box-shadow-left-1">
<LibrarySidebar />
<Col xs={3} md={3} className="box-shadow-left-1">
<LibrarySidebar library={libraryData} />
</Col>
)}
</Row>
Expand Down
Loading