Skip to content

Commit

Permalink
feat: add delete confirmation modal (openedx#570)
Browse files Browse the repository at this point in the history
  • Loading branch information
KristinAoki authored and snglth committed Jan 9, 2024
1 parent 31635f7 commit 8e21e44
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/custom-pages/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions src/files-and-uploads/ApiStatusToast.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ const ApiStatusToast = ({
selectedRowCount,
isOpen,
setClose,
setSelectedRowCount,
setSelectedRows,
// injected
intl,
}) => {
const handleClose = () => {
setSelectedRowCount(0);
setSelectedRows([]);
setClose();
};

Expand All @@ -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,
};
Expand Down
4 changes: 2 additions & 2 deletions src/files-and-uploads/FileInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import PropTypes from 'prop-types';

export const fileInput = ({
onAddFile,
setSelectedRowCount,
setSelectedRows,
setAddOpen,
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const ref = React.useRef();
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();
Expand Down
9 changes: 6 additions & 3 deletions src/files-and-uploads/FileMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import messages from './messages';

const FileMenu = ({
externalUrl,
handleDelete,
handleLock,
locked,
openAssetInfo,
openDeleteConfirmation,
portableUrl,
iconSrc,
id,
Expand Down Expand Up @@ -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>
Expand All @@ -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,
Expand Down
51 changes: 39 additions & 12 deletions src/files-and-uploads/FilesAndUploads.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
Dropzone,
CardView,
useToggle,
AlertModal,
ActionRow,
Button,
} from '@edx/paragon';
import Placeholder, { ErrorAlert } from '@edx/frontend-lib-content-components';

Expand Down Expand Up @@ -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));
Expand All @@ -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);
Expand All @@ -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)));
};

Expand All @@ -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,
}}
/>
);
Expand All @@ -119,8 +128,8 @@ const FilesAndUploads = ({
return (
<GalleryCard
{...{
handleBulkDelete,
handleLockedAsset,
handleOpenDeleteConfirmation,
className,
original,
}}
Expand All @@ -130,8 +139,8 @@ const FilesAndUploads = ({
return (
<ListCard
{...{
handleBulkDelete,
handleLockedAsset,
handleOpenDeleteConfirmation,
className,
original,
}}
Expand Down Expand Up @@ -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>
);
Expand Down
17 changes: 13 additions & 4 deletions src/files-and-uploads/FilesAndUploads.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/files-and-uploads/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
9 changes: 3 additions & 6 deletions src/files-and-uploads/table-components/GalleryCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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>
)}
Expand Down Expand Up @@ -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;
9 changes: 3 additions & 6 deletions src/files-and-uploads/table-components/ListCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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;
7 changes: 4 additions & 3 deletions src/files-and-uploads/table-components/TableActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import messages from '../messages';
const TableActions = ({
selectedFlatRows,
fileInputControl,
handleBulkDelete,
handleBulkDownload,
handleOpenDeleteConfirmation,
}) => (
<>
<Dropdown>
Expand All @@ -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} />
Expand Down Expand Up @@ -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,
};

Expand Down

0 comments on commit 8e21e44

Please sign in to comment.