feat: allow pasting units from a course into a library (#1812)
This commit is contained in:
16
src/__mocks__/clipboardSubsection.js
Normal file
16
src/__mocks__/clipboardSubsection.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
content: {
|
||||
id: 67,
|
||||
userId: 3,
|
||||
created: '2024-01-16T13:09:11.540615Z',
|
||||
purpose: 'clipboard',
|
||||
status: 'ready',
|
||||
blockType: 'sequential',
|
||||
blockTypeDisplay: 'Subsection',
|
||||
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/67/olx',
|
||||
displayName: 'Sequences',
|
||||
},
|
||||
sourceUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@sequential_0270f6de40fc',
|
||||
sourceContextTitle: 'Demonstration Course',
|
||||
sourceEditUrl: 'http://localhost:18010/container/block-v1:edX+DemoX+Demo_Course+type@sequential+block@sequential_0270f6de40fc',
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as clipboardUnit } from './clipboardUnit';
|
||||
export { default as clipboardSubsection } from './clipboardSubsection';
|
||||
export { default as clipboardXBlock } from './clipboardXBlock';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import {
|
||||
clipboardSubsection,
|
||||
clipboardUnit,
|
||||
clipboardXBlock,
|
||||
} from '../../../__mocks__';
|
||||
@@ -42,7 +43,7 @@ describe('useClipboard', () => {
|
||||
|
||||
axiosMock
|
||||
.onPost(getClipboardUrl())
|
||||
.reply(200, clipboardUnit);
|
||||
.reply(200, clipboardSubsection);
|
||||
|
||||
await result.current.copyToClipboard(unitId);
|
||||
|
||||
@@ -89,6 +90,7 @@ describe('useClipboard', () => {
|
||||
describe('broadcast channel message handling', () => {
|
||||
it('updates states correctly on receiving a broadcast message', async () => {
|
||||
const { result, rerender } = renderHook(() => useClipboard(true), { wrapper: makeWrapper() });
|
||||
// Subsections cannot be pasted:
|
||||
clipboardBroadcastChannelMock.postMessage({ data: clipboardUnit });
|
||||
|
||||
rerender();
|
||||
|
||||
@@ -207,6 +207,23 @@ describe('<AddContent />', () => {
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
|
||||
});
|
||||
|
||||
it('should show error toast on paste failure', async () => {
|
||||
// Simulate having an HTML block in the clipboard:
|
||||
mockClipboardHtml.applyMock();
|
||||
|
||||
const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
|
||||
axiosMock.onPost(pasteUrl).reply(500, { block_type: 'Unsupported block type.' });
|
||||
|
||||
render();
|
||||
const pasteButton = await screen.findByRole('button', { name: /paste from clipboard/i });
|
||||
fireEvent.click(pasteButton);
|
||||
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
|
||||
expect(mockShowToast).toHaveBeenCalledWith(
|
||||
'There was an error pasting the content: {"block_type":"Unsupported block type."}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should paste content inside a collection', async () => {
|
||||
// Simulate having an HTML block in the clipboard:
|
||||
const getClipboardSpy = mockClipboardHtml.applyMock();
|
||||
|
||||
@@ -191,7 +191,15 @@ export const parseErrorMsg = (
|
||||
) => {
|
||||
try {
|
||||
const { response: { data } } = error;
|
||||
const detail = data && (Array.isArray(data) ? data.join() : String(data));
|
||||
let detail = '';
|
||||
if (Array.isArray(data)) {
|
||||
detail = data.join(', ');
|
||||
} else if (typeof data === 'string') {
|
||||
/* istanbul ignore next */
|
||||
detail = data.substring(0, 400); // In case this is a giant HTML response, only show the first little bit.
|
||||
} else if (data) {
|
||||
detail = JSON.stringify(data);
|
||||
}
|
||||
if (detail) {
|
||||
return intl.formatMessage(detailedMessage, { detail });
|
||||
}
|
||||
@@ -217,7 +225,7 @@ const AddContent = () => {
|
||||
const pasteClipboardMutation = useLibraryPasteClipboard();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const canEdit = useSelector(getCanEdit);
|
||||
const { showPasteXBlock, sharedClipboardData } = useClipboard(canEdit);
|
||||
const { showPasteUnit, showPasteXBlock, sharedClipboardData } = useClipboard(canEdit);
|
||||
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
const [isAdvancedListOpen, showAdvancedList, closeAdvancedList] = useToggle();
|
||||
@@ -281,7 +289,7 @@ const AddContent = () => {
|
||||
|
||||
// Include the 'Paste from Clipboard' button if there is an Xblock in the clipboard
|
||||
// that can be pasted
|
||||
if (showPasteXBlock) {
|
||||
if (showPasteXBlock || showPasteUnit) {
|
||||
const pasteButton = {
|
||||
name: intl.formatMessage(messages.pasteButton),
|
||||
disabled: false,
|
||||
@@ -317,7 +325,6 @@ const AddContent = () => {
|
||||
}
|
||||
pasteClipboardMutation.mutateAsync({
|
||||
libraryId,
|
||||
blockId: `${uuid4()}`,
|
||||
}).then((data) => {
|
||||
linkComponent(data.id);
|
||||
showToast(intl.formatMessage(messages.successPasteClipboardMessage));
|
||||
|
||||
@@ -186,7 +186,6 @@ export async function mockCreateLibraryBlock(
|
||||
}
|
||||
mockCreateLibraryBlock.newHtmlData = {
|
||||
id: 'lb:Axim:TEST:html:123',
|
||||
defKey: '123',
|
||||
blockType: 'html',
|
||||
displayName: 'New Text Component',
|
||||
hasUnpublishedChanges: true,
|
||||
@@ -201,7 +200,6 @@ mockCreateLibraryBlock.newHtmlData = {
|
||||
} satisfies api.LibraryBlockMetadata;
|
||||
mockCreateLibraryBlock.newProblemData = {
|
||||
id: 'lb:Axim:TEST:problem:prob1',
|
||||
defKey: 'prob1',
|
||||
blockType: 'problem',
|
||||
displayName: 'New Problem',
|
||||
hasUnpublishedChanges: true,
|
||||
@@ -216,7 +214,6 @@ mockCreateLibraryBlock.newProblemData = {
|
||||
} satisfies api.LibraryBlockMetadata;
|
||||
mockCreateLibraryBlock.newVideoData = {
|
||||
id: 'lb:Axim:TEST:video:vid1',
|
||||
defKey: 'vid1',
|
||||
blockType: 'video',
|
||||
displayName: 'New Video',
|
||||
hasUnpublishedChanges: true,
|
||||
@@ -349,7 +346,6 @@ mockLibraryBlockMetadata.usageKeyError404 = 'lb:Axim:error404:html:123';
|
||||
mockLibraryBlockMetadata.usageKeyNeverPublished = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
|
||||
mockLibraryBlockMetadata.dataNeverPublished = {
|
||||
id: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1',
|
||||
defKey: null,
|
||||
blockType: 'html',
|
||||
displayName: 'Introduction to Testing 1',
|
||||
lastPublished: null,
|
||||
@@ -365,7 +361,6 @@ mockLibraryBlockMetadata.dataNeverPublished = {
|
||||
mockLibraryBlockMetadata.usageKeyPublished = 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
|
||||
mockLibraryBlockMetadata.dataPublished = {
|
||||
id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2',
|
||||
defKey: null,
|
||||
blockType: 'html',
|
||||
displayName: 'Introduction to Testing 2',
|
||||
lastPublished: '2024-06-22T00:00:00',
|
||||
@@ -394,7 +389,6 @@ mockLibraryBlockMetadata.usageKeyForTags = mockContentTaxonomyTagsData.largeTags
|
||||
mockLibraryBlockMetadata.usageKeyWithCollections = 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd';
|
||||
mockLibraryBlockMetadata.dataWithCollections = {
|
||||
id: 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd',
|
||||
defKey: null,
|
||||
blockType: 'html',
|
||||
displayName: 'Introduction to Testing 2',
|
||||
lastPublished: '2024-06-21T00:00:00',
|
||||
@@ -411,7 +405,6 @@ mockLibraryBlockMetadata.usageKeyPublishedWithChanges = 'lb:Axim:TEST:html:571fe
|
||||
mockLibraryBlockMetadata.usageKeyPublishedWithChangesV2 = 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fv2';
|
||||
mockLibraryBlockMetadata.dataPublishedWithChanges = {
|
||||
id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fvv',
|
||||
defKey: null,
|
||||
blockType: 'html',
|
||||
displayName: 'Introduction to Testing 2',
|
||||
lastPublished: '2024-06-22T00:00:00',
|
||||
@@ -492,7 +485,7 @@ mockGetContainerMetadata.containerIdLoading = 'lct:org:lib:unit:container_loadin
|
||||
mockGetContainerMetadata.containerIdForTags = mockContentTaxonomyTagsData.largeTagsId;
|
||||
mockGetContainerMetadata.containerIdWithCollections = 'lct:org:lib:unit:container_collections';
|
||||
mockGetContainerMetadata.containerData = {
|
||||
containerKey: 'lct:org:lib:unit:test-unit-9a2072',
|
||||
id: 'lct:org:lib:unit:test-unit-9a2072',
|
||||
containerType: 'unit',
|
||||
displayName: 'Test Unit',
|
||||
created: '2024-09-19T10:00:00Z',
|
||||
@@ -552,7 +545,6 @@ mockGetContainerChildren.sixChildren = 'lct:org1:Demo_Course:unit:unit-6';
|
||||
mockGetContainerChildren.childTemplate = {
|
||||
id: 'lb:org1:Demo_course:html:text',
|
||||
blockType: 'html',
|
||||
defKey: 'def_key',
|
||||
displayName: 'text block',
|
||||
lastPublished: null,
|
||||
publishedBy: null,
|
||||
|
||||
@@ -245,7 +245,6 @@ export interface CollectionMetadata {
|
||||
export interface LibraryBlockMetadata {
|
||||
id: string;
|
||||
blockType: string;
|
||||
defKey: string | null;
|
||||
displayName: string;
|
||||
lastPublished: string | null;
|
||||
publishedBy: string | null;
|
||||
@@ -270,7 +269,6 @@ export interface UpdateLibraryDataRequest {
|
||||
|
||||
export interface LibraryPasteClipboardRequest {
|
||||
libraryId: string;
|
||||
blockId: string;
|
||||
}
|
||||
|
||||
export interface UpdateXBlockFieldsRequest {
|
||||
@@ -426,16 +424,10 @@ export async function getBlockTypes(libraryId: string): Promise<BlockTypeMetadat
|
||||
*/
|
||||
export async function libraryPasteClipboard({
|
||||
libraryId,
|
||||
blockId,
|
||||
}: LibraryPasteClipboardRequest): Promise<LibraryBlockMetadata> {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
const { data } = await client.post(
|
||||
getLibraryPasteClipboardUrl(libraryId),
|
||||
{
|
||||
block_id: blockId,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
const { data } = await client.post(getLibraryPasteClipboardUrl(libraryId), {});
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -597,7 +589,7 @@ export async function createLibraryContainer(
|
||||
}
|
||||
|
||||
export interface Container {
|
||||
containerKey: string;
|
||||
id: string;
|
||||
containerType: 'unit';
|
||||
displayName: string;
|
||||
lastPublished: string | null;
|
||||
|
||||
@@ -191,7 +191,6 @@ describe('library api hooks', () => {
|
||||
{
|
||||
id: 'lb:org1:Demo_course:html:text',
|
||||
block_type: 'html',
|
||||
def_key: 'def_key',
|
||||
display_name: 'text block',
|
||||
last_published: null,
|
||||
published_by: null,
|
||||
@@ -206,7 +205,6 @@ describe('library api hooks', () => {
|
||||
{
|
||||
id: 'lb:org1:Demo_course:video:video1',
|
||||
block_type: 'video',
|
||||
def_key: 'def_key',
|
||||
display_name: 'video block',
|
||||
last_published: null,
|
||||
published_by: null,
|
||||
@@ -227,7 +225,6 @@ describe('library api hooks', () => {
|
||||
{
|
||||
id: 'lb:org1:Demo_course:html:text',
|
||||
blockType: 'html',
|
||||
defKey: 'def_key',
|
||||
displayName: 'text block',
|
||||
lastPublished: null,
|
||||
publishedBy: null,
|
||||
@@ -242,7 +239,6 @@ describe('library api hooks', () => {
|
||||
{
|
||||
id: 'lb:org1:Demo_course:video:video1',
|
||||
blockType: 'video',
|
||||
defKey: 'def_key',
|
||||
displayName: 'video block',
|
||||
lastPublished: null,
|
||||
publishedBy: null,
|
||||
|
||||
Reference in New Issue
Block a user