Просмотр исходного кода

Merge pull request #7570 from weseek/feat/rich-attachment-imprv-delete-modal

feat: Update AttachmentDeleteModal component calling position
Yuki Takei 3 лет назад
Родитель
Сommit
c0fba9bcfc

+ 2 - 0
packages/app/src/components/Layout/BasicLayout.tsx

@@ -10,6 +10,7 @@ import Sidebar from '../Sidebar';
 import { RawLayout } from './RawLayout';
 
 const AlertSiteUrlUndefined = dynamic(() => import('../AlertSiteUrlUndefined').then(mod => mod.AlertSiteUrlUndefined), { ssr: false });
+const AttachmentDeleteModal = dynamic(() => import('../PageAttachment/AttachmentDeleteModal').then(mod => mod.AttachmentDeleteModal), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
 const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
@@ -55,6 +56,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
         <PageDeleteModal />
         <PageRenameModal />
         <PageAccessoriesModal />
+        <AttachmentDeleteModal />
       </DndProvider>
 
       <PagePresentationModal />

+ 56 - 25
packages/app/src/components/PageAttachment/AttachmentDeleteModal.tsx

@@ -1,37 +1,68 @@
-import React, { useCallback, useMemo } from 'react';
+import React, {
+  useCallback, useMemo, useState,
+} from 'react';
 
-import { HasObjectId, IAttachment, IUser } from '@growi/core';
+import type { IUser } from '@growi/core';
 import { UserPicture } from '@growi/ui';
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useAttachmentDeleteModal } from '~/stores/modal';
+import loggerFactory from '~/utils/logger';
+
 import { Username } from '../User/Username';
 
 import styles from './DeleteAttachmentModal.module.scss';
 
+const logger = loggerFactory('growi:attachmentDelete');
+
 const iconByFormat = (format: string): string => {
   return format.match(/image\/.+/i) ? 'icon-picture' : 'icon-doc';
 };
 
