Yuki Takei 5 месяцев назад
Родитель
Сommit
80dfe74a47

+ 25 - 13
.serena/memories/apps-app-modal-performance-optimization-progress-tracker.md

@@ -5,7 +5,7 @@
 - [x] **SearchOptionModal.tsx** - 検索オプション
 - [x] **SearchOptionModal.tsx** - 検索オプション
 - [x] **DescendantsPageListModal.tsx** - 子ページリスト
 - [x] **DescendantsPageListModal.tsx** - 子ページリスト
 - [x] **GrantedGroupsInheritanceSelectModal.tsx** - 権限グループ継承選択  
 - [x] **GrantedGroupsInheritanceSelectModal.tsx** - 権限グループ継承選択  
-- [x] **ImageCropModal.tsx** - 画像クロップ### ✅ **完了済み (22個)** - Phase 1+2+3+4 完了
+- [x] **ImageCropModal.tsx** - 画像クロップ### ✅ **完了済み (29個)** - Phase 1+2+3+4+5(進行中) 完了
 1. **SearchModal.tsx** ✅ (検索機能)
 1. **SearchModal.tsx** ✅ (検索機能)
 2. **PageBulkExportSelectModal.tsx** ✅ (一括エクスポート)
 2. **PageBulkExportSelectModal.tsx** ✅ (一括エクスポート)
 3. **PageSelectModal.tsx** ✅ (ページ選択)
 3. **PageSelectModal.tsx** ✅ (ページ選択)
@@ -28,6 +28,13 @@
 20. **DescendantsPageListModal.tsx** ✅ (子ページリスト) - **3ハンドラーメモ化、early return最適化**
 20. **DescendantsPageListModal.tsx** ✅ (子ページリスト) - **3ハンドラーメモ化、early return最適化**
 21. **GrantedGroupsInheritanceSelectModal.tsx** ✅ (権限グループ継承選択) - **3ハンドラーメモ化、early return最適化**
 21. **GrantedGroupsInheritanceSelectModal.tsx** ✅ (権限グループ継承選択) - **3ハンドラーメモ化、early return最適化**
 22. **ImageCropModal.tsx** ✅ (画像クロップ) - **6関数メモ化、Canvas/Blob処理最適化**
 22. **ImageCropModal.tsx** ✅ (画像クロップ) - **6関数メモ化、Canvas/Blob処理最適化**
+23. **DeleteCommentModal.tsx** ✅ (コメント削除) - **3コンテンツメモ化、日付/本文処理メモ化**
+24. **DeleteAttachmentModal.tsx** ✅ (添付ファイル削除) - **early return追加(既に最適化済み)**
+25. **AssociateModal.tsx** ✅ (アカウント連携) - **5ハンドラーメモ化、タブ切替最適化**
+26. **DisassociateModal.tsx** ✅ (アカウント連携解除) - **Props分割代入、early return**
+27. **DeleteSlackBotSettingsModal.tsx** ✅ (Slack Bot設定削除) - **3コンテンツメモ化、条件分岐最適化**
+28. **PrivateLegacyPagesMigrationModal.tsx** ✅ (プライベートページ移行) - **Submit関数・レンダリング関数メモ化**
+29. **DeleteAiAssistantModal.tsx** ✅ (AI アシスタント削除) - **3コンテンツメモ化、early return**
 
 
 ---
 ---
 
 
@@ -59,18 +66,18 @@
 
 
 ### 🔄 **未完了 - 中優先度 (15個)**
 ### 🔄 **未完了 - 中優先度 (15個)**
 
 
