Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
115 changes: 115 additions & 0 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { mockContentLibrary, mockLibraryBlockTypes, mockXBlockFields } from './d
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
import { mockBroadcastChannel } from '../generic/data/api.mock';
import { LibraryLayout } from '.';
import { getLibraryCollectionsApiUrl } from './data/api';

mockContentSearchConfig.applyMock();
mockContentLibrary.applyMock();
Expand Down Expand Up @@ -535,6 +536,120 @@ describe('<LibraryAuthoringPage />', () => {
expect(screen.getByText(/no matching components/i)).toBeInTheDocument();
});

it('should create a collection', async () => {
await renderLibraryPage();
const title = 'This is a Test';
const description = 'This is the description of the Test';
const url = getLibraryCollectionsApiUrl(mockContentLibrary.libraryId);
const { axiosMock } = initializeMocks();
axiosMock.onPost(url).reply(200, {
id: '1',
slug: 'this-is-a-test',
title,
description,
});

expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();

// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New collection Modal
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();

// Click on Cancel button
const cancelButton = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelButton);
expect(collectionModalHeading).not.toBeInTheDocument();

// Open new collection modal again and create a collection
fireEvent.click(newCollectionButton);
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: /name your collection/i });
const descriptionField = screen.getByRole('textbox', { name: /add a description \(optional\)/i });

fireEvent.change(nameField, { target: { value: title } });
fireEvent.change(descriptionField, { target: { value: description } });
fireEvent.click(createButton);
});

it('should show validations in create collection', async () => {
await renderLibraryPage();

const title = 'This is a Test';
const description = 'This is the description of the Test';
const url = getLibraryCollectionsApiUrl(mockContentLibrary.libraryId);
const { axiosMock } = initializeMocks();
axiosMock.onPost(url).reply(200, {
id: '1',
slug: 'this-is-a-test',
title,
description,
});

expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();

// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New collection Modal
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();

const nameField = screen.getByRole('textbox', { name: /name your collection/i });
fireEvent.focus(nameField);
fireEvent.blur(nameField);

// Click on create with an empty name
const createButton = screen.getByRole('button', { name: /create/i });
fireEvent.click(createButton);

expect(await screen.findByText(/collection name is required/i)).toBeInTheDocument();
});

it('should show error on create collection', async () => {
await renderLibraryPage();
const title = 'This is a Test';
const description = 'This is the description of the Test';
const url = getLibraryCollectionsApiUrl(mockContentLibrary.libraryId);
const { axiosMock } = initializeMocks();
axiosMock.onPost(url).reply(500);

expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();

// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New collection Modal
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();

// Create a normal collection
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: /name your collection/i });
const descriptionField = screen.getByRole('textbox', { name: /add a description \(optional\)/i });

fireEvent.change(nameField, { target: { value: title } });
fireEvent.change(descriptionField, { target: { value: description } });
fireEvent.click(createButton);
});

it('shows both components and collections in recently modified section', async () => {
await renderLibraryPage();

Expand Down
2 changes: 2 additions & 0 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useQueryClient } from '@tanstack/react-query';
import EditorContainer from '../editors/EditorContainer';
import LibraryAuthoringPage from './LibraryAuthoringPage';
import { LibraryProvider } from './common/context';
import { CreateCollectionModal } from './create-collection';
import { invalidateComponentData } from './data/apiHooks';

const LibraryLayout = () => {
Expand Down Expand Up @@ -49,6 +50,7 @@ const LibraryLayout = () => {
element={<LibraryAuthoringPage />}
/>
</Routes>
<CreateCollectionModal />
</LibraryProvider>
);
};
Expand Down
66 changes: 49 additions & 17 deletions src/library-authoring/add-content/AddContentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,39 @@ import { useCreateLibraryBlock, useLibraryPasteClipboard } from '../data/apiHook
import { getEditUrl } from '../components/utils';

import messages from './messages';
import { LibraryContext } from '../common/context';

type ContentType = {
name: string,
disabled: boolean,
icon: React.ComponentType,
blockType: string,
};

type AddContentButtonProps = {
contentType: ContentType,
onCreateContent: (blockType: string) => void,
};

const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonProps) => {
const {
name,
disabled,
icon,
blockType,
} = contentType;
return (
<Button
variant="outline-primary"
disabled={disabled}
className="m-2"
iconBefore={icon}
onClick={() => onCreateContent(blockType)}
>
{name}
</Button>
);
};

