feat: Delete unit [FC-0083] (#1773)
* Adds the delete menu item in unit cards. * Delete a unit with a confirmation modal. * Restore a component
This commit is contained in:
@@ -51,6 +51,7 @@ const DeleteModal = ({
|
||||
e.stopPropagation();
|
||||
await onDeleteSubmit();
|
||||
}}
|
||||
variant="brand"
|
||||
label={defaultBtnLabel}
|
||||
/>
|
||||
</ActionRow>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import {
|
||||
initializeMocks, render as baseRender, screen, waitFor,
|
||||
fireEvent,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import { mockContentLibrary, mockGetContainerChildren } from '../data/api.mocks';
|
||||
import { type ContainerHit, PublishStatus } from '../../search-manager';
|
||||
import ContainerCard from './ContainerCard';
|
||||
import { getLibraryContainerApiUrl, getLibraryContainerRestoreApiUrl } from '../data/api';
|
||||
|
||||
const containerHitSample: ContainerHit = {
|
||||
id: 'lctorg1democourse-unit-display-name-123',
|
||||
@@ -33,6 +36,8 @@ const containerHitSample: ContainerHit = {
|
||||
tags: {},
|
||||
publishStatus: PublishStatus.Published,
|
||||
};
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast;
|
||||
|
||||
mockContentLibrary.applyMock();
|
||||
mockGetContainerChildren.applyMock();
|
||||
@@ -50,7 +55,7 @@ const render = (ui: React.ReactElement, showOnlyPublished: boolean = false) => b
|
||||
|
||||
describe('<ContainerCard />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
({ axiosMock, mockShowToast } = initializeMocks());
|
||||
});
|
||||
|
||||
it('should render the card with title', () => {
|
||||
@@ -85,6 +90,68 @@ describe('<ContainerCard />', () => {
|
||||
// );
|
||||
});
|
||||
|
||||
it('should delete the container from the menu & restore the container', async () => {
|
||||
axiosMock.onDelete(getLibraryContainerApiUrl(containerHitSample.usageKey)).reply(200);
|
||||
|
||||
render(<ContainerCard hit={containerHitSample} />);
|
||||
|
||||
// Open menu
|
||||
expect(screen.getByTestId('container-card-menu-toggle')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('container-card-menu-toggle'));
|
||||
|
||||
// Click on Delete Item
|
||||
const deleteMenuItem = screen.getByRole('button', { name: 'Delete' });
|
||||
expect(deleteMenuItem).toBeInTheDocument();
|
||||
fireEvent.click(deleteMenuItem);
|
||||
|
||||
// Confirm delete Modal is open
|
||||
expect(screen.getByText('Delete Unit'));
|
||||
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.delete.length).toBe(1);
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalled();
|
||||
|
||||
// Get restore / undo func from the toast
|
||||
const restoreFn = mockShowToast.mock.calls[0][1].onClick;
|
||||
|
||||
const restoreUrl = getLibraryContainerRestoreApiUrl(containerHitSample.usageKey);
|
||||
axiosMock.onPost(restoreUrl).reply(200);
|
||||
// restore collection
|
||||
restoreFn();
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post.length).toEqual(1);
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Undo successful');
|
||||
});
|
||||
|
||||
it('should show error on delete the container from the menu', async () => {
|
||||
axiosMock.onDelete(getLibraryContainerApiUrl(containerHitSample.usageKey)).reply(400);
|
||||
|
||||
render(<ContainerCard hit={containerHitSample} />);
|
||||
|
||||
// Open menu
|
||||
expect(screen.getByTestId('container-card-menu-toggle')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('container-card-menu-toggle'));
|
||||
|
||||
// Click on Delete Item
|
||||
const deleteMenuItem = screen.getByRole('button', { name: 'Delete' });
|
||||
expect(deleteMenuItem).toBeInTheDocument();
|
||||
fireEvent.click(deleteMenuItem);
|
||||
|
||||
// Confirm delete Modal is open
|
||||
expect(screen.getByText('Delete Unit'));
|
||||
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.delete.length).toBe(1);
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to delete unit');
|
||||
});
|
||||
|
||||
it('should render no child blocks in card preview', async () => {
|
||||
render(<ContainerCard hit={containerHitSample} />);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { useCallback, ReactNode } from 'react';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Dropdown,
|
||||
Icon,
|
||||
IconButton,
|
||||
useToggle,
|
||||
Stack,
|
||||
} from '@openedx/paragon';
|
||||
import { MoreVert } from '@openedx/paragon/icons';
|
||||
@@ -16,9 +17,10 @@ import { useComponentPickerContext } from '../common/context/ComponentPickerCont
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import BaseCard from './BaseCard';
|
||||
import { useContainerChildren } from '../data/apiHooks';
|
||||
import { useLibraryRoutes } from '../routes';
|
||||
import messages from './messages';
|
||||
import { useContainerChildren } from '../data/apiHooks';
|
||||
import ContainerDeleter from './ContainerDeleter';
|
||||
|
||||
type ContainerMenuProps = {
|
||||
hit: ContainerHit,
|
||||
@@ -26,29 +28,47 @@ type ContainerMenuProps = {
|
||||
|
||||
const ContainerMenu = ({ hit } : ContainerMenuProps) => {
|
||||
const intl = useIntl();
|
||||
const { contextKey, blockId } = hit;
|
||||
const {
|
||||
contextKey,
|
||||
blockId,
|
||||
usageKey: containerId,
|
||||
displayName,
|
||||
} = hit;
|
||||
|
||||
const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false);
|
||||
|
||||
return (
|
||||
<Dropdown id="container-card-dropdown">
|
||||
<Dropdown.Toggle
|
||||
id="container-card-menu-toggle"
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt={intl.formatMessage(messages.collectionCardMenuAlt)}
|
||||
data-testid="container-card-menu-toggle"
|
||||
<>
|
||||
<Dropdown id="container-card-dropdown">
|
||||
<Dropdown.Toggle
|
||||
id="container-card-menu-toggle"
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt={intl.formatMessage(messages.collectionCardMenuAlt)}
|
||||
data-testid="container-card-menu-toggle"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
to={`/library/${contextKey}/container/${blockId}`}
|
||||
disabled
|
||||
>
|
||||
<FormattedMessage {...messages.menuOpen} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={confirmDelete}>
|
||||
<FormattedMessage {...messages.menuDeleteContainer} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<ContainerDeleter
|
||||
isOpen={isConfirmingDelete}
|
||||
close={cancelDelete}
|
||||
containerId={containerId}
|
||||
displayName={displayName}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
as={Link}
|
||||
to={`/library/${contextKey}/container/${blockId}`}
|
||||
disabled
|
||||
>
|
||||
<FormattedMessage {...messages.menuOpen} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
96
src/library-authoring/components/ContainerDeleter.tsx
Normal file
96
src/library-authoring/components/ContainerDeleter.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ReactNode, useCallback, useContext } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Warning, School, Widgets } from '@openedx/paragon/icons';
|
||||
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { useDeleteContainer, useRestoreContainer } from '../data/apiHooks';
|
||||
import messages from './messages';
|
||||
|
||||
type ContainerDeleterProps = {
|
||||
isOpen: boolean,
|
||||
close: () => void,
|
||||
containerId: string,
|
||||
displayName: string,
|
||||
};
|
||||
|
||||
const ContainerDeleter = ({
|
||||
isOpen,
|
||||
close,
|
||||
containerId,
|
||||
displayName,
|
||||
}: ContainerDeleterProps) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
sidebarComponentInfo,
|
||||
closeLibrarySidebar,
|
||||
} = useSidebarContext();
|
||||
const deleteContainerMutation = useDeleteContainer(containerId);
|
||||
const restoreContainerMutation = useRestoreContainer(containerId);
|
||||
const { showToast } = useContext(ToastContext);
|
||||
|
||||
// TODO: support other container types besides 'unit'
|
||||
const deleteWarningTitle = intl.formatMessage(messages.deleteUnitWarningTitle);
|
||||
const deleteText = intl.formatMessage(messages.deleteUnitConfirm, {
|
||||
unitName: <b>{displayName}</b>,
|
||||
message: (
|
||||
<>
|
||||
<div className="d-flex mt-2">
|
||||
<Icon className="mr-2" src={School} />
|
||||
{intl.formatMessage(messages.deleteUnitConfirmMsg1)}
|
||||
</div>
|
||||
<div className="d-flex mt-2">
|
||||
<Icon className="mr-2" src={Widgets} />
|
||||
{intl.formatMessage(messages.deleteUnitConfirmMsg2)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
}) as ReactNode as string;
|
||||
const deleteSuccess = intl.formatMessage(messages.deleteUnitSuccess);
|
||||
const deleteError = intl.formatMessage(messages.deleteUnitFailed);
|
||||
const undoDeleteError = messages.undoDeleteUnitToastFailed;
|
||||
|
||||
const restoreComponent = useCallback(async () => {
|
||||
try {
|
||||
await restoreContainerMutation.mutateAsync();
|
||||
showToast(intl.formatMessage(messages.undoDeleteContainerToastMessage));
|
||||
} catch (e) {
|
||||
showToast(intl.formatMessage(undoDeleteError));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
await deleteContainerMutation.mutateAsync().then(() => {
|
||||
if (sidebarComponentInfo?.id === containerId) {
|
||||
closeLibrarySidebar();
|
||||
}
|
||||
showToast(
|
||||
deleteSuccess,
|
||||
{
|
||||
label: intl.formatMessage(messages.undoDeleteContainerToastAction),
|
||||
onClick: restoreComponent,
|
||||
},
|
||||
);
|
||||
}).catch(() => {
|
||||
showToast(deleteError);
|
||||
}).finally(() => {
|
||||
close();
|
||||
});
|
||||
}, [sidebarComponentInfo, showToast, deleteContainerMutation]);
|
||||
|
||||
return (
|
||||
<DeleteModal
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
variant="warning"
|
||||
title={deleteWarningTitle}
|
||||
icon={Warning}
|
||||
description={deleteText}
|
||||
onDeleteSubmit={onDelete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContainerDeleter;
|
||||
@@ -176,6 +176,56 @@ const messages = defineMessages({
|
||||
defaultMessage: 'This component can be synced in courses after publish.',
|
||||
description: 'Alert text of the modal to confirm publish a component in a library.',
|
||||
},
|
||||
menuDeleteContainer: {
|
||||
id: 'course-authoring.library-authoring.container.delete-menu-text',
|
||||
defaultMessage: 'Delete',
|
||||
description: 'Menu item to delete a container.',
|
||||
},
|
||||
deleteUnitWarningTitle: {
|
||||
id: 'course-authoring.library-authoring.unit.delete-confirmation-title',
|
||||
defaultMessage: 'Delete Unit',
|
||||
description: 'Title text for the warning displayed before deleting a Unit',
|
||||
},
|
||||
deleteUnitConfirm: {
|
||||
id: 'course-authoring.library-authoring.unit.delete-confirmation-text',
|
||||
defaultMessage: 'Delete {unitName}? {message}',
|
||||
description: 'Confirmation text to display before deleting a unit',
|
||||
},
|
||||
deleteUnitConfirmMsg1: {
|
||||
id: 'course-authoring.library-authoring.unit.delete-confirmation-msg-1',
|
||||
defaultMessage: 'Any course instances will stop receiving updates.',
|
||||
description: 'First part of confirmation message to display before deleting a unit',
|
||||
},
|
||||
deleteUnitConfirmMsg2: {
|
||||
id: 'course-authoring.library-authoring.unit.delete-confirmation-msg-2',
|
||||
defaultMessage: 'Any components will remain in the library.',
|
||||
description: 'Second part of confirmation message to display before deleting a unit',
|
||||
},
|
||||
deleteUnitSuccess: {
|
||||
id: 'course-authoring.library-authoring.unit.delete.success',
|
||||
defaultMessage: 'Unit deleted',
|
||||
description: 'Message to display on delete unit success',
|
||||
},
|
||||
deleteUnitFailed: {
|
||||
id: 'course-authoring.library-authoring.unit.delete-failed-error',
|
||||
defaultMessage: 'Failed to delete unit',
|
||||
description: 'Message to display on failure to delete a unit',
|
||||
},
|
||||
undoDeleteContainerToastAction: {
|
||||
id: 'course-authoring.library-authoring.container.undo-delete-container-toast-button',
|
||||
defaultMessage: 'Undo',
|
||||
description: 'Toast message to undo deletion of container',
|
||||
},
|
||||
undoDeleteContainerToastMessage: {
|
||||
id: 'course-authoring.library-authoring.container.undo-delete-container-toast-text',
|
||||
defaultMessage: 'Undo successful',
|
||||
description: 'Message to display on undo delete container success',
|
||||
},
|
||||
undoDeleteUnitToastFailed: {
|
||||
id: 'course-authoring.library-authoring.unit.undo-delete-unit-failed',
|
||||
defaultMessage: 'Failed to undo delete Unit operation',
|
||||
description: 'Message to display on failure to undo delete unit',
|
||||
},
|
||||
containerPreviewMoreBlocks: {
|
||||
id: 'course-authoring.library-authoring.component.container-card-preview.more-blocks',
|
||||
defaultMessage: '+{count}',
|
||||
|
||||
64
src/library-authoring/containers/UnitInfo.test.tsx
Normal file
64
src/library-authoring/containers/UnitInfo.test.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import {
|
||||
initializeMocks, render as baseRender, screen, waitFor,
|
||||
fireEvent,
|
||||
} from '../../testUtils';
|
||||
import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import UnitInfo from './UnitInfo';
|
||||
import { getLibraryContainerApiUrl } from '../data/api';
|
||||
import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext';
|
||||
|
||||
mockGetContainerMetadata.applyMock();
|
||||
const { libraryId } = mockContentLibrary;
|
||||
const { containerId } = mockGetContainerMetadata;
|
||||
const render = () => baseRender(<UnitInfo />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyComponentId.UnitInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</LibraryProvider>
|
||||
),
|
||||
});
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast;
|
||||
|
||||
describe('<UnitInfo />', () => {
|
||||
beforeEach(() => {
|
||||
({ axiosMock, mockShowToast } = initializeMocks());
|
||||
});
|
||||
|
||||
it('should detele the unit using the menu', async () => {
|
||||
axiosMock.onDelete(getLibraryContainerApiUrl(containerId)).reply(200);
|
||||
render();
|
||||
|
||||
// Open menu
|
||||
expect(await screen.findByTestId('unit-info-menu-toggle')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('unit-info-menu-toggle'));
|
||||
|
||||
// Click on Delete Item
|
||||
const deleteMenuItem = screen.getByRole('button', { name: 'Delete' });
|
||||
expect(deleteMenuItem).toBeInTheDocument();
|
||||
fireEvent.click(deleteMenuItem);
|
||||
|
||||
// Confirm delete Modal is open
|
||||
expect(screen.getByText('Delete Unit'));
|
||||
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.delete.length).toBe(1);
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,16 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button,
|
||||
Stack,
|
||||
Tab,
|
||||
Tabs,
|
||||
Dropdown,
|
||||
Icon,
|
||||
IconButton,
|
||||
useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { MoreVert } from '@openedx/paragon/icons';
|
||||
|
||||
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
|
||||
import {
|
||||
type UnitInfoTab,
|
||||
@@ -14,6 +20,47 @@ import {
|
||||
} from '../common/context/SidebarContext';
|
||||
import ContainerOrganize from './ContainerOrganize';
|
||||
import messages from './messages';
|
||||
import componentMessages from '../components/messages';
|
||||
import ContainerDeleter from '../components/ContainerDeleter';
|
||||
import { useContainer } from '../data/apiHooks';
|
||||
|
||||
type ContainerMenuProps = {
|
||||
containerId: string,
|
||||
displayName: string,
|
||||
};
|
||||
|
||||
const UnitMenu = ({ containerId, displayName }: ContainerMenuProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown id="unit-info-dropdown">
|
||||
<Dropdown.Toggle
|
||||
id="unit-info-menu-toggle"
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt={intl.formatMessage(componentMessages.containerCardMenuAlt)}
|
||||
data-testid="unit-info-menu-toggle"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={confirmDelete}>
|
||||
<FormattedMessage {...componentMessages.menuDeleteContainer} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<ContainerDeleter
|
||||
isOpen={isConfirmingDelete}
|
||||
close={cancelDelete}
|
||||
containerId={containerId}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitInfo = () => {
|
||||
const intl = useIntl();
|
||||
@@ -31,11 +78,17 @@ const UnitInfo = () => {
|
||||
throw new Error('unitId is required');
|
||||
}
|
||||
|
||||
const showOpenCollectionButton = !componentPickerMode;
|
||||
const showOpenUnitButton = !componentPickerMode;
|
||||
|
||||
const { data: container } = useContainer(unitId);
|
||||
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{showOpenCollectionButton && (
|
||||
{showOpenUnitButton && (
|
||||
<div className="d-flex flex-wrap">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
@@ -44,6 +97,10 @@ const UnitInfo = () => {
|
||||
>
|
||||
{intl.formatMessage(messages.openUnitButton)}
|
||||
</Button>
|
||||
<UnitMenu
|
||||
containerId={unitId}
|
||||
displayName={container.displayName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Tabs
|
||||
|
||||
@@ -93,4 +93,26 @@ describe('library data API', () => {
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should delete a container', async () => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
const containerId = 'lct:org:lib1';
|
||||
const url = api.getLibraryContainerApiUrl(containerId);
|
||||
|
||||
axiosMock.onDelete(url).reply(200);
|
||||
|
||||
await api.deleteContainer(containerId);
|
||||
expect(axiosMock.history.delete[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should restore a container', async () => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
const containerId = 'lct:org:lib1';
|
||||
const url = api.getLibraryContainerRestoreApiUrl(containerId);
|
||||
|
||||
axiosMock.onPost(url).reply(200);
|
||||
|
||||
await api.restoreContainer(containerId);
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,6 +111,10 @@ export const getLibraryContainersApiUrl = (libraryId: string) => `${getApiBaseUr
|
||||
* Get the URL for the container detail api.
|
||||
*/
|
||||
export const getLibraryContainerApiUrl = (containerId: string) => `${getApiBaseUrl()}/api/libraries/v2/containers/${containerId}/`;
|
||||
/**
|
||||
* Get the URL for restore a container
|
||||
*/
|
||||
export const getLibraryContainerRestoreApiUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}restore/`;
|
||||
/**
|
||||
* Get the URL for a single container children api.
|
||||
*/
|
||||
@@ -621,6 +625,22 @@ export async function updateContainerMetadata(
|
||||
await client.patch(getLibraryContainerApiUrl(containerId), snakeCaseObject(containerData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a container
|
||||
*/
|
||||
export async function deleteContainer(containerId: string) {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
await client.delete(getLibraryContainerApiUrl(containerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a container
|
||||
*/
|
||||
export async function restoreContainer(containerId: string) {
|
||||
const client = getAuthenticatedHttpClient();
|
||||
await client.post(getLibraryContainerRestoreApiUrl(containerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a library container's children's metadata.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getLibraryCollectionApiUrl,
|
||||
getBlockTypesMetaDataUrl,
|
||||
getLibraryContainerApiUrl,
|
||||
getLibraryContainerRestoreApiUrl,
|
||||
getLibraryContainerChildrenApiUrl,
|
||||
} from './api';
|
||||
import {
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
useCollection,
|
||||
useBlockTypesMetadata,
|
||||
useContainer,
|
||||
useDeleteContainer,
|
||||
useRestoreContainer,
|
||||
useContainerChildren,
|
||||
} from './apiHooks';
|
||||
|
||||
@@ -155,6 +158,30 @@ describe('library api hooks', () => {
|
||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should delete a container', async () => {
|
||||
const containerId = 'lct:org:lib1';
|
||||
const url = getLibraryContainerApiUrl(containerId);
|
||||
|
||||
axiosMock.onDelete(url).reply(200);
|
||||
const { result } = renderHook(() => useDeleteContainer(containerId), { wrapper });
|
||||
await result.current.mutateAsync();
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.delete[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
it('should restore a container', async () => {
|
||||
const containerId = 'lct:org:lib1';
|
||||
const url = getLibraryContainerRestoreApiUrl(containerId);
|
||||
|
||||
axiosMock.onPost(url).reply(200);
|
||||
const { result } = renderHook(() => useRestoreContainer(containerId), { wrapper });
|
||||
await result.current.mutateAsync();
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get container children', async () => {
|
||||
const containerId = 'lct:lib:org:unit:unit1';
|
||||
const url = getLibraryContainerChildrenApiUrl(containerId);
|
||||
|
||||
@@ -50,7 +50,9 @@ import {
|
||||
type CreateLibraryContainerDataRequest,
|
||||
getContainerMetadata,
|
||||
updateContainerMetadata,
|
||||
deleteContainer,
|
||||
type UpdateContainerDataRequest,
|
||||
restoreContainer,
|
||||
getContainerChildren,
|
||||
} from './api';
|
||||
import { VersionSpec } from '../LibraryBlock';
|
||||
@@ -621,6 +623,35 @@ export const useUpdateContainer = (containerId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this mutation to soft delete containers in a library
|
||||
*/
|
||||
export const useDeleteContainer = (containerId: string) => {
|
||||
const libraryId = getLibraryId(containerId);
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => deleteContainer(containerId),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
|
||||
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this mutation to restore a container
|
||||
*/
|
||||
export const useRestoreContainer = (containerId: string) => {
|
||||
const libraryId = getLibraryId(containerId);
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => restoreContainer(containerId),
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the metadata and children for a container in a library
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user