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, };