feat: add delete confirmation modal (#570)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={handleDelete}>
|
||||
<Dropdown.Item
|
||||
data-testid="open-delete-confirmation-button"
|
||||
onClick={openDeleteConfirmation}
|
||||
>
|
||||
{intl.formatMessage(messages.deleteTitle)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) => (
|
||||
<TableActions
|
||||
{...{
|
||||
selectedFlatRows,
|
||||
fileInputControl,
|
||||
handleBulkDelete,
|
||||
handleBulkDownload,
|
||||
handleOpenDeleteConfirmation,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -119,8 +128,8 @@ const FilesAndUploads = ({
|
||||
return (
|
||||
<GalleryCard
|
||||
{...{
|
||||
handleBulkDelete,
|
||||
handleLockedAsset,
|
||||
handleOpenDeleteConfirmation,
|
||||
className,
|
||||
original,
|
||||
}}
|
||||
@@ -130,8 +139,8 @@ const FilesAndUploads = ({
|
||||
return (
|
||||
<ListCard
|
||||
{...{
|
||||
handleBulkDelete,
|
||||
handleLockedAsset,
|
||||
handleOpenDeleteConfirmation,
|
||||
className,
|
||||
original,
|
||||
}}
|
||||
@@ -244,22 +253,40 @@ const FilesAndUploads = ({
|
||||
<DataTable.TableFooter />
|
||||
<ApiStatusToast
|
||||
actionType={intl.formatMessage(messages.apiStatusDeletingAction)}
|
||||
selectedRowCount={selectedRowCount}
|
||||
selectedRowCount={selectedRows.length}
|
||||
isOpen={isDeleteOpen}
|
||||
setClose={setDeleteClose}
|
||||
setSelectedRowCount={setSelectedRowCount}
|
||||
setSelectedRows={setSelectedRows}
|
||||
/>
|
||||
<ApiStatusToast
|
||||
actionType={intl.formatMessage(messages.apiStatusAddingAction)}
|
||||
selectedRowCount={selectedRowCount}
|
||||
selectedRowCount={selectedRows.length}
|
||||
isOpen={isAddOpen}
|
||||
setClose={setAddClose}
|
||||
setSelectedRowCount={setSelectedRowCount}
|
||||
setSelectedRows={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DataTable>
|
||||
<FileInput fileInput={fileInputControl} />
|
||||
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.deleteConfirmationTitle)}
|
||||
isOpen={isDeleteConfirmationOpen}
|
||||
onClose={closeDeleteConfirmation}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={closeDeleteConfirmation}>
|
||||
{intl.formatMessage(messages.cancelButtonLabel)}
|
||||
</Button>
|
||||
<Button onClick={handleBulkDelete}>
|
||||
{intl.formatMessage(messages.deleteFileButtonLabel)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(messages.deleteConfirmationMessage, { fileNumber: selectedRows.length })}
|
||||
</AlertModal>
|
||||
</main>
|
||||
</FilesAndUploadsProvider>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = ({
|
||||
<ActionRow>
|
||||
<FileMenu
|
||||
externalUrl={original.externalUrl}
|
||||
handleDelete={deleteAsset}
|
||||
handleLock={lockAsset}
|
||||
locked={original.locked}
|
||||
openAssetInfo={openAssetInfo}
|
||||
portableUrl={original.portableUrl}
|
||||
iconSrc={MoreVert}
|
||||
id={original.id}
|
||||
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
||||
/>
|
||||
</ActionRow>
|
||||
)}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = ({
|
||||
<ActionRow>
|
||||
<FileMenu
|
||||
externalUrl={original.externalUrl}
|
||||
handleDelete={deleteAsset}
|
||||
handleLock={lockAsset}
|
||||
locked={original.locked}
|
||||
openAssetInfo={openAssetInfo}
|
||||
portableUrl={original.portableUrl}
|
||||
iconSrc={MoreVert}
|
||||
id={original.id}
|
||||
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
||||
/>
|
||||
</ActionRow>
|
||||
</Card.Footer>
|
||||
@@ -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;
|
||||
|
||||
@@ -9,8 +9,8 @@ import messages from '../messages';
|
||||
const TableActions = ({
|
||||
selectedFlatRows,
|
||||
fileInputControl,
|
||||
handleBulkDelete,
|
||||
handleBulkDownload,
|
||||
handleOpenDeleteConfirmation,
|
||||
}) => (
|
||||
<>
|
||||
<Dropdown>
|
||||
@@ -30,7 +30,8 @@ const TableActions = ({
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
onClick={() => handleBulkDelete(selectedFlatRows)}
|
||||
data-testid="open-delete-confirmation-button"
|
||||
onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
|
||||
disabled={_.isEmpty(selectedFlatRows)}
|
||||
>
|
||||
<FormattedMessage {...messages.deleteTitle} />
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user