Explorar el Código

refactor some modals

Yuki Takei hace 5 meses
padre
commit
eb19df69b7

+ 64 - 35
apps/app/src/client/components/DescendantsPageListModal.tsx

@@ -25,37 +25,45 @@ const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./De
 
 const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
 
-export const DescendantsPageListModal = (): React.JSX.Element => {
+/**
+ * DescendantsPageListModalSubstance - Presentation component (all logic here)
+ */
+type DescendantsPageListModalSubstanceProps = {
+  path: string | undefined;
+  closeModal: () => void;
+  onExpandedChange?: (isExpanded: boolean) => void;
+};
+
+const DescendantsPageListModalSubstance = ({
+  path,
+  closeModal,
+  onExpandedChange,
+}: DescendantsPageListModalSubstanceProps): React.JSX.Element => {
   const { t } = useTranslation();
 
   const [activeTab, setActiveTab] = useState('pagelist');
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
   const isSharedUser = useIsSharedUser();
-
-  const status = useDescendantsPageListModalStatus();
-  const { close } = useDescendantsPageListModalActions();
-
   const { events } = useRouter();
-
   const [isDeviceLargerThanLg] = useDeviceLargerThanLg();
 
   useEffect(() => {
-    events.on('routeChangeStart', close);
+    events.on('routeChangeStart', closeModal);
     return () => {
-      events.off('routeChangeStart', close);
+      events.off('routeChangeStart', closeModal);
     };
-  }, [close, events]);
+  }, [closeModal, events]);
 
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
         Icon: () => <span className="material-symbols-outlined">subject</span>,
         Content: () => {
-          if (status == null || status.path == null || !status.isOpened) {
+          if (path == null) {
             return <></>;
           }
-          return <DescendantsPageList path={status.path} />;
+          return <DescendantsPageList path={path} />;
         },
         i18n: t('page_list'),
         isLinkEnabled: () => !isSharedUser,
@@ -63,20 +71,23 @@ export const DescendantsPageListModal = (): React.JSX.Element => {
       timeline: {
         Icon: () => <span data-testid="timeline-tab-button" className="material-symbols-outlined">timeline</span>,
         Content: () => {
-          if (status == null || !status.isOpened) {
-            return <></>;
-          }
           return <PageTimeline />;
         },
         i18n: t('Timeline View'),
         isLinkEnabled: () => !isSharedUser,
       },
     };
-  }, [isSharedUser, status, t]);
+  }, [isSharedUser, path, t]);
 
   // Memoize event handlers