-export const AttachmentDeleteModal: React.FC<{
-  isOpen: boolean,
-  toggle: () => void,
-  attachmentToDelete: IAttachment & HasObjectId,
-  deleting: boolean,
-  deleteError: string,
-  onAttachmentDeleteHandler: (attachment: IAttachment & HasObjectId) => Promise<void>,
-}> = ({
-  isOpen, toggle,
-  attachmentToDelete, deleting, deleteError,
-  onAttachmentDeleteHandler,
-}) => {
-
-  const onDeleteClicked = useCallback(() => {
-    onAttachmentDeleteHandler(attachmentToDelete);
-  }, [attachmentToDelete, onAttachmentDeleteHandler]);
-
-  const renderByFileFormat = useCallback((attachment: IAttachment & HasObjectId) => {
+export const AttachmentDeleteModal: React.FC = () => {
+  const [deleting, setDeleting] = useState<boolean>(false);
+  const [deleteError, setDeleteError] = useState<string>('');
+
+  const { data: attachmentDeleteModal, close: closeAttachmentDeleteModal } = useAttachmentDeleteModal();
+  const isOpen = attachmentDeleteModal?.isOpened;
+  const attachment = attachmentDeleteModal?.attachment;
+  const remove = attachmentDeleteModal?.remove;
+
+  const toggleHandler = useCallback(() => {
+    closeAttachmentDeleteModal();
+    setDeleting(false);
+    setDeleteError('');
+  }, [closeAttachmentDeleteModal]);
+
+  const onClickDeleteButtonHandler = useCallback(async() => {
+    if (remove == null || attachment == null) {
+      return;
+    }
+
+    setDeleting(true);
+
+    try {
+      await remove({ attachment_id: attachment._id });
+      setDeleting(false);
+      closeAttachmentDeleteModal();
+      toastSuccess(`Delete ${attachment.originalName}`);
+    }
+    catch (err) {
+      setDeleting(false);
+      setDeleteError('Something went wrong.');
+      toastError(err);
+      logger.error(err);
+    }
+  }, [attachment, closeAttachmentDeleteModal, remove]);
+
+  const attachmentFileFormat = useMemo(() => {
+    if (attachment == null) {
+      return;
+    }
+
     const content = (attachment.fileFormat.match(/image\/.+/i))
       // eslint-disable-next-line @next/next/no-img-element
       ? <img src={attachment.filePathProxied} alt="deleting image" />
@@ -48,7 +79,7 @@ export const AttachmentDeleteModal: React.FC<{
         {content}
       </div>
     );
-  }, []);
+  }, [attachment]);
 
   const deletingIndicator = useMemo(() => {
     if (deleting) {
@@ -68,11 +99,11 @@ export const AttachmentDeleteModal: React.FC<{
       aria-labelledby="contained-modal-title-lg"
       fade={false}
     >
-      <ModalHeader tag="h4" toggle={toggle} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
         <span id="contained-modal-title-lg">Delete attachment?</span>
       </ModalHeader>
       <ModalBody>
-        {renderByFileFormat(attachmentToDelete)}
+        {attachmentFileFormat}
       </ModalBody>
       <ModalFooter>
         <div className="mr-3 d-inline-block">
@@ -80,9 +111,9 @@ export const AttachmentDeleteModal: React.FC<{
         </div>
         <Button
           color="danger"
-          onClick={onDeleteClicked}
+          onClick={onClickDeleteButtonHandler}
           disabled={deleting}
-        >Delete!
+        >Delete
         </Button>
       </ModalFooter>
     </Modal>

+ 32 - 76
packages/app/src/components/ReactMarkdownComponents/Attachment.tsx

@@ -1,17 +1,11 @@
-import React, { useMemo, useCallback, useState } from 'react';
+import React, { useMemo, useCallback } from 'react';
 
-import { HasObjectId, IAttachment } from '@growi/core';
 import { UserPicture } from '@growi/ui';
 import prettyBytes from 'pretty-bytes';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { AttachmentDeleteModal } from '~/components/PageAttachment/AttachmentDeleteModal';
 import { useSWRxAttachments } from '~/stores/attachment';
 import { useAttachmentDeleteModal } from '~/stores/modal';
 import { useCurrentPageId } from '~/stores/page';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:attachmentDelete');
 
 export const Attachment: React.FC<{
   attachmentId: string,
@@ -21,10 +15,7 @@ export const Attachment: React.FC<{
   const { data: pageId } = useCurrentPageId();
   // TODO: We need to be able to get it from all pages if there are a lot of attachments.
   const { data: dataAttachments, remove } = useSWRxAttachments(pageId, 1);
-  const { data: attachmentDeleteModal, open: openAttachmentDeleteModal, close: closeAttachmentDeleteModal } = useAttachmentDeleteModal();
-  const [attachmentToDelete, setAttachmentToDelete] = useState<(IAttachment & HasObjectId) | null>(null);
-  const [deleting, setDeleting] = useState<boolean>(false);
-  const [deleteError, setDeleteError] = useState<string>('');
+  const { open: openAttachmentDeleteModal } = useAttachmentDeleteModal();
 
   const attachment = useMemo(() => {
     if (dataAttachments == null) {
@@ -33,33 +24,12 @@ export const Attachment: React.FC<{
     return dataAttachments.attachments.find(item => item._id === attachmentId);
   }, [attachmentId, dataAttachments]);
 
-  const onAttachmentDeleteClicked = useCallback((attachment: IAttachment & HasObjectId) => {
-    setAttachmentToDelete(attachment);
-    openAttachmentDeleteModal();
-  }, [openAttachmentDeleteModal]);
-
-  const onToggleHandler = useCallback(() => {
-    setAttachmentToDelete(null);
-    setDeleteError('');
-  }, []);
-
-  const onAttachmentDeleteHandler = useCallback(async(attachment: IAttachment & HasObjectId) => {
-    setDeleting(true);
-
-    try {
-      await remove({ attachment_id: attachment._id });
-      setAttachmentToDelete(null);
-      setDeleting(false);
-      closeAttachmentDeleteModal();
-      toastSuccess(`Delete ${attachmentName}`);
-    }
-    catch (err) {
-      setDeleteError('Something went wrong.');
-      closeAttachmentDeleteModal();
-      toastError(err);
-      logger.error(err);
+  const onClickTrashButtonHandler = useCallback(() => {
+    if (attachment == null) {
+      return;
     }
-  }, [attachmentName, closeAttachmentDeleteModal, remove]);
+    openAttachmentDeleteModal(attachment, remove);
+  }, [attachment, openAttachmentDeleteModal, remove]);
 
   if (attachment == null) {
     return (
@@ -68,50 +38,36 @@ export const Attachment: React.FC<{
   }
 
   return (
-    <>
-      <div className="card my-3" style={{ width: 'fit-content' }}>
-        <div className="card-body pr-0">
-          <div className='row'>
-            <div className='col-2'>
-              <div className='icon-doc' style={{ fontSize: '2.7rem', opacity: '0.5' }}/>
+    <div className="card my-3" style={{ width: 'fit-content' }}>
+      <div className="card-body pr-0">
+        <div className='row'>
+          <div className='col-2'>
+            <div className='icon-doc' style={{ fontSize: '2.7rem', opacity: '0.5' }}/>
+          </div>
+          <div className='col-10'>
+            <div>
+              <a className='' href={attachment.downloadPathProxied}>{attachment.originalName}</a>
+              <span className='ml-2'>
+                <a className="attachment-download" href={attachment.downloadPathProxied}>
+                  <i className="icon-cloud-download" />
+                </a>
+              </span>
+              <span className='ml-2'>
+                <a className="text-danger attachment-delete" onClick={onClickTrashButtonHandler}>
+                  <i className="icon-trash" />
+                </a>
+              </span>
             </div>
-            <div className='col-10'>
-              <div>
-                <a className='' href={attachment.downloadPathProxied}>{attachment.originalName}</a>
-                <span className='ml-2'>
-                  <a className="attachment-download" href={attachment.downloadPathProxied}>
-                    <i className="icon-cloud-download" />
-                  </a>
-                </span>
-                <span className='ml-2'>
-                  <a className="text-danger attachment-delete" onClick={() => onAttachmentDeleteClicked(attachment)}>
-                    <i className="icon-trash" />
-                  </a>
-                </span>
-              </div>
-              <div>
-                <UserPicture user={attachment.creator} size="sm"></UserPicture>
-                {/* TODO: check locale */}
-                <span className='ml-2 text-muted'>{new Date(attachment.createdAt).toLocaleString()}</span>
-                <span className='border-left ml-2 pl-2 text-muted'>{prettyBytes(attachment.fileSize)}</span>
-              </div>
+            <div>
+              <UserPicture user={attachment.creator} size="sm"></UserPicture>
+              {/* TODO: check locale */}
+              <span className='ml-2 text-muted'>{new Date(attachment.createdAt).toLocaleString()}</span>
+              <span className='border-left ml-2 pl-2 text-muted'>{prettyBytes(attachment.fileSize)}</span>
             </div>
           </div>
         </div>
       </div>
-
-      {/* TODO: move rendering position */}
-      {attachmentToDelete != null && (
-        <AttachmentDeleteModal
-          isOpen={attachmentDeleteModal?.isOpened || false}
-          toggle={onToggleHandler}
-          attachmentToDelete={attachmentToDelete}
-          deleting={deleting}
-          deleteError={deleteError}
-          onAttachmentDeleteHandler={onAttachmentDeleteHandler}
-        />
-      )}
-    </>
+    </div>
   );
 });
 Attachment.displayName = 'Attachment';

+ 30 - 10
packages/app/src/stores/modal.tsx

@@ -1,5 +1,6 @@
 import { useCallback, useMemo } from 'react';
 
+import type { HasObjectId, IAttachment } from '@growi/core';
 import { SWRResponse } from 'swr';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
@@ -611,25 +612,44 @@ export const useTemplateModal = (): SWRResponse<TemplateModalStatus, Error> & Te
 /**
  * AttachmentDeleteModal
  */
+type Remove =
+  (body: {
+    attachment_id: string;
+  }) => Promise<void>
+
 type AttachmentDeleteModalStatus = {
   isOpened: boolean,
+  attachment?: IAttachment & HasObjectId,
+  remove?: Remove,
 }
 
 type AttachmentDeleteModalUtils = {
-  open(): void,
+  open(
+    attachment: IAttachment & HasObjectId,
+    remove: Remove,
+  ): void,
   close(): void,
 }
 
 export const useAttachmentDeleteModal = (): SWRResponse<AttachmentDeleteModalStatus, Error> & AttachmentDeleteModalUtils => {
-  const initialStatus: AttachmentDeleteModalStatus = { isOpened: false };
+  const initialStatus: AttachmentDeleteModalStatus = {
+    isOpened: false,
+    attachment: undefined,
+    remove: undefined,
+  };
   const swrResponse = useStaticSWR<AttachmentDeleteModalStatus, Error>('attachmentDeleteModal', undefined, { fallbackData: initialStatus });
+  const { mutate } = swrResponse;
 
-  return Object.assign(swrResponse, {
-    open: () => {
-      swrResponse.mutate({ isOpened: true });
-    },
-    close: () => {
-      swrResponse.mutate({ isOpened: false });
-    },
-  });
+  const open = useCallback((attachment: IAttachment & HasObjectId, remove: Remove) => {
+    mutate({ isOpened: true, attachment, remove });
+  }, [mutate]);
+  const close = useCallback((): void => {
+    mutate({ isOpened: false });
+  }, [mutate]);
+
+  return {
+    ...swrResponse,
+    open,
+    close,
+  };
 };