-#### **💬 コメント/添付ファイル系 (2個)**
-- [ ] **DeleteCommentModal.tsx** - コメント削除
-- [ ] **DeleteAttachmentModal.tsx** - 添付ファイル削除
+#### **💬 コメント/添付ファイル系 (2個)** ✅ **全完了**
+- [x] **DeleteCommentModal.tsx** - コメント削除
+- [x] **DeleteAttachmentModal.tsx** - 添付ファイル削除
 
 
-#### **🔌 機能統合系 (4個)**  
-- [ ] **AssociateModal.tsx** - アカウント連携
-- [ ] **DisassociateModal.tsx** - アカウント連携解除
-- [ ] **DeleteSlackBotSettingsModal.tsx** - Slack Bot設定削除
-- [ ] **PrivateLegacyPagesMigrationModal.tsx** - プライベートページ移行
+#### **🔌 機能統合系 (4個)**  **全完了**
+- [x] **AssociateModal.tsx** - アカウント連携
+- [x] **DisassociateModal.tsx** - アカウント連携解除
+- [x] **DeleteSlackBotSettingsModal.tsx** - Slack Bot設定削除
+- [x] **PrivateLegacyPagesMigrationModal.tsx** - プライベートページ移行
 
 
-#### **🤖 AI機能系 (3個)**
-- [ ] **DeleteAiAssistantModal.tsx** - AI アシスタント削除
+#### **🤖 AI機能系 (3個中1個完了)**
+- [x] **DeleteAiAssistantModal.tsx** - AI アシスタント削除
 - [ ] **ShareScopeWarningModal.tsx** - 共有スコープ警告
 - [ ] **ShareScopeWarningModal.tsx** - 共有スコープ警告
 - [ ] **SelectUserGroupModal.tsx** - ユーザーグループ選択
 - [ ] **SelectUserGroupModal.tsx** - ユーザーグループ選択
 
 
@@ -93,12 +100,17 @@
 
 
 ## 📈 **統計情報**
 ## 📈 **統計情報**
 
 
-- **完了済み**: 22モーダル (42%)
+- **完了済み**: 29モーダル (55%)
 - **高優先度**: 5モーダル (9%) - Admin系のみ残存
 - **高優先度**: 5モーダル (9%) - Admin系のみ残存
-- **中優先度**: 15モーダル (28%)
+- **中優先度**: 8モーダル (15%) - 7個完了、残り8個
 - **低優先度**: 11モーダル (21%)
 - **低優先度**: 11モーダル (21%)
 - **総計**: 53モーダル
 - **総計**: 53モーダル
 
 
+### 🎉 **Phase 5進行中: 中優先度モーダル 7個完了**
+- DeleteCommentModal.tsx, DeleteAttachmentModal.tsx (コメント/添付ファイル系)
+- AssociateModal.tsx, DisassociateModal.tsx, DeleteSlackBotSettingsModal.tsx, PrivateLegacyPagesMigrationModal.tsx (機能統合系)
+- DeleteAiAssistantModal.tsx (AI機能系)
+
 ### 🎉 **Phase 3完了: Page操作系 6モーダル最適化完了**
 ### 🎉 **Phase 3完了: Page操作系 6モーダル最適化完了**
 - PageCreateModal.tsx, PageRenameModal.tsx, PageDuplicateModal.tsx
 - PageCreateModal.tsx, PageRenameModal.tsx, PageDuplicateModal.tsx
 - ConflictDiffModal.tsx, LinkEditModal.tsx, PagePresentationModal.tsx
 - ConflictDiffModal.tsx, LinkEditModal.tsx, PagePresentationModal.tsx

+ 56 - 39
apps/app/src/client/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
@@ -29,53 +29,70 @@ export const DeleteSlackBotSettingsModal = React.memo((props: DeleteSlackBotSett
     onClose?.();
     onClose?.();
   }, [onClose]);
   }, [onClose]);
 
 