-  const expandWindow = useCallback(() => setIsWindowExpanded(true), []);
-  const contractWindow = useCallback(() => setIsWindowExpanded(false), []);
+  const expandWindow = useCallback(() => {
+    setIsWindowExpanded(true);
+    onExpandedChange?.(true);
+  }, [onExpandedChange]);
+  const contractWindow = useCallback(() => {
+    setIsWindowExpanded(false);
+    onExpandedChange?.(false);
+  }, [onExpandedChange]);
   const onNavSelected = useCallback((v: string) => setActiveTab(v), []);
 
   const buttons = useMemo(() => (
@@ -86,26 +97,13 @@ export const DescendantsPageListModal = (): React.JSX.Element => {
         expandWindow={expandWindow}
         contractWindow={contractWindow}
       />
-      <button type="button" className="btn btn-close ms-2" onClick={close} aria-label="Close"></button>
+      <button type="button" className="btn btn-close ms-2" onClick={closeModal} aria-label="Close"></button>
     </span>
-  ), [close, isWindowExpanded, expandWindow, contractWindow]);
-
-  // Early return after all hooks
-  if (status == null || !status.isOpened) {
-    return <></>;
-  }
-
-  const { isOpened } = status;
+  ), [closeModal, isWindowExpanded, expandWindow, contractWindow]);
 
   return (
-    <Modal
-      size="xl"
-      isOpen={isOpened}
-      toggle={close}
-      data-testid="descendants-page-list-modal"
-      className={`grw-descendants-page-list-modal ${styles['grw-descendants-page-list-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
-    >
-      <ModalHeader className={isDeviceLargerThanLg ? 'p-0' : ''} toggle={close} close={buttons}>
+    <div>
+      <ModalHeader className={isDeviceLargerThanLg ? 'p-0' : ''} toggle={closeModal} close={buttons}>
         {isDeviceLargerThanLg && (
           <CustomNavTab
             activeTab={activeTab}
@@ -130,7 +128,38 @@ export const DescendantsPageListModal = (): React.JSX.Element => {
           additionalClassNames={!isDeviceLargerThanLg ? ['grw-tab-content-style-md-down'] : undefined}
         />
       </ModalBody>
-    </Modal>
+    </div>
   );
+};
 
+/**
+ * DescendantsPageListModal - Container component (lightweight, always rendered)
+ */
+export const DescendantsPageListModal = (): React.JSX.Element => {
+  const [isWindowExpanded, setIsWindowExpanded] = useState(false);
+  const status = useDescendantsPageListModalStatus();
+  const { close } = useDescendantsPageListModalActions();
+  const isOpened = status?.isOpened ?? false;
+
+  const handleExpandedChange = useCallback((isExpanded: boolean) => {
+    setIsWindowExpanded(isExpanded);
+  }, []);
+
+  return (
+    <Modal
+      size="xl"
+      isOpen={isOpened}
+      toggle={close}
+      data-testid="descendants-page-list-modal"
+      className={`grw-descendants-page-list-modal ${styles['grw-descendants-page-list-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''}`}
+    >
+      {isOpened && (
+        <DescendantsPageListModalSubstance
+          path={status?.path}
+          closeModal={close}
+          onExpandedChange={handleExpandedChange}
+        />
+      )}
+    </Modal>
+  );
 };

+ 43 - 17
apps/app/src/features/growi-plugin/client/Admin/components/PluginsExtensionPageContents/PluginDeleteModal.tsx

@@ -13,19 +13,28 @@ import {
 } from '../../states/modal/plugin-delete';
 import { useSWRxAdminPlugins } from '../../stores/admin-plugins';
 
-export const PluginDeleteModal: React.FC = () => {
+/**
+ * PluginDeleteModalSubstance - Presentation component (all logic here)
+ */
+type PluginDeleteModalSubstanceProps = {
+  id: string;
+  name: string;
+  url: string;
+  closeModal: () => void;
+};
+
+const PluginDeleteModalSubstance = ({
+  id,
+  name,
+  url,
+  closeModal,
+}: PluginDeleteModalSubstanceProps): React.JSX.Element => {
   const { t } = useTranslation('admin');
   const { mutate } = useSWRxAdminPlugins();
-  const pluginDeleteModalData = usePluginDeleteModalStatus();
-  const { close: closePluginDeleteModal } = usePluginDeleteModalActions();
-  const isOpen = pluginDeleteModalData.isOpened;
-  const id = pluginDeleteModalData.id;
-  const name = pluginDeleteModalData.name;
-  const url = pluginDeleteModalData.url;
 
   const toggleHandler = useCallback(() => {
-    closePluginDeleteModal();
-  }, [closePluginDeleteModal]);
+    closeModal();
+  }, [closeModal]);
 
   const onClickDeleteButtonHandler = useCallback(async () => {
     const reqUrl = `/plugins/${id}/remove`;
@@ -33,21 +42,16 @@ export const PluginDeleteModal: React.FC = () => {
     try {
       const res = await apiv3Delete(reqUrl);
       const pluginName = res.data.pluginName;
-      closePluginDeleteModal();
+      closeModal();
       toastSuccess(t('toaster.remove_plugin_success', { pluginName }));
       mutate();
     } catch (err) {
       toastError(err);
     }
-  }, [id, closePluginDeleteModal, t, mutate]);
-
-  // Early return optimization
-  if (!isOpen) {
-    return <></>;
-  }
+  }, [id, closeModal, t, mutate]);
 
   return (
-    <Modal isOpen={isOpen} toggle={toggleHandler}>
+    <div>
       <ModalHeader
         tag="h4"
         toggle={toggleHandler}
@@ -72,6 +76,28 @@ export const PluginDeleteModal: React.FC = () => {
           {t('Delete')}
         </Button>
       </ModalFooter>
+    </div>
+  );
+};
+
+/**
+ * PluginDeleteModal - Container component (lightweight, always rendered)
+ */
+export const PluginDeleteModal: React.FC = () => {
+  const pluginDeleteModalData = usePluginDeleteModalStatus();
+  const { close: closeModal } = usePluginDeleteModalActions();
+  const isOpen = pluginDeleteModalData.isOpened;
+
+  return (
+    <Modal isOpen={isOpen} toggle={closeModal}>
+      {isOpen && pluginDeleteModalData.id != null && (
+        <PluginDeleteModalSubstance
+          id={pluginDeleteModalData.id}
+          name={pluginDeleteModalData.name ?? ''}
+          url={pluginDeleteModalData.url ?? ''}
+          closeModal={closeModal}
+        />
+      )}
     </Modal>
   );
 };

+ 11 - 10
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectUserGroupModal.tsx

@@ -66,22 +66,23 @@ const SelectUserGroupModalSubstance: React.FC<Props> = (props: Props) => {
   );
 };
 