const AddContentContainer = () => {
const intl = useIntl();
Expand All @@ -35,7 +68,16 @@ const AddContentContainer = () => {
const { showToast } = useContext(ToastContext);
const canEdit = useSelector(getCanEdit);
const { showPasteXBlock } = useCopyToClipboard(canEdit);
const {
openCreateCollectionModal,
} = React.useContext(LibraryContext);

const collectionButtonData = {
name: intl.formatMessage(messages.collectionButton),
disabled: false,
icon: BookOpen,
blockType: 'collection',
};
const contentTypes = [
{
name: intl.formatMessage(messages.textTypeButton),
Expand Down Expand Up @@ -98,6 +140,8 @@ const AddContentContainer = () => {
}).catch(() => {
showToast(intl.formatMessage(messages.errorPasteClipboardMessage));
});
} else if (blockType === 'collection') {
openCreateCollectionModal();
} else {
createBlockMutation.mutateAsync({
libraryId,
Expand All @@ -124,26 +168,14 @@ const AddContentContainer = () => {

return (
<Stack direction="vertical">
<Button
variant="outline-primary"
disabled
className="m-2 rounded-0"
iconBefore={BookOpen}
>
{intl.formatMessage(messages.collectionButton)}
</Button>
<AddContentButton contentType={collectionButtonData} onCreateContent={onCreateContent} />
<hr className="w-100 bg-gray-500" />
{contentTypes.map((contentType) => (
<Button
<AddContentButton
key={`add-content-${contentType.blockType}`}
variant="outline-primary"
disabled={contentType.disabled}
className="m-2 rounded-0"
iconBefore={contentType.icon}
onClick={() => onCreateContent(contentType.blockType)}
>
{contentType.name}
</Button>
contentType={contentType}
onCreateContent={onCreateContent}
/>
))}
</Stack>
);
Expand Down
14 changes: 14 additions & 0 deletions src/library-authoring/common/context.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useToggle } from '@openedx/paragon';
import React from 'react';

export enum SidebarBodyComponentId {
Expand All @@ -13,6 +14,9 @@ export interface LibraryContextData {
openInfoSidebar: () => void;
openComponentInfoSidebar: (usageKey: string) => void;
currentComponentUsageKey?: string;
isCreateCollectionModalOpen: boolean;
openCreateCollectionModal: () => void;
closeCreateCollectionModal: () => void;
}

export const LibraryContext = React.createContext({
Expand All @@ -21,6 +25,9 @@ export const LibraryContext = React.createContext({
openAddContentSidebar: () => {},
openInfoSidebar: () => {},
openComponentInfoSidebar: (_usageKey: string) => {}, // eslint-disable-line @typescript-eslint/no-unused-vars
isCreateCollectionModalOpen: false,
openCreateCollectionModal: () => {},
closeCreateCollectionModal: () => {},
} as LibraryContextData);

/**
Expand All @@ -29,6 +36,7 @@ export const LibraryContext = React.createContext({
export const LibraryProvider = (props: { children?: React.ReactNode }) => {
const [sidebarBodyComponent, setSidebarBodyComponent] = React.useState<SidebarBodyComponentId | null>(null);
const [currentComponentUsageKey, setCurrentComponentUsageKey] = React.useState<string>();
const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false);

const closeLibrarySidebar = React.useCallback(() => {
setSidebarBodyComponent(null);
Expand Down Expand Up @@ -57,13 +65,19 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => {
openInfoSidebar,
openComponentInfoSidebar,
currentComponentUsageKey,
isCreateCollectionModalOpen,
openCreateCollectionModal,
closeCreateCollectionModal,
}), [
sidebarBodyComponent,
closeLibrarySidebar,
openAddContentSidebar,
openInfoSidebar,
openComponentInfoSidebar,
currentComponentUsageKey,
isCreateCollectionModalOpen,
openCreateCollectionModal,
closeCreateCollectionModal,
]);

return (
Expand Down
Loading