فهرست منبع

refactor some modals

Yuki Takei 5 ماه پیش
والد
کامیت
dca2afd2c1

+ 38 - 18
apps/app/src/client/components/DeleteBookmarkFolderModal.tsx

@@ -10,44 +10,45 @@ import {
 import { FolderIcon } from '~/client/components/Icons/FolderIcon';
 import { deleteBookmarkFolder } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
+import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { useDeleteBookmarkFolderModalStatus, useDeleteBookmarkFolderModalActions } from '~/states/ui/modal/delete-bookmark-folder';
 
+/**
+ * DeleteBookmarkFolderModalSubstance - Presentation component (all logic here)
+ */
+type DeleteBookmarkFolderModalSubstanceProps = {
+  bookmarkFolder: BookmarkFolderItems;
+  onDeleted?: (folderId: string) => void;
+  closeModal: () => void;
+};
 
-const DeleteBookmarkFolderModal: FC = () => {
+const DeleteBookmarkFolderModalSubstance = ({
+  bookmarkFolder,
+  onDeleted,
+  closeModal,
+}: DeleteBookmarkFolderModalSubstanceProps): React.JSX.Element => {
   const { t } = useTranslation();
 
-  const { isOpened, bookmarkFolder, opts } = useDeleteBookmarkFolderModalStatus();
-  const { close: closeBookmarkFolderDeleteModal } = useDeleteBookmarkFolderModalActions();
-
   const deleteBookmark = useCallback(async() => {
-    if (bookmarkFolder == null) {
-      return;
-    }
     try {
       await deleteBookmarkFolder(bookmarkFolder._id);
-      const onDeleted = opts?.onDeleted;
       if (onDeleted != null) {
         onDeleted(bookmarkFolder._id);
       }
-      closeBookmarkFolderDeleteModal();
+      closeModal();
     }
     catch (err) {
       toastError(err);
     }
-  }, [bookmarkFolder, closeBookmarkFolderDeleteModal, opts?.onDeleted]);
+  }, [bookmarkFolder, onDeleted, closeModal]);
 
   const onClickDeleteButton = useCallback(async() => {
     await deleteBookmark();
   }, [deleteBookmark]);
 
-  // Early return optimization
-  if (!isOpened || bookmarkFolder == null) {
-    return <></>;
-  }
-
   return (
-    <Modal size="md" isOpen={isOpened} toggle={closeBookmarkFolderDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
-      <ModalHeader tag="h4" toggle={closeBookmarkFolderDeleteModal} className="text-danger">
+    <div>
+      <ModalHeader tag="h4" toggle={closeModal} className="text-danger">
         <span className="material-symbols-outlined">delete</span>
         {t('bookmark_folder.delete_modal.modal_header_label')}
       </ModalHeader>
@@ -68,8 +69,27 @@ const DeleteBookmarkFolderModal: FC = () => {
           {t('bookmark_folder.delete_modal.modal_footer_button')}
         </button>
       </ModalFooter>
-    </Modal>
+    </div>
+  );
+};
 
+/**
+ * DeleteBookmarkFolderModal - Container component (lightweight, always rendered)
+ */
+const DeleteBookmarkFolderModal: FC = () => {
+  const { isOpened, bookmarkFolder, opts } = useDeleteBookmarkFolderModalStatus();
+  const { close: closeModal } = useDeleteBookmarkFolderModalActions();
+
+  return (
+    <Modal size="md" isOpen={isOpened} toggle={closeModal} data-testid="page-delete-modal" className="grw-create-page">
+      {isOpened && bookmarkFolder != null && (
+        <DeleteBookmarkFolderModalSubstance
+          bookmarkFolder={bookmarkFolder}
+          onDeleted={opts?.onDeleted}
+          closeModal={closeModal}
+        />
+      )}
+    </Modal>
   );
 };
 

+ 43 - 18
apps/app/src/client/components/EmptyTrashModal.tsx

@@ -1,6 +1,7 @@
 import type { FC } from 'react';
 import React, { useState, useCallback, useMemo } from 'react';
 
+import type { IPageToDeleteWithMeta } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -11,13 +12,23 @@ import { useEmptyTrashModalStatus, useEmptyTrashModalActions } from '~/states/ui
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
-const EmptyTrashModal: FC = () => {
-  const { t } = useTranslation();
-
-  const { isOpened, pages, opts } = useEmptyTrashModalStatus();
-  const { close: closeEmptyTrashModal } = useEmptyTrashModalActions();
+/**
+ * EmptyTrashModalSubstance - Presentation component (all logic here)
+ */
+type EmptyTrashModalSubstanceProps = {
+  pages: IPageToDeleteWithMeta[] | undefined;
+  canDeleteAllPages: boolean;
+  onEmptiedTrash?: () => void;
+  closeModal: () => void;
+};
 
-  const canDeleteAllpages = opts?.canDeleteAllPages ?? false;
+const EmptyTrashModalSubstance = ({
+  pages,
+  canDeleteAllPages,
+  onEmptiedTrash,
+  closeModal,
+}: EmptyTrashModalSubstanceProps): React.JSX.Element => {
+  const { t } = useTranslation();
 
   const [errs, setErrs] = useState<Error[] | null>(null);
 
@@ -28,16 +39,15 @@ const EmptyTrashModal: FC = () => {
 
     try {
       await apiv3Delete('/pages/empty-trash');
-      const onEmptiedTrash = opts?.onEmptiedTrash;
       if (onEmptiedTrash != null) {
         onEmptiedTrash();
       }
-      closeEmptyTrashModal();
+      closeModal();
     }
     catch (err) {
       setErrs([err]);
     }
-  }, [pages, opts?.onEmptiedTrash, closeEmptyTrashModal]);
+  }, [pages, onEmptiedTrash, closeModal]);
 
   const emptyTrashButtonHandler = useCallback(async() => {
     await emptyTrash();
@@ -55,14 +65,9 @@ const EmptyTrashModal: FC = () => {
     return <></>;
   }, [pages]);
 
-  // Early return optimization
-  if (!isOpened) {
-    return <></>;
-  }
-
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal">
-      <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="text-danger">
+    <div>
+      <ModalHeader tag="h4" toggle={closeModal} className="text-danger">
         <span className="material-symbols-outlined">delete_forever</span>
         {t('modal_empty.empty_the_trash')}
       </ModalHeader>
@@ -72,7 +77,7 @@ const EmptyTrashModal: FC = () => {
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {renderPagePaths}
         </div>
-        {!canDeleteAllpages && t('modal_empty.not_deletable_notice')}<br />
+        {!canDeleteAllPages && t('modal_empty.not_deletable_notice')}<br />
         {t('modal_empty.notice')}
       </ModalBody>
       <ModalFooter>
@@ -86,8 +91,28 @@ const EmptyTrashModal: FC = () => {
           {t('modal_empty.empty_the_trash_button')}
         </button>
       </ModalFooter>
-    </Modal>
+    </div>
+  );
+};
+
+/**
+ * EmptyTrashModal - Container component (lightweight, always rendered)
+ */
+const EmptyTrashModal: FC = () => {
+  const { isOpened, pages, opts } = useEmptyTrashModalStatus();
+  const { close: closeModal } = useEmptyTrashModalActions();
 
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={closeModal} data-testid="page-delete-modal">
+      {isOpened && (
+        <EmptyTrashModalSubstance
+          pages={pages}
+          canDeleteAllPages={opts?.canDeleteAllPages ?? false}
+          onEmptiedTrash={opts?.onEmptiedTrash}
+          closeModal={closeModal}
+        />
+      )}
+    </Modal>
   );
 };
 

+ 53 - 22
apps/app/src/client/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useMemo, useState,
 } from 'react';
 