+/**
+ * SelectUserGroupModal - Container component (lightweight, always rendered)
+ */
 export const SelectUserGroupModal: React.FC<Props> = (props) => {
   const { t } = useTranslation();
-
   const { isOpen, closeModal } = props;
 
-  // Early return optimization
-  if (!isOpen) {
-    return <></>;
-  }
-
   return (
     <Modal isOpen={isOpen} toggle={closeModal}>
-      <ModalHeader toggle={closeModal}>
-        {t('user_group.select_group')}
-      </ModalHeader>
-      <SelectUserGroupModalSubstance {...props} />
+      {isOpen && (
+        <>
+          <ModalHeader toggle={closeModal}>
+            {t('user_group.select_group')}
+          </ModalHeader>
+          <SelectUserGroupModalSubstance {...props} />
+        </>
+      )}
     </Modal>
   );
 };

+ 45 - 19
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx

@@ -7,20 +7,20 @@ import {
 
 import type { SelectablePage } from '../../../../interfaces/selectable-page';
 
-type Props = {
-  isOpen: boolean,
-  selectedPages: SelectablePage[],
-  closeModal: () => void,
-  onSubmit: () => Promise<void>,
-}
+/**
+ * ShareScopeWarningModalSubstance - Presentation component (all logic here)
+ */
+type ShareScopeWarningModalSubstanceProps = {
+  selectedPages: SelectablePage[];
+  closeModal: () => void;
+  onSubmit: () => Promise<void>;
+};
 
-export const ShareScopeWarningModal = (props: Props): React.JSX.Element => {
-  const {
-    isOpen,
-    selectedPages,
-    closeModal,
-    onSubmit,
-  } = props;
+const ShareScopeWarningModalSubstance = ({
+  selectedPages,
+  closeModal,
+  onSubmit,
+}: ShareScopeWarningModalSubstanceProps): React.JSX.Element => {
 
   const { t } = useTranslation();
 
@@ -38,13 +38,8 @@ export const ShareScopeWarningModal = (props: Props): React.JSX.Element => {
     ));
   }, [selectedPages]);
 
-  // Early return optimization
-  if (!isOpen) {
-    return <></>;
-  }
-
   return (
-    <Modal size="lg" isOpen={isOpen} toggle={closeModal}>
+    <div>
       <ModalHeader toggle={closeModal}>
         <div className="d-flex align-items-center">
           <span className="material-symbols-outlined text-warning me-2 fs-4">warning</span>
@@ -86,6 +81,37 @@ export const ShareScopeWarningModal = (props: Props): React.JSX.Element => {
           {t('share_scope_warning_modal.button.proceed')}
         </button>
       </ModalFooter>
+    </div>
+  );
+};
+
+/**
+ * ShareScopeWarningModal - Container component (lightweight, always rendered)
+ */
+type Props = {
+  isOpen: boolean;
+  selectedPages: SelectablePage[];
+  closeModal: () => void;
+  onSubmit: () => Promise<void>;
+};
+
+export const ShareScopeWarningModal = (props: Props): React.JSX.Element => {
+  const {
+    isOpen,
+    selectedPages,
+    closeModal,
+    onSubmit,
+  } = props;
+
+  return (
+    <Modal size="lg" isOpen={isOpen} toggle={closeModal}>
+      {isOpen && (
+        <ShareScopeWarningModalSubstance
+          selectedPages={selectedPages}
+          closeModal={closeModal}
+          onSubmit={onSubmit}
+        />
+      )}
     </Modal>
   );
 };