From 8e21e44c3a81f23e28a838450ce52b1a0fad0d83 Mon Sep 17 00:00:00 2001
From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
Date: Mon, 21 Aug 2023 17:28:49 -0400
Subject: [PATCH] feat: add delete confirmation modal (#570)
---
src/custom-pages/messages.js | 2 +-
src/files-and-uploads/ApiStatusToast.jsx | 6 +--
src/files-and-uploads/FileInput.jsx | 4 +-
src/files-and-uploads/FileMenu.jsx | 9 ++--
src/files-and-uploads/FilesAndUploads.jsx | 51 ++++++++++++++-----
.../FilesAndUploads.test.jsx | 17 +++++--
src/files-and-uploads/messages.js | 16 ++++++
.../table-components/GalleryCard.jsx | 9 ++--
.../table-components/ListCard.jsx | 9 ++--
.../table-components/TableActions.jsx | 7 +--
10 files changed, 90 insertions(+), 40 deletions(-)
diff --git a/src/custom-pages/messages.js b/src/custom-pages/messages.js
index af6b7ca6d4..60e8da9e04 100644
--- a/src/custom-pages/messages.js
+++ b/src/custom-pages/messages.js
@@ -95,7 +95,7 @@ const messages = defineMessages({
},
deletePageLabel: {
id: 'course-authoring.custom-pages.deleteConfirmation.deletePage.label',
- defaultMessage: 'Ok',
+ defaultMessage: 'Delete',
},
deletingPageBodyLabel: {
id: 'course-authoring.custom-pages.deleteConfirmation.deletingPage.label',
diff --git a/src/files-and-uploads/ApiStatusToast.jsx b/src/files-and-uploads/ApiStatusToast.jsx
index 4d997b7e05..ade1d2aa16 100644
--- a/src/files-and-uploads/ApiStatusToast.jsx
+++ b/src/files-and-uploads/ApiStatusToast.jsx
@@ -9,12 +9,12 @@ const ApiStatusToast = ({
selectedRowCount,
isOpen,
setClose,
- setSelectedRowCount,
+ setSelectedRows,
// injected
intl,
}) => {
const handleClose = () => {
- setSelectedRowCount(0);
+ setSelectedRows([]);
setClose();
};
@@ -33,7 +33,7 @@ ApiStatusToast.propTypes = {
selectedRowCount: PropTypes.number.isRequired,
isOpen: PropTypes.bool.isRequired,
setClose: PropTypes.func.isRequired,
- setSelectedRowCount: PropTypes.func.isRequired,
+ setSelectedRows: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
diff --git a/src/files-and-uploads/FileInput.jsx b/src/files-and-uploads/FileInput.jsx
index 9b0b8510d8..b7be691f11 100644
--- a/src/files-and-uploads/FileInput.jsx
+++ b/src/files-and-uploads/FileInput.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
export const fileInput = ({
onAddFile,
- setSelectedRowCount,
+ setSelectedRows,
setAddOpen,
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -11,7 +11,7 @@ export const fileInput = ({
const click = () => ref.current.click();
const addFile = (e) => {
const { files } = e.target;
- setSelectedRowCount(files.length);
+ setSelectedRows(files);
Object.values(files).forEach(file => {
onAddFile(file);
setAddOpen();
diff --git a/src/files-and-uploads/FileMenu.jsx b/src/files-and-uploads/FileMenu.jsx
index 0bfab16fb6..46b9defe10 100644
--- a/src/files-and-uploads/FileMenu.jsx
+++ b/src/files-and-uploads/FileMenu.jsx
@@ -10,10 +10,10 @@ import messages from './messages';
const FileMenu = ({
externalUrl,
- handleDelete,
handleLock,
locked,
openAssetInfo,
+ openDeleteConfirmation,
portableUrl,
iconSrc,
id,
@@ -50,7 +50,10 @@ const FileMenu = ({
{intl.formatMessage(messages.infoTitle)}
-
+
{intl.formatMessage(messages.deleteTitle)}
@@ -59,10 +62,10 @@ const FileMenu = ({
FileMenu.propTypes = {
externalUrl: PropTypes.string.isRequired,
- handleDelete: PropTypes.func.isRequired,
handleLock: PropTypes.func.isRequired,
locked: PropTypes.bool.isRequired,
openAssetInfo: PropTypes.func.isRequired,
+ openDeleteConfirmation: PropTypes.func.isRequired,
portableUrl: PropTypes.string.isRequired,
iconSrc: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
diff --git a/src/files-and-uploads/FilesAndUploads.jsx b/src/files-and-uploads/FilesAndUploads.jsx
index 416866fbbf..e7494ec0be 100644
--- a/src/files-and-uploads/FilesAndUploads.jsx
+++ b/src/files-and-uploads/FilesAndUploads.jsx
@@ -10,6 +10,9 @@ import {
Dropzone,
CardView,
useToggle,
+ AlertModal,
+ ActionRow,
+ Button,
} from '@edx/paragon';
import Placeholder, { ErrorAlert } from '@edx/frontend-lib-content-components';
@@ -48,7 +51,8 @@ const FilesAndUploads = ({
const [currentView, setCurrentView] = useState(defaultVal);
const [isDeleteOpen, setDeleteOpen, setDeleteClose] = useToggle(false);
const [isAddOpen, setAddOpen, setAddClose] = useToggle(false);
- const [selectedRowCount, setSelectedRowCount] = useState(0);
+ const [selectedRows, setSelectedRows] = useState([]);
+ const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false);
useEffect(() => {
dispatch(fetchAssets(courseId));
@@ -64,7 +68,7 @@ const FilesAndUploads = ({
const errorMessages = useSelector(state => state.assets.errors);
const fileInputControl = fileInput({
onAddFile: (file) => dispatch(addAssetFile(courseId, file, totalCount)),
- setSelectedRowCount,
+ setSelectedRows,
setAddOpen,
});
const assets = useModels('assets', assetIds);
@@ -78,10 +82,10 @@ const FilesAndUploads = ({
}
};
- const handleBulkDelete = (selectedFlatRows) => {
- setSelectedRowCount(selectedFlatRows.length);
+ const handleBulkDelete = () => {
+ closeDeleteConfirmation();
setDeleteOpen();
- const assetIdsToDelete = selectedFlatRows.map(row => row.original.id);
+ const assetIdsToDelete = selectedRows.map(row => row.original.id);
assetIdsToDelete.forEach(id => dispatch(deleteAssetFile(courseId, id, totalCount)));
};
@@ -103,13 +107,18 @@ const FilesAndUploads = ({
dispatch(updateAssetLock({ courseId, assetId, locked }));
};
+ const handleOpenDeleteConfirmation = (selectedFlatRows) => {
+ setSelectedRows(selectedFlatRows);
+ openDeleteConfirmation();
+ };
+
const headerActions = ({ selectedFlatRows }) => (
);
@@ -119,8 +128,8 @@ const FilesAndUploads = ({
return (
)}
+
+
+
+
+
+ )}
+ >
+ {intl.formatMessage(messages.deleteConfirmationMessage, { fileNumber: selectedRows.length })}
+
);
diff --git a/src/files-and-uploads/FilesAndUploads.test.jsx b/src/files-and-uploads/FilesAndUploads.test.jsx
index 0815025899..dbd76a5f74 100644
--- a/src/files-and-uploads/FilesAndUploads.test.jsx
+++ b/src/files-and-uploads/FilesAndUploads.test.jsx
@@ -194,9 +194,12 @@ describe('FilesAndUploads', () => {
const deleteButton = screen.getByText(messages.deleteTitle.defaultMessage).closest('a');
expect(deleteButton).not.toHaveClass('disabled');
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
- await act(async () => {
+ await waitFor(() => {
fireEvent.click(deleteButton);
- await executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
+ expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
+ fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
+ expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
+ executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
});
const deleteStatus = store.getState().assets.deletingStatus;
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
@@ -284,7 +287,10 @@ describe('FilesAndUploads', () => {
await waitFor(() => {
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
- fireEvent.click(screen.getByText('Delete'));
+ fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
+ expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
+ fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
+ expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
});
const deleteStatus = store.getState().assets.deletingStatus;
@@ -330,7 +336,10 @@ describe('FilesAndUploads', () => {
await waitFor(() => {
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(404);
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
- fireEvent.click(screen.getByText('Delete'));
+ fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
+ expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
+ fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
+ expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
});
const deleteStatus = store.getState().assets.deletingStatus;
diff --git a/src/files-and-uploads/messages.js b/src/files-and-uploads/messages.js
index a1f7c7bb2b..21cd6196e9 100644
--- a/src/files-and-uploads/messages.js
+++ b/src/files-and-uploads/messages.js
@@ -101,6 +101,22 @@ const messages = defineMessages({
id: 'course-authoring.files-and-uploads.cardMenu.deleteTitle',
defaultMessage: 'Delete',
},
+ deleteConfirmationTitle: {
+ id: 'course-authoring.files-and-uploads..deleteConfirmation.title',
+ defaultMessage: 'Delete File(s) Confirmation',
+ },
+ deleteConfirmationMessage: {
+ id: 'course-authoring.files-and-uploads..deleteConfirmation.message',
+ defaultMessage: 'Are you sure you want to delete {fileNumber} file(s)? This action cannot be undone.',
+ },
+ deleteFileButtonLabel: {
+ id: 'course-authoring.files-and-uploads.deleteConfirmation.deleteFile.label',
+ defaultMessage: 'Delete',
+ },
+ cancelButtonLabel: {
+ id: 'course-authoring.files-and-uploads.deleteConfirmation.cancelButton.label',
+ defaultMessage: 'Cancel',
+ },
});
export default messages;
diff --git a/src/files-and-uploads/table-components/GalleryCard.jsx b/src/files-and-uploads/table-components/GalleryCard.jsx
index 7d69de6d52..dde7c2f86c 100644
--- a/src/files-and-uploads/table-components/GalleryCard.jsx
+++ b/src/files-and-uploads/table-components/GalleryCard.jsx
@@ -19,13 +19,10 @@ import { getSrc } from '../data/utils';
const GalleryCard = ({
className,
original,
- handleBulkDelete,
handleLockedAsset,
+ handleOpenDeleteConfirmation,
}) => {
const [isAssetInfoOpen, openAssetInfo, closeAssetinfo] = useToggle(false);
- const deleteAsset = () => {
- handleBulkDelete([{ original }]);
- };
const lockAsset = () => {
const { locked } = original;
handleLockedAsset(original.id, !locked);
@@ -43,13 +40,13 @@ const GalleryCard = ({
handleOpenDeleteConfirmation([{ original }])}
/>
)}
@@ -99,7 +96,7 @@ GalleryCard.propTypes = {
portableUrl: PropTypes.string.isRequired,
}).isRequired,
handleLockedAsset: PropTypes.func.isRequired,
- handleBulkDelete: PropTypes.func.isRequired,
+ handleOpenDeleteConfirmation: PropTypes.func.isRequired,
};
export default GalleryCard;
diff --git a/src/files-and-uploads/table-components/ListCard.jsx b/src/files-and-uploads/table-components/ListCard.jsx
index a2dd0cfab0..5f987f76d7 100644
--- a/src/files-and-uploads/table-components/ListCard.jsx
+++ b/src/files-and-uploads/table-components/ListCard.jsx
@@ -19,13 +19,10 @@ import { getSrc } from '../data/utils';
const ListCard = ({
className,
original,
- handleBulkDelete,
handleLockedAsset,
+ handleOpenDeleteConfirmation,
}) => {
const [isAssetInfoOpen, openAssetInfo, closeAssetinfo] = useToggle(false);
- const deleteAsset = () => {
- handleBulkDelete([{ original }]);
- };
const lockAsset = () => {
const { locked } = original;
handleLockedAsset(original.id, !locked);
@@ -65,13 +62,13 @@ const ListCard = ({
handleOpenDeleteConfirmation([{ original }])}
/>
@@ -101,7 +98,7 @@ ListCard.propTypes = {
portableUrl: PropTypes.string.isRequired,
}).isRequired,
handleLockedAsset: PropTypes.func.isRequired,
- handleBulkDelete: PropTypes.func.isRequired,
+ handleOpenDeleteConfirmation: PropTypes.func.isRequired,
};
export default ListCard;
diff --git a/src/files-and-uploads/table-components/TableActions.jsx b/src/files-and-uploads/table-components/TableActions.jsx
index db4ccdddf5..76d6ae4180 100644
--- a/src/files-and-uploads/table-components/TableActions.jsx
+++ b/src/files-and-uploads/table-components/TableActions.jsx
@@ -9,8 +9,8 @@ import messages from '../messages';
const TableActions = ({
selectedFlatRows,
fileInputControl,
- handleBulkDelete,
handleBulkDownload,
+ handleOpenDeleteConfirmation,
}) => (
<>
@@ -30,7 +30,8 @@ const TableActions = ({
handleBulkDelete(selectedFlatRows)}
+ data-testid="open-delete-confirmation-button"
+ onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
>
@@ -63,7 +64,7 @@ TableActions.propTypes = {
fileInputControl: PropTypes.shape({
click: PropTypes.func.isRequired,
}).isRequired,
- handleBulkDelete: PropTypes.func.isRequired,
+ handleOpenDeleteConfirmation: PropTypes.func.isRequired,
handleBulkDownload: PropTypes.func.isRequired,
};