+import type { IAttachmentHasId } from '@growi/core';
 import { UserPicture, LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import {
@@ -22,22 +23,30 @@ const iconByFormat = (format: string): string => {
   return format.match(/image\/.+/i) ? 'image' : 'description';
 };
 
-export const DeleteAttachmentModal: React.FC = () => {
+/**
+ * DeleteAttachmentModalSubstance - Presentation component (all logic here)
+ */
+type DeleteAttachmentModalSubstanceProps = {
+  attachment: IAttachmentHasId | undefined;
+  remove: ((args: { attachment_id: string }) => Promise<void>) | undefined;
+  closeModal: () => void;
+};
+
+const DeleteAttachmentModalSubstance = ({
+  attachment,
+  remove,
+  closeModal,
+}: DeleteAttachmentModalSubstanceProps): React.JSX.Element => {
   const [deleting, setDeleting] = useState<boolean>(false);
   const [deleteError, setDeleteError] = useState<string>('');
 
   const { t } = useTranslation();
-  const deleteAttachmentModal = useDeleteAttachmentModalStatus();
-  const { close: closeDeleteAttachmentModal } = useDeleteAttachmentModalActions();
-  const isOpen = deleteAttachmentModal?.isOpened;
-  const attachment = deleteAttachmentModal?.attachment;
-  const remove = deleteAttachmentModal?.remove;
 
   const toggleHandler = useCallback(() => {
-    closeDeleteAttachmentModal();
+    closeModal();
     setDeleting(false);
     setDeleteError('');
-  }, [closeDeleteAttachmentModal]);
+  }, [closeModal]);
 
   const onClickDeleteButtonHandler = useCallback(async() => {
     if (remove == null || attachment == null) {
@@ -49,7 +58,7 @@ export const DeleteAttachmentModal: React.FC = () => {
     try {
       await remove({ attachment_id: attachment._id });
       setDeleting(false);
-      closeDeleteAttachmentModal();
+      closeModal();
       toastSuccess(`Delete ${attachment.originalName}`);
     }
     catch (err) {
@@ -58,7 +67,7 @@ export const DeleteAttachmentModal: React.FC = () => {
       toastError(err);
       logger.error(err);
     }
-  }, [attachment, closeDeleteAttachmentModal, remove]);
+  }, [attachment, closeModal, remove]);
 
   const attachmentFileFormat = useMemo(() => {
     if (attachment == null) {
@@ -93,19 +102,8 @@ export const DeleteAttachmentModal: React.FC = () => {
     return <></>;
   }, [deleting, deleteError]);
 
-  // Early return optimization
-  if (!isOpen) {
-    return <></>;
-  }
-
   return (
-    <Modal
-      isOpen={isOpen}
-      className={`${styles['attachment-delete-modal']} attachment-delete-modal`}
-      size="lg"
-      aria-labelledby="contained-modal-title-lg"
-      fade={false}
-    >
+    <div>
       <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger">
         <span id="contained-modal-title-lg">{t('delete_attachment_modal.confirm_delete_attachment')}</span>
       </ModalHeader>
@@ -122,7 +120,40 @@ export const DeleteAttachmentModal: React.FC = () => {
           disabled={deleting}
         >{t('commons:Delete')}
         </Button>
+        <Button
+          color="secondary"
+          onClick={toggleHandler}
+        >
+          {t('modal_delete.cancel')}
+        </Button>
       </ModalFooter>
+    </div>
+  );
+};
+
+/**
+ * DeleteAttachmentModal - Container component (lightweight, always rendered)
+ */
+export const DeleteAttachmentModal: React.FC = () => {
+  const deleteAttachmentModal = useDeleteAttachmentModalStatus();
+  const { close: closeModal } = useDeleteAttachmentModalActions();
+  const isOpen = deleteAttachmentModal?.isOpened;
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      className={`${styles['attachment-delete-modal']} attachment-delete-modal`}
+      size="lg"
+      aria-labelledby="contained-modal-title-lg"
+      fade={false}
+    >
+      {isOpen && (
+        <DeleteAttachmentModalSubstance
+          attachment={deleteAttachmentModal?.attachment}
+          remove={deleteAttachmentModal?.remove}
+          closeModal={closeModal}
+        />
+      )}
     </Modal>
   );
 };

+ 35 - 13
apps/app/src/client/components/PrivateLegacyPagesMigrationModal.tsx

@@ -6,18 +6,26 @@ import {
 } from 'reactstrap';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
+import type { ILegacyPrivatePage, PrivateLegacyPagesMigrationModalSubmitedHandler } from '~/states/ui/modal/private-legacy-pages-migration';
 import { usePrivateLegacyPagesMigrationModalActions, usePrivateLegacyPagesMigrationModalStatus } from '~/states/ui/modal/private-legacy-pages-migration';
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
-export const PrivateLegacyPagesMigrationModal = (): React.JSX.Element => {
-  const { t } = useTranslation();
-
-  const status = usePrivateLegacyPagesMigrationModalStatus();
-  const { close } = usePrivateLegacyPagesMigrationModalActions();
+/**
+ * PrivateLegacyPagesMigrationModalSubstance - Presentation component (all logic here)
+ */
+type PrivateLegacyPagesMigrationModalSubstanceProps = {
+  status: {
+    isOpened: boolean;
+    pages?: ILegacyPrivatePage[];
+    onSubmit?: PrivateLegacyPagesMigrationModalSubmitedHandler;
+  } | null;
+  close: () => void;
+};
 
-  const isOpened = status?.isOpened ?? false;
+const PrivateLegacyPagesMigrationModalSubstance = ({ status, close }: PrivateLegacyPagesMigrationModalSubstanceProps): React.JSX.Element => {
+  const { t } = useTranslation();
 
   const [isRecursively, setIsRecursively] = useState(true);
 
@@ -79,13 +87,8 @@ export const PrivateLegacyPagesMigrationModal = (): React.JSX.Element => {
     return <></>;
   }, [status]);
 
-  // Early return optimization
-  if (!isOpened) {
-    return <></>;
-  }
-
   return (
-    <Modal size="lg" isOpen={isOpened} toggle={close}>
+    <div>
       <ModalHeader tag="h4" toggle={close}>
         { t('private_legacy_pages.modal.title') }
       </ModalHeader>
@@ -105,7 +108,26 @@ export const PrivateLegacyPagesMigrationModal = (): React.JSX.Element => {
           { t('private_legacy_pages.modal.button_label') }
         </button>
       </ModalFooter>
-    </Modal>
+    </div>
+  );
+};
 
+/**
+ * PrivateLegacyPagesMigrationModal - Container component (lightweight, always rendered)
+ */
+export const PrivateLegacyPagesMigrationModal = (): React.JSX.Element => {
+  const status = usePrivateLegacyPagesMigrationModalStatus();
+  const { close } = usePrivateLegacyPagesMigrationModalActions();
+  const isOpened = status?.isOpened ?? false;
+
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={close}>
+      {isOpened && (
+        <PrivateLegacyPagesMigrationModalSubstance
+          status={status}
+          close={close}
+        />
+      )}
+    </Modal>
   );
 };