DeleteAttachmentModal.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import type React from 'react';
  2. import { useCallback, useMemo, useState } from 'react';
  3. import type { IAttachmentHasId } from '@growi/core';
  4. import { LoadingSpinner, UserPicture } from '@growi/ui/dist/components';
  5. import { useTranslation } from 'next-i18next';
  6. import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
  7. import { toastError, toastSuccess } from '~/client/util/toastr';
  8. import {
  9. useDeleteAttachmentModalActions,
  10. useDeleteAttachmentModalStatus,
  11. } from '~/states/ui/modal/delete-attachment';
  12. import loggerFactory from '~/utils/logger';
  13. import { Username } from '../../../components/User/Username';
  14. import styles from './DeleteAttachmentModal.module.scss';
  15. const logger = loggerFactory('growi:attachmentDelete');
  16. const iconByFormat = (format: string): string => {
  17. return format.match(/image\/.+/i) ? 'image' : 'description';
  18. };
  19. /**
  20. * DeleteAttachmentModalSubstance - Presentation component (all logic here)
  21. */
  22. type DeleteAttachmentModalSubstanceProps = {
  23. attachment: IAttachmentHasId | undefined;
  24. remove: ((args: { attachment_id: string }) => Promise<void>) | undefined;
  25. closeModal: () => void;
  26. };
  27. const DeleteAttachmentModalSubstance = ({
  28. attachment,
  29. remove,
  30. closeModal,
  31. }: DeleteAttachmentModalSubstanceProps): React.JSX.Element => {
  32. const [deleting, setDeleting] = useState<boolean>(false);
  33. const [deleteError, setDeleteError] = useState<string>('');
  34. const { t } = useTranslation();
  35. const toggleHandler = useCallback(() => {
  36. closeModal();
  37. setDeleting(false);
  38. setDeleteError('');
  39. }, [closeModal]);
  40. const onClickDeleteButtonHandler = useCallback(async () => {
  41. if (remove == null || attachment == null) {
  42. return;
  43. }
  44. setDeleting(true);
  45. try {
  46. await remove({ attachment_id: attachment._id });
  47. setDeleting(false);
  48. closeModal();
  49. toastSuccess(`Delete ${attachment.originalName}`);
  50. } catch (err) {
  51. setDeleting(false);
  52. setDeleteError('Attachment could not be deleted.');
  53. toastError(err);
  54. logger.error(err);
  55. }
  56. }, [attachment, closeModal, remove]);
  57. const attachmentFileFormat = useMemo(() => {
  58. if (attachment == null) {
  59. return;
  60. }
  61. const content = attachment.fileFormat.match(/image\/.+/i) ? (
  62. <img src={attachment.filePathProxied} alt="deleting attachment" />
  63. ) : (
  64. ''
  65. );
  66. return (
  67. <div className="attachment-delete-image">
  68. <p>
  69. <span className="material-symbols-outlined">
  70. {iconByFormat(attachment.fileFormat)}
  71. </span>{' '}
  72. {attachment.originalName}
  73. </p>
  74. <p>
  75. uploaded by{' '}
  76. <UserPicture user={attachment.creator} size="sm"></UserPicture>{' '}
  77. <Username user={attachment.creator}></Username>
  78. </p>
  79. {content}
  80. </div>
  81. );
  82. }, [attachment]);
  83. const deletingIndicator = useMemo(() => {
  84. if (deleting) {
  85. return <LoadingSpinner />;
  86. }
  87. if (deleteError) {
  88. return <span>{deleteError}</span>;
  89. }
  90. return <></>;
  91. }, [deleting, deleteError]);
  92. return (
  93. <div>
  94. <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger">
  95. <span id="contained-modal-title-lg">
  96. {t('delete_attachment_modal.confirm_delete_attachment')}
  97. </span>
  98. </ModalHeader>
  99. <ModalBody>{attachmentFileFormat}</ModalBody>
  100. <ModalFooter>
  101. <div className="me-3 d-inline-block">{deletingIndicator}</div>
  102. <Button color="outline-neutral-secondary" onClick={toggleHandler}>
  103. {t('commons:Cancel')}
  104. </Button>
  105. <Button
  106. color="danger"
  107. onClick={onClickDeleteButtonHandler}
  108. disabled={deleting}
  109. >
  110. {t('commons:Delete')}
  111. </Button>
  112. </ModalFooter>
  113. </div>
  114. );
  115. };
  116. /**
  117. * DeleteAttachmentModal - Container component (lightweight, always rendered)
  118. */
  119. export const DeleteAttachmentModal: React.FC = () => {
  120. const deleteAttachmentModal = useDeleteAttachmentModalStatus();
  121. const { close: closeModal } = useDeleteAttachmentModalActions();
  122. const isOpen = deleteAttachmentModal?.isOpened;
  123. return (
  124. <Modal
  125. isOpen={isOpen}
  126. className={`${styles['attachment-delete-modal']} attachment-delete-modal`}
  127. size="lg"
  128. aria-labelledby="contained-modal-title-lg"
  129. fade={false}
  130. >
  131. {isOpen && (
  132. <DeleteAttachmentModalSubstance
  133. attachment={deleteAttachmentModal?.attachment}
  134. remove={deleteAttachmentModal?.remove}
  135. closeModal={closeModal}
  136. />
  137. )}
  138. </Modal>
  139. );
  140. };