+  // Memoize conditional content
+  const headerContent = useMemo(() => {
+    if (isResetAll) {
+      return (
+        <>
+          <span className="material-symbols-outlined">delete_forever</span>
+          {t('admin:slack_integration.reset_all_settings')}
+        </>
+      );
+    }
+    return (
+      <>
+        <span className="material-symbols-outlined">delete</span>
+        {t('admin:slack_integration.delete_slackbot_settings')}
+      </>
+    );
+  }, [isResetAll, t]);
+
+  const bodyContent = useMemo(() => {
+    const htmlContent = isResetAll
+      ? t('admin:slack_integration.all_settings_of_the_bot_will_be_reset')
+      : t('admin:slack_integration.slackbot_settings_notice');
+    return (
+      <span
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{ __html: htmlContent }}
+      />
+    );
+  }, [isResetAll, t]);
+
+  const deleteButtonContent = useMemo(() => {
+    if (isResetAll) {
+      return (
+        <>
+          <span className="material-symbols-outlined">delete_forever</span>
+          {t('admin:slack_integration.reset')}
+        </>
+      );
+    }
+    return (
+      <>
+        <span className="material-symbols-outlined">delete</span>
+        {t('admin:slack_integration.delete')}
+      </>
+    );
+  }, [isResetAll, t]);
+
+  // Early return optimization
+  if (!isOpen) {
+    return <></>;
+  }
+
   return (
   return (
     <Modal isOpen={isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
     <Modal isOpen={isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
       <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
       <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
-        <span>
-          {isResetAll && (
-            <>
-              <span className="material-symbols-outlined">delete_forever</span>
-              {t('admin:slack_integration.reset_all_settings')}
-            </>
-          )}
-          {!isResetAll && (
-            <>
-              <span className="material-symbols-outlined">delete</span>
-              {t('admin:slack_integration.delete_slackbot_settings')}
-            </>
-          )}
-        </span>
+        <span>{headerContent}</span>
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        {isResetAll && (
-          <span
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.all_settings_of_the_bot_will_be_reset') }}
-          />
-        )}
-        {!isResetAll && (
-          <span
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.slackbot_settings_notice') }}
-          />
-        )}
+        {bodyContent}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
-          {isResetAll && (
-            <>
-              <span className="material-symbols-outlined">delete_forever</span>
-              {t('admin:slack_integration.reset')}
-            </>
-          )}
-          {!isResetAll && (
-            <>
-              <span className="material-symbols-outlined">delete</span>
-              {t('admin:slack_integration.delete')}
-            </>
-          )}
+          {deleteButtonContent}
         </Button>
         </Button>
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>

+ 16 - 5
apps/app/src/client/components/Me/AssociateModal.tsx

@@ -53,6 +53,17 @@ const AssociateModal = (props: Props): JSX.Element => {
 
 
   }, [associateLdapAccount, closeModalHandler, mutatePersonalExternalAccounts, password, t, username]);
   }, [associateLdapAccount, closeModalHandler, mutatePersonalExternalAccounts, password, t, username]);
 
 
+  // Memoize event handlers
+  const setTabToLdap = useCallback(() => setActiveTab(1), []);
+  const setTabToGithub = useCallback(() => setActiveTab(2), []);
+  const setTabToGoogle = useCallback(() => setActiveTab(3), []);
+  const handleUsernameChange = useCallback((username: string) => setUsername(username), []);
+  const handlePasswordChange = useCallback((password: string) => setPassword(password), []);
+
+  // Early return optimization
+  if (!isOpen) {
+    return <></>;
+  }
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={closeModalHandler} size="lg" data-testid="grw-associate-modal">
     <Modal isOpen={isOpen} toggle={closeModalHandler} size="lg" data-testid="grw-associate-modal">
@@ -64,19 +75,19 @@ const AssociateModal = (props: Props): JSX.Element => {
           <Nav tabs className="mb-2">
           <Nav tabs className="mb-2">
             <NavLink
             <NavLink
               className={`${activeTab === 1 ? 'active' : ''} d-flex gap-1 align-items-center`}
               className={`${activeTab === 1 ? 'active' : ''} d-flex gap-1 align-items-center`}
-              onClick={() => setActiveTab(1)}
+              onClick={setTabToLdap}
             >
             >
               <span className="material-symbols-outlined fs-5">network_node</span> LDAP
               <span className="material-symbols-outlined fs-5">network_node</span> LDAP
             </NavLink>
             </NavLink>
             <NavLink
             <NavLink
               className={`${activeTab === 2 ? 'active' : ''} d-flex gap-1 align-items-center`}
               className={`${activeTab === 2 ? 'active' : ''} d-flex gap-1 align-items-center`}
-              onClick={() => setActiveTab(2)}
+              onClick={setTabToGithub}
             >
             >
               <span className="growi-custom-icons">github</span> (TBD) GitHub
               <span className="growi-custom-icons">github</span> (TBD) GitHub
             </NavLink>
             </NavLink>
             <NavLink
             <NavLink
               className={`${activeTab === 3 ? 'active' : ''} d-flex gap-1 align-items-center`}
               className={`${activeTab === 3 ? 'active' : ''} d-flex gap-1 align-items-center`}
-              onClick={() => setActiveTab(3)}
+              onClick={setTabToGoogle}
             >
             >
               <span className="growi-custom-icons">google</span> (TBD) Google OAuth
               <span className="growi-custom-icons">google</span> (TBD) Google OAuth
             </NavLink>
             </NavLink>
@@ -86,8 +97,8 @@ const AssociateModal = (props: Props): JSX.Element => {
               <LdapAuthTest
               <LdapAuthTest
                 username={username}
                 username={username}
                 password={password}
                 password={password}
-                onChangeUsername={username => setUsername(username)}
-                onChangePassword={password => setPassword(password)}
+                onChangeUsername={handleUsernameChange}
+                onChangePassword={handlePasswordChange}
               />
               />
             </TabPane>
             </TabPane>
             <TabPane tabId={2}>
             <TabPane tabId={2}>

+ 14 - 8
apps/app/src/client/components/Me/DisassociateModal.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, type JSX } from 'react';
+import React, { useCallback } from 'react';
 
 
 import type { HasObjectId, IExternalAccount } from '@growi/core';
 import type { HasObjectId, IExternalAccount } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -20,19 +20,20 @@ type Props = {
 }
 }
 
 
 
 
-const DisassociateModal = (props: Props): JSX.Element => {
+const DisassociateModal = (props: Props): React.JSX.Element => {
+  const { isOpen, onClose, accountForDisassociate } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { mutate: mutatePersonalExternalAccounts } = useSWRxPersonalExternalAccounts();
   const { mutate: mutatePersonalExternalAccounts } = useSWRxPersonalExternalAccounts();
   const { trigger: disassociateLdapAccount } = useDisassociateLdapAccount();
   const { trigger: disassociateLdapAccount } = useDisassociateLdapAccount();
 
 
-  const { providerType, accountId } = props.accountForDisassociate;
+  const { providerType, accountId } = accountForDisassociate;
 
 
   const disassociateAccountHandler = useCallback(async() => {
   const disassociateAccountHandler = useCallback(async() => {
 
 
     try {
     try {
       await disassociateLdapAccount({ providerType, accountId });
       await disassociateLdapAccount({ providerType, accountId });
-      props.onClose();
+      onClose();
       toastSuccess(t('security_settings.updated_general_security_setting'));
       toastSuccess(t('security_settings.updated_general_security_setting'));
     }
     }
     catch (err) {
     catch (err) {
@@ -42,11 +43,16 @@ const DisassociateModal = (props: Props): JSX.Element => {
     if (mutatePersonalExternalAccounts != null) {
     if (mutatePersonalExternalAccounts != null) {
       mutatePersonalExternalAccounts();
       mutatePersonalExternalAccounts();
     }
     }
-  }, [accountId, disassociateLdapAccount, mutatePersonalExternalAccounts, props, providerType, t]);
+  }, [accountId, disassociateLdapAccount, mutatePersonalExternalAccounts, onClose, providerType, t]);
+
+  // Early return optimization
+  if (!isOpen) {
+    return <></>;
+  }
 
 
   return (
   return (
-    <Modal isOpen={props.isOpen} toggle={props.onClose}>
-      <ModalHeader className="text-info" toggle={props.onClose}>
+    <Modal isOpen={isOpen} toggle={onClose}>
+      <ModalHeader className="text-info" toggle={onClose}>
         {t('personal_settings.disassociate_external_account')}
         {t('personal_settings.disassociate_external_account')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
@@ -54,7 +60,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
         <p dangerouslySetInnerHTML={{ __html: t('personal_settings.disassociate_external_account_desc', { providerType, accountId }) }} />
         <p dangerouslySetInnerHTML={{ __html: t('personal_settings.disassociate_external_account_desc', { providerType, accountId }) }} />
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <button type="button" className="btn btn-sm btn-outline-secondary" onClick={props.onClose}>
+        <button type="button" className="btn btn-sm btn-outline-secondary" onClick={onClose}>
           { t('Cancel') }
           { t('Cancel') }
         </button>
         </button>
         <button type="button" className="btn btn-sm btn-danger" onClick={disassociateAccountHandler}>
         <button type="button" className="btn btn-sm btn-danger" onClick={disassociateAccountHandler}>

+ 5 - 0
apps/app/src/client/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -93,6 +93,11 @@ export const DeleteAttachmentModal: React.FC = () => {
     return <></>;
     return <></>;
   }, [deleting, deleteError]);
   }, [deleting, deleteError]);
 
 
+  // Early return optimization
+  if (!isOpen) {
+    return <></>;
+  }
+
   return (
   return (
     <Modal
     <Modal
       isOpen={isOpen}
       isOpen={isOpen}

+ 57 - 55
apps/app/src/client/components/PageComment/DeleteCommentModal.tsx

@@ -1,4 +1,4 @@
-import React, { type JSX } from 'react';
+import React, { useMemo } from 'react';
 
 
 import { isPopulated } from '@growi/core';
 import { isPopulated } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -22,79 +22,81 @@ export type DeleteCommentModalProps = {
   confirmToDelete: () => void,
   confirmToDelete: () => void,
 }
 }
 
 
-export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element => {
+export const DeleteCommentModal = (props: DeleteCommentModalProps): React.JSX.Element => {
   const {
   const {
     isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
     isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const headerContent = () => {
-    if (comment == null || isShown === false) {
-      return <></>;
-    }
-    return (
-      <span>
-        <span className="material-symbols-outlined">delete_forever</span>
-        {t('page_comment.delete_comment')}
-      </span>
-    );
-  };
-
-  const bodyContent = () => {
-    if (comment == null || isShown === false) {
-      return <></>;
-    }
-
-    // the threshold for omitting body
+  // Memoize formatted date
+  const commentDate = useMemo(() => {
+    if (comment == null) return '';
+    return format(new Date(comment.createdAt), 'yyyy/MM/dd HH:mm');
+  }, [comment]);
+
+  // Memoize creator
+  const creator = useMemo(() => {
+    if (comment == null) return undefined;
+    return isPopulated(comment.creator) ? comment.creator : undefined;
+  }, [comment]);
+
+  // Memoize processed comment body
+  const commentBodyElement = useMemo(() => {
+    if (comment == null) return null;
     const OMIT_BODY_THRES = 400;
     const OMIT_BODY_THRES = 400;
-
-    const commentDate = format(new Date(comment.createdAt), 'yyyy/MM/dd HH:mm');
-
-    const creator = isPopulated(comment.creator) ? comment.creator : undefined;
-
     let commentBody = comment.comment;
     let commentBody = comment.comment;
-    if (commentBody.length > OMIT_BODY_THRES) { // omit
+    if (commentBody.length > OMIT_BODY_THRES) {
       commentBody = `${commentBody.substr(0, OMIT_BODY_THRES)}...`;
       commentBody = `${commentBody.substr(0, OMIT_BODY_THRES)}...`;
     }
     }
-    const commentBodyElement = <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
-
-    return (
-      <>
-        <UserPicture user={creator} size="xs" /> <strong className="me-2"><Username user={creator}></Username></strong>{commentDate}:
-        <div className="card mt-2">
-          <div className="card-body comment-body px-3 py-2">{commentBodyElement}</div>
-        </div>
-      </>
-    );
-  };
+    return <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
+  }, [comment]);
+
+  // Memoize header content
+  const headerContent = useMemo(() => (
+    <span>
+      <span className="material-symbols-outlined">delete_forever</span>
+      {t('page_comment.delete_comment')}
+    </span>
+  ), [t]);
+
+  // Memoize body content
+  const bodyContent = useMemo(() => (
+    <>
+      <UserPicture user={creator} size="xs" /> <strong className="me-2"><Username user={creator}></Username></strong>{commentDate}:
+      <div className="card mt-2">
+        <div className="card-body comment-body px-3 py-2">{commentBodyElement}</div>
+      </div>
+    </>
+  ), [creator, commentDate, commentBodyElement]);
+
+  // Memoize footer content
+  const footerContent = useMemo(() => (
+    <>
+      <span className="text-danger">{errorMessage}</span>&nbsp;
+      <Button onClick={cancelToDelete}>{t('Cancel')}</Button>
+      <Button data-testid="delete-comment-button" color="danger" onClick={confirmToDelete}>
+        <span className="material-symbols-outlined">delete_forever</span>
+        {t('Delete')}
+      </Button>
+    </>
+  ), [errorMessage, cancelToDelete, confirmToDelete, t]);
 
 
-  const footerContent = () => {
-    if (comment == null || isShown === false) {
-      return <></>;
-    }
-    return (
-      <>
-        <span className="text-danger">{errorMessage}</span>&nbsp;
-        <Button onClick={cancelToDelete}>{t('Cancel')}</Button>
-        <Button data-testid="delete-comment-button" color="danger" onClick={confirmToDelete}>
-          <span className="material-symbols-outlined">delete_forever</span>
-          {t('Delete')}
-        </Button>
-      </>
-    );
-  };
+  // Early return after all hooks
+  if (!isShown || comment == null) {
+    return <></>;
+  }
 
 
   return (
   return (
     <Modal data-testid="page-comment-delete-modal" isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
     <Modal data-testid="page-comment-delete-modal" isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
       <ModalHeader tag="h4" toggle={cancelToDelete} className="text-danger">
       <ModalHeader tag="h4" toggle={cancelToDelete} className="text-danger">
-        {headerContent()}
+        {headerContent}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        {bodyContent()}
+        {bodyContent}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        {footerContent()}
+        {footerContent}
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>
   );
   );

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

@@ -1,4 +1,4 @@
-import React, { useState, type JSX } from 'react';
+import React, { useState, useCallback, useMemo } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
@@ -11,7 +11,7 @@ import { usePrivateLegacyPagesMigrationModalActions, usePrivateLegacyPagesMigrat
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 
 
-export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
+export const PrivateLegacyPagesMigrationModal = (): React.JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const status = usePrivateLegacyPagesMigrationModalStatus();
   const status = usePrivateLegacyPagesMigrationModalStatus();
@@ -24,7 +24,8 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [errs, setErrs] = useState<Error[] | null>(null);
   const [errs, setErrs] = useState<Error[] | null>(null);
 
 
-  async function submit() {
+  // Memoize submit handler
+  const submit = useCallback(async() => {
     if (status == null || status.pages == null || status.pages.length === 0) {
     if (status == null || status.pages == null || status.pages.length === 0) {
       return;
       return;
     }
     }
@@ -44,9 +45,15 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
     catch (err) {
     catch (err) {
       setErrs([err]);
       setErrs([err]);
     }
     }
-  }
+  }, [status, isRecursively]);
+
+  // Memoize checkbox handler
+  const handleRecursivelyChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+    setIsRecursively(e.target.checked);
+  }, []);
 
 
-  function renderForm() {
+  // Memoize form rendering
+  const renderForm = useMemo(() => {
     return (
     return (
       <div className="form-check form-check-warning">
       <div className="form-check form-check-warning">
         <input
         <input
@@ -54,9 +61,7 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
           id="convertRecursively"
           id="convertRecursively"
           type="checkbox"
           type="checkbox"
           checked={isRecursively}
           checked={isRecursively}
-          onChange={(e) => {
-            setIsRecursively(e.target.checked);
-          }}
+          onChange={handleRecursivelyChange}
         />
         />
         <label className="form-label form-check-label" htmlFor="convertRecursively">
         <label className="form-label form-check-label" htmlFor="convertRecursively">
           { t('private_legacy_pages.modal.convert_recursively_label') }
           { t('private_legacy_pages.modal.convert_recursively_label') }
@@ -64,14 +69,20 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
         </label>
         </label>
       </div>
       </div>
     );
     );
-  }
+  }, [isRecursively, handleRecursivelyChange, t]);
 
 
-  const renderPageIds = () => {
+  // Memoize page IDs rendering
+  const renderPageIds = useMemo(() => {
     if (status != null && status.pages != null) {
     if (status != null && status.pages != null) {
       return status.pages.map(page => <div key={page.pageId}><code>{ page.path }</code></div>);
       return status.pages.map(page => <div key={page.pageId}><code>{ page.path }</code></div>);
     }
     }
     return <></>;
     return <></>;
-  };
+  }, [status]);
+
+  // Early return optimization
+  if (!isOpened) {
+    return <></>;
+  }
 
 
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={close}>
     <Modal size="lg" isOpen={isOpened} toggle={close}>
@@ -83,9 +94,9 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
           <label>{ t('private_legacy_pages.modal.converting_pages') }:</label><br />
           <label>{ t('private_legacy_pages.modal.converting_pages') }:</label><br />
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {/* https://redmine.weseek.co.jp/issues/82787 */}
           {/* https://redmine.weseek.co.jp/issues/82787 */}
-          {renderPageIds()}
+          {renderPageIds}
         </div>
         </div>
-        {renderForm()}
+        {renderForm}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <ApiErrorMessageList errs={errs} />
         <ApiErrorMessageList errs={errs} />

+ 32 - 37
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/DeleteAiAssistantModal.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
@@ -20,52 +20,47 @@ export const DeleteAiAssistantModal: React.FC<DeleteAiAssistantModalProps> = ({
 }) => {
 }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const headerContent = () => {
-    if (!isShown || aiAssistant == null) {
-      return null;
-    }
-    return (
-      <>
-        <span className="material-symbols-outlined me-1">delete_forever</span>
-        <span className="fw-bold">{t('ai_assistant_substance.delete_modal.title')}</span>
-      </>
-    );
-  };
+  // Memoize header content
+  const headerContent = useMemo(() => (
+    <>
+      <span className="material-symbols-outlined me-1">delete_forever</span>
+      <span className="fw-bold">{t('ai_assistant_substance.delete_modal.title')}</span>
+    </>
+  ), [t]);
 
 
-  const bodyContent = () => {
-    if (!isShown || aiAssistant == null) {
-      return null;
-    }
-    return <p className="fw-bold mb-0">{t('ai_assistant_substance.delete_modal.confirm_message')}</p>;
-  };
+  // Memoize body content
+  const bodyContent = useMemo(() => (
+    <p className="fw-bold mb-0">{t('ai_assistant_substance.delete_modal.confirm_message')}</p>
+  ), [t]);
 
 
-  const footerContent = () => {
-    if (!isShown || aiAssistant == null) {
-      return null;
-    }
-    return (
-      <>
-        {errorMessage && <span className="text-danger">{errorMessage}</span>}
-        <Button color="outline-neutral-secondary" onClick={onCancel}>
-          {t('Cancel')}
-        </Button>
-        <Button color="danger" onClick={onConfirm}>
-          {t('Delete')}
-        </Button>
-      </>
-    );
-  };
+  // Memoize footer content
+  const footerContent = useMemo(() => (
+    <>
+      {errorMessage && <span className="text-danger">{errorMessage}</span>}
+      <Button color="outline-neutral-secondary" onClick={onCancel}>
+        {t('Cancel')}
+      </Button>
+      <Button color="danger" onClick={onConfirm}>
+        {t('Delete')}
+      </Button>
+    </>
+  ), [errorMessage, onCancel, onConfirm, t]);
+
+  // Early return optimization
+  if (!isShown || aiAssistant == null) {
+    return <></>;
+  }
 
 
   return (
   return (
     <Modal isOpen={isShown} toggle={onCancel} centered>
     <Modal isOpen={isShown} toggle={onCancel} centered>
       <ModalHeader tag="h5" toggle={onCancel} className="text-danger px-4">
       <ModalHeader tag="h5" toggle={onCancel} className="text-danger px-4">
-        {headerContent()}
+        {headerContent}
       </ModalHeader>
       </ModalHeader>
       <ModalBody className="px-4">
       <ModalBody className="px-4">
-        {bodyContent()}
+        {bodyContent}
       </ModalBody>
       </ModalBody>
       <ModalFooter className="px-4 gap-2">
       <ModalFooter className="px-4 gap-2">
-        {footerContent()}
+        {footerContent}
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>
   );
   );