RichAttachment.tsx 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import React, { useCallback } from 'react';
  2. import { UserPicture } from '@growi/ui/dist/components';
  3. import { useTranslation } from 'next-i18next';
  4. import prettyBytes from 'pretty-bytes';
  5. import { useSWRxAttachment } from '~/stores/attachment';
  6. import { useDeleteAttachmentModal } from '~/stores/modal';
  7. import styles from './RichAttachment.module.scss';
  8. type RichAttachmentProps = {
  9. attachmentId: string,
  10. url: string,
  11. attachmentName: string,
  12. }
  13. export const RichAttachment = React.memo((props: RichAttachmentProps) => {
  14. const { attachmentId, attachmentName } = props;
  15. const { t } = useTranslation();
  16. const { data: attachment, remove } = useSWRxAttachment(attachmentId);
  17. const { open: openDeleteAttachmentModal } = useDeleteAttachmentModal();
  18. const onClickTrashButtonHandler = useCallback(() => {
  19. if (attachment == null) {
  20. return;
  21. }
  22. openDeleteAttachmentModal(attachment, remove);
  23. }, [attachment, openDeleteAttachmentModal, remove]);
  24. if (attachment == null) {
  25. return <span className="text-muted">{t('rich_attachment.attachment_not_be_found')}</span>;
  26. }
  27. const {
  28. filePathProxied,
  29. originalName,
  30. downloadPathProxied,
  31. creator,
  32. createdAt,
  33. fileSize,
  34. } = attachment;
  35. // Guard here because attachment properties might be deleted in turn when an attachment is removed
  36. if (filePathProxied == null
  37. || originalName == null
  38. || downloadPathProxied == null
  39. || creator == null
  40. || createdAt == null
  41. || fileSize == null
  42. ) {
  43. return <span className="text-muted">{t('rich_attachment.attachment_not_be_found')}</span>;
  44. }
  45. return (
  46. <div data-testid="rich-attachment" className={`${styles.attachment} d-inline-block`}>
  47. <div className="my-2 p-2 card">
  48. <div className="p-1 card-body d-flex align-items-center">
  49. <div className="me-2 px-0 d-flex align-items-center justify-content-center">
  50. <img src="/images/icons/editor/attachment.svg" className="attachment-icon" alt="attachment icon" />
  51. </div>
  52. <div className="ps-0">
  53. <div className="d-inline-block">
  54. {/* Since we need to include the "referer" to view the attachment on the shared page */}
  55. {/* eslint-disable-next-line react/jsx-no-target-blank */}
  56. <a target="_blank" rel="noopener" href={filePathProxied}>
  57. {attachmentName || originalName}
  58. </a>
  59. <a className="ms-2 attachment-download" href={downloadPathProxied}>
  60. <span className="material-symbols-outlined">cloud_download</span>
  61. </a>
  62. <a className="ml-2 text-danger attachment-delete d-share-link-none" type="button" onClick={onClickTrashButtonHandler}>
  63. <span className="material-symbols-outlined">delete</span>
  64. </a>
  65. </div>
  66. <div className="d-flex align-items-center">
  67. <UserPicture user={creator} size="sm" />
  68. <span className="ms-2 text-muted">
  69. {new Date(createdAt).toLocaleString('en-US')}
  70. </span>
  71. <span className="ms-2 ps-2 border-start text-muted">{prettyBytes(fileSize)}</span>
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. </div>
  77. );
  78. });
  79. RichAttachment.displayName = 'RichAttachment';