diff --git a/src/__mocks__/clipboardSubsection.js b/src/__mocks__/clipboardSubsection.js
new file mode 100644
index 0000000000..541e95f979
--- /dev/null
+++ b/src/__mocks__/clipboardSubsection.js
@@ -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',
+};
diff --git a/src/__mocks__/index.js b/src/__mocks__/index.js
index b3b5984d3e..abacaea4a2 100644
--- a/src/__mocks__/index.js
+++ b/src/__mocks__/index.js
@@ -1,2 +1,3 @@
export { default as clipboardUnit } from './clipboardUnit';
+export { default as clipboardSubsection } from './clipboardSubsection';
export { default as clipboardXBlock } from './clipboardXBlock';
diff --git a/src/generic/clipboard/hooks/useClipboard.test.tsx b/src/generic/clipboard/hooks/useClipboard.test.tsx
index 0b73ef8b4e..74a0fb1e0f 100644
--- a/src/generic/clipboard/hooks/useClipboard.test.tsx
+++ b/src/generic/clipboard/hooks/useClipboard.test.tsx
@@ -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();
diff --git a/src/library-authoring/add-content/AddContent.test.tsx b/src/library-authoring/add-content/AddContent.test.tsx
index 5d75b89761..6d1dd7fecb 100644
--- a/src/library-authoring/add-content/AddContent.test.tsx
+++ b/src/library-authoring/add-content/AddContent.test.tsx
@@ -203,6 +203,23 @@ describe('', () => {
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();
diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx
index e518d42e8e..00777ed2bc 100644
--- a/src/library-authoring/add-content/AddContent.tsx
+++ b/src/library-authoring/add-content/AddContent.tsx
@@ -187,7 +187,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 });
}
@@ -213,7 +221,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();
@@ -277,7 +285,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,
@@ -313,7 +321,6 @@ const AddContent = () => {
}
pasteClipboardMutation.mutateAsync({
libraryId,
- blockId: `${uuid4()}`,
}).then((data) => {
linkComponent(data.id);
showToast(intl.formatMessage(messages.successPasteClipboardMessage));
diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts
index 54fb567a7b..787faeea8b 100644
--- a/src/library-authoring/data/api.mocks.ts
+++ b/src/library-authoring/data/api.mocks.ts
@@ -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',
@@ -489,7 +482,7 @@ mockGetContainerMetadata.containerIdError = 'lct:org:lib:unit:container_error';
mockGetContainerMetadata.containerIdLoading = 'lct:org:lib:unit:container_loading';
mockGetContainerMetadata.containerIdForTags = mockContentTaxonomyTagsData.largeTagsId;
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',
@@ -544,7 +537,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,
diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts
index ca8ec0aef3..526ddc5fa1 100644
--- a/src/library-authoring/data/api.ts
+++ b/src/library-authoring/data/api.ts
@@ -241,7 +241,6 @@ export interface CollectionMetadata {
export interface LibraryBlockMetadata {
id: string;
blockType: string;
- defKey: string | null;
displayName: string;
lastPublished: string | null;
publishedBy: string | null;
@@ -266,7 +265,6 @@ export interface UpdateLibraryDataRequest {
export interface LibraryPasteClipboardRequest {
libraryId: string;
- blockId: string;
}
export interface UpdateXBlockFieldsRequest {
@@ -422,16 +420,10 @@ export async function getBlockTypes(libraryId: string): Promise {
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);
}
/**
@@ -589,7 +581,7 @@ export async function createLibraryContainer(libraryId: string, containerData: C
}
export interface Container {
- containerKey: string;
+ id: string;
containerType: 'unit';
displayName: string;
lastPublished: string | null;
diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx
index f1a8b3820a..502a245d44 100644
--- a/src/library-authoring/data/apiHooks.test.tsx
+++ b/src/library-authoring/data/apiHooks.test.tsx
@@ -193,7 +193,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,
@@ -208,7 +207,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,
@@ -229,7 +227,6 @@ describe('library api hooks', () => {
{
id: 'lb:org1:Demo_course:html:text',
blockType: 'html',
- defKey: 'def_key',
displayName: 'text block',
lastPublished: null,
publishedBy: null,
@@ -244,7 +241,6 @@ describe('library api hooks', () => {
{
id: 'lb:org1:Demo_course:video:video1',
blockType: 'video',
- defKey: 'def_key',
displayName: 'video block',
lastPublished: null,
publishedBy: null,