diff --git a/src/generic/delete-modal/DeleteModal.jsx b/src/generic/delete-modal/DeleteModal.jsx
index c7a22c5d9e..3384c0f3bc 100644
--- a/src/generic/delete-modal/DeleteModal.jsx
+++ b/src/generic/delete-modal/DeleteModal.jsx
@@ -18,6 +18,7 @@ const DeleteModal = ({
description,
variant,
btnLabel,
+ icon,
}) => {
const intl = useIntl();
@@ -31,6 +32,7 @@ const DeleteModal = ({
isOpen={isOpen}
onClose={close}
variant={variant}
+ icon={icon}
footerNode={(
- )}
- >
-
+ description={(
{
),
}}
/>
-
-
+)}
+ onDeleteSubmit={doDelete}
+ />
);
};
diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts
index b591d956f5..0e466736e6 100644
--- a/src/library-authoring/components/messages.ts
+++ b/src/library-authoring/components/messages.ts
@@ -73,7 +73,7 @@ const messages = defineMessages({
},
deleteComponentConfirm: {
id: 'course-authoring.library-authoring.component.delete-confirmation-text',
- defaultMessage: 'Delete {componentName} permanently? If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.',
+ defaultMessage: 'Delete {componentName}? If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.',
description: 'Confirmation text to display before deleting a component',
},
deleteComponentCancelButton: {
@@ -86,6 +86,26 @@ const messages = defineMessages({
defaultMessage: 'Delete',
description: 'Button to confirm deletion of a component',
},
+ deleteComponentSuccess: {
+ id: 'course-authoring.library-authoring.component.delete-error-success',
+ defaultMessage: 'Component deleted',
+ description: 'Message to display on delete component success',
+ },
+ undoDeleteComponentToastAction: {
+ id: 'course-authoring.library-authoring.component.undo-delete-component-toast-button',
+ defaultMessage: 'Undo',
+ description: 'Toast message to undo deletion of component',
+ },
+ undoDeleteComponentToastSuccess: {
+ id: 'course-authoring.library-authoring.component.undo-delete-component-toast-text',
+ defaultMessage: 'Undo successful',
+ description: 'Message to display on undo delete component success',
+ },
+ undoDeleteComponentToastFailed: {
+ id: 'course-authoring.library-authoring.component.undo-delete-component-failed',
+ defaultMessage: 'Failed to undo delete component operation',
+ description: 'Message to display on failure to undo delete component',
+ },
deleteCollection: {
id: 'course-authoring.library-authoring.collection.delete-menu-text',
defaultMessage: 'Delete',
diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts
index bfd4d81ef5..0e064f8a0f 100644
--- a/src/library-authoring/data/api.mocks.ts
+++ b/src/library-authoring/data/api.mocks.ts
@@ -243,6 +243,17 @@ mockDeleteLibraryBlock.applyMock = () => (
jest.spyOn(api, 'deleteLibraryBlock').mockImplementation(mockDeleteLibraryBlock)
);
+/**
+ * Mock for `restoreLibraryBlock()`
+ */
+export async function mockRestoreLibraryBlock(): ReturnType {
+ // no-op
+}
+/** Apply this mock. Returns a spy object that can tell you if it's been called. */
+mockRestoreLibraryBlock.applyMock = () => (
+ jest.spyOn(api, 'restoreLibraryBlock').mockImplementation(mockRestoreLibraryBlock)
+);
+
/**
* Mock for `getXBlockFields()`
*
diff --git a/src/library-authoring/data/api.test.ts b/src/library-authoring/data/api.test.ts
index 9cace9b3ae..86a5249905 100644
--- a/src/library-authoring/data/api.test.ts
+++ b/src/library-authoring/data/api.test.ts
@@ -29,6 +29,17 @@ describe('library data API', () => {
});
});
+ describe('restoreLibraryBlock', () => {
+ it('should restore a soft-deleted library block', async () => {
+ const { axiosMock } = initializeMocks();
+ const usageKey = 'lib:org:1';
+ const url = api.getLibraryBlockRestoreUrl(usageKey);
+ axiosMock.onPost(url).reply(200);
+ await api.restoreLibraryBlock({ usageKey });
+ expect(axiosMock.history.post[0].url).toEqual(url);
+ });
+ });
+
describe('commitLibraryChanges', () => {
it('should commit library changes', async () => {
const { axiosMock } = initializeMocks();
diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts
index 35615df1c6..6432aaafd3 100644
--- a/src/library-authoring/data/api.ts
+++ b/src/library-authoring/data/api.ts
@@ -29,6 +29,11 @@ export const getLibraryTeamMemberApiUrl = (libraryId: string, username: string)
*/
export const getLibraryBlockMetadataUrl = (usageKey: string) => `${getApiBaseUrl()}/api/libraries/v2/blocks/${usageKey}/`;
+/**
+ * Get the URL for restoring deleted library block.
+ */
+export const getLibraryBlockRestoreUrl = (usageKey: string) => `${getLibraryBlockMetadataUrl(usageKey)}restore/`;
+
/**
* Get the URL for library block metadata.
*/
@@ -281,6 +286,11 @@ export async function deleteLibraryBlock({ usageKey }: DeleteBlockDataRequest):
await client.delete(getLibraryBlockMetadataUrl(usageKey));
}
+export async function restoreLibraryBlock({ usageKey }: DeleteBlockDataRequest): Promise {
+ const client = getAuthenticatedHttpClient();
+ await client.post(getLibraryBlockRestoreUrl(usageKey));
+}
+
/**
* Update library metadata.
*/
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 549e211b8f..46cd148925 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -44,6 +44,7 @@ import {
removeComponentsFromCollection,
publishXBlock,
deleteXBlockAsset,
+ restoreLibraryBlock,
} from './api';
import { VersionSpec } from '../LibraryBlock';
@@ -115,6 +116,7 @@ export function invalidateComponentData(queryClient: QueryClient, contentLibrary
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.xblockFields(usageKey) });
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentMetadata(usageKey) });
// The description and display name etc. may have changed, so refresh everything in the library too:
+ // This might fail in case this helper is called after deleting the block.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(contentLibraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
}
@@ -158,6 +160,20 @@ export const useDeleteLibraryBlock = () => {
});
};
+/**
+ * Use this mutation to restore a deleted block in a library
+ */
+export const useRestoreLibraryBlock = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: restoreLibraryBlock,
+ onSettled: (_data, _error, variables) => {
+ const libraryId = getLibraryId(variables.usageKey);
+ invalidateComponentData(queryClient, libraryId, variables.usageKey);
+ },
+ });
+};
+
export const useUpdateLibraryMetadata = () => {
const queryClient = useQueryClient();
return useMutation({