diff --git a/src/__mocks__/clipboardSubsection.js b/src/__mocks__/clipboardSubsection.js
new file mode 100644
index 000000000..541e95f97
--- /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 b3b5984d3..abacaea4a 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 0b73ef8b4..74a0fb1e0 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 b761b3e92..d6c1ea1b8 100644
--- a/src/library-authoring/add-content/AddContent.test.tsx
+++ b/src/library-authoring/add-content/AddContent.test.tsx
@@ -207,6 +207,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 08b924d7a..7610b743f 100644
--- a/src/library-authoring/add-content/AddContent.tsx
+++ b/src/library-authoring/add-content/AddContent.tsx
@@ -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));
diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts
index 5b5ba4348..9a71c8aa8 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',
@@ -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,
diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts
index 30feaa9bc..b4a7bca67 100644
--- a/src/library-authoring/data/api.ts
+++ b/src/library-authoring/data/api.ts
@@ -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 {
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;
diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx
index e4c6a7dba..57e0a8fa7 100644
--- a/src/library-authoring/data/apiHooks.test.tsx
+++ b/src/library-authoring/data/apiHooks.test.tsx
@@ -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,