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

Merge branch 'master' into fix/handle-blank-configurations

Yuki Takei 3 месяцев назад
Родитель
Сommit
59168bc098
100 измененных файлов с 5810 добавлено и 2840 удалено
  1. 0 1
      .github/mergify.yml
  2. 37 0
      .serena/memories/apps-app-google-workspace-oauth2-mail.md
  3. 14 0
      apps/app/.eslintrc.js
  4. 1 1
      apps/app/public/static/locales/en_US/admin.json
  5. 1 1
      apps/app/public/static/locales/fr_FR/admin.json
  6. 1 1
      apps/app/public/static/locales/ja_JP/admin.json
  7. 1 1
      apps/app/public/static/locales/ko_KR/admin.json
  8. 1 1
      apps/app/public/static/locales/zh_CN/admin.json
  9. 57 29
      apps/app/src/client/components/Admin/AdminHome/AdminHome.jsx
  10. 8 8
      apps/app/src/client/components/Admin/AdminHome/EnvVarsTable.tsx
  11. 22 15
      apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx
  12. 32 13
      apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx
  13. 10 4
      apps/app/src/client/components/Admin/AuditLog/AuditLogDisableMode.tsx
  14. 38 14
      apps/app/src/client/components/Admin/AuditLog/AuditLogSettings.tsx
  15. 49 44
      apps/app/src/client/components/Admin/AuditLog/DateRangePicker.tsx
  16. 54 26
      apps/app/src/client/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  17. 151 88
      apps/app/src/client/components/Admin/AuditLog/SelectActionDropdown.tsx
  18. 143 73
      apps/app/src/client/components/Admin/AuditLogManagement.tsx
  19. 2 6
      apps/app/src/client/components/Admin/Common/Accordion.jsx
  20. 11 5
      apps/app/src/client/components/Admin/Common/AdminInstallButtonRow.tsx
  21. 5 6
      apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx
  22. 17 12
      apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx
  23. 8 10
      apps/app/src/client/components/Admin/Customize/Customize.jsx
  24. 36 28
      apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx
  25. 7 11
      apps/app/src/client/components/Admin/Customize/CustomizeFunctionOption.tsx
  26. 105 44
      apps/app/src/client/components/Admin/Customize/CustomizeFunctionSetting.tsx
  27. 36 21
      apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx
  28. 100 46
      apps/app/src/client/components/Admin/Customize/CustomizeLogoSetting.tsx
  29. 54 38
      apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  30. 36 18
      apps/app/src/client/components/Admin/Customize/CustomizePresentationSetting.tsx
  31. 52 38
      apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx
  32. 32 27
      apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx
  33. 19 15
      apps/app/src/client/components/Admin/Customize/CustomizeThemeOptions.tsx
  34. 25 19
      apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx
  35. 62 30
      apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx
  36. 12 11
      apps/app/src/client/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  37. 52 24
      apps/app/src/client/components/Admin/Customize/ThemeColorBox.tsx
  38. 29 34
      apps/app/src/client/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  39. 12 9
      apps/app/src/client/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx
  40. 11 15
      apps/app/src/client/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  41. 11 11
      apps/app/src/client/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  42. 83 44
      apps/app/src/client/components/Admin/ElasticsearchManagement/StatusTable.jsx
  43. 13 6
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  44. 35 12
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  45. 137 83
      apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  46. 40 27
      apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx
  47. 0 2
      apps/app/src/client/components/Admin/ForbiddenPage.tsx
  48. 4 2
      apps/app/src/client/components/Admin/FullTextSearchManagement.tsx
  49. 103 49
      apps/app/src/client/components/Admin/G2GDataTransfer.tsx
  50. 309 136
      apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx
  51. 49 10
      apps/app/src/client/components/Admin/G2GDataTransferStatusIcon.tsx
  52. 12 7
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  53. 106 36
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  54. 86 34
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  55. 164 76
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  56. 21 15
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  57. 25 24
      apps/app/src/client/components/Admin/ImportData/GrowiArchiveSection.jsx
  58. 4 2
      apps/app/src/client/components/Admin/ImportData/ImportDataPageContents.jsx
  59. 25 15
      apps/app/src/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  60. 151 64
      apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  61. 32 27
      apps/app/src/client/components/Admin/ManageExternalAccount.tsx
  62. 60 25
      apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx
  63. 57 21
      apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx
  64. 28 18
      apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  65. 33 19
      apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx
  66. 70 37
      apps/app/src/client/components/Admin/MarkdownSetting/XssForm.jsx
  67. 1 4
      apps/app/src/client/components/Admin/NotFoundPage.tsx
  68. 71 24
      apps/app/src/client/components/Admin/Notification/GlobalNotification.jsx
  69. 90 40
      apps/app/src/client/components/Admin/Notification/GlobalNotificationList.jsx
  70. 160 102
      apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx
  71. 18 11
      apps/app/src/client/components/Admin/Notification/NotificationDeleteModal.jsx
  72. 93 54
      apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx
  73. 8 6
      apps/app/src/client/components/Admin/Notification/NotificationTypeIcon.tsx
  74. 5 5
      apps/app/src/client/components/Admin/Notification/TriggerEventCheckBox.jsx
  75. 18 13
      apps/app/src/client/components/Admin/Notification/UserNotificationRow.jsx
  76. 66 29
      apps/app/src/client/components/Admin/Notification/UserTriggerNotification.jsx
  77. 7 10
      apps/app/src/client/components/Admin/Security/DeleteAllShareLinksModal.jsx
  78. 11 13
      apps/app/src/client/components/Admin/Security/GitHubSecuritySetting.jsx
  79. 162 58
      apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.tsx
  80. 11 13
      apps/app/src/client/components/Admin/Security/GoogleSecuritySetting.jsx
  81. 175 60
      apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.tsx
  82. 51 31
      apps/app/src/client/components/Admin/Security/LdapAuthTest.tsx
  83. 1 12
      apps/app/src/client/components/Admin/Security/LdapAuthTestModal.jsx
  84. 10 11
      apps/app/src/client/components/Admin/Security/LdapSecuritySetting.jsx
  85. 323 136
      apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.tsx
  86. 10 12
      apps/app/src/client/components/Admin/Security/LocalSecuritySetting.jsx
  87. 117 55
      apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.tsx
  88. 10 11
      apps/app/src/client/components/Admin/Security/OidcSecuritySetting.jsx
  89. 398 108
      apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.tsx
  90. 10 11
      apps/app/src/client/components/Admin/Security/SamlSecuritySetting.jsx
  91. 369 116
      apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx
  92. 10 9
      apps/app/src/client/components/Admin/Security/SecurityManagement.tsx
  93. 65 42
      apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx
  94. 20 7
      apps/app/src/client/components/Admin/Security/SecuritySetting/CommentManageRightsSettings.tsx
  95. 19 7
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageAccessRightsSettings.tsx
  96. 321 170
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageDeleteRightsSettings.tsx
  97. 39 16
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageListDisplaySettings.tsx
  98. 15 5
      apps/app/src/client/components/Admin/Security/SecuritySetting/SessionMaxAgeSettings.tsx
  99. 44 13
      apps/app/src/client/components/Admin/Security/SecuritySetting/UserHomepageDeletionSettings.tsx
  100. 81 37
      apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx

+ 0 - 1
.github/mergify.yml

@@ -1,6 +1,5 @@
 queue_rules:
 queue_rules:
   - name: default
   - name: default
-    allow_inplace_checks: false
     queue_conditions:
     queue_conditions:
       - check-success ~= ci-app-lint
       - check-success ~= ci-app-lint
       - check-success ~= ci-app-test
       - check-success ~= ci-app-test

+ 37 - 0
.serena/memories/apps-app-google-workspace-oauth2-mail.md

@@ -0,0 +1,37 @@
+# Google Workspace OAuth 2.0 メール送信機能実装計画
+
+## 概要
+
+Google Workspace (Gmail) の OAuth 2.0 (XOAUTH2) 認証を使ったメール送信機能を実装する。2025年5月1日以降、Gmail SMTP ではユーザー名とパスワード認証がサポートされなくなったため、OAuth 2.0 への移行が必要。
+
+## 背景
+
+- **問題**: Gmail SMTP でのユーザー名・パスワード認証が2025年5月1日にサポート終了
+- **解決策**: OAuth 2.0 (XOAUTH2) 認証方式の実装
+- **参考**: https://support.google.com/a/answer/2956491?hl=ja
+- **ライブラリ**: nodemailer v6.9.15 は OAuth 2.0 をサポート済み(バージョンアップ不要)
+
+## 技術仕様
+
+### 必須設定パラメータ
+
+| パラメータ | 説明 | セキュリティ |
+|-----------|------|------------|
+| `mail:oauth2ClientId` | Google Cloud Console で取得する OAuth 2.0 クライアント ID | 通常 |
+| `mail:oauth2ClientSecret` | OAuth 2.0 クライアントシークレット | `isSecret: true` |
+| `mail:oauth2RefreshToken` | OAuth 2.0 リフレッシュトークン | `isSecret: true` |
+| `mail:oauth2User` | 送信者のGmailアドレス | 通常 |
+
+### nodemailer 設定例
+
+```typescript
+const transportOptions = {
+  service: 'gmail',
+  auth: {
+    type: 'OAuth2',
+    user: 'user@example.com',
+    clientId: 'CLIENT_ID',
+    clientSecret: 'CLIENT_SECRET',
+    refreshToken: 'REFRESH_TOKEN',
+  },
+};

+ 14 - 0
apps/app/.eslintrc.js

@@ -55,7 +55,21 @@ module.exports = {
     'src/client/components/*.jsx',
     'src/client/components/*.jsx',
     'src/client/components/*.ts',
     'src/client/components/*.ts',
     'src/client/components/*.js',
     'src/client/components/*.js',
+    'src/client/components/Admin/*.ts',
+    'src/client/components/Admin/*.tsx',
+    'src/client/components/Admin/*.scss',
+    'src/client/components/Admin/AdminHome/**',
+    'src/client/components/Admin/Common/**',
+    'src/client/components/Admin/ElasticsearchManagement/**',
+    'src/client/components/Admin/ExportArchiveData/**',
+    'src/client/components/Admin/ImportData/**',
+    'src/client/components/Admin/LegacySlackIntegration/**',
+    'src/client/components/Admin/MarkdownSetting/**',
     'src/client/components/Admin/App/**',
     'src/client/components/Admin/App/**',
+    'src/client/components/Admin/AuditLog/**',
+    'src/client/components/Admin/Customize/**',
+    'src/client/components/Admin/Notification/**',
+    'src/client/components/Admin/Security/**',
     'src/client/components/Admin/SlackIntegration/**',
     'src/client/components/Admin/SlackIntegration/**',
     'src/client/components/Admin/Users/**',
     'src/client/components/Admin/Users/**',
     'src/client/components/Admin/UserGroup/**',
     'src/client/components/Admin/UserGroup/**',

+ 1 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -731,7 +731,7 @@
       "description1": "Temporarily issue new users by email addresses.",
       "description1": "Temporarily issue new users by email addresses.",
       "description2": "A temporary password will be generated for the first login.",
       "description2": "A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
       "invite_thru_email": "Send invitation email",
-      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span class='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
       "send_new_password": "Please send the new password to the user.",

+ 1 - 1
apps/app/public/static/locales/fr_FR/admin.json

@@ -731,7 +731,7 @@
       "description1": "Créer des utilisateurs temporaires avec une adresse courriel.",
       "description1": "Créer des utilisateurs temporaires avec une adresse courriel.",
       "description2": "Un mot de passe temporaire est généré automatiquement.",
       "description2": "Un mot de passe temporaire est généré automatiquement.",
       "invite_thru_email": "Courriel d'invitation",
       "invite_thru_email": "Courriel d'invitation",
-      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Paramètres courriel</a>",
+      "mail_setting_link": "<span class='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Paramètres courriel</a>",
       "valid_email": "Adresse courriel valide requise",
       "valid_email": "Adresse courriel valide requise",
       "temporary_password": "Cette utilisateur a un mot de passe temporaire",
       "temporary_password": "Cette utilisateur a un mot de passe temporaire",
       "send_new_password": "Envoyez le nouveau mot de passe à l'utilisateur.",
       "send_new_password": "Envoyez le nouveau mot de passe à l'utilisateur.",

+ 1 - 1
apps/app/public/static/locales/ja_JP/admin.json

@@ -740,7 +740,7 @@
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description2": "初回のログイン時に使用する仮パスワードが生成されます。",
       "description2": "初回のログイン時に使用する仮パスワードが生成されます。",
       "invite_thru_email": "招待メールを送信する",
       "invite_thru_email": "招待メールを送信する",
-      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>メールの設定</a>",
+      "mail_setting_link": "<span class='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>メールの設定</a>",
       "valid_email": "メールアドレスを入力してください。",
       "valid_email": "メールアドレスを入力してください。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",

+ 1 - 1
apps/app/public/static/locales/ko_KR/admin.json

@@ -731,7 +731,7 @@
       "description1": "이메일 주소로 새 사용자를 임시 발급합니다.",
       "description1": "이메일 주소로 새 사용자를 임시 발급합니다.",
       "description2": "첫 로그인 시 임시 비밀번호가 생성됩니다.",
       "description2": "첫 로그인 시 임시 비밀번호가 생성됩니다.",
       "invite_thru_email": "초대 이메일 전송",
       "invite_thru_email": "초대 이메일 전송",
-      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>이메일 설정</a>",
+      "mail_setting_link": "<span class='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>이메일 설정</a>",
       "valid_email": "유효한 이메일 주소가 필요합니다.",
       "valid_email": "유효한 이메일 주소가 필요합니다.",
       "temporary_password": "생성된 사용자에게는 임시 비밀번호가 있습니다.",
       "temporary_password": "생성된 사용자에게는 임시 비밀번호가 있습니다.",
       "send_new_password": "새 비밀번호를 사용자에게 보내주십시오.",
       "send_new_password": "새 비밀번호를 사용자에게 보내주십시오.",

+ 1 - 1
apps/app/public/static/locales/zh_CN/admin.json

@@ -739,7 +739,7 @@
       "emails": "电子邮件",
       "emails": "电子邮件",
       "description1": "通过电子邮件地址临时发布新用户。",
       "description1": "通过电子邮件地址临时发布新用户。",
       "description2": "将为首次登录生成一个临时密码。",
       "description2": "将为首次登录生成一个临时密码。",
-      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span class='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "需要有效的电子邮件地址",
       "valid_email": "需要有效的电子邮件地址",
       "invite_thru_email": "发送邀请电子邮件",
       "invite_thru_email": "发送邀请电子邮件",
       "temporary_password": "创建的用户具有临时密码",
       "temporary_password": "创建的用户具有临时密码",

+ 57 - 29
apps/app/src/client/components/Admin/AdminHome/AdminHome.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -10,14 +9,10 @@ import { toastError } from '~/client/util/toastr';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import { EnvVarsTable } from './EnvVarsTable';
 import { EnvVarsTable } from './EnvVarsTable';
 import SystemInfomationTable from './SystemInfomationTable';
 import SystemInfomationTable from './SystemInfomationTable';
 
 
-
 const logger = loggerFactory('growi:admin');
 const logger = loggerFactory('growi:admin');
 
 
 const AdminHome = (props) => {
 const AdminHome = (props) => {
@@ -25,11 +20,10 @@ const AdminHome = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
 
-  const fetchAdminHomeData = useCallback(async() => {
+  const fetchAdminHomeData = useCallback(async () => {
     try {
     try {
       await adminHomeContainer.retrieveAdminHomeData();
       await adminHomeContainer.retrieveAdminHomeData();
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
@@ -48,25 +42,36 @@ const AdminHome = (props) => {
             <h3 className="alert-heading">
             <h3 className="alert-heading">
               {t('admin:maintenance_mode.maintenance_mode')}
               {t('admin:maintenance_mode.maintenance_mode')}
             </h3>
             </h3>
-            <p>
-              {t('admin:maintenance_mode.description')}
-            </p>
+            <p>{t('admin:maintenance_mode.description')}</p>
             <hr />
             <hr />
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
-              <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
+              <span
+                className="material-symbols-outlined ms-1"
+                aria-hidden="true"
+              >
+                link
+              </span>
+              <strong>
+                {t('admin:maintenance_mode.end_maintenance_mode')}
+              </strong>
             </a>
             </a>
           </div>
           </div>
         )
         )
       }
       }
       {
       {
         // Alert message will be displayed in case that V5 migration has not been compleated
         // Alert message will be displayed in case that V5 migration has not been compleated
-        (migrationStatus != null && !migrationStatus.isV5Compatible)
-        && (
-          <div className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}>
+        migrationStatus != null && !migrationStatus.isV5Compatible && (
+          <div
+            className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}
+          >
             {t('admin:v5_page_migration.migration_desc')}
             {t('admin:v5_page_migration.migration_desc')}
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
+              <span
+                className="material-symbols-outlined ms-1"
+                aria-hidden="true"
+              >
+                link
+              </span>
               <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
               <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
             </a>
             </a>
           </div>
           </div>
@@ -80,43 +85,65 @@ const AdminHome = (props) => {
 
 
       <div className="row mb-5">
       <div className="row mb-5">
         <div className="col-lg-12">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:admin_top.system_information')}
+          </h2>
           <SystemInfomationTable />
           <SystemInfomationTable />
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="row mb-5">
       <div className="row mb-5">
         <div className="col-md-12">
         <div className="col-md-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.list_of_env_vars')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:admin_top.list_of_env_vars')}
+          </h2>
           <p>{t('admin:admin_top.env_var_priority')}</p>
           <p>{t('admin:admin_top.env_var_priority')}</p>
-          {/* eslint-disable-next-line react/no-danger */}
-          <p dangerouslySetInnerHTML={{ __html: t('admin:admin_top.about_security') }} />
+          <p
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+            dangerouslySetInnerHTML={{
+              __html: t('admin:admin_top.about_security'),
+            }}
+          />
           <EnvVarsTable envVars={adminHomeContainer.state.envVars} />
           <EnvVarsTable envVars={adminHomeContainer.state.envVars} />
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="row mb-5">
       <div className="row mb-5">
         <div className="col-md-12">
         <div className="col-md-12">
-          <h2 className="admin-setting-header">{t('admin:admin_top.bug_report')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:admin_top.bug_report')}
+          </h2>
           <div className="d-flex align-items-center">
           <div className="d-flex align-items-center">
             <CopyToClipboard
             <CopyToClipboard
               text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
               text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
               onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
               onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
             >
             >
-              <button id="prefilledHostInformationButton" type="button" className="btn btn-primary">
+              <button
+                id="prefilledHostInformationButton"
+                type="button"
+                className="btn btn-primary"
+              >
                 {t('admin:admin_top:copy_prefilled_host_information:default')}
                 {t('admin:admin_top:copy_prefilled_host_information:default')}
               </button>
               </button>
             </CopyToClipboard>
             </CopyToClipboard>
             <Tooltip
             <Tooltip
               placement="bottom"
               placement="bottom"
-              isOpen={adminHomeContainer.state.copyState === adminHomeContainer.copyStateValues.DONE}
+              isOpen={
+                adminHomeContainer.state.copyState ===
+                adminHomeContainer.copyStateValues.DONE
+              }
               target="prefilledHostInformationButton"
               target="prefilledHostInformationButton"
               fade={false}
               fade={false}
             >
             >
               {t('admin:admin_top:copy_prefilled_host_information:done')}
               {t('admin:admin_top:copy_prefilled_host_information:done')}
             </Tooltip>
             </Tooltip>
-            {/* eslint-disable-next-line react/no-danger */}
-            <span className="ms-2" dangerouslySetInnerHTML={{ __html: t('admin:admin_top:submit_bug_report') }} />
+            <span
+              className="ms-2"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+              dangerouslySetInnerHTML={{
+                __html: t('admin:admin_top:submit_bug_report'),
+              }}
+            />
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -124,8 +151,9 @@ const AdminHome = (props) => {
   );
   );
 };
 };
 
 
-
-const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AdminHomeContainer]);
+const AdminHomeWrapper = withUnstatedContainers(AdminHome, [
+  AdminHomeContainer,
+]);
 
 
 AdminHome.propTypes = {
 AdminHome.propTypes = {
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,

+ 8 - 8
apps/app/src/client/components/Admin/AdminHome/EnvVarsTable.tsx

@@ -1,12 +1,14 @@
-import React, { type JSX } from 'react';
-
+import type React from 'react';
+import type { JSX } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
 
 type EnvVarsTableProps = {
 type EnvVarsTableProps = {
-  envVars?: Record<string, string | number | boolean>,
-}
+  envVars?: Record<string, string | number | boolean>;
+};
 
 
-export const EnvVarsTable: React.FC<EnvVarsTableProps> = (props: EnvVarsTableProps) => {
+export const EnvVarsTable: React.FC<EnvVarsTableProps> = (
+  props: EnvVarsTableProps,
+) => {
   const { envVars } = props;
   const { envVars } = props;
   if (envVars == null) {
   if (envVars == null) {
     return <LoadingSpinner />;
     return <LoadingSpinner />;
@@ -27,9 +29,7 @@ export const EnvVarsTable: React.FC<EnvVarsTableProps> = (props: EnvVarsTablePro
 
 
   return (
   return (
     <table className="table table-bordered">
     <table className="table table-bordered">
-      <tbody>
-        {envVarRows}
-      </tbody>
+      <tbody>{envVarRows}</tbody>
     </table>
     </table>
   );
   );
 };
 };

+ 22 - 15
apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -1,55 +1,62 @@
 import React from 'react';
 import React from 'react';
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
 
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-
 type Props = {
 type Props = {
-  adminHomeContainer: AdminHomeContainer,
-}
+  adminHomeContainer: AdminHomeContainer;
+};
 
 
 const SystemInformationTable = (props: Props) => {
 const SystemInformationTable = (props: Props) => {
   const { adminHomeContainer } = props;
   const { adminHomeContainer } = props;
 
 
-  const {
-    growiVersion, nodeVersion, npmVersion, pnpmVersion,
-  } = adminHomeContainer.state;
+  const { growiVersion, nodeVersion, npmVersion, pnpmVersion } =
+    adminHomeContainer.state;
 
 
-  if (growiVersion == null || nodeVersion == null || npmVersion == null || pnpmVersion == null) {
+  if (
+    growiVersion == null ||
+    nodeVersion == null ||
+    npmVersion == null ||
+    pnpmVersion == null
+  ) {
     return <LoadingSpinner />;
     return <LoadingSpinner />;
   }
   }
 
 
   return (
   return (
-    <table data-testid="admin-system-information-table" className="table table-bordered">
+    <table
+      data-testid="admin-system-information-table"
+      className="table table-bordered"
+    >
       <tbody>
       <tbody>
         <tr>
         <tr>
           <th>GROWI</th>
           <th>GROWI</th>
-          <td data-vrt-blackout>{ growiVersion }</td>
+          <td data-vrt-blackout>{growiVersion}</td>
         </tr>
         </tr>
         <tr>
         <tr>
           <th>node.js</th>
           <th>node.js</th>
-          <td>{ nodeVersion }</td>
+          <td>{nodeVersion}</td>
         </tr>
         </tr>
         <tr>
         <tr>
           <th>npm</th>
           <th>npm</th>
-          <td>{ npmVersion }</td>
+          <td>{npmVersion}</td>
         </tr>
         </tr>
         <tr>
         <tr>
           <th>pnpm</th>
           <th>pnpm</th>
-          <td>{ pnpmVersion }</td>
+          <td>{pnpmVersion}</td>
         </tr>
         </tr>
       </tbody>
       </tbody>
     </table>
     </table>
   );
   );
-
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SystemInformationTableWrapper = withUnstatedContainers(SystemInformationTable, [AdminHomeContainer]);
+const SystemInformationTableWrapper = withUnstatedContainers(
+  SystemInformationTable,
+  [AdminHomeContainer],
+);
 
 
 export default SystemInformationTableWrapper;
 export default SystemInformationTableWrapper;

+ 32 - 13
apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,6 +1,5 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React, { useState, useCallback } from 'react';
-
+import React, { useCallback, useState } from 'react';
 import { isPopulated } from '@growi/core';
 import { isPopulated } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -11,19 +10,18 @@ import { Tooltip } from 'reactstrap';
 
 
 import type { IActivityHasId } from '~/interfaces/activity';
 import type { IActivityHasId } from '~/interfaces/activity';
 
 
- type Props = {
-   activityList: IActivityHasId[]
- }
+type Props = {
+  activityList: IActivityHasId[];
+};
 
 
 const formatDate = (date: Date): string => {
 const formatDate = (date: Date): string => {
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
 };
 };
 
 
-export const ActivityTable : FC<Props> = (props: Props) => {
+export const ActivityTable: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [activeTooltipId, setActiveTooltipId] = useState<string | null>(null);
   const [activeTooltipId, setActiveTooltipId] = useState<string | null>(null);
 
 
-
   const showToolTip = useCallback((id: string) => {
   const showToolTip = useCallback((id: string) => {
     setActiveTooltipId(id);
     setActiveTooltipId(id);
     setTimeout(() => {
     setTimeout(() => {
@@ -48,12 +46,16 @@ export const ActivityTable : FC<Props> = (props: Props) => {
             return (
             return (
               <tr data-testid="activity-table" key={activity._id}>
               <tr data-testid="activity-table" key={activity._id}>
                 <td>
                 <td>
-                  { activity.user != null && (
+                  {activity.user != null && (
                     <>
                     <>
                       <UserPicture user={activity.user} />
                       <UserPicture user={activity.user} />
                       <a
                       <a
                         className="ms-2"
                         className="ms-2"
-                        href={isPopulated(activity.user) ? pagePathUtils.userHomepagePath(activity.user) : undefined}
+                        href={
+                          isPopulated(activity.user)
+                            ? pagePathUtils.userHomepagePath(activity.user)
+                            : undefined
+                        }
                       >
                       >
                         {activity.snapshot?.username}
                         {activity.snapshot?.username}
                       </a>
                       </a>
@@ -68,12 +70,29 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                     <span className="flex-grow-1 text-truncate">
                     <span className="flex-grow-1 text-truncate">
                       {activity.endpoint}
                       {activity.endpoint}
                     </span>
                     </span>
-                    <CopyToClipboard text={activity.endpoint} onCopy={() => showToolTip(activity._id)}>
-                      <button type="button" className="btn btn-outline-secondary border-0 ms-2" id={`tooltipTarget-${activity._id}`}>
-                        <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
+                    <CopyToClipboard
+                      text={activity.endpoint}
+                      onCopy={() => showToolTip(activity._id)}
+                    >
+                      <button
+                        type="button"
+                        className="btn btn-outline-secondary border-0 ms-2"
+                        id={`tooltipTarget-${activity._id}`}
+                      >
+                        <span
+                          className="material-symbols-outlined"
+                          aria-hidden="true"
+                        >
+                          content_paste
+                        </span>
                       </button>
                       </button>
                     </CopyToClipboard>
                     </CopyToClipboard>
-                    <Tooltip placement="top" isOpen={activeTooltipId === activity._id} fade={false} target={`tooltipTarget-${activity._id}`}>
+                    <Tooltip
+                      placement="top"
+                      isOpen={activeTooltipId === activity._id}
+                      fade={false}
+                      target={`tooltipTarget-${activity._id}`}
+                    >
                       copied!
                       copied!
                     </Tooltip>
                     </Tooltip>
                   </div>
                   </div>

+ 10 - 4
apps/app/src/client/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -1,6 +1,5 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 export const AuditLogDisableMode: FC = () => {
 export const AuditLogDisableMode: FC = () => {
@@ -13,11 +12,18 @@ export const AuditLogDisableMode: FC = () => {
           <div className="col-md-6 mt-5">
           <div className="col-md-6 mt-5">
             <div className="text-center">
             <div className="text-center">
               {/* error icon large */}
               {/* error icon large */}
-              <h1><span className="material-symbols-outlined">error</span></h1>
-              <h1 className="text-center">{t('audit_log_management.audit_log')}</h1>
+              <h1>
+                <span className="material-symbols-outlined">error</span>
+              </h1>
+              <h1 className="text-center">
+                {t('audit_log_management.audit_log')}
+              </h1>
               <h3
               <h3
                 // eslint-disable-next-line react/no-danger
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('audit_log_management.disable_mode_explanation') }}
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('audit_log_management.disable_mode_explanation'),
+                }}
               />
               />
             </div>
             </div>
           </div>
           </div>

+ 38 - 14
apps/app/src/client/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -1,36 +1,45 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, { useState } from 'react';
 import React, { useState } from 'react';
-
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
 import { AllSupportedActions } from '~/interfaces/activity';
 import { AllSupportedActions } from '~/interfaces/activity';
-import { activityExpirationSecondsAtom, auditLogAvailableActionsAtom } from '~/states/server-configurations';
+import {
+  activityExpirationSecondsAtom,
+  auditLogAvailableActionsAtom,
+} from '~/states/server-configurations';
 
 
 export const AuditLogSettings: FC = () => {
 export const AuditLogSettings: FC = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [isExpandActionList, setIsExpandActionList] = useState(false);
   const [isExpandActionList, setIsExpandActionList] = useState(false);
 
 
-  const activityExpirationSeconds = useAtomValue(activityExpirationSecondsAtom) || 2592000;
+  const activityExpirationSeconds =
+    useAtomValue(activityExpirationSecondsAtom) || 2592000;
 
 
   const availableActions = useAtomValue(auditLogAvailableActionsAtom);
   const availableActions = useAtomValue(auditLogAvailableActionsAtom);
 
 
   return (
   return (
     <>
     <>
-      <h4 className="mt-4">{t('admin:audit_log_management.activity_expiration_date')}</h4>
+      <h4 className="mt-4">
+        {t('admin:audit_log_management.activity_expiration_date')}
+      </h4>
       <p className="form-text text-muted">
       <p className="form-text text-muted">
         {t('admin:audit_log_management.activity_expiration_date_explanation')}
         {t('admin:audit_log_management.activity_expiration_date_explanation')}
       </p>
       </p>
       <p className="alert alert-warning col-6">
       <p className="alert alert-warning col-6">
         <span className="material-symbols-outlined">error</span>
         <span className="material-symbols-outlined">error</span>
-        <b>FIXED</b><br />
+        <b>FIXED</b>
+        <br />
         <b
         <b
           // eslint-disable-next-line react/no-danger
           // eslint-disable-next-line react/no-danger
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
           dangerouslySetInnerHTML={{
           dangerouslySetInnerHTML={{
-            __html: t('admin:audit_log_management.fixed_by_env_var',
-              { key: 'ACTIVITY_EXPIRATION_SECONDS', value: activityExpirationSeconds }),
+            __html: t('admin:audit_log_management.fixed_by_env_var', {
+              key: 'ACTIVITY_EXPIRATION_SECONDS',
+              value: activityExpirationSeconds,
+            }),
           }}
           }}
         />
         />
       </p>
       </p>
@@ -45,24 +54,39 @@ export const AuditLogSettings: FC = () => {
           href={t('admin:audit_log_management.docs_url.log_type')}
           href={t('admin:audit_log_management.docs_url.log_type')}
           target="_blank"
           target="_blank"
           rel="noopener noreferrer"
           rel="noopener noreferrer"
+          aria-label="Help"
         >
         >
-          <span className="material-symbols-outlined" aria-hidden="true">help</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            help
+          </span>
+          <span className="visually-hidden">Help</span>
         </a>
         </a>
       </h4>
       </h4>
       <p className="form-text text-muted">
       <p className="form-text text-muted">
         {t('admin:audit_log_management.available_action_list_explanation')}
         {t('admin:audit_log_management.available_action_list_explanation')}
       </p>
       </p>
       <p className="mt-1">
       <p className="mt-1">
-        <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
-          <span className={`material-symbols-outlined me-1 ${isExpandActionList ? 'rotate-90' : ''}`}>navigate_next</span>
-          { t('admin:audit_log_management.action_list') }
+        <button
+          type="button"
+          className="btn btn-link p-0"
+          aria-expanded="false"
+          onClick={() => setIsExpandActionList(!isExpandActionList)}
+        >
+          <span
+            className={`material-symbols-outlined me-1 ${isExpandActionList ? 'rotate-90' : ''}`}
+          >
+            navigate_next
+          </span>
+          {t('admin:audit_log_management.action_list')}
         </button>
         </button>
       </p>
       </p>
       <Collapse isOpen={isExpandActionList}>
       <Collapse isOpen={isExpandActionList}>
         <ul className="list-group">
         <ul className="list-group">
-          { availableActions.map(action => (
-            <li key={action} className="list-group-item">{t(`admin:audit_log_action.${action}`)}</li>
-          )) }
+          {availableActions.map((action) => (
+            <li key={action} className="list-group-item">
+              {t(`admin:audit_log_action.${action}`)}
+            </li>
+          ))}
         </ul>
         </ul>
       </Collapse>
       </Collapse>
     </>
     </>

+ 49 - 44
apps/app/src/client/components/Admin/AuditLog/DateRangePicker.tsx

@@ -1,64 +1,69 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, { forwardRef, useCallback } from 'react';
 import React, { forwardRef, useCallback } from 'react';
-
 import { addDays, format } from 'date-fns';
 import { addDays, format } from 'date-fns';
 import DatePicker from 'react-datepicker';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 import 'react-datepicker/dist/react-datepicker.css';
 
 
-
 type CustomInputProps = {
 type CustomInputProps = {
-  value?: string
-  onChange?: () => void
-  onFocus?: () => void
-}
+  value?: string;
+  onChange?: () => void;
+  onFocus?: () => void;
+};
 
 
-const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>((props: CustomInputProps, ref) => {
-  const dateFormat = 'MM/dd/yyyy';
-  const date = new Date();
-  const placeholder = `${format(date, dateFormat)} - ${format(addDays(date, 1), dateFormat)}`;
+const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
+  (props: CustomInputProps, ref) => {
+    const dateFormat = 'MM/dd/yyyy';
+    const date = new Date();
+    const placeholder = `${format(date, dateFormat)} - ${format(addDays(date, 1), dateFormat)}`;
 
 
-  return (
-    <div className="input-group admin-audit-log">
-      <span className="input-group-text">
-        <span className="material-symbols-outlined me-1">calendar_month</span>
-      </span>
-      <input
-        ref={ref}
-        type="text"
-        value={props?.value}
-        onFocus={props?.onFocus}
-        onChange={props?.onChange}
-        placeholder={placeholder}
-        className="form-control date-range-picker"
-        aria-describedby="basic-addon1"
-      />
-    </div>
-  );
-});
+    return (
+      <div className="input-group admin-audit-log">
+        <span className="input-group-text">
+          <span className="material-symbols-outlined me-1">calendar_month</span>
+        </span>
+        <input
+          ref={ref}
+          type="text"
+          value={props?.value}
+          onFocus={props?.onFocus}
+          onChange={props?.onChange}
+          placeholder={placeholder}
+          className="form-control date-range-picker"
+          aria-describedby="basic-addon1"
+        />
+      </div>
+    );
+  },
+);
 
 
 CustomInput.displayName = 'CustomInput';
 CustomInput.displayName = 'CustomInput';
 
 
 type DateRangePickerProps = {
 type DateRangePickerProps = {
-  startDate: Date | null
-  endDate: Date | null
-  onChange: (dateList: Date[] | null[]) => void
-}
+  startDate: Date | null;
+  endDate: Date | null;
+  onChange: (dateList: Date[] | null[]) => void;
+};
 
 
-export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
+export const DateRangePicker: FC<DateRangePickerProps> = (
+  props: DateRangePickerProps,
+) => {
   const { startDate, endDate, onChange } = props;
   const { startDate, endDate, onChange } = props;
 
 
-  const changeHandler = useCallback((dateList: Date[] | null[]) => {
-    if (onChange != null) {
-      const [start, end] = dateList;
-      const isSameTime = (start != null && end != null) && (start.getTime() === end.getTime());
-      if (isSameTime) {
-        onChange([null, null]);
+  const changeHandler = useCallback(
+    (dateList: Date[] | null[]) => {
+      if (onChange != null) {
+        const [start, end] = dateList;
+        const isSameTime =
+          start != null && end != null && start.getTime() === end.getTime();
+        if (isSameTime) {
+          onChange([null, null]);
+        } else {
+          onChange(dateList);
+        }
       }
       }
-      else {
-        onChange(dateList);
-      }
-    }
-  }, [onChange]);
+    },
+    [onChange],
+  );
 
 
   return (
   return (
     <div className="me-2">
     <div className="me-2">

+ 54 - 26
apps/app/src/client/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,8 +1,12 @@
 import type { ForwardRefRenderFunction } from 'react';
 import type { ForwardRefRenderFunction } from 'react';
 import React, {
 import React, {
-  Fragment, useState, useCallback, forwardRef, useRef, useImperativeHandle,
+  Fragment,
+  forwardRef,
+  useCallback,
+  useImperativeHandle,
+  useRef,
+  useState,
 } from 'react';
 } from 'react';
-
 import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
@@ -10,25 +14,27 @@ import { useTranslation } from 'react-i18next';
 import type { IClearable } from '~/client/interfaces/clearable';
 import type { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 import { useSWRxUsernames } from '~/stores/user';
 
 
-
 const Categories = {
 const Categories = {
   activeUser: 'Active User',
   activeUser: 'Active User',
   inactiveUser: 'Inactive User',
   inactiveUser: 'Inactive User',
   activitySnapshotUser: 'Activity Snapshot User',
   activitySnapshotUser: 'Activity Snapshot User',
 } as const;
 } as const;
 
 
-type CategoryType = typeof Categories[keyof typeof Categories]
+type CategoryType = (typeof Categories)[keyof typeof Categories];
 
 
 type UserDataType = {
 type UserDataType = {
-  username: string
-  category: CategoryType
-}
+  username: string;
+  category: CategoryType;
+};
 
 
 type Props = {
 type Props = {
-  onChange: (text: string[]) => void
-}
+  onChange: (text: string[]) => void;
+};
 
 
-const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Props> = ((props: Props, ref) => {
+const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<
+  IClearable,
+  Props
+> = (props: Props, ref) => {
   const { onChange } = props;
   const { onChange } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -42,16 +48,35 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   /*
   /*
    * Fetch
    * Fetch
    */
    */
-  const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
-  const { data: usernameData, error, isLoading: _isLoading } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
-  const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
-  const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
-  const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
+  const requestOptions = {
+    isIncludeActiveUser: true,
+    isIncludeInactiveUser: true,
+    isIncludeActivitySnapshotUser: true,
+  };
+  const {
+    data: usernameData,
+    error,
+    isLoading: _isLoading,
+  } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
+  const activeUsernames =
+    usernameData?.activeUser?.usernames != null
+      ? usernameData.activeUser.usernames
+      : [];
+  const inactiveUsernames =
+    usernameData?.inactiveUser?.usernames != null
+      ? usernameData.inactiveUser.usernames
+      : [];
+  const activitySnapshotUsernames =
+    usernameData?.activitySnapshotUser?.usernames != null
+      ? usernameData.activitySnapshotUser.usernames
+      : [];
   const isLoading = _isLoading === true && error == null;
   const isLoading = _isLoading === true && error == null;
 
 
   const allUser: UserDataType[] = [];
   const allUser: UserDataType[] = [];
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
-    usernames.forEach(username => allUser.push({ username, category }));
+    usernames.forEach((username) => {
+      allUser.push({ username, category });
+    });
   };
   };
   pushToAllUser(activeUsernames, Categories.activeUser);
   pushToAllUser(activeUsernames, Categories.activeUser);
   pushToAllUser(inactiveUsernames, Categories.inactiveUser);
   pushToAllUser(inactiveUsernames, Categories.inactiveUser);
@@ -60,10 +85,13 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   /*
   /*
    * Functions
    * Functions
    */
    */
-  const changeHandler = useCallback((userData: UserDataType[]) => {
-    const usernames = userData.map(user => user.username);
-    onChange(usernames);
-  }, [onChange]);
+  const changeHandler = useCallback(
+    (userData: UserDataType[]) => {
+      const usernames = userData.map((user) => user.username);
+      onChange(usernames);
+    },
+    [onChange],
+  );
 
 
   const searchHandler = useCallback((text: string) => {
   const searchHandler = useCallback((text: string) => {
     setSearchKeyword(text);
     setSearchKeyword(text);
@@ -76,7 +104,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
 
 
     let index = 0;
     let index = 0;
     const items = Object.values(Categories).map((category) => {
     const items = Object.values(Categories).map((category) => {
-      const userData = allUser.filter(user => user.category === category);
+      const userData = allUser.filter((user) => user.category === category);
       return (
       return (
         <Fragment key={category}>
         <Fragment key={category}>
           {index !== 0 && <Menu.Divider />}
           {index !== 0 && <Menu.Divider />}
@@ -94,9 +122,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
       );
       );
     });
     });
 
 
-    return (
-      <Menu {...menuProps}>{items}</Menu>
-    );
+    return <Menu {...menuProps}>{items}</Menu>;
   }, []);
   }, []);
 
 
   useImperativeHandle(ref, () => ({
   useImperativeHandle(ref, () => ({
@@ -129,6 +155,8 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
       />
       />
     </div>
     </div>
   );
   );
-});
+};
 
 
-export const SearchUsernameTypeahead = forwardRef(SearchUsernameTypeaheadSubstance);
+export const SearchUsernameTypeahead = forwardRef(
+  SearchUsernameTypeaheadSubstance,
+);

+ 151 - 88
apps/app/src/client/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -1,122 +1,185 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React, { useMemo, useCallback } from 'react';
-
+import React, { useCallback, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import type { SupportedActionType, SupportedActionCategoryType } from '~/interfaces/activity';
+import type {
+  SupportedActionCategoryType,
+  SupportedActionType,
+} from '~/interfaces/activity';
 import {
 import {
+  AdminActions,
+  AttachmentActions,
+  CommentActions,
+  InAppNotificationActions,
+  PageActions,
+  SearchActions,
+  ShareLinkActions,
   SupportedActionCategory,
   SupportedActionCategory,
-  PageActions, CommentActions, TagActions, ShareLinkActions, AttachmentActions, InAppNotificationActions, SearchActions, UserActions, AdminActions,
+  TagActions,
+  UserActions,
 } from '~/interfaces/activity';
 } from '~/interfaces/activity';
 
 
 type Props = {
 type Props = {
-  actionMap: Map<SupportedActionType, boolean>
-  availableActions: SupportedActionType[]
-  onChangeAction: (action: SupportedActionType) => void
-  onChangeMultipleAction: (actions: SupportedActionType[], isChecked: boolean) => void
-}
+  actionMap: Map<SupportedActionType, boolean>;
+  availableActions: SupportedActionType[];
+  onChangeAction: (action: SupportedActionType) => void;
+  onChangeMultipleAction: (
+    actions: SupportedActionType[],
+    isChecked: boolean,
+  ) => void;
+};
 
 
 export const SelectActionDropdown: FC<Props> = (props: Props) => {
 export const SelectActionDropdown: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
-    actionMap, availableActions, onChangeAction, onChangeMultipleAction,
+    actionMap,
+    availableActions,
+    onChangeAction,
+    onChangeMultipleAction,
   } = props;
   } = props;
 
 
-  const dropdownItems = useMemo<Array<{actionCategory: SupportedActionCategoryType, actions: SupportedActionType[]}>>(() => {
-    return (
-      [
-        {
-          actionCategory: SupportedActionCategory.PAGE,
-          actions: PageActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.COMMENT,
-          actions: CommentActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.TAG,
-          actions: TagActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.ATTACHMENT,
-          actions: AttachmentActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.SHARE_LINK,
-          actions: ShareLinkActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.IN_APP_NOTIFICATION,
-          actions: InAppNotificationActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.SEARCH,
-          actions: SearchActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.USER,
-          actions: UserActions.filter(action => availableActions.includes(action)),
-        },
-        {
-          actionCategory: SupportedActionCategory.ADMIN,
-          actions: AdminActions.filter(action => availableActions.includes(action)),
-        },
-      ]
-    );
-  }, [availableActions]).filter(item => item.actions.length !== 0);
+  const dropdownItems = useMemo<
+    Array<{
+      actionCategory: SupportedActionCategoryType;
+      actions: SupportedActionType[];
+    }>
+  >(() => {
+    return [
+      {
+        actionCategory: SupportedActionCategory.PAGE,
+        actions: PageActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.COMMENT,
+        actions: CommentActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.TAG,
+        actions: TagActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.ATTACHMENT,
+        actions: AttachmentActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.SHARE_LINK,
+        actions: ShareLinkActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.IN_APP_NOTIFICATION,
+        actions: InAppNotificationActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.SEARCH,
+        actions: SearchActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.USER,
+        actions: UserActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+      {
+        actionCategory: SupportedActionCategory.ADMIN,
+        actions: AdminActions.filter((action) =>
+          availableActions.includes(action),
+        ),
+      },
+    ];
+  }, [availableActions]).filter((item) => item.actions.length !== 0);
 
 
-  const actionCheckboxChangedHandler = useCallback((action) => {
-    if (onChangeAction != null) {
-      onChangeAction(action);
-    }
-  }, [onChangeAction]);
+  const actionCheckboxChangedHandler = useCallback(
+    (action) => {
+      if (onChangeAction != null) {
+        onChangeAction(action);
+      }
+    },
+    [onChangeAction],
+  );
 
 
-  const multipleActionCheckboxChangedHandler = useCallback((actions, isChecked) => {
-    if (onChangeMultipleAction != null) {
-      onChangeMultipleAction(actions, isChecked);
-    }
-  }, [onChangeMultipleAction]);
+  const multipleActionCheckboxChangedHandler = useCallback(
+    (actions, isChecked) => {
+      if (onChangeMultipleAction != null) {
+        onChangeMultipleAction(actions, isChecked);
+      }
+    },
+    [onChangeMultipleAction],
+  );
 
 
   return (
   return (
     <div className="btn-group me-2 admin-audit-log">
     <div className="btn-group me-2 admin-audit-log">
-      <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown">
-        <span className="material-symbols-outlined me-1">bolt</span>{t('admin:audit_log_management.action')}
+      <button
+        className="btn btn-outline-secondary dropdown-toggle"
+        type="button"
+        id="dropdownMenuButton"
+        data-bs-toggle="dropdown"
+      >
+        <span className="material-symbols-outlined me-1">bolt</span>
+        {t('admin:audit_log_management.action')}
       </button>
       </button>
-      <ul className="dropdown-menu select-action-dropdown" aria-labelledby="dropdownMenuButton">
-        {dropdownItems.map(item => (
+      <ul
+        className="dropdown-menu select-action-dropdown"
+        aria-labelledby="dropdownMenuButton"
+      >
+        {dropdownItems.map((item) => (
           <div key={item.actionCategory}>
           <div key={item.actionCategory}>
             <div className="dropdown-item">
             <div className="dropdown-item">
               <div className="px-2 m-0">
               <div className="px-2 m-0">
                 <input
                 <input
                   type="checkbox"
                   type="checkbox"
                   className="form-check-input"
                   className="form-check-input"
+                  id={`checkboxCategory${item.actionCategory}`}
                   defaultChecked
                   defaultChecked
-                  onChange={(e) => { multipleActionCheckboxChangedHandler(item.actions, e.target.checked) }}
+                  onChange={(e) => {
+                    multipleActionCheckboxChangedHandler(
+                      item.actions,
+                      e.target.checked,
+                    );
+                  }}
                 />
                 />
-                <label className="form-label form-check-label">{t(`admin:audit_log_action_category.${item.actionCategory}`)}</label>
+                <label
+                  className="form-label form-check-label"
+                  htmlFor={`checkboxCategory${item.actionCategory}`}
+                >
+                  {t(`admin:audit_log_action_category.${item.actionCategory}`)}
+                </label>
               </div>
               </div>
             </div>
             </div>
-            {
-              item.actions.map(action => (
-                <div className="dropdown-item" key={action}>
-                  <div className="px-4 m-0">
-                    <input
-                      type="checkbox"
-                      className="form-check-input"
-                      id={`checkbox${action}`}
-                      onChange={() => { actionCheckboxChangedHandler(action) }}
-                      checked={actionMap.get(action)}
-                    />
-                    <label
-                      className="form-check-label"
-                      htmlFor={`checkbox${action}`}
-                    >
-                      {t(`admin:audit_log_action.${action}`)}
-                    </label>
-                  </div>
+            {item.actions.map((action) => (
+              <div className="dropdown-item" key={action}>
+                <div className="px-4 m-0">
+                  <input
+                    type="checkbox"
+                    className="form-check-input"
+                    id={`checkbox${action}`}
+                    onChange={() => {
+                      actionCheckboxChangedHandler(action);
+                    }}
+                    checked={actionMap.get(action)}
+                  />
+                  <label
+                    className="form-check-label"
+                    htmlFor={`checkbox${action}`}
+                  >
+                    {t(`admin:audit_log_action.${action}`)}
+                  </label>
                 </div>
                 </div>
-              ))
-            }
+              </div>
+            ))}
           </div>
           </div>
         ))}
         ))}
       </ul>
       </ul>

+ 143 - 73
apps/app/src/client/components/Admin/AuditLogManagement.tsx

@@ -1,6 +1,6 @@
+import type React from 'react';
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React, { useState, useCallback, useRef } from 'react';
-
+import { useCallback, useRef, useState } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { format } from 'date-fns/format';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
@@ -9,11 +9,13 @@ import { useTranslation } from 'react-i18next';
 import type { IClearable } from '~/client/interfaces/clearable';
 import type { IClearable } from '~/client/interfaces/clearable';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import type { SupportedActionType } from '~/interfaces/activity';
 import type { SupportedActionType } from '~/interfaces/activity';
-import { auditLogEnabledAtom, auditLogAvailableActionsAtom } from '~/states/server-configurations';
+import {
+  auditLogAvailableActionsAtom,
+  auditLogEnabledAtom,
+} from '~/states/server-configurations';
 import { useSWRxActivity } from '~/stores/activity';
 import { useSWRxActivity } from '~/stores/activity';
 
 
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
-
 import { ActivityTable } from './AuditLog/ActivityTable';
 import { ActivityTable } from './AuditLog/ActivityTable';
 import { AuditLogDisableMode } from './AuditLog/AuditLogDisableMode';
 import { AuditLogDisableMode } from './AuditLog/AuditLogDisableMode';
 import { AuditLogSettings } from './AuditLog/AuditLogSettings';
 import { AuditLogSettings } from './AuditLog/AuditLogSettings';
@@ -35,7 +37,9 @@ export const AuditLogManagement: FC = () => {
 
 
   const typeaheadRef = useRef<IClearable>(null);
   const typeaheadRef = useRef<IClearable>(null);
 
 
-  const auditLogAvailableActionsData = useAtomValue(auditLogAvailableActionsAtom);
+  const auditLogAvailableActionsData = useAtomValue(
+    auditLogAvailableActionsAtom,
+  );
 
 
   /*
   /*
    * State
    * State
@@ -48,20 +52,39 @@ export const AuditLogManagement: FC = () => {
   const [endDate, setEndDate] = useState<Date | null>(null);
   const [endDate, setEndDate] = useState<Date | null>(null);
   const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [actionMap, setActionMap] = useState(
   const [actionMap, setActionMap] = useState(
-    new Map<SupportedActionType, boolean>(auditLogAvailableActionsData != null ? auditLogAvailableActionsData.map(action => [action, true]) : []),
+    new Map<SupportedActionType, boolean>(
+      auditLogAvailableActionsData != null
+        ? auditLogAvailableActionsData.map((action) => [action, true])
+        : [],
+    ),
   );
   );
 
 
   /*
   /*
    * Fetch
    * Fetch
    */
    */
-  const selectedDate = { startDate: formatDate(startDate), endDate: formatDate(endDate) };
-  const selectedActionList = Array.from(actionMap.entries()).filter(v => v[1]).map(v => v[0]);
-  const searchFilter = { actions: selectedActionList, dates: selectedDate, usernames: selectedUsernames };
-
-  const { data: activityData, mutate: mutateActivity, error } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
+  const selectedDate = {
+    startDate: formatDate(startDate),
+    endDate: formatDate(endDate),
+  };
+  const selectedActionList = Array.from(actionMap.entries())
+    .filter((v) => v[1])
+    .map((v) => v[0]);
+  const searchFilter = {
+    actions: selectedActionList,
+    dates: selectedDate,
+    usernames: selectedUsernames,
+  };
+
+  const {
+    data: activityData,
+    mutate: mutateActivity,
+    error,
+  } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
   const activityList = activityData?.docs != null ? activityData.docs : [];
   const activityList = activityData?.docs != null ? activityData.docs : [];
-  const totalActivityNum = activityData?.totalDocs != null ? activityData.totalDocs : 0;
-  const totalPagingPages = activityData?.totalPages != null ? activityData.totalPages : 0;
+  const totalActivityNum =
+    activityData?.totalDocs != null ? activityData.totalDocs : 0;
+  const totalPagingPages =
+    activityData?.totalPages != null ? activityData.totalPages : 0;
   const isLoading = activityData === undefined && error == null;
   const isLoading = activityData === undefined && error == null;
 
 
   if (error != null) {
   if (error != null) {
@@ -83,17 +106,25 @@ export const AuditLogManagement: FC = () => {
     setEndDate(dateList[1]);
     setEndDate(dateList[1]);
   }, []);
   }, []);
 
 
-  const actionCheckboxChangedHandler = useCallback((action: SupportedActionType) => {
-    setActivePageNumber(1);
-    actionMap.set(action, !actionMap.get(action));
-    setActionMap(new Map(actionMap.entries()));
-  }, [actionMap, setActionMap]);
+  const actionCheckboxChangedHandler = useCallback(
+    (action: SupportedActionType) => {
+      setActivePageNumber(1);
+      actionMap.set(action, !actionMap.get(action));
+      setActionMap(new Map(actionMap.entries()));
+    },
+    [actionMap],
+  );
 
 
-  const multipleActionCheckboxChangedHandler = useCallback((actions: SupportedActionType[], isChecked) => {
-    setActivePageNumber(1);
-    actions.forEach(action => actionMap.set(action, isChecked));
-    setActionMap(new Map(actionMap.entries()));
-  }, [actionMap, setActionMap]);
+  const multipleActionCheckboxChangedHandler = useCallback(
+    (actions: SupportedActionType[], isChecked) => {
+      setActivePageNumber(1);
+      actions.forEach((action) => {
+        actionMap.set(action, isChecked);
+      });
+      setActionMap(new Map(actionMap.entries()));
+    },
+    [actionMap],
+  );
 
 
   const setUsernamesHandler = useCallback((usernames: string[]) => {
   const setUsernamesHandler = useCallback((usernames: string[]) => {
     setActivePageNumber(1);
     setActivePageNumber(1);
@@ -108,41 +139,55 @@ export const AuditLogManagement: FC = () => {
     typeaheadRef.current?.clear();
     typeaheadRef.current?.clear();
 
 
     if (auditLogAvailableActionsData != null) {
     if (auditLogAvailableActionsData != null) {
-      setActionMap(new Map<SupportedActionType, boolean>(auditLogAvailableActionsData.map(action => [action, true])));
+      setActionMap(
+        new Map<SupportedActionType, boolean>(
+          auditLogAvailableActionsData.map((action) => [action, true]),
+        ),
+      );
     }
     }
-  }, [setActivePageNumber, setStartDate, setEndDate, setSelectedUsernames, setActionMap, auditLogAvailableActionsData]);
+  }, [auditLogAvailableActionsData]);
 
 
   const reloadButtonPushedHandler = useCallback(() => {
   const reloadButtonPushedHandler = useCallback(() => {
     setActivePageNumber(1);
     setActivePageNumber(1);
     mutateActivity();
     mutateActivity();
   }, [mutateActivity]);
   }, [mutateActivity]);
 
 
-  const jumpPageInputChangeHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
-    const inputNumber = Number(e.target.value);
-    const isNan = Number.isNaN(inputNumber);
-
-    if (!isNan) {
-      // eslint-disable-next-line no-nested-ternary
-      const jumpPageNumber = inputNumber > totalPagingPages ? totalPagingPages : inputNumber <= 0 ? activePageNumber : inputNumber;
-      setJumpPageNumber(jumpPageNumber);
-    }
-    else {
-      setJumpPageNumber(activePageNumber);
-    }
-  }, [totalPagingPages, activePageNumber, setJumpPageNumber]);
+  const jumpPageInputChangeHandler = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      const inputNumber = Number(e.target.value);
+      const isNan = Number.isNaN(inputNumber);
+
+      if (!isNan) {
+        // eslint-disable-next-line no-nested-ternary
+        const jumpPageNumber =
+          inputNumber > totalPagingPages
+            ? totalPagingPages
+            : inputNumber <= 0
+              ? activePageNumber
+              : inputNumber;
+        setJumpPageNumber(jumpPageNumber);
+      } else {
+        setJumpPageNumber(activePageNumber);
+      }
+    },
+    [totalPagingPages, activePageNumber],
+  );
 
 
-  const jumpPageInputKeyDownHandler = useCallback((e) => {
-    if (e.key === 'Enter') {
-      setActivePageNumber(jumpPageNumber);
-    }
-  }, [setActivePageNumber, jumpPageNumber]);
+  const jumpPageInputKeyDownHandler = useCallback(
+    (e) => {
+      if (e.key === 'Enter') {
+        setActivePageNumber(jumpPageNumber);
+      }
+    },
+    [jumpPageNumber],
+  );
 
 
   const jumpPageButtonPushedHandler = useCallback(() => {
   const jumpPageButtonPushedHandler = useCallback(() => {
     setActivePageNumber(jumpPageNumber);
     setActivePageNumber(jumpPageNumber);
   }, [jumpPageNumber]);
   }, [jumpPageNumber]);
 
 
-  // eslint-disable-next-line max-len
-  const activityCounter = `<b>${activityList.length === 0 ? 0 : offset + 1}</b> - <b>${(PAGING_LIMIT * activePageNumber) - (PAGING_LIMIT - activityList.length)}</b> of <b>${totalActivityNum}<b/>`;
+  const startIndex = activityList.length === 0 ? 0 : offset + 1;
+  const endIndex = activityList.length === 0 ? 0 : offset + activityList.length;
 
 
   if (!auditLogEnabled) {
   if (!auditLogEnabled) {
     return <AuditLogDisableMode />;
     return <AuditLogDisableMode />;
@@ -150,20 +195,36 @@ export const AuditLogManagement: FC = () => {
 
 
   return (
   return (
     <div data-testid="admin-auditlog">
     <div data-testid="admin-auditlog">
-      <button type="button" className="btn btn-outline-secondary mb-4" onClick={() => setIsSettingPage(!isSettingPage)}>
-        {
-          isSettingPage
-            ? <><span className="material-symbols-outlined">arrow_left_alt</span>{t('admin:audit_log_management.return')}</>
-            : <><span className="material-symbols-outlined">settings</span>{t('admin:audit_log_management.settings')}</>
-        }
+      <button
+        type="button"
+        className="btn btn-outline-secondary mb-4"
+        onClick={() => setIsSettingPage(!isSettingPage)}
+      >
+        {isSettingPage ? (
+          <>
+            <span className="material-symbols-outlined">arrow_left_alt</span>
+            {t('admin:audit_log_management.return')}
+          </>
+        ) : (
+          <>
+            <span className="material-symbols-outlined">settings</span>
+            {t('admin:audit_log_management.settings')}
+          </>
+        )}
       </button>
       </button>
 
 
       <h2 className="admin-setting-header mb-3">
       <h2 className="admin-setting-header mb-3">
         <span>
         <span>
-          {isSettingPage ? t('audit_log_management.audit_log_settings') : t('audit_log_management.audit_log')}
+          {isSettingPage
+            ? t('audit_log_management.audit_log_settings')
+            : t('audit_log_management.audit_log')}
         </span>
         </span>
-        { !isSettingPage && (
-          <button type="button" className="btn btn-sm ms-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+        {!isSettingPage && (
+          <button
+            type="button"
+            className="btn btn-sm ms-auto grw-btn-reload"
+            onClick={reloadButtonPushedHandler}
+          >
             <span className="material-symbols-outlined">refresh</span>
             <span className="material-symbols-outlined">refresh</span>
           </button>
           </button>
         )}
         )}
@@ -199,28 +260,28 @@ export const AuditLogManagement: FC = () => {
             </div>
             </div>
 
 
             <div className="col-12">
             <div className="col-12">
-              <button type="button" className="btn btn-link" onClick={clearButtonPushedHandler}>
+              <button
+                type="button"
+                className="btn btn-link"
+                onClick={clearButtonPushedHandler}
+              >
                 {t('admin:audit_log_management.clear')}
                 {t('admin:audit_log_management.clear')}
               </button>
               </button>
             </div>
             </div>
           </div>
           </div>
 
 
-          <p
-            className="ms-2"
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: activityCounter }}
-          />
-
-          { isLoading
-            ? (
-              <div className="text-muted text-center mb-5">
-                <LoadingSpinner className="me-1 fs-3" />
-              </div>
-            )
-            : (
-              <ActivityTable activityList={activityList} />
-            )
-          }
+          <p className="ms-2">
+            <strong>{startIndex}</strong> - <strong>{endIndex}</strong> of{' '}
+            <strong>{totalActivityNum}</strong>
+          </p>
+
+          {isLoading ? (
+            <div className="text-muted text-center mb-5">
+              <LoadingSpinner className="me-1 fs-3" />
+            </div>
+          ) : (
+            <ActivityTable activityList={activityList} />
+          )}
 
 
           <div className="d-flex flex-row justify-content-center">
           <div className="d-flex flex-row justify-content-center">
             <PaginationWrapper
             <PaginationWrapper
@@ -233,7 +294,12 @@ export const AuditLogManagement: FC = () => {
             />
             />
 
 
             <div className="admin-audit-log ms-3">
             <div className="admin-audit-log ms-3">
-              <label htmlFor="jumpPageInput" className="form-label me-1 text-secondary">Jump To Page</label>
+              <label
+                htmlFor="jumpPageInput"
+                className="form-label me-1 text-secondary"
+              >
+                Jump To Page
+              </label>
               <input
               <input
                 id="jumpPageInput"
                 id="jumpPageInput"
                 type="text"
                 type="text"
@@ -241,7 +307,11 @@ export const AuditLogManagement: FC = () => {
                 onChange={jumpPageInputChangeHandler}
                 onChange={jumpPageInputChangeHandler}
                 onKeyDown={jumpPageInputKeyDownHandler}
                 onKeyDown={jumpPageInputKeyDownHandler}
               />
               />
-              <button className="btn btn-sm" type="button" onClick={jumpPageButtonPushedHandler}>
+              <button
+                className="btn btn-sm"
+                type="button"
+                onClick={jumpPageButtonPushedHandler}
+              >
                 <b>Go</b>
                 <b>Go</b>
               </button>
               </button>
             </div>
             </div>

+ 2 - 6
apps/app/src/client/components/Admin/Common/Accordion.jsx

@@ -1,9 +1,7 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
-
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
-
 const Accordion = (props) => {
 const Accordion = (props) => {
   const [isOpen, setIsOpen] = useState(props.isOpenDefault);
   const [isOpen, setIsOpen] = useState(props.isOpenDefault);
   return (
   return (
@@ -14,15 +12,13 @@ const Accordion = (props) => {
           type="button"
           type="button"
           data-bs-toggle="collapse"
           data-bs-toggle="collapse"
           aria-expanded="true"
           aria-expanded="true"
-          onClick={() => setIsOpen(prevState => !prevState)}
+          onClick={() => setIsOpen((prevState) => !prevState)}
         >
         >
           {props.title}
           {props.title}
         </button>
         </button>
       </p>
       </p>
       <Collapse isOpen={isOpen}>
       <Collapse isOpen={isOpen}>
-        <div className="accordion-body">
-          {props.children}
-        </div>
+        <div className="accordion-body">{props.children}</div>
       </Collapse>
       </Collapse>
     </div>
     </div>
   );
   );

+ 11 - 5
apps/app/src/client/components/Admin/Common/AdminInstallButtonRow.tsx

@@ -1,16 +1,22 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
 
 
 type Props = {
 type Props = {
-  onClick: () => void,
-  disabled: boolean,
-
-}
+  onClick: () => void;
+  disabled: boolean;
+};
 
 
 export const AdminInstallButtonRow = (props: Props): JSX.Element => {
 export const AdminInstallButtonRow = (props: Props): JSX.Element => {
   return (
   return (
     <div className="row my-3">
     <div className="row my-3">
       <div className="mx-auto">
       <div className="mx-auto">
-        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>Install</button>
+        <button
+          type="button"
+          className="btn btn-primary"
+          onClick={props.onClick}
+          disabled={props.disabled}
+        >
+          Install
+        </button>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 5 - 6
apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -1,12 +1,11 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 type Props = {
 type Props = {
-  onClick?: () => void,
-  disabled?: boolean,
-  type?: 'button' | 'submit' | 'reset',
-}
+  onClick?: () => void;
+  disabled?: boolean;
+  type?: 'button' | 'submit' | 'reset';
+};
 
 
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
@@ -22,7 +21,7 @@ const AdminUpdateButtonRow = (props: Props): JSX.Element => {
           onClick={props.onClick}
           onClick={props.onClick}
           disabled={props.disabled ?? false}
           disabled={props.disabled ?? false}
         >
         >
-          { t('Update') }
+          {t('Update')}
         </button>
         </button>
       </div>
       </div>
     </div>
     </div>

+ 17 - 12
apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx

@@ -1,18 +1,15 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { Progress } from 'reactstrap';
 import { Progress } from 'reactstrap';
 
 
 type Props = {
 type Props = {
-  header: string,
-  currentCount: number,
-  totalCount: number,
-  isInProgress?: boolean,
-}
+  header: string;
+  currentCount: number;
+  totalCount: number;
+  isInProgress?: boolean;
+};
 
 
 const LabeledProgressBar = (props: Props): JSX.Element => {
 const LabeledProgressBar = (props: Props): JSX.Element => {
-  const {
-    header, currentCount, totalCount, isInProgress,
-  } = props;
+  const { header, currentCount, totalCount, isInProgress } = props;
 
 
   const progressingColor = isInProgress ? 'info' : 'success';
   const progressingColor = isInProgress ? 'info' : 'success';
 
 
@@ -20,14 +17,22 @@ const LabeledProgressBar = (props: Props): JSX.Element => {
     <>
     <>
       <h6 className="my-1">
       <h6 className="my-1">
         {header}
         {header}
-        <div className="float-end">{currentCount} / {totalCount}</div>
+        <div className="float-end">
+          {currentCount} / {totalCount}
+        </div>
       </h6>
       </h6>
       <Progress multi>
       <Progress multi>
-        <Progress bar max={totalCount} color={progressingColor} striped={isInProgress} animated={isInProgress} value={currentCount} />
+        <Progress
+          bar
+          max={totalCount}
+          color={progressingColor}
+          striped={isInProgress}
+          animated={isInProgress}
+          value={currentCount}
+        />
       </Progress>
       </Progress>
     </>
     </>
   );
   );
-
 };
 };
 
 
 export default LabeledProgressBar;
 export default LabeledProgressBar;

+ 8 - 10
apps/app/src/client/components/Admin/Customize/Customize.jsx

@@ -1,6 +1,4 @@
-
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -9,7 +7,6 @@ import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
@@ -26,11 +23,10 @@ const logger = loggerFactory('growi:services:AdminCustomizePage');
 function Customize(props) {
 function Customize(props) {
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
 
 
-  const fetchCustomizeSettingsData = useCallback(async() => {
+  const fetchCustomizeSettingsData = useCallback(async () => {
     try {
     try {
       await adminCustomizeContainer.retrieveCustomizeData();
       await adminCustomizeContainer.retrieveCustomizeData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
       logger.error(errs);
       logger.error(errs);
@@ -41,7 +37,6 @@ function Customize(props) {
     fetchCustomizeSettingsData();
     fetchCustomizeSettingsData();
   }, [fetchCustomizeSettingsData]);
   }, [fetchCustomizeSettingsData]);
 
 
-
   return (
   return (
     <div data-testid="admin-customize">
     <div data-testid="admin-customize">
       <div className="mb-5">
       <div className="mb-5">
@@ -78,10 +73,13 @@ function Customize(props) {
   );
   );
 }
 }
 
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [
+  AdminCustomizeContainer,
+]);
 
 
 Customize.propTypes = {
 Customize.propTypes = {
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
+  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer)
+    .isRequired,
 };
 };
 
 
 export default CustomizePageWithUnstatedContainer;
 export default CustomizePageWithUnstatedContainer;

+ 36 - 28
apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -1,29 +1,23 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 type Props = {
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 
 const CustomizeCssSetting = (props: Props): JSX.Element => {
 const CustomizeCssSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
 
   // Sync form with container state
   // Sync form with container state
   useEffect(() => {
   useEffect(() => {
@@ -32,28 +26,38 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
     });
     });
   }, [adminCustomizeContainer.state.currentCustomizeCss, reset]);
   }, [adminCustomizeContainer.state.currentCustomizeCss, reset]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      // Update container state before API call
-      await adminCustomizeContainer.changeCustomizeCss(data.customizeCss);
-      await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_css'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        // Update container state before API call
+        await adminCustomizeContainer.changeCustomizeCss(data.customizeCss);
+        await adminCustomizeContainer.updateCustomizeCss();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_css'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminCustomizeContainer],
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_css')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_css')}
+          </h2>
 
 
           <Card className="card custom-card bg-body-tertiary my-3">
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
-              { t('admin:customize_settings.write_css') }<br />
-              { t('admin:customize_settings.reflect_change') }
+              {t('admin:customize_settings.write_css')}
+              <br />
+              {t('admin:customize_settings.reflect_change')}
             </CardBody>
             </CardBody>
           </Card>
           </Card>
 
 
@@ -66,15 +70,19 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
               />
               />
             </div>
             </div>
 
 
-            <AdminUpdateButtonRow type="submit" disabled={adminCustomizeContainer.state.retrieveError != null} />
+            <AdminUpdateButtonRow
+              type="submit"
+              disabled={adminCustomizeContainer.state.retrieveError != null}
+            />
           </form>
           </form>
         </div>
         </div>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
   );
   );
-
 };
 };
 
 
-const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AdminCustomizeContainer]);
+const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [
+  AdminCustomizeContainer,
+]);
 
 
 export default CustomizeCssSettingWrapper;
 export default CustomizeCssSettingWrapper;

+ 7 - 11
apps/app/src/client/components/Admin/Customize/CustomizeFunctionOption.tsx

@@ -1,18 +1,15 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
 
 
 type Props = {
 type Props = {
-  optionId: string
-  label: string,
-  isChecked: boolean,
-  onChecked: () => void,
-  children: React.ReactNode,
-}
+  optionId: string;
+  label: string;
+  isChecked: boolean;
+  onChecked: () => void;
+  children: React.ReactNode;
+};
 
 
 const CustomizeFunctionOption = (props: Props): JSX.Element => {
 const CustomizeFunctionOption = (props: Props): JSX.Element => {
-
-  const {
-    optionId, label, isChecked, onChecked, children,
-  } = props;
+  const { optionId, label, isChecked, onChecked, children } = props;
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
@@ -31,7 +28,6 @@ const CustomizeFunctionOption = (props: Props): JSX.Element => {
       {children}
       {children}
     </React.Fragment>
     </React.Fragment>
   );
   );
-
 };
 };
 
 
 export default CustomizeFunctionOption;
 export default CustomizeFunctionOption;

+ 105 - 44
apps/app/src/client/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -1,33 +1,33 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 
 
 type Props = {
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 
 const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 const CustomizeFunctionSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const onClickSubmit = useCallback(async() => {
-
+  const onClickSubmit = useCallback(async () => {
     try {
     try {
       await adminCustomizeContainer.updateCustomizeFunction();
       await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.function'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.function'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, [t, adminCustomizeContainer]);
   }, [t, adminCustomizeContainer]);
@@ -36,24 +36,33 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.function')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.function')}
+          </h2>
           <Card className="card custom-card bg-body-tertiary my-3">
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
               {t('admin:customize_settings.function_desc')}
               {t('admin:customize_settings.function_desc')}
             </CardBody>
             </CardBody>
           </Card>
           </Card>
 
 
-
           <div className="row mt-4">
           <div className="row mt-4">
             <div className="offset-md-2 col-md-7 text-start">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
               <CustomizeFunctionOption
                 optionId="isEnabledAttachTitleHeader"
                 optionId="isEnabledAttachTitleHeader"
-                label={t('admin:customize_settings.function_options.attach_title_header')}
-                isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
-                onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
+                label={t(
+                  'admin:customize_settings.function_options.attach_title_header',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state.isEnabledAttachTitleHeader
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchEnabledAttachTitleHeader();
+                }}
               >
               >
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.attach_title_header_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.attach_title_header_desc',
+                  )}
                 </p>
                 </p>
               </CustomizeFunctionOption>
               </CustomizeFunctionOption>
             </div>
             </div>
@@ -61,43 +70,67 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
 
           <PagingSizeUncontrolledDropdown
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_s')}
             label={t('admin:customize_settings.function_options.list_num_s')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_s')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_s',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationS || 20}
             toggleLabel={adminCustomizeContainer.state.pageLimitationS || 20}
             dropdownItemSize={[10, 20, 50, 100]}
             dropdownItemSize={[10, 20, 50, 100]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationS}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationS
+            }
           />
           />
           <PagingSizeUncontrolledDropdown
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_m')}
             label={t('admin:customize_settings.function_options.list_num_m')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_m')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_m',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationM || 10}
             toggleLabel={adminCustomizeContainer.state.pageLimitationM || 10}
             dropdownItemSize={[5, 10, 20, 50, 100]}
             dropdownItemSize={[5, 10, 20, 50, 100]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationM}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationM
+            }
           />
           />
           <PagingSizeUncontrolledDropdown
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_l')}
             label={t('admin:customize_settings.function_options.list_num_l')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_l')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_l',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationL || 50}
             toggleLabel={adminCustomizeContainer.state.pageLimitationL || 50}
             dropdownItemSize={[20, 50, 100, 200]}
             dropdownItemSize={[20, 50, 100, 200]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationL}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationL
+            }
           />
           />
           <PagingSizeUncontrolledDropdown
           <PagingSizeUncontrolledDropdown
             label={t('admin:customize_settings.function_options.list_num_xl')}
             label={t('admin:customize_settings.function_options.list_num_xl')}
-            desc={t('admin:customize_settings.function_options.list_num_desc_xl')}
+            desc={t(
+              'admin:customize_settings.function_options.list_num_desc_xl',
+            )}
             toggleLabel={adminCustomizeContainer.state.pageLimitationXL || 20}
             toggleLabel={adminCustomizeContainer.state.pageLimitationXL || 20}
             dropdownItemSize={[5, 10, 20, 50, 100]}
             dropdownItemSize={[5, 10, 20, 50, 100]}
-            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationXL}
+            onChangeDropdownItem={
+              adminCustomizeContainer.switchPageListLimitationXL
+            }
           />
           />
 
 
           <div className="row">
           <div className="row">
             <div className="offset-md-2 col-md-7 text-start">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
               <CustomizeFunctionOption
                 optionId="isEnabledStaleNotification"
                 optionId="isEnabledStaleNotification"
-                label={t('admin:customize_settings.function_options.stale_notification')}
-                isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
-                onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
+                label={t(
+                  'admin:customize_settings.function_options.stale_notification',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state.isEnabledStaleNotification
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchEnableStaleNotification();
+                }}
               >
               >
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.stale_notification_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.stale_notification_desc',
+                  )}
                 </p>
                 </p>
               </CustomizeFunctionOption>
               </CustomizeFunctionOption>
             </div>
             </div>
@@ -107,12 +140,20 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-2 col-md-7 text-start">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
               <CustomizeFunctionOption
                 optionId="isAllReplyShown"
                 optionId="isAllReplyShown"
-                label={t('admin:customize_settings.function_options.show_all_reply_comments')}
-                isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
-                onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
+                label={t(
+                  'admin:customize_settings.function_options.show_all_reply_comments',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state.isAllReplyShown || false
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchIsAllReplyShown();
+                }}
               >
               >
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.show_all_reply_comments_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.show_all_reply_comments_desc',
+                  )}
                 </p>
                 </p>
               </CustomizeFunctionOption>
               </CustomizeFunctionOption>
             </div>
             </div>
@@ -122,12 +163,21 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-2 col-md-7 text-start">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
               <CustomizeFunctionOption
                 optionId="isSearchScopeChildrenAsDefault"
                 optionId="isSearchScopeChildrenAsDefault"
-                label={t('admin:customize_settings.function_options.select_search_scope_children_as_default')}
-                isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
-                onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
+                label={t(
+                  'admin:customize_settings.function_options.select_search_scope_children_as_default',
+                )}
+                isChecked={
+                  adminCustomizeContainer.state
+                    .isSearchScopeChildrenAsDefault || false
+                }
+                onChecked={() => {
+                  adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault();
+                }}
               >
               >
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.select_search_scope_children_as_default_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.select_search_scope_children_as_default_desc',
+                  )}
                 </p>
                 </p>
               </CustomizeFunctionOption>
               </CustomizeFunctionOption>
             </div>
             </div>
@@ -137,25 +187,36 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             <div className="offset-md-2 col-md-7 text-start">
             <div className="offset-md-2 col-md-7 text-start">
               <CustomizeFunctionOption
               <CustomizeFunctionOption
                 optionId="showPageSideAuthors"
                 optionId="showPageSideAuthors"
-                label={t('admin:customize_settings.function_options.show_page_side_authors')}
+                label={t(
+                  'admin:customize_settings.function_options.show_page_side_authors',
+                )}
                 isChecked={adminCustomizeContainer.state.showPageSideAuthors}
                 isChecked={adminCustomizeContainer.state.showPageSideAuthors}
-                onChecked={() => { adminCustomizeContainer.switchShowPageSideAuthors() }}
+                onChecked={() => {
+                  adminCustomizeContainer.switchShowPageSideAuthors();
+                }}
               >
               >
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.show_page_side_authors_desc')}
+                  {t(
+                    'admin:customize_settings.function_options.show_page_side_authors_desc',
+                  )}
                 </p>
                 </p>
               </CustomizeFunctionOption>
               </CustomizeFunctionOption>
             </div>
             </div>
           </div>
           </div>
 
 
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          <AdminUpdateButtonRow
+            onClick={onClickSubmit}
+            disabled={adminCustomizeContainer.state.retrieveError != null}
+          />
         </div>
         </div>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
   );
   );
-
 };
 };
 
 
-const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AdminCustomizeContainer]);
+const CustomizeFunctionSettingWrapper = withUnstatedContainers(
+  CustomizeFunctionSetting,
+  [AdminCustomizeContainer],
+);
 
 
 export default CustomizeFunctionSettingWrapper;
 export default CustomizeFunctionSettingWrapper;

+ 36 - 21
apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -1,16 +1,14 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useNextThemes } from '~/stores-universal/use-next-themes';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 
 
 const useIsContainerFluid = () => {
 const useIsContainerFluid = () => {
-  const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();
+  const { data: layoutSetting, update: updateLayoutSetting } =
+    useSWRxLayoutSetting();
   const [isContainerFluid, setIsContainerFluid] = useState<boolean>();
   const [isContainerFluid, setIsContainerFluid] = useState<boolean>();
 
 
   useEffect(() => {
   useEffect(() => {
@@ -29,15 +27,22 @@ const CustomizeLayoutSetting = (): JSX.Element => {
 
 
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
 
 
-  const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
+  const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } =
+    useIsContainerFluid();
 
 
-  const onClickSubmit = useCallback(async() => {
-    if (isContainerFluid == null) { return }
+  const onClickSubmit = useCallback(async () => {
+    if (isContainerFluid == null) {
+      return;
+    }
     try {
     try {
       await updateLayoutSetting({ isContainerFluid });
       await updateLayoutSetting({ isContainerFluid });
-      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('customize_settings.layout'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, [isContainerFluid, updateLayoutSetting, t]);
   }, [isContainerFluid, updateLayoutSetting, t]);
@@ -54,15 +59,18 @@ const CustomizeLayoutSetting = (): JSX.Element => {
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
+          <h2 className="admin-setting-header">
+            {t('customize_settings.layout')}
+          </h2>
 
 
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
             <div className="row row-cols-2">
             <div className="row row-cols-2">
               <div className="col">
               <div className="col">
-                <div
+                <button
+                  type="button"
                   className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
                   className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(false)}
                   onClick={() => setIsContainerFluid(false)}
-                  role="button"
+                  aria-pressed={!isContainerFluid}
                 >
                 >
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   <img
                   <img
@@ -73,13 +81,14 @@ const CustomizeLayoutSetting = (): JSX.Element => {
                   <div className="card-body text-center">
                   <div className="card-body text-center">
                     {t('customize_settings.layout_options.default')}
                     {t('customize_settings.layout_options.default')}
                   </div>
                   </div>
-                </div>
+                </button>
               </div>
               </div>
               <div className="col">
               <div className="col">
-                <div
+                <button
+                  type="button"
                   className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
                   className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(true)}
                   onClick={() => setIsContainerFluid(true)}
-                  role="button"
+                  aria-pressed={isContainerFluid}
                 >
                 >
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   <img
                   <img
@@ -90,14 +99,20 @@ const CustomizeLayoutSetting = (): JSX.Element => {
                   <div className="card-body text-center">
                   <div className="card-body text-center">
                     {t('customize_settings.layout_options.expanded')}
                     {t('customize_settings.layout_options.expanded')}
                   </div>
                   </div>
-                </div>
+                </button>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
             <div className="mx-auto">
             <div className="mx-auto">
-              <button type="button" className="btn btn-primary" onClick={onClickSubmit}>{ t('Update') }</button>
+              <button
+                type="button"
+                className="btn btn-primary"
+                onClick={onClickSubmit}
+              >
+                {t('Update')}
+              </button>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>

+ 100 - 46
apps/app/src/client/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -1,11 +1,12 @@
-import React, { useCallback, useState, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useState } from 'react';
 import { useAtomValue, useSetAtom } from 'jotai';
 import { useAtomValue, useSetAtom } from 'jotai';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import ImageCropModal from '~/client/components/Common/ImageCropModal';
 import ImageCropModal from '~/client/components/Common/ImageCropModal';
 import {
 import {
-  apiv3Delete, apiv3PostForm, apiv3Put,
+  apiv3Delete,
+  apiv3PostForm,
+  apiv3Put,
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useIsDefaultLogo } from '~/states/global';
 import { useIsDefaultLogo } from '~/states/global';
@@ -13,20 +14,23 @@ import { isCustomizedLogoUploadedAtom } from '~/states/server-configurations';
 
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
-
 const DEFAULT_LOGO = '/images/logo.svg';
 const DEFAULT_LOGO = '/images/logo.svg';
 const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 
 
 const CustomizeLogoSetting = (): JSX.Element => {
 const CustomizeLogoSetting = (): JSX.Element => {
-
   const { t } = useTranslation();
   const { t } = useTranslation();
   const isDefaultLogo = useIsDefaultLogo();
   const isDefaultLogo = useIsDefaultLogo();
   const isCustomizedLogoUploaded = useAtomValue(isCustomizedLogoUploadedAtom);
   const isCustomizedLogoUploaded = useAtomValue(isCustomizedLogoUploadedAtom);
   const setIsCustomizedLogoUploaded = useSetAtom(isCustomizedLogoUploadedAtom);
   const setIsCustomizedLogoUploaded = useSetAtom(isCustomizedLogoUploadedAtom);
 
 
-  const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
-  const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
-  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
+  const [uploadLogoSrc, setUploadLogoSrc] = useState<
+    ArrayBuffer | string | null
+  >(null);
+  const [isImageCropModalShow, setIsImageCropModalShow] =
+    useState<boolean>(false);
+  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(
+    isDefaultLogo ?? true,
+  );
   const [retrieveError, setRetrieveError] = useState<any>();
   const [retrieveError, setRetrieveError] = useState<any>();
 
 
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -40,10 +44,16 @@ const CustomizeLogoSetting = (): JSX.Element => {
 
 
   const onClickSubmit = useCallback(async () => {
   const onClickSubmit = useCallback(async () => {
     try {
     try {
-      await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
-    }
-    catch (err) {
+      await apiv3Put('/customize-setting/customize-logo', {
+        isDefaultLogo: isDefaultLogoSelected,
+      });
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.custom_logo'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, [t, isDefaultLogoSelected]);
   }, [t, isDefaultLogoSelected]);
@@ -52,37 +62,49 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       await apiv3Delete('/customize-setting/delete-brand-logo');
       setIsCustomizedLogoUploaded(false);
       setIsCustomizedLogoUploaded(false);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.current_logo'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
       setRetrieveError(err);
       setRetrieveError(err);
       throw new Error('Failed to delete logo');
       throw new Error('Failed to delete logo');
     }
     }
   }, [setIsCustomizedLogoUploaded, t]);
   }, [setIsCustomizedLogoUploaded, t]);
 
 
-
-  const processImageCompletedHandler = useCallback(async (croppedImage) => {
-    try {
-      const formData = new FormData();
-      formData.append('file', croppedImage);
-      await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
-      setIsCustomizedLogoUploaded(true);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      setRetrieveError(err);
-      throw new Error('Failed to upload brand logo');
-    }
-  }, [setIsCustomizedLogoUploaded, t]);
+  const processImageCompletedHandler = useCallback(
+    async (croppedImage) => {
+      try {
+        const formData = new FormData();
+        formData.append('file', croppedImage);
+        await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+        setIsCustomizedLogoUploaded(true);
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.current_logo'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        setRetrieveError(err);
+        throw new Error('Failed to upload brand logo');
+      }
+    },
+    [setIsCustomizedLogoUploaded, t],
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
           <div className="mb-5">
           <div className="mb-5">
-            <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_settings.custom_logo')}</h2>
+            <h2 className="border-bottom my-4 admin-setting-header">
+              {t('admin:customize_settings.custom_logo')}
+            </h2>
             <div className="row">
             <div className="row">
               <div className="col-md-6 col-12 mb-3 mb-md-0">
               <div className="col-md-6 col-12 mb-3 mb-md-0">
                 <h4>
                 <h4>
@@ -94,14 +116,23 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
                       checked={isDefaultLogoSelected}
                       checked={isDefaultLogoSelected}
-                      onChange={() => { setIsDefaultLogoSelected(true) }}
+                      onChange={() => {
+                        setIsDefaultLogoSelected(true);
+                      }}
                     />
                     />
-                    <label className="form-check-label" htmlFor="radioDefaultLogo">
+                    <label
+                      className="form-check-label"
+                      htmlFor="radioDefaultLogo"
+                    >
                       {t('admin:customize_settings.default_logo')}
                       {t('admin:customize_settings.default_logo')}
                     </label>
                     </label>
                   </div>
                   </div>
                 </h4>
                 </h4>
-                <img src={DEFAULT_LOGO} width="64" />
+                <img
+                  src={DEFAULT_LOGO}
+                  width="64"
+                  alt={t('admin:customize_settings.default_logo')}
+                />
               </div>
               </div>
               <div className="col-md-6 col-12">
               <div className="col-md-6 col-12">
                 <h4>
                 <h4>
@@ -113,24 +144,38 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
                       checked={!isDefaultLogoSelected}
                       checked={!isDefaultLogoSelected}
-                      onChange={() => { setIsDefaultLogoSelected(false) }}
+                      onChange={() => {
+                        setIsDefaultLogoSelected(false);
+                      }}
                     />
                     />
-                    <label className="form-check-label" htmlFor="radioUploadLogo">
+                    <label
+                      className="form-check-label"
+                      htmlFor="radioUploadLogo"
+                    >
                       {t('admin:customize_settings.upload_logo')}
                       {t('admin:customize_settings.upload_logo')}
                     </label>
                     </label>
                   </div>
                   </div>
                 </h4>
                 </h4>
                 <div className="row mb-3">
                 <div className="row mb-3">
-                  <label className="col-sm-4 col-12 col-form-label text-start">
+                  <span className="col-sm-4 col-12 col-form-label text-start">
                     {t('admin:customize_settings.current_logo')}
                     {t('admin:customize_settings.current_logo')}
-                  </label>
+                  </span>
                   <div className="col-sm-8 col-12">
                   <div className="col-sm-8 col-12">
                     {isCustomizedLogoUploaded && (
                     {isCustomizedLogoUploaded && (
                       <>
                       <>
                         <p>
                         <p>
-                          <img src={CUSTOMIZED_LOGO} id="settingBrandLogo" width="64" />
+                          <img
+                            src={CUSTOMIZED_LOGO}
+                            id="settingBrandLogo"
+                            width="64"
+                            alt={t('admin:customize_settings.current_logo')}
+                          />
                         </p>
                         </p>
-                        <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                        <button
+                          type="button"
+                          className="btn btn-danger"
+                          onClick={onClickDeleteBtn}
+                        >
                           {t('admin:customize_settings.delete_logo')}
                           {t('admin:customize_settings.delete_logo')}
                         </button>
                         </button>
                       </>
                       </>
@@ -138,16 +183,28 @@ const CustomizeLogoSetting = (): JSX.Element => {
                   </div>
                   </div>
                 </div>
                 </div>
                 <div className="row">
                 <div className="row">
-                  <label className="col-sm-4 col-12 col-form-label text-start">
+                  <label
+                    className="col-sm-4 col-12 col-form-label text-start"
+                    htmlFor="uploadLogoInput"
+                  >
                     {t('admin:customize_settings.upload_new_logo')}
                     {t('admin:customize_settings.upload_new_logo')}
                   </label>
                   </label>
                   <div className="col-sm-8 col-12">
                   <div className="col-sm-8 col-12">
-                    <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />
+                    <input
+                      id="uploadLogoInput"
+                      type="file"
+                      onChange={onSelectFile}
+                      name="brandLogo"
+                      accept="image/*"
+                    />
                   </div>
                   </div>
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
-            <AdminUpdateButtonRow onClick={onClickSubmit} disabled={retrieveError != null} />
+            <AdminUpdateButtonRow
+              onClick={onClickSubmit}
+              disabled={retrieveError != null}
+            />
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -162,9 +219,6 @@ const CustomizeLogoSetting = (): JSX.Element => {
       />
       />
     </React.Fragment>
     </React.Fragment>
   );
   );
-
-
 };
 };
 
 
-
 export default CustomizeLogoSetting;
 export default CustomizeLogoSetting;

+ 54 - 38
apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -1,5 +1,4 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
@@ -7,56 +6,66 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 type Props = {
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 
 const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
 const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
 
   // Sync form with container state
   // Sync form with container state
   useEffect(() => {
   useEffect(() => {
     reset({
     reset({
-      customizeNoscript: adminCustomizeContainer.state.currentCustomizeNoscript || '',
+      customizeNoscript:
+        adminCustomizeContainer.state.currentCustomizeNoscript || '',
     });
     });
   }, [adminCustomizeContainer.state.currentCustomizeNoscript, reset]);
   }, [adminCustomizeContainer.state.currentCustomizeNoscript, reset]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      // Update container state before API call
-      await adminCustomizeContainer.changeCustomizeNoscript(data.customizeNoscript);
-      await adminCustomizeContainer.updateCustomizeNoscript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_noscript'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        // Update container state before API call
+        await adminCustomizeContainer.changeCustomizeNoscript(
+          data.customizeNoscript,
+        );
+        await adminCustomizeContainer.updateCustomizeNoscript();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_noscript'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminCustomizeContainer],
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_noscript')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_noscript')}
+          </h2>
 
 
           <Card className="card custom-card bg-body-tertiary my-3">
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
               <span
               <span
                 // eslint-disable-next-line react/no-danger
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_noscript_detail') }}
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('admin:customize_settings.custom_noscript_detail'),
+                }}
               />
               />
             </CardBody>
             </CardBody>
           </Card>
           </Card>
@@ -70,22 +79,24 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
               />
               />
             </div>
             </div>
 
 
-            <a
-              className="text-muted"
+            <button
+              type="button"
+              className="btn btn-link text-muted p-0"
               data-bs-toggle="collapse"
               data-bs-toggle="collapse"
-              href="#collapseExampleHtml"
-              role="button"
+              data-bs-target="#collapseExampleHtml"
               aria-expanded="false"
               aria-expanded="false"
               aria-controls="collapseExampleHtml"
               aria-controls="collapseExampleHtml"
             >
             >
-              <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
+              <span
+                className="material-symbols-outlined me-1"
+                aria-hidden="true"
+              >
+                navigate_next
+              </span>
               Example for Google Tag Manager
               Example for Google Tag Manager
-            </a>
+            </button>
             <div className="collapse" id="collapseExampleHtml">
             <div className="collapse" id="collapseExampleHtml">
-              <PrismAsyncLight
-                style={oneDark}
-                language="javascript"
-              >
+              <PrismAsyncLight style={oneDark} language="javascript">
                 {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
                 {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
   height="0"
   height="0"
   width="0"
   width="0"
@@ -93,15 +104,20 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
               </PrismAsyncLight>
               </PrismAsyncLight>
             </div>
             </div>
 
 
-            <AdminUpdateButtonRow type="submit" disabled={adminCustomizeContainer.state.retrieveError != null} />
+            <AdminUpdateButtonRow
+              type="submit"
+              disabled={adminCustomizeContainer.state.retrieveError != null}
+            />
           </form>
           </form>
         </div>
         </div>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
   );
   );
-
 };
 };
 
 
-const CustomizeNoscriptSettingWrapper = withUnstatedContainers(CustomizeNoscriptSetting, [AdminCustomizeContainer]);
+const CustomizeNoscriptSettingWrapper = withUnstatedContainers(
+  CustomizeNoscriptSetting,
+  [AdminCustomizeContainer],
+);
 
 
 export default CustomizeNoscriptSettingWrapper;
 export default CustomizeNoscriptSettingWrapper;

+ 36 - 18
apps/app/src/client/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -1,69 +1,87 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import CustomizePresentationOption from './CustomizeFunctionOption';
 import CustomizePresentationOption from './CustomizeFunctionOption';
 
 
 type Props = {
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 
 const CustomizePresentationSetting = (props: Props): JSX.Element => {
 const CustomizePresentationSetting = (props: Props): JSX.Element => {
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const onClickSubmit = useCallback(async() => {
+  const onClickSubmit = useCallback(async () => {
     try {
     try {
       await adminCustomizeContainer.updateCustomizePresentation();
       await adminCustomizeContainer.updateCustomizePresentation();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.presentation'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.presentation'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, [adminCustomizeContainer, t]);
   }, [adminCustomizeContainer, t]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-      <h2 className="admin-setting-header">{t('admin:customize_settings.custom_presentation')}</h2>
+      <h2 className="admin-setting-header">
+        {t('admin:customize_settings.custom_presentation')}
+      </h2>
       <div className="form-group row">
       <div className="form-group row">
         <div className="offset-md-3 col-md-6 text-left">
         <div className="offset-md-3 col-md-6 text-left">
           <CustomizePresentationOption
           <CustomizePresentationOption
             optionId="isEnabledMarp"
             optionId="isEnabledMarp"
-            label={t('admin:customize_settings.presentation_options.enable_marp')}
+            label={t(
+              'admin:customize_settings.presentation_options.enable_marp',
+            )}
             isChecked={adminCustomizeContainer?.state.isEnabledMarp || false}
             isChecked={adminCustomizeContainer?.state.isEnabledMarp || false}
-            onChecked={() => { adminCustomizeContainer.switchIsEnabledMarp() }}
+            onChecked={() => {
+              adminCustomizeContainer.switchIsEnabledMarp();
+            }}
           >
           >
             <p className="form-text text-muted">
             <p className="form-text text-muted">
-              {t('admin:customize_settings.presentation_options.enable_marp_desc')}
+              {t(
+                'admin:customize_settings.presentation_options.enable_marp_desc',
+              )}
               <br></br>
               <br></br>
               <a
               <a
                 href={`${t('admin:customize_settings.presentation_options.marp_official_site_link')}`}
                 href={`${t('admin:customize_settings.presentation_options.marp_official_site_link')}`}
                 target="_blank"
                 target="_blank"
                 rel="noopener noreferrer"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presentation_options.marp_official_site')}`}
+              >
+                {`${t('admin:customize_settings.presentation_options.marp_official_site')}`}
               </a>
               </a>
               <br></br>
               <br></br>
               <a
               <a
                 href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 target="_blank"
                 target="_blank"
                 rel="noopener noreferrer"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
+              >
+                {`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
               </a>
               </a>
             </p>
             </p>
           </CustomizePresentationOption>
           </CustomizePresentationOption>
         </div>
         </div>
       </div>
       </div>
 
 
-      <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+      <AdminUpdateButtonRow
+        onClick={onClickSubmit}
+        disabled={adminCustomizeContainer.state.retrieveError != null}
+      />
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };
-const CustomizePresentationSettingWrapper = withUnstatedContainers(CustomizePresentationSetting, [AdminCustomizeContainer]);
+const CustomizePresentationSettingWrapper = withUnstatedContainers(
+  CustomizePresentationSetting,
+  [AdminCustomizeContainer],
+);
 
 
 export default CustomizePresentationSettingWrapper;
 export default CustomizePresentationSettingWrapper;

+ 52 - 38
apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -1,5 +1,4 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
+import React, { type JSX, useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
@@ -7,53 +6,61 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 type Props = {
 type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
+  adminCustomizeContainer: AdminCustomizeContainer;
+};
 
 
 const CustomizeScriptSetting = (props: Props): JSX.Element => {
 const CustomizeScriptSetting = (props: Props): JSX.Element => {
-
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
 
   // Sync form with container state
   // Sync form with container state
   useEffect(() => {
   useEffect(() => {
     reset({
     reset({
-      customizeScript: adminCustomizeContainer.state.currentCustomizeScript || '',
+      customizeScript:
+        adminCustomizeContainer.state.currentCustomizeScript || '',
     });
     });
   }, [adminCustomizeContainer.state.currentCustomizeScript, reset]);
   }, [adminCustomizeContainer.state.currentCustomizeScript, reset]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      // Update container state before API call
-      await adminCustomizeContainer.changeCustomizeScript(data.customizeScript);
-      await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_script'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        // Update container state before API call
+        await adminCustomizeContainer.changeCustomizeScript(
+          data.customizeScript,
+        );
+        await adminCustomizeContainer.updateCustomizeScript();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_script'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminCustomizeContainer],
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_script')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_script')}
+          </h2>
           <Card className="card custom-card bg-body-tertiary mb-3">
           <Card className="card custom-card bg-body-tertiary mb-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
-              {t('admin:customize_settings.write_java')}<br />
+              {t('admin:customize_settings.write_java')}
+              <br />
               {t('admin:customize_settings.reflect_change')}
               {t('admin:customize_settings.reflect_change')}
             </CardBody>
             </CardBody>
           </Card>
           </Card>
@@ -67,22 +74,24 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
               />
               />
             </div>
             </div>
 
 
-            <a
-              className="text-muted"
+            <button
+              type="button"
+              className="btn btn-link text-muted p-0"
               data-bs-toggle="collapse"
               data-bs-toggle="collapse"
-              href="#collapseExampleScript"
-              role="button"
+              data-bs-target="#collapseExampleScript"
               aria-expanded="false"
               aria-expanded="false"
               aria-controls="collapseExampleScript"
               aria-controls="collapseExampleScript"
             >
             >
-              <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
+              <span
+                className="material-symbols-outlined me-1"
+                aria-hidden="true"
+              >
+                navigate_next
+              </span>
               Example for Google Tag Manager
               Example for Google Tag Manager
-            </a>
+            </button>
             <div className="collapse" id="collapseExampleScript">
             <div className="collapse" id="collapseExampleScript">
-              <PrismAsyncLight
-                style={oneDark}
-                language="javascript"
-              >
+              <PrismAsyncLight style={oneDark} language="javascript">
                 {`(function(w,d,s,l,i){
                 {`(function(w,d,s,l,i){
 w[l]=w[l]||[];
 w[l]=w[l]||[];
 w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
 w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
@@ -95,15 +104,20 @@ j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefo
               </PrismAsyncLight>
               </PrismAsyncLight>
             </div>
             </div>
 
 
-            <AdminUpdateButtonRow type="submit" disabled={adminCustomizeContainer.state.retrieveError != null} />
+            <AdminUpdateButtonRow
+              type="submit"
+              disabled={adminCustomizeContainer.state.retrieveError != null}
+            />
           </form>
           </form>
         </div>
         </div>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
   );
   );
-
 };
 };
 
 
-const CustomizeScriptSettingWrapper = withUnstatedContainers(CustomizeScriptSetting, [AdminCustomizeContainer]);
+const CustomizeScriptSettingWrapper = withUnstatedContainers(
+  CustomizeScriptSetting,
+  [AdminCustomizeContainer],
+);
 
 
 export default CustomizeScriptSettingWrapper;
 export default CustomizeScriptSettingWrapper;

+ 32 - 27
apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -1,30 +1,31 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useNextThemes } from '~/stores-universal/use-next-themes';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSWRxSidebarConfig } from '~/stores/admin/sidebar-config';
 import { useSWRxSidebarConfig } from '~/stores/admin/sidebar-config';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 
 
 const CustomizeSidebarsetting = (): JSX.Element => {
 const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
 
 
-  const {
-    data, update, setIsSidebarCollapsedMode,
-  } = useSWRxSidebarConfig();
+  const { data, update, setIsSidebarCollapsedMode } = useSWRxSidebarConfig();
 
 
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
   const collapsedIconFileName = `/images/customize-settings/collapsed-${resolvedTheme}.svg`;
   const collapsedIconFileName = `/images/customize-settings/collapsed-${resolvedTheme}.svg`;
   const dockIconFileName = `/images/customize-settings/dock-${resolvedTheme}.svg`;
   const dockIconFileName = `/images/customize-settings/dock-${resolvedTheme}.svg`;
 
 
-  const onClickSubmit = useCallback(async() => {
+  const onClickSubmit = useCallback(async () => {
     try {
     try {
       await update();
       await update();
-      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.default_sidebar_mode.title'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('customize_settings.default_sidebar_mode.title'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, [t, update]);
   }, [t, update]);
@@ -39,8 +40,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-
-          <h2 className="admin-setting-header">{t('customize_settings.default_sidebar_mode.title')}</h2>
+          <h2 className="admin-setting-header">
+            {t('customize_settings.default_sidebar_mode.title')}
+          </h2>
 
 
           <Card className="card custom-card bg-body-tertiary my-3">
           <Card className="card custom-card bg-body-tertiary my-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
@@ -51,40 +53,43 @@ const CustomizeSidebarsetting = (): JSX.Element => {
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
             <div className="row row-cols-2">
             <div className="row row-cols-2">
               <div className="col">
               <div className="col">
-                <div
+                <button
+                  type="button"
                   className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(true)}
                   onClick={() => setIsSidebarCollapsedMode(true)}
-                  role="button"
+                  aria-pressed={isSidebarCollapsedMode}
                 >
                 >
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   <img src={collapsedIconFileName} alt="Collapsed Mode" />
                   <img src={collapsedIconFileName} alt="Collapsed Mode" />
-                  <div className="card-body text-center">
-                    Collapsed Mode
-                  </div>
-                </div>
+                  <div className="card-body text-center">Collapsed Mode</div>
+                </button>
               </div>
               </div>
               <div className="col">
               <div className="col">
-                <div
+                <button
+                  type="button"
                   className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(false)}
                   onClick={() => setIsSidebarCollapsedMode(false)}
-                  role="button"
+                  aria-pressed={!isSidebarCollapsedMode}
                 >
                 >
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   {/* eslint-disable-next-line @next/next/no-img-element */}
                   <img src={dockIconFileName} alt="Dock Mode" />
                   <img src={dockIconFileName} alt="Dock Mode" />
-                  <div className="card-body  text-center">
-                    Dock Mode
-                  </div>
-                </div>
+                  <div className="card-body  text-center">Dock Mode</div>
+                </button>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
             <div className="mx-auto">
             <div className="mx-auto">
-              <button type="button" onClick={onClickSubmit} className="btn btn-primary">{ t('Update') }</button>
+              <button
+                type="button"
+                onClick={onClickSubmit}
+                className="btn btn-primary"
+              >
+                {t('Update')}
+              </button>
             </div>
             </div>
           </div>
           </div>
-
         </div>
         </div>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>

+ 19 - 15
apps/app/src/client/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -1,15 +1,13 @@
-import React, { useMemo, type JSX } from 'react';
-
+import React, { type JSX, useMemo } from 'react';
 import { type GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import { type GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { ThemeColorBox } from './ThemeColorBox';
 import { ThemeColorBox } from './ThemeColorBox';
 
 
-
 type Props = {
 type Props = {
-  availableThemes: GrowiThemeMetadata[],
-  selectedTheme?: string,
-  onSelected?: (themeName: string) => void,
+  availableThemes: GrowiThemeMetadata[];
+  selectedTheme?: string;
+  onSelected?: (themeName: string) => void;
 };
 };
 
 
 const CustomizeThemeOptions = (props: Props): JSX.Element => {
 const CustomizeThemeOptions = (props: Props): JSX.Element => {
@@ -18,24 +16,31 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
   const { availableThemes, selectedTheme, onSelected } = props;
   const { availableThemes, selectedTheme, onSelected } = props;
 
 
   const lightNDarkThemes = useMemo(() => {
   const lightNDarkThemes = useMemo(() => {
-    return availableThemes.filter(s => s.schemeType === GrowiThemeSchemeType.BOTH);
+    return availableThemes.filter(
+      (s) => s.schemeType === GrowiThemeSchemeType.BOTH,
+    );
   }, [availableThemes]);
   }, [availableThemes]);
   const oneModeThemes = useMemo(() => {
   const oneModeThemes = useMemo(() => {
-    return availableThemes.filter(s => s.schemeType !== GrowiThemeSchemeType.BOTH);
+    return availableThemes.filter(
+      (s) => s.schemeType !== GrowiThemeSchemeType.BOTH,
+    );
   }, [availableThemes]);
   }, [availableThemes]);
 
 
   return (
   return (
     <>
     <>
-
       {/* Light and Dark Themes */}
       {/* Light and Dark Themes */}
       <div>
       <div>
-        <h3 className="mb-3">{t('customize_settings.theme_desc.light_and_dark')}</h3>
+        <h3 className="mb-3">
+          {t('customize_settings.theme_desc.light_and_dark')}
+        </h3>
         <div className="hstack gap-3 flex-wrap">
         <div className="hstack gap-3 flex-wrap">
           {lightNDarkThemes.map((theme) => {
           {lightNDarkThemes.map((theme) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
                 key={theme.name}
                 key={theme.name}
-                isSelected={selectedTheme != null && selectedTheme === theme.name}
+                isSelected={
+                  selectedTheme != null && selectedTheme === theme.name
+                }
                 metadata={theme}
                 metadata={theme}
                 onSelected={() => onSelected?.(theme.name)}
                 onSelected={() => onSelected?.(theme.name)}
               />
               />
@@ -52,7 +57,9 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
                 key={theme.name}
                 key={theme.name}
-                isSelected={selectedTheme != null && selectedTheme === theme.name}
+                isSelected={
+                  selectedTheme != null && selectedTheme === theme.name
+                }
                 metadata={theme}
                 metadata={theme}
                 onSelected={() => onSelected?.(theme.name)}
                 onSelected={() => onSelected?.(theme.name)}
               />
               />
@@ -60,11 +67,8 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
           })}
         </div>
         </div>
       </div>
       </div>
-
     </>
     </>
   );
   );
-
 };
 };
 
 
-
 export default CustomizeThemeOptions;
 export default CustomizeThemeOptions;

+ 25 - 19
apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -1,21 +1,15 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
+import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { useSWRxGrowiThemeSetting } from '~/stores/admin/customize';
 import { useSWRxGrowiThemeSetting } from '~/stores/admin/customize';
 
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 
 
-
 // eslint-disable-next-line @typescript-eslint/ban-types
 // eslint-disable-next-line @typescript-eslint/ban-types
-type Props = {
-}
+type Props = {};
 
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
@@ -32,7 +26,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
     setCurrentTheme(themeName);
     setCurrentTheme(themeName);
   }, []);
   }, []);
 
 
-  const submitHandler = useCallback(async() => {
+  const submitHandler = useCallback(async () => {
     if (currentTheme == null) {
     if (currentTheme == null) {
       toastWarning('The selected theme is undefined');
       toastWarning('The selected theme is undefined');
       return;
       return;
@@ -43,29 +37,41 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
         theme: currentTheme,
         theme: currentTheme,
       });
       });
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('admin:customize_settings.theme'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, [currentTheme, t, update]);
   }, [currentTheme, t, update]);
 
 
-  const availableThemes = data?.pluginThemesMetadatas == null
-    ? PresetThemesMetadatas
-    : PresetThemesMetadatas.concat(data.pluginThemesMetadatas);
+  const availableThemes =
+    data?.pluginThemesMetadatas == null
+      ? PresetThemesMetadatas
+      : PresetThemesMetadatas.concat(data.pluginThemesMetadatas);
 
 
-  const selectedTheme = availableThemes.find(t => t.name === currentTheme)?.name ?? PresetThemes.DEFAULT;
+  const selectedTheme =
+    availableThemes.find((t) => t.name === currentTheme)?.name ??
+    PresetThemes.DEFAULT;
 
 
   return (
   return (
     <div className="row">
     <div className="row">
       <div className="col-12">
       <div className="col-12">
-        <h2 className="admin-setting-header">{t('admin:customize_settings.theme')}</h2>
+        <h2 className="admin-setting-header">
+          {t('admin:customize_settings.theme')}
+        </h2>
         <CustomizeThemeOptions
         <CustomizeThemeOptions
           onSelected={selectedHandler}
           onSelected={selectedHandler}
           availableThemes={availableThemes}
           availableThemes={availableThemes}
           selectedTheme={selectedTheme}
           selectedTheme={selectedTheme}
         />
         />
-        <AdminUpdateButtonRow onClick={submitHandler} disabled={error != null} />
+        <AdminUpdateButtonRow
+          onClick={submitHandler}
+          disabled={error != null}
+        />
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 62 - 30
apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx

@@ -1,27 +1,21 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, { useCallback, useEffect } from 'react';
 import React, { useCallback, useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useCustomTitleTemplate } from '~/states/global';
 import { useCustomTitleTemplate } from '~/states/global';
 
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 export const CustomizeTitle: FC = () => {
 export const CustomizeTitle: FC = () => {
-
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
   const customTitleTemplate = useCustomTitleTemplate();
   const customTitleTemplate = useCustomTitleTemplate();
 
 
-  const {
-    register,
-    handleSubmit,
-    reset,
-  } = useForm();
+  const { register, handleSubmit, reset } = useForm();
 
 
   // Sync form with store data
   // Sync form with store data
   useEffect(() => {
   useEffect(() => {
@@ -30,39 +24,74 @@ export const CustomizeTitle: FC = () => {
     });
     });
   }, [customTitleTemplate, reset]);
   }, [customTitleTemplate, reset]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await apiv3Put('/customize-setting/customize-title', {
-        customizeTitle: data.customizeTitle,
-      });
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await apiv3Put('/customize-setting/customize-title', {
+          customizeTitle: data.customizeTitle,
+        });
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('admin:customize_settings.custom_title'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t],
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_title')}</h2>
+          <h2 className="admin-setting-header">
+            {t('admin:customize_settings.custom_title')}
+          </h2>
         </div>
         </div>
 
 
         <div className="col-12">
         <div className="col-12">
           <Card className="card custom-card bg-body-tertiary mb-3">
           <Card className="card custom-card bg-body-tertiary mb-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
               {/* eslint-disable react/no-danger */}
               {/* eslint-disable react/no-danger */}
-              <p dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail') }} />
+              <p
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('admin:customize_settings.custom_title_detail'),
+                }}
+              />
               <ul>
               <ul>
                 <li>
                 <li>
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder1') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'admin:customize_settings.custom_title_detail_placeholder1',
+                      ),
+                    }}
+                  />
                 </li>
                 </li>
                 <li>
                 <li>
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder2') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'admin:customize_settings.custom_title_detail_placeholder2',
+                      ),
+                    }}
+                  />
                 </li>
                 </li>
                 <li>
                 <li>
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_title_detail_placeholder3') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'admin:customize_settings.custom_title_detail_placeholder3',
+                      ),
+                    }}
+                  />
                 </li>
                 </li>
               </ul>
               </ul>
               {/* eslint-enable react/no-danger */}
               {/* eslint-enable react/no-danger */}
@@ -72,16 +101,19 @@ export const CustomizeTitle: FC = () => {
 
 
         {/* TODO i18n */}
         {/* TODO i18n */}
         <div className="form-text text-muted col-12 mb-3">
         <div className="form-text text-muted col-12 mb-3">
-          Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+          Default Value:{' '}
+          <code>
+            &#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;
+          </code>
           <br />
           <br />
-          Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>
+          Default Output Example:{' '}
+          <code className="xml">
+            &lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;
+          </code>
         </div>
         </div>
         <form onSubmit={handleSubmit(onSubmit)}>
         <form onSubmit={handleSubmit(onSubmit)}>
           <div className="col-12">
           <div className="col-12">
-            <input
-              className="form-control"
-              {...register('customizeTitle')}
-            />
+            <input className="form-control" {...register('customizeTitle')} />
           </div>
           </div>
           <div className="col-12">
           <div className="col-12">
             <AdminUpdateButtonRow type="submit" disabled={false} />
             <AdminUpdateButtonRow type="submit" disabled={false} />

+ 12 - 11
apps/app/src/client/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx

@@ -1,13 +1,13 @@
 import React from 'react';
 import React from 'react';
-
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import {
 import {
-  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  UncontrolledDropdown,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-
 const PagingSizeUncontrolledDropdown = (props) => {
 const PagingSizeUncontrolledDropdown = (props) => {
-
   function dropdownItemOnClickHandler(num) {
   function dropdownItemOnClickHandler(num) {
     if (props.onChangeDropdownItem === null) {
     if (props.onChangeDropdownItem === null) {
       return;
       return;
@@ -20,7 +20,7 @@ const PagingSizeUncontrolledDropdown = (props) => {
       <div className="row">
       <div className="row">
         <div className="offset-md-2 col-md-7 text-start">
         <div className="offset-md-2 col-md-7 text-start">
           <div className="my-0 w-100">
           <div className="my-0 w-100">
-            <label className="form-label">{props.label}</label>
+            <span className="form-label">{props.label}</span>
           </div>
           </div>
           <UncontrolledDropdown>
           <UncontrolledDropdown>
             <DropdownToggle className="text-end col-6" caret>
             <DropdownToggle className="text-end col-6" caret>
@@ -29,23 +29,24 @@ const PagingSizeUncontrolledDropdown = (props) => {
             <DropdownMenu className="dropdown-menu" role="menu">
             <DropdownMenu className="dropdown-menu" role="menu">
               {props.dropdownItemSize.map((num) => {
               {props.dropdownItemSize.map((num) => {
                 return (
                 return (
-                  <DropdownItem key={num} role="presentation" onClick={() => dropdownItemOnClickHandler(num)}>
-                    <a role="menuitem">{num}</a>
+                  <DropdownItem
+                    key={num}
+                    role="presentation"
+                    onClick={() => dropdownItemOnClickHandler(num)}
+                  >
+                    {num}
                   </DropdownItem>
                   </DropdownItem>
                 );
                 );
               })}
               })}
             </DropdownMenu>
             </DropdownMenu>
           </UncontrolledDropdown>
           </UncontrolledDropdown>
-          <p className="form-text text-muted">
-            {props.desc}
-          </p>
+          <p className="form-text text-muted">{props.desc}</p>
         </div>
         </div>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };
 
 
-
 PagingSizeUncontrolledDropdown.propTypes = {
 PagingSizeUncontrolledDropdown.propTypes = {
   label: PropTypes.string,
   label: PropTypes.string,
   toggleLabel: PropTypes.number,
   toggleLabel: PropTypes.number,

+ 52 - 24
apps/app/src/client/components/Admin/Customize/ThemeColorBox.tsx

@@ -1,42 +1,52 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import type { GrowiThemeMetadata } from '@growi/core';
 import type { GrowiThemeMetadata } from '@growi/core';
 
 
 import styles from './ThemeColorBox.module.scss';
 import styles from './ThemeColorBox.module.scss';
 
 
 const themeOptionClass = styles['theme-option-container'];
 const themeOptionClass = styles['theme-option-container'];
 
 
-
 type Props = {
 type Props = {
-  isSelected: boolean,
-  metadata: GrowiThemeMetadata,
-  onSelected?: () => void,
+  isSelected: boolean;
+  metadata: GrowiThemeMetadata;
+  onSelected?: () => void;
 };
 };
 
 
 export const ThemeColorBox = (props: Props): JSX.Element => {
 export const ThemeColorBox = (props: Props): JSX.Element => {
-
+  const { isSelected, metadata, onSelected } = props;
   const {
   const {
-    isSelected, metadata, onSelected,
-  } = props;
-  const {
-    name, lightBg, darkBg, lightSidebar, darkSidebar, lightIcon, darkIcon, createBtn, isPresetTheme,
+    name,
+    lightBg,
+    darkBg,
+    lightSidebar,
+    darkSidebar,
+    lightIcon,
+    darkIcon,
+    createBtn,
+    isPresetTheme,
   } = metadata;
   } = metadata;
 
 
   return (
   return (
-    <div
+    <button
+      type="button"
       id={`theme-option-${name}`}
       id={`theme-option-${name}`}
       className={`${themeOptionClass} d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
       className={`${themeOptionClass} d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
       onClick={onSelected}
       onClick={onSelected}
+      aria-pressed={isSelected}
     >
     >
-      <a
+      <div
         id={name}
         id={name}
-        role="button"
         className={`
         className={`
           m-0 rounded rounded-3
           m-0 rounded rounded-3
-          border border-4 border-primary ${isSelected ? '' : 'border-opacity-10'}`
-        }
+          border border-4 border-primary ${isSelected ? '' : 'border-opacity-10'}`}
       >
       >
-        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          viewBox="0 0 64 64"
+          width="64"
+          height="64"
+          className="rounded"
+        >
+          <title>{name}</title>
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
           <path
           <path
@@ -47,17 +57,35 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
             d="M6.436,53.44H26.065V55.5H6.436Zm14.831-11.4h4.8v2.061H17.189L10,47.743H26.065V49.8l-19.629,0v-.259L0,52.8V64H32.5V36.364Z"
             d="M6.436,53.44H26.065V55.5H6.436Zm14.831-11.4h4.8v2.061H17.189L10,47.743H26.065V49.8l-19.629,0v-.259L0,52.8V64H32.5V36.364Z"
             fill={darkSidebar}
             fill={darkSidebar}
           />
           />
-          <path d="M22.338,31.19l6.087-10.543L22.338,10.1H10.163L4.077,20.647,10.163,31.19Z" fill={createBtn} />
+          <path
+            d="M22.338,31.19l6.087-10.543L22.338,10.1H10.163L4.077,20.647,10.163,31.19Z"
+            fill={createBtn}
+          />
           <path d="M6.436,49.543,10,47.742H6.436Z" fill={lightIcon} />
           <path d="M6.436,49.543,10,47.742H6.436Z" fill={lightIcon} />
           <path d="M6.436,44.106H17.189l4.078-2.062H6.436Z" fill={lightIcon} />
           <path d="M6.436,44.106H17.189l4.078-2.062H6.436Z" fill={lightIcon} />
-          <path d="M6.436,49.8l19.629,0V47.742H10l-3.561,1.8Z" fill={darkIcon} />
+          <path
+            d="M6.436,49.8l19.629,0V47.742H10l-3.561,1.8Z"
+            fill={darkIcon}
+          />
           <path d="M26.065,44.106V42.044h-4.8L17.19,44.106Z" fill={darkIcon} />
           <path d="M26.065,44.106V42.044h-4.8L17.19,44.106Z" fill={darkIcon} />
-          <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
+          <rect
+            width="19.629"
+            height="2.062"
+            transform="translate(6.436 53.439)"
+            fill={darkIcon}
+          />
         </svg>
         </svg>
-      </a>
-      <span className={`mt-2 ${isSelected ? '' : 'opacity-25'}`}><b>{ name }</b></span>
-      { !isPresetTheme && <span className={`badge bg-primary mt-1 ${isSelected ? '' : 'opacity-25'}`}>Plugin</span> }
-    </div>
+      </div>
+      <span className={`mt-2 ${isSelected ? '' : 'opacity-25'}`}>
+        <b>{name}</b>
+      </span>
+      {!isPresetTheme && (
+        <span
+          className={`badge bg-primary mt-1 ${isSelected ? '' : 'opacity-25'}`}
+        >
+          Plugin
+        </span>
+      )}
+    </button>
   );
   );
-
 };
 };

+ 29 - 34
apps/app/src/client/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -1,10 +1,9 @@
-import React, { useEffect, useState, useCallback } from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
 import { isSearchServiceReachableAtom } from '~/states/server-configurations';
@@ -24,7 +23,8 @@ const ElasticsearchManagement = (): JSX.Element => {
 
 
   const [isConnected, setIsConnected] = useState(false);
   const [isConnected, setIsConnected] = useState(false);
   const [isConfigured, setIsConfigured] = useState(false);
   const [isConfigured, setIsConfigured] = useState(false);
-  const [isReconnectingProcessing, setIsReconnectingProcessing] = useState(false);
+  const [isReconnectingProcessing, setIsReconnectingProcessing] =
+    useState(false);
   const [isRebuildingProcessing, setIsRebuildingProcessing] = useState(false);
   const [isRebuildingProcessing, setIsRebuildingProcessing] = useState(false);
   const [isRebuildingCompleted, setIsRebuildingCompleted] = useState(false);
   const [isRebuildingCompleted, setIsRebuildingCompleted] = useState(false);
 
 
@@ -32,8 +32,7 @@ const ElasticsearchManagement = (): JSX.Element => {
   const [indicesData, setIndicesData] = useState(null);
   const [indicesData, setIndicesData] = useState(null);
   const [aliasesData, setAliasesData] = useState(null);
   const [aliasesData, setAliasesData] = useState(null);
 
 
-
-  const retrieveIndicesStatus = useCallback(async() => {
+  const retrieveIndicesStatus = useCallback(async () => {
     try {
     try {
       const { data } = await apiv3Get('/search/indices');
       const { data } = await apiv3Get('/search/indices');
       const { info } = data;
       const { info } = data;
@@ -46,8 +45,7 @@ const ElasticsearchManagement = (): JSX.Element => {
       setIsNormalized(info.isNormalized);
       setIsNormalized(info.isNormalized);
 
 
       return info.isNormalized;
       return info.isNormalized;
-    }
-    catch (errors: unknown) {
+    } catch (errors: unknown) {
       setIsConnected(false);
       setIsConnected(false);
 
 
       // evaluate whether configured or not
       // evaluate whether configured or not
@@ -58,14 +56,12 @@ const ElasticsearchManagement = (): JSX.Element => {
           }
           }
         }
         }
         toastError(errors as Error[]);
         toastError(errors as Error[]);
-      }
-      else {
+      } else {
         toastError(errors as Error);
         toastError(errors as Error);
       }
       }
 
 
       return false;
       return false;
-    }
-    finally {
+    } finally {
       setIsInitialized(true);
       setIsInitialized(true);
     }
     }
   }, []);
   }, []);
@@ -82,12 +78,12 @@ const ElasticsearchManagement = (): JSX.Element => {
       setIsRebuildingProcessing(true);
       setIsRebuildingProcessing(true);
     });
     });
 
 
-    socket.on(SocketEventName.FinishAddPage, async(data) => {
+    socket.on(SocketEventName.FinishAddPage, async (data) => {
       let retryCount = 0;
       let retryCount = 0;
       const maxRetries = 5;
       const maxRetries = 5;
       const retryDelay = 500;
       const retryDelay = 500;
 
 
-      const retrieveIndicesStatusWithRetry = async() => {
+      const retrieveIndicesStatusWithRetry = async () => {
         const isNormalizedResult = await retrieveIndicesStatus();
         const isNormalizedResult = await retrieveIndicesStatus();
         if (!isNormalizedResult && retryCount < maxRetries) {
         if (!isNormalizedResult && retryCount < maxRetries) {
           retryCount++;
           retryCount++;
@@ -111,13 +107,12 @@ const ElasticsearchManagement = (): JSX.Element => {
     };
     };
   }, [retrieveIndicesStatus, socket]);
   }, [retrieveIndicesStatus, socket]);
 
 
-  const reconnect = async() => {
+  const reconnect = async () => {
     setIsReconnectingProcessing(true);
     setIsReconnectingProcessing(true);
 
 
     try {
     try {
       await apiv3Post('/search/connection');
       await apiv3Post('/search/connection');
-    }
-    catch (e) {
+    } catch (e) {
       toastError(e);
       toastError(e);
       return;
       return;
     }
     }
@@ -126,12 +121,10 @@ const ElasticsearchManagement = (): JSX.Element => {
     window.location.reload();
     window.location.reload();
   };
   };
 
 
-  const normalizeIndices = async() => {
-
+  const normalizeIndices = async () => {
     try {
     try {
       await apiv3Put('/search/indices', { operation: 'normalize' });
       await apiv3Put('/search/indices', { operation: 'normalize' });
-    }
-    catch (e) {
+    } catch (e) {
       toastError(e);
       toastError(e);
     }
     }
 
 
@@ -140,14 +133,13 @@ const ElasticsearchManagement = (): JSX.Element => {
     toastSuccess('Normalizing has succeeded');
     toastSuccess('Normalizing has succeeded');
   };
   };
 
 
-  const rebuildIndices = async() => {
+  const rebuildIndices = async () => {
     setIsRebuildingProcessing(true);
     setIsRebuildingProcessing(true);
 
 
     try {
     try {
       await apiv3Put('/search/indices', { operation: 'rebuild' });
       await apiv3Put('/search/indices', { operation: 'rebuild' });
       toastSuccess('Rebuilding is requested');
       toastSuccess('Rebuilding is requested');
-    }
-    catch (e) {
+    } catch (e) {
       toastError(e);
       toastError(e);
     }
     }
 
 
@@ -156,7 +148,9 @@ const ElasticsearchManagement = (): JSX.Element => {
 
 
   const isErrorOccuredOnSearchService = !isSearchServiceReachable;
   const isErrorOccuredOnSearchService = !isSearchServiceReachable;
 
 
-  const isReconnectBtnEnabled = !isReconnectingProcessing && (!isInitialized || !isConnected || isErrorOccuredOnSearchService);
+  const isReconnectBtnEnabled =
+    !isReconnectingProcessing &&
+    (!isInitialized || !isConnected || isErrorOccuredOnSearchService);
 
 
   return (
   return (
     <>
     <>
@@ -178,7 +172,9 @@ const ElasticsearchManagement = (): JSX.Element => {
 
 
       {/* Controls */}
       {/* Controls */}
       <div className="row">
       <div className="row">
-        <label className="col-md-3 col-form-label text-start text-md-end">{ t('full_text_search_management.reconnect') }</label>
+        <div className="col-md-3 col-form-label text-start text-md-end">
+          {t('full_text_search_management.reconnect')}
+        </div>
         <div className="col-md-6">
         <div className="col-md-6">
           <ReconnectControls
           <ReconnectControls
             isEnabled={isReconnectBtnEnabled}
             isEnabled={isReconnectBtnEnabled}
@@ -191,7 +187,9 @@ const ElasticsearchManagement = (): JSX.Element => {
       <hr />
       <hr />
 
 
       <div className="row">
       <div className="row">
-        <label className="col-md-3 col-form-label text-start text-md-end">{ t('full_text_search_management.normalize') }</label>
+        <div className="col-md-3 col-form-label text-start text-md-end">
+          {t('full_text_search_management.normalize')}
+        </div>
         <div className="col-md-6">
         <div className="col-md-6">
           <NormalizeIndicesControls
           <NormalizeIndicesControls
             isRebuildingProcessing={isRebuildingProcessing}
             isRebuildingProcessing={isRebuildingProcessing}
@@ -204,7 +202,9 @@ const ElasticsearchManagement = (): JSX.Element => {
       <hr />
       <hr />
 
 
       <div className="row">
       <div className="row">
-        <label className="col-md-3 col-form-label text-start text-md-end">{ t('full_text_search_management.rebuild') }</label>
+        <div className="col-md-3 col-form-label text-start text-md-end">
+          {t('full_text_search_management.rebuild')}
+        </div>
         <div className="col-md-6">
         <div className="col-md-6">
           <RebuildIndexControls
           <RebuildIndexControls
             isRebuildingProcessing={isRebuildingProcessing}
             isRebuildingProcessing={isRebuildingProcessing}
@@ -214,15 +214,10 @@ const ElasticsearchManagement = (): JSX.Element => {
           />
           />
         </div>
         </div>
       </div>
       </div>
-
     </>
     </>
   );
   );
-
 };
 };
 
 
-
-ElasticsearchManagement.propTypes = {
-
-};
+ElasticsearchManagement.propTypes = {};
 
 
 export default ElasticsearchManagement;
 export default ElasticsearchManagement;

+ 12 - 9
apps/app/src/client/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx

@@ -1,32 +1,35 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 type Props = {
 type Props = {
-  isRebuildingProcessing: boolean,
-  onNormalizingRequested: () => void,
-  isNormalized?: boolean,
-}
+  isRebuildingProcessing: boolean;
+  onNormalizingRequested: () => void;
+  isNormalized?: boolean;
+};
 
 
 const NormalizeIndicesControls = (props: Props): JSX.Element => {
 const NormalizeIndicesControls = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { isNormalized, isRebuildingProcessing } = props;
   const { isNormalized, isRebuildingProcessing } = props;
 
 
-  const isEnabled = (isNormalized != null) && !isNormalized && !isRebuildingProcessing;
+  const isEnabled =
+    isNormalized != null && !isNormalized && !isRebuildingProcessing;
 
 
   return (
   return (
     <>
     <>
       <button
       <button
         type="submit"
         type="submit"
         className={`btn ${isEnabled ? 'btn-outline-info' : 'btn-outline-secondary'}`}
         className={`btn ${isEnabled ? 'btn-outline-info' : 'btn-outline-secondary'}`}
-        onClick={() => { props.onNormalizingRequested() }}
+        onClick={() => {
+          props.onNormalizingRequested();
+        }}
         disabled={!isEnabled}
         disabled={!isEnabled}
       >
       >
-        { t('full_text_search_management.normalize_button') }
+        {t('full_text_search_management.normalize_button')}
       </button>
       </button>
 
 
       <p className="form-text text-muted">
       <p className="form-text text-muted">
-        { t('full_text_search_management.normalize_description') }<br />
+        {t('full_text_search_management.normalize_description')}
+        <br />
       </p>
       </p>
     </>
     </>
   );
   );

+ 11 - 15
apps/app/src/client/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
@@ -9,7 +8,6 @@ import { SocketEventName } from '~/interfaces/websocket';
 import LabeledProgressBar from '../Common/LabeledProgressBar';
 import LabeledProgressBar from '../Common/LabeledProgressBar';
 
 
 class RebuildIndexControls extends React.Component {
 class RebuildIndexControls extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -44,12 +42,8 @@ class RebuildIndexControls extends React.Component {
   }
   }
 
 
   renderProgressBar() {
   renderProgressBar() {
-    const {
-      isRebuildingProcessing, isRebuildingCompleted,
-    } = this.props;
-    const {
-      total, current,
-    } = this.state;
+    const { isRebuildingProcessing, isRebuildingCompleted } = this.props;
+    const { total, current } = this.state;
     const showProgressBar = isRebuildingProcessing || isRebuildingCompleted;
     const showProgressBar = isRebuildingProcessing || isRebuildingCompleted;
 
 
     if (!showProgressBar) {
     if (!showProgressBar) {
@@ -76,25 +70,28 @@ class RebuildIndexControls extends React.Component {
 
 
     return (
     return (
       <>
       <>
-        { this.renderProgressBar() }
+        {this.renderProgressBar()}
 
 
         <button
         <button
           type="submit"
           type="submit"
           className="btn btn-primary"
           className="btn btn-primary"
-          onClick={() => { this.props.onRebuildingRequested() }}
+          onClick={() => {
+            this.props.onRebuildingRequested();
+          }}
           disabled={!isEnabled}
           disabled={!isEnabled}
         >
         >
-          { t('full_text_search_management.rebuild_button') }
+          {t('full_text_search_management.rebuild_button')}
         </button>
         </button>
 
 
         <p className="form-text text-muted">
         <p className="form-text text-muted">
-          { t('full_text_search_management.rebuild_description_1') }<br />
-          { t('full_text_search_management.rebuild_description_2') }<br />
+          {t('full_text_search_management.rebuild_description_1')}
+          <br />
+          {t('full_text_search_management.rebuild_description_2')}
+          <br />
         </p>
         </p>
       </>
       </>
     );
     );
   }
   }
-
 }
 }
 
 
 const RebuildIndexControlsFC = (props) => {
 const RebuildIndexControlsFC = (props) => {
@@ -103,7 +100,6 @@ const RebuildIndexControlsFC = (props) => {
   return <RebuildIndexControls t={t} socket={socket} {...props} />;
   return <RebuildIndexControls t={t} socket={socket} {...props} />;
 };
 };
 
 
-
 RebuildIndexControls.propTypes = {
 RebuildIndexControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
 
 

+ 11 - 11
apps/app/src/client/components/Admin/ElasticsearchManagement/ReconnectControls.tsx

@@ -1,14 +1,12 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-
 type Props = {
 type Props = {
-  isEnabled?: boolean,
-  isProcessing?: boolean,
-  onReconnectingRequested: () => void,
-}
+  isEnabled?: boolean;
+  isProcessing?: boolean;
+  onReconnectingRequested: () => void;
+};
 
 
 const ReconnectControls = (props: Props): JSX.Element => {
 const ReconnectControls = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
@@ -20,19 +18,21 @@ const ReconnectControls = (props: Props): JSX.Element => {
       <button
       <button
         type="submit"
         type="submit"
         className={`btn ${isEnabled ? 'btn-outline-success' : 'btn-outline-secondary'}`}
         className={`btn ${isEnabled ? 'btn-outline-success' : 'btn-outline-secondary'}`}
-        onClick={() => { props.onReconnectingRequested() }}
+        onClick={() => {
+          props.onReconnectingRequested();
+        }}
         disabled={!isEnabled}
         disabled={!isEnabled}
       >
       >
-        { isProcessing && <LoadingSpinner className="me-2" /> }
-        { t('full_text_search_management.reconnect_button') }
+        {isProcessing && <LoadingSpinner className="me-2" />}
+        {t('full_text_search_management.reconnect_button')}
       </button>
       </button>
 
 
       <p className="form-text text-muted">
       <p className="form-text text-muted">
-        { t('full_text_search_management.reconnect_description') }<br />
+        {t('full_text_search_management.reconnect_description')}
+        <br />
       </p>
       </p>
     </>
     </>
   );
   );
-
 };
 };
 
 
 export default ReconnectControls;
 export default ReconnectControls;

+ 83 - 44
apps/app/src/client/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -1,43 +1,54 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 class StatusTable extends React.PureComponent {
 class StatusTable extends React.PureComponent {
-
   renderPreInitializedLabel() {
   renderPreInitializedLabel() {
     return <span className="badge text-bg-default">――</span>;
     return <span className="badge text-bg-default">――</span>;
   }
   }
 
 
   renderConnectionStatusLabels() {
   renderConnectionStatusLabels() {
     const { t } = this.props;
     const { t } = this.props;
-    const {
-      isErrorOccuredOnSearchService,
-      isConnected, isConfigured,
-    } = this.props;
+    const { isErrorOccuredOnSearchService, isConnected, isConfigured } =
+      this.props;
 
 
-    const errorOccuredLabel = isErrorOccuredOnSearchService
-      ? <span className="badge text-bg-danger ms-2">{ t('full_text_search_management.connection_status_label_erroroccured') }</span>
-      : null;
+    const errorOccuredLabel = isErrorOccuredOnSearchService ? (
+      <span className="badge text-bg-danger ms-2">
+        {t('full_text_search_management.connection_status_label_erroroccured')}
+      </span>
+    ) : null;
 
 
     let connectionStatusLabel = null;
     let connectionStatusLabel = null;
     if (!isConfigured) {
     if (!isConfigured) {
       connectionStatusLabel = (
       connectionStatusLabel = (
         <span className="badge text-bg-default">
         <span className="badge text-bg-default">
-          { t('full_text_search_management.connection_status_label_unconfigured') }
+          {t(
+            'full_text_search_management.connection_status_label_unconfigured',
+          )}
         </span>
         </span>
       );
       );
-    }
-    else {
-      connectionStatusLabel = isConnected
+    } else {
+      connectionStatusLabel = isConnected ? (
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
-        ? <span data-testid="connection-status-badge-connected" className="badge text-bg-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
-        : <span className="badge text-bg-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
+        <span
+          data-testid="connection-status-badge-connected"
+          className="badge text-bg-success"
+        >
+          {t('full_text_search_management.connection_status_label_connected')}
+        </span>
+      ) : (
+        <span className="badge text-bg-danger">
+          {t(
+            'full_text_search_management.connection_status_label_disconnected',
+          )}
+        </span>
+      );
     }
     }
 
 
     return (
     return (
       <>
       <>
-        {connectionStatusLabel}{errorOccuredLabel}
+        {connectionStatusLabel}
+        {errorOccuredLabel}
       </>
       </>
     );
     );
   }
   }
@@ -45,9 +56,15 @@ class StatusTable extends React.PureComponent {
   renderIndicesStatusLabel() {
   renderIndicesStatusLabel() {
     const { t, isNormalized } = this.props;
     const { t, isNormalized } = this.props;
 
 
-    return isNormalized
-      ? <span className="badge text-bg-info">{ t('full_text_search_management.indices_status_label_normalized') }</span>
-      : <span className="badge text-bg-warning">{ t('full_text_search_management.indices_status_label_unnormalized') }</span>;
+    return isNormalized ? (
+      <span className="badge text-bg-info">
+        {t('full_text_search_management.indices_status_label_normalized')}
+      </span>
+    ) : (
+      <span className="badge text-bg-warning">
+        {t('full_text_search_management.indices_status_label_unnormalized')}
+      </span>
+    );
   }
   }
 
 
   renderIndexInfoPanel(indexName, body = {}, aliases = []) {
   renderIndexInfoPanel(indexName, body = {}, aliases = []) {
@@ -55,7 +72,10 @@ class StatusTable extends React.PureComponent {
 
 
     const aliasLabels = aliases.map((aliasName) => {
     const aliasLabels = aliases.map((aliasName) => {
       return (
       return (
-        <span key={`badge-${indexName}-${aliasName}`} className="badge text-bg-primary me-2">
+        <span
+          key={`badge-${indexName}-${aliasName}`}
+          className="badge text-bg-primary me-2"
+        >
           <span className="material-symbols-outlined">sell</span>
           <span className="material-symbols-outlined">sell</span>
           <span>{aliasName}</span>
           <span>{aliasName}</span>
         </span>
         </span>
@@ -65,17 +85,22 @@ class StatusTable extends React.PureComponent {
     return (
     return (
       <div className="card">
       <div className="card">
         <div className="card-header">
         <div className="card-header">
-
-          <a role="button" className="text-nowrap me-2" data-bs-toggle="collapse" href={`#${collapseId}`} aria-expanded="true" aria-controls={collapseId}>
-            <span className="material-symbols-outlined">database</span> {indexName}
-          </a>
+          <button
+            type="button"
+            className="text-nowrap me-2 btn btn-link p-0"
+            data-bs-toggle="collapse"
+            data-bs-target={`#${collapseId}`}
+            aria-expanded="true"
+            aria-controls={collapseId}
+          >
+            <span className="material-symbols-outlined">database</span>{' '}
+            {indexName}
+          </button>
           <span className="ms-md-3">{aliasLabels}</span>
           <span className="ms-md-3">{aliasLabels}</span>
         </div>
         </div>
         <div id={collapseId} className="collapse">
         <div id={collapseId} className="collapse">
           <div className="card-body">
           <div className="card-body">
-            <pre>
-              {JSON.stringify(body, null, 2)}
-            </pre>
+            <pre>{JSON.stringify(body, null, 2)}</pre>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -83,10 +108,7 @@ class StatusTable extends React.PureComponent {
   }
   }
 
 
   renderIndexInfoPanels() {
   renderIndexInfoPanels() {
-    const {
-      indicesData,
-      aliasesData,
-    } = this.props;
+    const { indicesData, aliasesData } = this.props;
 
 
     // data is null
     // data is null
     if (indicesData == null) {
     if (indicesData == null) {
@@ -126,43 +148,60 @@ class StatusTable extends React.PureComponent {
 
 
     return (
     return (
       <div className="row">
       <div className="row">
-        { Object.keys(indexNameToDataMap).map((indexName) => {
+        {Object.keys(indexNameToDataMap).map((indexName) => {
           return (
           return (
             <div key={`col-${indexName}`} className="col-md-6">
             <div key={`col-${indexName}`} className="col-md-6">
-              { this.renderIndexInfoPanel(indexName, indexNameToDataMap[indexName], indexNameToAliasMap[indexName]) }
+              {this.renderIndexInfoPanel(
+                indexName,
+                indexNameToDataMap[indexName],
+                indexNameToAliasMap[indexName],
+              )}
             </div>
             </div>
           );
           );
-        }) }
+        })}
       </div>
       </div>
     );
     );
   }
   }
 
 
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
-    const {
-      isInitialized,
-    } = this.props;
+    const { isInitialized } = this.props;
 
 
     return (
     return (
       <table className="table table-bordered">
       <table className="table table-bordered">
         <tbody>
         <tbody>
           <tr>
           <tr>
-            <th className="w-25">{t('full_text_search_management.connection_status')}</th>
-            <td className="w-75">{ isInitialized ? this.renderConnectionStatusLabels() : this.renderPreInitializedLabel() }</td>
+            <th className="w-25">
+              {t('full_text_search_management.connection_status')}
+            </th>
+            <td className="w-75">
+              {isInitialized
+                ? this.renderConnectionStatusLabels()
+                : this.renderPreInitializedLabel()}
+            </td>
           </tr>
           </tr>
           <tr>
           <tr>
-            <th className="w-25">{t('full_text_search_management.indices_status')}</th>
-            <td className="w-75">{ isInitialized ? this.renderIndicesStatusLabel() : this.renderPreInitializedLabel() }</td>
+            <th className="w-25">
+              {t('full_text_search_management.indices_status')}
+            </th>
+            <td className="w-75">
+              {isInitialized
+                ? this.renderIndicesStatusLabel()
+                : this.renderPreInitializedLabel()}
+            </td>
           </tr>
           </tr>
           <tr>
           <tr>
-            <th className="w-25">{t('full_text_search_management.indices_summary')}</th>
-            <td className="p-4 w-75">{ isInitialized && this.renderIndexInfoPanels() }</td>
+            <th className="w-25">
+              {t('full_text_search_management.indices_summary')}
+            </th>
+            <td className="p-4 w-75">
+              {isInitialized && this.renderIndexInfoPanels()}
+            </td>
           </tr>
           </tr>
         </tbody>
         </tbody>
       </table>
       </table>
     );
     );
   }
   }
-
 }
 }
 
 
 const StatusTableWrapperFC = (props) => {
 const StatusTableWrapperFC = (props) => {

+ 13 - 6
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx

@@ -1,14 +1,13 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { format } from 'date-fns/format';
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
 import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
 
 
 type ArchiveFilesTableProps = {
 type ArchiveFilesTableProps = {
-  zipFileStats: any[],
-  onZipFileStatRemove: (fileName: string) => void,
-}
+  zipFileStats: any[];
+  onZipFileStatRemove: (fileName: string) => void;
+};
 
 
 const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
 const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -30,8 +29,16 @@ const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
             <tr key={fileName}>
             <tr key={fileName}>
               <th>{fileName}</th>
               <th>{fileName}</th>
               <td>{meta.version}</td>
               <td>{meta.version}</td>
-              <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
-              <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
+              <td className="text-capitalize">
+                {innerFileStats
+                  .map((fileStat) => fileStat.collectionName)
+                  .join(', ')}
+              </td>
+              <td>
+                {meta.exportedAt
+                  ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss')
+                  : ''}
+              </td>
               <td>
               <td>
                 <ArchiveFilesTableMenu
                 <ArchiveFilesTableMenu
                   fileName={fileName}
                   fileName={fileName}

+ 35 - 12
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx

@@ -1,29 +1,52 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 
 
 type ArchiveFilesTableMenuProps = {
 type ArchiveFilesTableMenuProps = {
-  fileName: string,
-  onZipFileStatRemove: (fileName: string) => void,
-}
+  fileName: string;
+  onZipFileStatRemove: (fileName: string) => void;
+};
 
 
-const ArchiveFilesTableMenu = (props: ArchiveFilesTableMenuProps):JSX.Element => {
+const ArchiveFilesTableMenu = (
+  props: ArchiveFilesTableMenuProps,
+): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   return (
   return (
     <div className="dropdown">
     <div className="dropdown">
-      <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
-        <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
+      <button
+        type="button"
+        className="btn btn-sm btn-outline-secondary dropdown-toggle"
+        data-bs-toggle="dropdown"
+        aria-expanded="false"
+      >
+        <span className="material-symbols-outlined">settings</span>{' '}
+        <span className="caret"></span>
       </button>
       </button>
       <ul className="dropdown-menu dropdown-menu-end">
       <ul className="dropdown-menu dropdown-menu-end">
-        <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
-        <button type="button" className="dropdown-item" onClick={() => { window.location.href = `/admin/export/${props.fileName}` }}>
-          <span className="material-symbols-outlined">cloud_download</span> {t('admin:export_management.download')}
+        <li className="dropdown-header">
+          {t('admin:export_management.export_menu')}
+        </li>
+        <button
+          type="button"
+          className="dropdown-item"
+          onClick={() => {
+            window.location.href = `/admin/export/${props.fileName}`;
+          }}
+        >
+          <span className="material-symbols-outlined">cloud_download</span>{' '}
+          {t('admin:export_management.download')}
         </button>
         </button>
-        <button type="button" className="dropdown-item" role="button" onClick={() => props.onZipFileStatRemove(props.fileName)}>
-          <span className="text-danger"><span className="material-symbols-outlined">delete</span> {t('admin:export_management.delete')}</span>
+        <button
+          type="button"
+          className="dropdown-item"
+          onClick={() => props.onZipFileStatRemove(props.fileName)}
+        >
+          <span className="text-danger">
+            <span className="material-symbols-outlined">delete</span>{' '}
+            {t('admin:export_management.delete')}
+          </span>
         </button>
         </button>
       </ul>
       </ul>
     </div>
     </div>

+ 137 - 83
apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -1,47 +1,61 @@
-import React, {
-  useCallback, useState, useEffect, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
-
 const GROUPS_PAGE = [
 const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations', 'pageredirects', 'comments', 'sharelinks',
+  'pages',
+  'revisions',
+  'tags',
+  'pagetagrelations',
+  'pageredirects',
+  'comments',
+  'sharelinks',
 ];
 ];
 const GROUPS_USER = [
 const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-  'externalusergroups', 'externalusergrouprelations',
-  'useruisettings', 'editorsettings', 'bookmarks', 'bookmarkfolders', 'subscriptions',
+  'users',
+  'externalaccounts',
+  'usergroups',
+  'usergrouprelations',
+  'externalusergroups',
+  'externalusergrouprelations',
+  'useruisettings',
+  'editorsettings',
+  'bookmarks',
+  'bookmarkfolders',
+  'subscriptions',
   'inappnotificationsettings',
   'inappnotificationsettings',
 ];
 ];
 const GROUPS_CONFIG = [
 const GROUPS_CONFIG = [
-  'configs', 'migrations', 'updateposts', 'globalnotificationsettings', 'slackappintegrations',
+  'configs',
+  'migrations',
+  'updateposts',
+  'globalnotificationsettings',
+  'slackappintegrations',
   'growiplugins',
   'growiplugins',
 ];
 ];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+const ALL_GROUPED_COLLECTIONS =
+  GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
 
 type Props = {
 type Props = {
-  isOpen: boolean,
-  onExportingRequested: () => void,
-  onClose: () => void,
-  collections: string[],
-  isAllChecked?: boolean,
+  isOpen: boolean;
+  onExportingRequested: () => void;
+  onClose: () => void;
+  collections: string[];
+  isAllChecked?: boolean;
 };
 };
 
 
 const SelectCollectionsModal = (props: Props): JSX.Element => {
 const SelectCollectionsModal = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const {
-    isOpen, onExportingRequested, onClose, collections, isAllChecked,
-  } = props;
+  const { isOpen, onExportingRequested, onClose, collections, isAllChecked } =
+    props;
 
 
-  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set());
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(
+    new Set(),
+  );
 
 
   const toggleCheckbox = useCallback((e) => {
   const toggleCheckbox = useCallback((e) => {
     const { target } = e;
     const { target } = e;
@@ -51,8 +65,7 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
       const selectedCollections = new Set(prevState);
       const selectedCollections = new Set(prevState);
       if (checked) {
       if (checked) {
         selectedCollections.add(name);
         selectedCollections.add(name);
-      }
-      else {
+      } else {
         selectedCollections.delete(name);
         selectedCollections.delete(name);
       }
       }
 
 
@@ -68,27 +81,31 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     setSelectedCollections(new Set());
     setSelectedCollections(new Set());
   }, []);
   }, []);
 
 
-  const doExport = useCallback(async(e) => {
-    e.preventDefault();
+  const doExport = useCallback(
+    async (e) => {
+      e.preventDefault();
 
 
-    try {
-      // TODO: use apiv3Post
-      const result = await apiPost<any>('/v3/export', { collections: Array.from(selectedCollections) });
+      try {
+        // TODO: use apiv3Post
+        const result = await apiPost<any>('/v3/export', {
+          collections: Array.from(selectedCollections),
+        });
 
 
-      if (!result.ok) {
-        throw new Error('Error occured.');
-      }
+        if (!result.ok) {
+          throw new Error('Error occured.');
+        }
 
 
-      toastSuccess('Export process has requested.');
+        toastSuccess('Export process has requested.');
 
 
-      onExportingRequested();
-      onClose();
-      uncheckAll();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [onClose, onExportingRequested, selectedCollections, uncheckAll]);
+        onExportingRequested();
+        onClose();
+        uncheckAll();
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [onClose, onExportingRequested, selectedCollections, uncheckAll],
+  );
 
 
   const validateForm = useCallback(() => {
   const validateForm = useCallback(() => {
     return selectedCollections.size > 0;
     return selectedCollections.size > 0;
@@ -109,49 +126,58 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     return (
     return (
       <div className="card">
       <div className="card">
         <div className="card-body">
         <div className="card-body">
-          {/* eslint-disable-next-line react/no-danger */}
+          {/** biome-ignore lint/security/noDangerouslySetInnerHtml: ignore */}
           <p className="card-text" dangerouslySetInnerHTML={{ __html: html }} />
           <p className="card-text" dangerouslySetInnerHTML={{ __html: html }} />
         </div>
         </div>
       </div>
       </div>
     );
     );
   }, [selectedCollections, t]);
   }, [selectedCollections, t]);
 
 
-  const renderCheckboxes = useCallback((collectionNames, color?) => {
-    const checkboxColor = color ? `form-check-${color}` : 'form-check-info';
+  const renderCheckboxes = useCallback(
+    (collectionNames, color?) => {
+      const checkboxColor = color ? `form-check-${color}` : 'form-check-info';
 
 
-    return (
-      <div className={`form-check ${checkboxColor}`}>
-        <div className="row">
-          {collectionNames.map((collectionName) => {
-            return (
-              <div className="col-sm-6 my-1" key={collectionName}>
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id={collectionName}
-                  name={collectionName}
-                  value={collectionName}
-                  checked={selectedCollections.has(collectionName)}
-                  onChange={toggleCheckbox}
-                />
-                <label className="form-label text-capitalize form-check-label ms-3" htmlFor={collectionName}>
-                  {collectionName}
-                </label>
-              </div>
-            );
-          })}
+      return (
+        <div className={`form-check ${checkboxColor}`}>
+          <div className="row">
+            {collectionNames.map((collectionName) => {
+              return (
+                <div className="col-sm-6 my-1" key={collectionName}>
+                  <input
+                    type="checkbox"
+                    className="form-check-input"
+                    id={collectionName}
+                    name={collectionName}
+                    value={collectionName}
+                    checked={selectedCollections.has(collectionName)}
+                    onChange={toggleCheckbox}
+                  />
+                  <label
+                    className="form-label text-capitalize form-check-label ms-3"
+                    htmlFor={collectionName}
+                  >
+                    {collectionName}
+                  </label>
+                </div>
+              );
+            })}
+          </div>
         </div>
         </div>
-      </div>
-    );
-  }, [selectedCollections, toggleCheckbox]);
+      );
+    },
+    [selectedCollections, toggleCheckbox],
+  );
 
 
-  const renderGroups = useCallback((groupList, color?) => {
-    const collectionNames = groupList.filter((collectionName) => {
-      return collections.includes(collectionName);
-    });
+  const renderGroups = useCallback(
+    (groupList, color?) => {
+      const collectionNames = groupList.filter((collectionName) => {
+        return collections.includes(collectionName);
+      });
 
 
-    return renderCheckboxes(collectionNames, color);
-  }, [collections, renderCheckboxes]);
+      return renderCheckboxes(collectionNames, color);
+    },
+    [collections, renderCheckboxes],
+  );
 
 
   const renderOthers = useCallback(() => {
   const renderOthers = useCallback(() => {
     const collectionNames = collections.filter((collectionName) => {
     const collectionNames = collections.filter((collectionName) => {
@@ -175,11 +201,23 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
         <ModalBody>
         <ModalBody>
           <div className="row">
           <div className="row">
             <div className="col-sm-12">
             <div className="col-sm-12">
-              <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-                <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
+              <button
+                type="button"
+                className="btn btn-sm btn-outline-secondary me-2"
+                onClick={checkAll}
+              >
+                <span className="material-symbols-outlined">check_box</span>{' '}
+                {t('admin:export_management.check_all')}
               </button>
               </button>
-              <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-                <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
+              <button
+                type="button"
+                className="btn btn-sm btn-outline-secondary me-2"
+                onClick={uncheckAll}
+              >
+                <span className="material-symbols-outlined">
+                  check_box_outline_blank
+                </span>{' '}
+                {t('admin:export_management.uncheck_all')}
               </button>
               </button>
             </div>
             </div>
           </div>
           </div>
@@ -198,21 +236,37 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
           </div>
           </div>
           <div className="row mt-4">
           <div className="row mt-4">
             <div className="col-sm-12">
             <div className="col-sm-12">
-              <h3 className="admin-setting-header">MongoDB Config Collections</h3>
+              <h3 className="admin-setting-header">
+                MongoDB Config Collections
+              </h3>
               {renderGroups(GROUPS_CONFIG)}
               {renderGroups(GROUPS_CONFIG)}
             </div>
             </div>
           </div>
           </div>
           <div className="row mt-4">
           <div className="row mt-4">
             <div className="col-sm-12">
             <div className="col-sm-12">
-              <h3 className="admin-setting-header">MongoDB Other Collections</h3>
+              <h3 className="admin-setting-header">
+                MongoDB Other Collections
+              </h3>
               {renderOthers()}
               {renderOthers()}
             </div>
             </div>
           </div>
           </div>
         </ModalBody>
         </ModalBody>
 
 
         <ModalFooter>
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={onClose}>{t('admin:export_management.cancel')}</button>
-          <button type="submit" className="btn btn-sm btn-primary" disabled={!validateForm()}>{t('admin:export_management.export')}</button>
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary"
+            onClick={onClose}
+          >
+            {t('admin:export_management.cancel')}
+          </button>
+          <button
+            type="submit"
+            className="btn btn-sm btn-primary"
+            disabled={!validateForm()}
+          >
+            {t('admin:export_management.export')}
+          </button>
         </ModalFooter>
         </ModalFooter>
       </form>
       </form>
     </Modal>
     </Modal>

+ 40 - 27
apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx

@@ -1,10 +1,6 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-
 import { apiDelete } from '~/client/util/apiv1-client';
 import { apiDelete } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
@@ -14,9 +10,11 @@ import LabeledProgressBar from './Common/LabeledProgressBar';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 
 
-
 const IGNORED_COLLECTION_NAMES = [
 const IGNORED_COLLECTION_NAMES = [
-  'sessions', 'rlflx', 'yjs-writings', 'transferkeys',
+  'sessions',
+  'rlflx',
+  'yjs-writings',
+  'transferkeys',
 ];
 ];
 
 
 const ExportArchiveDataPage = (): JSX.Element => {
 const ExportArchiveDataPage = (): JSX.Element => {
@@ -31,16 +29,26 @@ const ExportArchiveDataPage = (): JSX.Element => {
   const [isZipping, setZipping] = useState(false);
   const [isZipping, setZipping] = useState(false);
   const [isExported, setExported] = useState(false);
   const [isExported, setExported] = useState(false);
 
 
-  const fetchData = useCallback(async() => {
-    const [{ data: collectionsData }, { data: statusData }] = await Promise.all([
-      apiv3Get<{collections: any[]}>('/mongo/collections', {}),
-      apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
-    ]);
+  const fetchData = useCallback(async () => {
+    const [{ data: collectionsData }, { data: statusData }] = await Promise.all(
+      [
+        apiv3Get<{ collections: any[] }>('/mongo/collections', {}),
+        apiv3Get<{
+          status: {
+            zipFileStats: any[];
+            isExporting: boolean;
+            progressList: any[];
+          };
+        }>('/export/status', {}),
+      ],
+    );
 
 
     // filter only not ignored collection names
     // filter only not ignored collection names
-    const filteredCollections = collectionsData.collections.filter((collectionName) => {
-      return !IGNORED_COLLECTION_NAMES.includes(collectionName);
-    });
+    const filteredCollections = collectionsData.collections.filter(
+      (collectionName) => {
+        return !IGNORED_COLLECTION_NAMES.includes(collectionName);
+      },
+    );
 
 
     const { zipFileStats, isExporting, progressList } = statusData.status;
     const { zipFileStats, isExporting, progressList } = statusData.status;
     setCollections(filteredCollections);
     setCollections(filteredCollections);
@@ -67,7 +75,7 @@ const ExportArchiveDataPage = (): JSX.Element => {
       setExporting(false);
       setExporting(false);
       setZipping(false);
       setZipping(false);
       setExported(true);
       setExported(true);
-      setZipFileStats(prev => prev.concat([addedZipFileStat]));
+      setZipFileStats((prev) => prev.concat([addedZipFileStat]));
 
 
       toastSuccess(`New Archive Data '${addedZipFileStat.fileName}' is added`);
       toastSuccess(`New Archive Data '${addedZipFileStat.fileName}' is added`);
     };
     };
@@ -83,18 +91,18 @@ const ExportArchiveDataPage = (): JSX.Element => {
       socket.off('admin:onStartZippingForExport', onStartZipping);
       socket.off('admin:onStartZippingForExport', onStartZipping);
       socket.off('admin:onTerminateForExport', onTerminateForExport);
       socket.off('admin:onTerminateForExport', onTerminateForExport);
     };
     };
-
   }, [socket]);
   }, [socket]);
 
 
-  const onZipFileStatRemove = useCallback(async(fileName) => {
+  const onZipFileStatRemove = useCallback(async (fileName) => {
     try {
     try {
       await apiDelete(`/v3/export/${fileName}`, {});
       await apiDelete(`/v3/export/${fileName}`, {});
 
 
-      setZipFileStats(prev => prev.filter(stat => stat.fileName !== fileName));
+      setZipFileStats((prev) =>
+        prev.filter((stat) => stat.fileName !== fileName),
+      );
 
 
       toastSuccess(`Deleted ${fileName}`);
       toastSuccess(`Deleted ${fileName}`);
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }, []);
   }, []);
@@ -148,23 +156,28 @@ const ExportArchiveDataPage = (): JSX.Element => {
     };
     };
   }, [fetchData, setupWebsocketEventHandler]);
   }, [fetchData, setupWebsocketEventHandler]);
 
 
-  const showExportingData = (isExported || isExporting) && (progressList != null);
+  const showExportingData = (isExported || isExporting) && progressList != null;
 
 
   return (
   return (
     <div data-testid="admin-export-archive-data">
     <div data-testid="admin-export-archive-data">
       <h2>{t('export_management.export_archive_data')}</h2>
       <h2>{t('export_management.export_archive_data')}</h2>
 
 
-      <button type="button" className="btn btn-outline-secondary" disabled={isExporting} onClick={() => setExportModalOpen(true)}>
+      <button
+        type="button"
+        className="btn btn-outline-secondary"
+        disabled={isExporting}
+        onClick={() => setExportModalOpen(true)}
+      >
         {t('export_management.create_new_archive_data')}
         {t('export_management.create_new_archive_data')}
       </button>
       </button>
 
 
-      { showExportingData && (
+      {showExportingData && (
         <div className="mt-5">
         <div className="mt-5">
           <h3>{t('export_management.exporting_collection_list')}</h3>
           <h3>{t('export_management.exporting_collection_list')}</h3>
-          { renderProgressBarsForCollections() }
-          { renderProgressBarForZipping() }
+          {renderProgressBarsForCollections()}
+          {renderProgressBarForZipping()}
         </div>
         </div>
-      ) }
+      )}
 
 
       <div className="mt-5">
       <div className="mt-5">
         <h3 className="mb-3">{t('export_management.exported_data_list')}</h3>
         <h3 className="mb-3">{t('export_management.exported_data_list')}</h3>

+ 0 - 2
apps/app/src/client/components/Admin/ForbiddenPage.tsx

@@ -1,9 +1,7 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import DefaultErrorPage from 'next/error';
 import DefaultErrorPage from 'next/error';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-
 export const ForbiddenPage = (): JSX.Element => {
 export const ForbiddenPage = (): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 

+ 4 - 2
apps/app/src/client/components/Admin/FullTextSearchManagement.tsx

@@ -1,5 +1,4 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
 import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
@@ -9,7 +8,10 @@ export const FullTextSearchManagement = (): JSX.Element => {
 
 
   return (
   return (
     <div data-testid="admin-full-text-search">
     <div data-testid="admin-full-text-search">
-      <h2 className="mb-4"> { t('full_text_search_management.elasticsearch_management') } </h2>
+      <h2 className="mb-4">
+        {' '}
+        {t('full_text_search_management.elasticsearch_management')}{' '}
+      </h2>
       <ElasticsearchManagement />
       <ElasticsearchManagement />
     </div>
     </div>
   );
   );

+ 103 - 49
apps/app/src/client/components/Admin/G2GDataTransfer.tsx

@@ -1,24 +1,27 @@
-import React, {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { useGenerateTransferKey } from '~/client/services/g2g-transfer';
 import { useGenerateTransferKey } from '~/client/services/g2g-transfer';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
-import { G2G_PROGRESS_STATUS, type G2GProgress } from '~/interfaces/g2g-transfer';
+import {
+  G2G_PROGRESS_STATUS,
+  type G2GProgress,
+} from '~/interfaces/g2g-transfer';
 import { useGrowiDocumentationUrl } from '~/states/context';
 import { useGrowiDocumentationUrl } from '~/states/context';
 
 
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
-
 // import { FileUploadSettingMolecule } from './App/FileUploadSetting';
 // import { FileUploadSettingMolecule } from './App/FileUploadSetting';
 import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 import G2GDataTransferStatusIcon from './G2GDataTransferStatusIcon';
 import G2GDataTransferStatusIcon from './G2GDataTransferStatusIcon';
 
 
 const IGNORED_COLLECTION_NAMES = [
 const IGNORED_COLLECTION_NAMES = [
-  'sessions', 'rlflx', 'activities', 'attachmentFiles.files', 'attachmentFiles.chunks',
+  'sessions',
+  'rlflx',
+  'activities',
+  'attachmentFiles.files',
+  'attachmentFiles.chunks',
 ];
 ];
 
 
 const G2GDataTransfer = (): JSX.Element => {
 const G2GDataTransfer = (): JSX.Element => {
@@ -27,7 +30,9 @@ const G2GDataTransfer = (): JSX.Element => {
 
 
   const [startTransferKey, setStartTransferKey] = useState('');
   const [startTransferKey, setStartTransferKey] = useState('');
   const [collections, setCollections] = useState<string[]>([]);
   const [collections, setCollections] = useState<string[]>([]);
-  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set());
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(
+    new Set(),
+  );
   const [optionsMap, setOptionsMap] = useState<any>({});
   const [optionsMap, setOptionsMap] = useState<any>({});
   const [isShowExportForm, setShowExportForm] = useState(false);
   const [isShowExportForm, setShowExportForm] = useState(false);
   const [isTransferring, setTransferring] = useState(false);
   const [isTransferring, setTransferring] = useState(false);
@@ -61,13 +66,18 @@ const G2GDataTransfer = (): JSX.Element => {
     setStartTransferKey(e.target.value);
     setStartTransferKey(e.target.value);
   }, []);
   }, []);
 
 
-  const setCollectionsAndSelectedCollections = useCallback(async() => {
-    const { data: collectionsData } = await apiv3Get<{collections: any[]}>('/mongo/collections', {});
+  const setCollectionsAndSelectedCollections = useCallback(async () => {
+    const { data: collectionsData } = await apiv3Get<{ collections: any[] }>(
+      '/mongo/collections',
+      {},
+    );
 
 
     // filter only not ignored collection names
     // filter only not ignored collection names
-    const filteredCollections = collectionsData.collections.filter((collectionName) => {
-      return !IGNORED_COLLECTION_NAMES.includes(collectionName);
-    });
+    const filteredCollections = collectionsData.collections.filter(
+      (collectionName) => {
+        return !IGNORED_COLLECTION_NAMES.includes(collectionName);
+      },
+    );
 
 
     setCollections(filteredCollections);
     setCollections(filteredCollections);
     setSelectedCollections(new Set(filteredCollections));
     setSelectedCollections(new Set(filteredCollections));
@@ -78,7 +88,10 @@ const G2GDataTransfer = (): JSX.Element => {
       socket.on('admin:g2gProgress', (g2gProgress: G2GProgress) => {
       socket.on('admin:g2gProgress', (g2gProgress: G2GProgress) => {
         setG2GProgress(g2gProgress);
         setG2GProgress(g2gProgress);
 
 
-        if (g2gProgress.mongo === G2G_PROGRESS_STATUS.COMPLETED && g2gProgress.attachments === G2G_PROGRESS_STATUS.COMPLETED) {
+        if (
+          g2gProgress.mongo === G2G_PROGRESS_STATUS.COMPLETED &&
+          g2gProgress.attachments === G2G_PROGRESS_STATUS.COMPLETED
+        ) {
           toastSuccess(t('admin:g2g:transfer_success'));
           toastSuccess(t('admin:g2g:transfer_success'));
         }
         }
       });
       });
@@ -88,7 +101,7 @@ const G2GDataTransfer = (): JSX.Element => {
         toastError(t(key));
         toastError(t(key));
       });
       });
     }
     }
-  }, [socket, t, setTransferring, setG2GProgress]);
+  }, [socket, t]);
 
 
   const cleanUpWebsocketEventHandler = useCallback(() => {
   const cleanUpWebsocketEventHandler = useCallback(() => {
     if (socket != null) {
     if (socket != null) {
@@ -99,30 +112,31 @@ const G2GDataTransfer = (): JSX.Element => {
 
 
   const { transferKey, generateTransferKey } = useGenerateTransferKey();
   const { transferKey, generateTransferKey } = useGenerateTransferKey();
 
 
-  const onClickHandler = useCallback(async() => {
+  const onClickHandler = useCallback(async () => {
     try {
     try {
       await generateTransferKey();
       await generateTransferKey();
-    }
-    catch (errs) {
+    } catch (errs) {
       toastError(errs);
       toastError(errs);
     }
     }
   }, [generateTransferKey]);
   }, [generateTransferKey]);
 
 
-  const startTransfer = useCallback(async(e) => {
-    e.preventDefault();
-    setTransferring(true);
-
-    try {
-      await apiv3Post('/g2g-transfer/transfer', {
-        transferKey: startTransferKey,
-        collections: Array.from(selectedCollections),
-        optionsMap,
-      });
-    }
-    catch (errs) {
-      toastError(errs);
-    }
-  }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
+  const startTransfer = useCallback(
+    async (e) => {
+      e.preventDefault();
+      setTransferring(true);
+
+      try {
+        await apiv3Post('/g2g-transfer/transfer', {
+          transferKey: startTransferKey,
+          collections: Array.from(selectedCollections),
+          optionsMap,
+        });
+      } catch (errs) {
+        toastError(errs);
+      }
+    },
+    [startTransferKey, selectedCollections, optionsMap],
+  );
 
 
   const documentationUrl = useGrowiDocumentationUrl();
   const documentationUrl = useGrowiDocumentationUrl();
 
 
@@ -173,7 +187,6 @@ const G2GDataTransfer = (): JSX.Element => {
   //   setGcsUploadNamespace(val);
   //   setGcsUploadNamespace(val);
   // }, []);
   // }, []);
 
 
-
   useEffect(() => {
   useEffect(() => {
     setCollectionsAndSelectedCollections();
     setCollectionsAndSelectedCollections();
     setupWebsocketEventHandler();
     setupWebsocketEventHandler();
@@ -181,13 +194,24 @@ const G2GDataTransfer = (): JSX.Element => {
     return () => {
     return () => {
       cleanUpWebsocketEventHandler();
       cleanUpWebsocketEventHandler();
     };
     };
-  }, [setCollectionsAndSelectedCollections, setupWebsocketEventHandler, cleanUpWebsocketEventHandler]);
+  }, [
+    setCollectionsAndSelectedCollections,
+    setupWebsocketEventHandler,
+    cleanUpWebsocketEventHandler,
+  ]);
 
 
   return (
   return (
     <div data-testid="admin-export-archive-data">
     <div data-testid="admin-export-archive-data">
-      <h2 className="border-bottom">{t('admin:g2g_data_transfer.transfer_data_to_another_growi')}</h2>
-
-      <button type="button" className="btn btn-outline-secondary mt-4" disabled={isTransferring} onClick={() => setShowExportForm(!isShowExportForm)}>
+      <h2 className="border-bottom">
+        {t('admin:g2g_data_transfer.transfer_data_to_another_growi')}
+      </h2>
+
+      <button
+        type="button"
+        className="btn btn-outline-secondary mt-4"
+        disabled={isTransferring}
+        onClick={() => setShowExportForm(!isShowExportForm)}
+      >
         {t('admin:g2g_data_transfer.advanced_options')}
         {t('admin:g2g_data_transfer.advanced_options')}
       </button>
       </button>
 
 
@@ -243,7 +267,9 @@ const G2GDataTransfer = (): JSX.Element => {
             />
             />
           </div>
           </div>
           <div className="col-3">
           <div className="col-3">
-            <button type="submit" className="btn btn-primary w-100">{t('admin:g2g_data_transfer.start_transfer')}</button>
+            <button type="submit" className="btn btn-primary w-100">
+              {t('admin:g2g_data_transfer.start_transfer')}
+            </button>
           </div>
           </div>
         </div>
         </div>
       </form>
       </form>
@@ -251,38 +277,66 @@ const G2GDataTransfer = (): JSX.Element => {
       {isTransferring && (
       {isTransferring && (
         <div className="border rounded p-4">
         <div className="border rounded p-4">
           <div className="my-2">
           <div className="my-2">
-            <G2GDataTransferStatusIcon className="me-2" status={g2gProgress.mongo} /> MongoDB
+            <G2GDataTransferStatusIcon
+              className="me-2"
+              status={g2gProgress.mongo}
+            />{' '}
+            MongoDB
           </div>
           </div>
           <div className="my-2">
           <div className="my-2">
-            <G2GDataTransferStatusIcon className="me-2" status={g2gProgress.attachments} /> Attachments
+            <G2GDataTransferStatusIcon
+              className="me-2"
+              status={g2gProgress.attachments}
+            />{' '}
+            Attachments
           </div>
           </div>
         </div>
         </div>
       )}
       )}
 
 
-      <h2 className="border-bottom mt-5">{t('commons:g2g_data_transfer.transfer_data_to_this_growi')}</h2>
+      <h2 className="border-bottom mt-5">
+        {t('commons:g2g_data_transfer.transfer_data_to_this_growi')}
+      </h2>
 
 
       <div className="row mt-4">
       <div className="row mt-4">
         <div className="col-md-3">
         <div className="col-md-3">
-          <button type="button" className="btn btn-primary w-100" onClick={onClickHandler}>
+          <button
+            type="button"
+            className="btn btn-primary w-100"
+            onClick={onClickHandler}
+          >
             {t('commons:g2g_data_transfer.publish_transfer_key')}
             {t('commons:g2g_data_transfer.publish_transfer_key')}
           </button>
           </button>
         </div>
         </div>
         <div className="col-md-9">
         <div className="col-md-9">
           <div className=" mx-1">
           <div className=" mx-1">
-            <input className="form-control" type="text" value={transferKey} readOnly />
-            <CustomCopyToClipBoard textToBeCopied={transferKey} message="admin:slack_integration.copied_to_clipboard" />
+            <input
+              className="form-control"
+              type="text"
+              value={transferKey}
+              readOnly
+            />
+            <CustomCopyToClipBoard
+              textToBeCopied={transferKey}
+              message="admin:slack_integration.copied_to_clipboard"
+            />
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="alert alert-warning mt-4">
       <div className="alert alert-warning mt-4">
-        <p className="mb-1">{t('commons:g2g_data_transfer.transfer_key_limit')}</p>
-        <p className="mb-1">{t('commons:g2g_data_transfer.once_transfer_key_used')}</p>
+        <p className="mb-1">
+          {t('commons:g2g_data_transfer.transfer_key_limit')}
+        </p>
+        <p className="mb-1">
+          {t('commons:g2g_data_transfer.once_transfer_key_used')}
+        </p>
         <p
         <p
           className="mb-0"
           className="mb-0"
-          // eslint-disable-next-line react/no-danger
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML link
           dangerouslySetInnerHTML={{
           dangerouslySetInnerHTML={{
-            __html: t('commons:g2g_data_transfer.transfer_to_growi_cloud', { documentationUrl }),
+            __html: t('commons:g2g_data_transfer.transfer_to_growi_cloud', {
+              documentationUrl,
+            }),
           }}
           }}
         />
         />
       </div>
       </div>

+ 309 - 136
apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx

@@ -1,7 +1,10 @@
 import React, {
 import React, {
-  useState, useEffect, useCallback, useMemo, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
 } from 'react';
 } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
@@ -9,41 +12,210 @@ import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 
 
 import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
 import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportData/GrowiArchive/ImportCollectionItem';
+import ImportCollectionItem, {
+  DEFAULT_MODE,
+  MODE_RESTRICTED_COLLECTION,
+} from './ImportData/GrowiArchive/ImportCollectionItem';
 
 
-const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
-];
+const GROUPS_PAGE = ['pages', 'revisions', 'tags', 'pagetagrelations'];
 const GROUPS_USER = [
 const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
-];
-const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
+  'users',
+  'externalaccounts',
+  'usergroups',
+  'usergrouprelations',
 ];
 ];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
-
-const IMPORT_OPTION_CLASS_MAPPING: Record<string, typeof GrowiArchiveImportOption> = {
+const GROUPS_CONFIG = ['configs', 'updateposts', 'globalnotificationsettings'];
+const ALL_GROUPED_COLLECTIONS =
+  GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+
+const IMPORT_OPTION_CLASS_MAPPING: Record<
+  string,
+  typeof GrowiArchiveImportOption
+> = {
   pages: ImportOptionForPages,
   pages: ImportOptionForPages,
   revisions: ImportOptionForRevisions,
   revisions: ImportOptionForRevisions,
 };
 };
 
 
 type Props = {
 type Props = {
-  allCollectionNames: string[],
-  selectedCollections: Set<string>,
-  updateSelectedCollections: (newSelectedCollections: Set<string>) => void,
-  optionsMap: any,
-  updateOptionsMap: (newOptionsMap: any) => void,
+  allCollectionNames: string[];
+  selectedCollections: Set<string>;
+  updateSelectedCollections: (newSelectedCollections: Set<string>) => void;
+  optionsMap: any;
+  updateOptionsMap: (newOptionsMap: any) => void;
+};
+
+type ImportItemsProps = {
+  collectionNames: string[];
+  selectedCollections: Set<string>;
+  optionsMap: Record<string, GrowiArchiveImportOption>;
+  onToggleCollection: (collectionName: string, isChecked: boolean) => void;
+  onOptionChange: (collectionName: string, data: any) => void;
+  onConfigButtonClicked: (collectionName: string) => void;
+};
+
+const ImportItems = ({
+  collectionNames,
+  selectedCollections,
+  optionsMap,
+  onToggleCollection,
+  onOptionChange,
+  onConfigButtonClicked,
+}: ImportItemsProps): JSX.Element => {
+  return (
+    <div className="row">
+      {collectionNames.map((collectionName) => {
+        const isConfigButtonAvailable = Object.keys(
+          IMPORT_OPTION_CLASS_MAPPING,
+        ).includes(collectionName);
+
+        if (optionsMap[collectionName] == null) {
+          return null;
+        }
+
+        return (
+          <div className="col-md-6 my-1" key={collectionName}>
+            <ImportCollectionItem
+              isImporting={false}
+              isImported={false}
+              insertedCount={0}
+              modifiedCount={0}
+              errorsCount={0}
+              collectionName={collectionName}
+              isSelected={selectedCollections.has(collectionName)}
+              option={optionsMap[collectionName]}
+              isConfigButtonAvailable={isConfigButtonAvailable}
+              // events
+              onChange={onToggleCollection}
+              onOptionChange={onOptionChange}
+              onConfigButtonClicked={onConfigButtonClicked}
+              // TODO: show progress
+              isHideProgress
+            />
+          </div>
+        );
+      })}
+    </div>
+  );
+};
+
+type WarnForGroupsProps = {
+  errors: Error[];
+};
+
+const WarnForGroups = ({ errors }: WarnForGroupsProps): JSX.Element => {
+  if (errors.length === 0) {
+    return <></>;
+  }
+
+  return (
+    <div className="alert alert-warning">
+      <ul>
+        {errors.map((error, index) => {
+          return <li key={`${error.message}-${index}`}>{error.message}</li>;
+        })}
+      </ul>
+    </div>
+  );
+};
+
+type GroupImportItemsProps = {
+  groupList: string[];
+  groupName: string;
+  errors: Error[];
+  allCollectionNames: string[];
+  selectedCollections: Set<string>;
+  optionsMap: Record<string, GrowiArchiveImportOption>;
+  onToggleCollection: (collectionName: string, isChecked: boolean) => void;
+  onOptionChange: (collectionName: string, data: any) => void;
+  onConfigButtonClicked: (collectionName: string) => void;
+};
+
+const GroupImportItems = ({
+  groupList,
+  groupName,
+  errors,
+  allCollectionNames,
+  selectedCollections,
+  optionsMap,
+  onToggleCollection,
+  onOptionChange,
+  onConfigButtonClicked,
+}: GroupImportItemsProps): JSX.Element => {
+  const collectionNames = groupList.filter((groupCollectionName) => {
+    return allCollectionNames.includes(groupCollectionName);
+  });
+
+  if (collectionNames.length === 0) {
+    return <></>;
+  }
+
+  return (
+    <div className="mt-4">
+      <legend>{groupName} Collections</legend>
+      <ImportItems
+        collectionNames={collectionNames}
+        selectedCollections={selectedCollections}
+        optionsMap={optionsMap}
+        onToggleCollection={onToggleCollection}
+        onOptionChange={onOptionChange}
+        onConfigButtonClicked={onConfigButtonClicked}
+      />
+      <WarnForGroups errors={errors} />
+    </div>
+  );
+};
+
+type OtherImportItemsProps = {
+  allCollectionNames: string[];
+  selectedCollections: Set<string>;
+  optionsMap: Record<string, GrowiArchiveImportOption>;
+  onToggleCollection: (collectionName: string, isChecked: boolean) => void;
+  onOptionChange: (collectionName: string, data: any) => void;
+  onConfigButtonClicked: (collectionName: string) => void;
+};
+
+const OtherImportItems = ({
+  allCollectionNames,
+  selectedCollections,
+  optionsMap,
+  onToggleCollection,
+  onOptionChange,
+  onConfigButtonClicked,
+}: OtherImportItemsProps): JSX.Element => {
+  const collectionNames = allCollectionNames.filter((collectionName) => {
+    return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
+  });
+
+  // TODO: エラー対応
+  return (
+    <GroupImportItems
+      groupList={collectionNames}
+      groupName="Other"
+      errors={[]}
+      allCollectionNames={allCollectionNames}
+      selectedCollections={selectedCollections}
+      optionsMap={optionsMap}
+      onToggleCollection={onToggleCollection}
+      onOptionChange={onOptionChange}
+      onConfigButtonClicked={onConfigButtonClicked}
+    />
+  );
 };
 };
 
 
 const G2GDataTransferExportForm = (props: Props): JSX.Element => {
 const G2GDataTransferExportForm = (props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
   const {
   const {
-    allCollectionNames, selectedCollections, updateSelectedCollections, optionsMap, updateOptionsMap,
+    allCollectionNames,
+    selectedCollections,
+    updateSelectedCollections,
+    optionsMap,
+    updateOptionsMap,
   } = props;
   } = props;
 
 
   const [isConfigurationModalOpen, setConfigurationModalOpen] = useState(false);
   const [isConfigurationModalOpen, setConfigurationModalOpen] = useState(false);
-  const [collectionNameForConfiguration, setCollectionNameForConfiguration] = useState<any>();
+  const [collectionNameForConfiguration, setCollectionNameForConfiguration] =
+    useState<any>();
 
 
   const checkAll = useCallback(() => {
   const checkAll = useCallback(() => {
     updateSelectedCollections(new Set(allCollectionNames));
     updateSelectedCollections(new Set(allCollectionNames));
@@ -53,26 +225,28 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     updateSelectedCollections(new Set());
     updateSelectedCollections(new Set());
   }, [updateSelectedCollections]);
   }, [updateSelectedCollections]);
 
 
-  const updateOption = useCallback((collectionName, data) => {
-    const options = optionsMap[collectionName];
+  const updateOption = useCallback(
+    (collectionName, data) => {
+      const options = optionsMap[collectionName];
 
 
-    // merge
-    Object.assign(options, data);
+      // merge
+      Object.assign(options, data);
 
 
-    const updatedOptionsMap = {};
-    updatedOptionsMap[collectionName] = options;
-    updateOptionsMap((prev) => {
-      return { ...prev, updatedOptionsMap };
-    });
-  }, [optionsMap, updateOptionsMap]);
+      const updatedOptionsMap = {};
+      updatedOptionsMap[collectionName] = options;
+      updateOptionsMap((prev) => {
+        return { ...prev, updatedOptionsMap };
+      });
+    },
+    [optionsMap, updateOptionsMap],
+  );
 
 
-  const ImportItems = ({ collectionNames }): JSX.Element => {
-    const toggleCheckbox = (collectionName, bool) => {
+  const toggleCheckbox = useCallback(
+    (collectionName, bool) => {
       const collections = new Set(selectedCollections);
       const collections = new Set(selectedCollections);
       if (bool) {
       if (bool) {
         collections.add(collectionName);
         collections.add(collectionName);
-      }
-      else {
+      } else {
         collections.delete(collectionName);
         collections.delete(collectionName);
       }
       }
 
 
@@ -80,90 +254,14 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
 
 
       // TODO: validation
       // TODO: validation
       // this.validate();
       // this.validate();
-    };
-
-    const openConfigurationModal = (collectionName) => {
-      setConfigurationModalOpen(true);
-      setCollectionNameForConfiguration(collectionName);
-    };
-
-    return (
-      <div className="row">
-        {collectionNames.map((collectionName) => {
-          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
-
-          if (optionsMap[collectionName] == null) {
-            return null;
-          }
-
-          return (
-            <div className="col-md-6 my-1" key={collectionName}>
-              <ImportCollectionItem
-                isImporting={false}
-                isImported={false}
-                insertedCount={0}
-                modifiedCount={0}
-                errorsCount={0}
-                collectionName={collectionName}
-                isSelected={selectedCollections.has(collectionName)}
-                option={optionsMap[collectionName]}
-                isConfigButtonAvailable={isConfigButtonAvailable}
-                // events
-                onChange={toggleCheckbox}
-                onOptionChange={updateOption}
-                onConfigButtonClicked={openConfigurationModal}
-                // TODO: show progress
-                isHideProgress
-              />
-            </div>
-          );
-        })}
-      </div>
-    );
-  };
-
-  const WarnForGroups = ({ errors }: { errors: Error[] }): JSX.Element => {
-    if (errors.length === 0) {
-      return <></>;
-    }
-
-    return (
-      <div className="alert alert-warning">
-        <ul>
-          {errors.map((error) => {
-            return <li>{error.message}</li>;
-          })}
-        </ul>
-      </div>
-    );
-  };
-
-  const GroupImportItems = ({ groupList, groupName, errors }): JSX.Element => {
-    const collectionNames = groupList.filter((groupCollectionName) => {
-      return allCollectionNames.includes(groupCollectionName);
-    });
-
-    if (collectionNames.length === 0) {
-      return <></>;
-    }
-
-    return (
-      <div className="mt-4">
-        <legend>{groupName} Collections</legend>
-        <ImportItems collectionNames={collectionNames} />
-        <WarnForGroups errors={errors} />
-      </div>
-    );
-  };
-
-  const OtherImportItems = (): JSX.Element => {
-    const collectionNames = allCollectionNames.filter((collectionName) => {
-      return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
-    });
+    },
+    [selectedCollections, updateSelectedCollections],
+  );
 
 
-    // TODO: エラー対応
-    return <GroupImportItems groupList={collectionNames} groupName="Other" errors={[]} />;
-  };
+  const openConfigurationModal = useCallback((collectionName) => {
+    setConfigurationModalOpen(true);
+    setCollectionNameForConfiguration(collectionName);
+  }, []);
 
 
   const configurationModal = useMemo(() => {
   const configurationModal = useMemo(() => {
     if (collectionNameForConfiguration == null) {
     if (collectionNameForConfiguration == null) {
@@ -179,55 +277,130 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
         option={optionsMap[collectionNameForConfiguration]}
         option={optionsMap[collectionNameForConfiguration]}
       />
       />
     );
     );
-  }, [collectionNameForConfiguration, isConfigurationModalOpen, optionsMap, updateOption]);
+  }, [
+    collectionNameForConfiguration,
+    isConfigurationModalOpen,
+    optionsMap,
+    updateOption,
+  ]);
 
 
   const setInitialOptionsMap = useCallback(() => {
   const setInitialOptionsMap = useCallback(() => {
     const initialOptionsMap = {};
     const initialOptionsMap = {};
     allCollectionNames.forEach((collectionName) => {
     allCollectionNames.forEach((collectionName) => {
-      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
-        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
-        : DEFAULT_MODE;
-      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
-      initialOptionsMap[collectionName] = new ImportOption(collectionName, initialMode);
+      const initialMode =
+        MODE_RESTRICTED_COLLECTION[collectionName] != null
+          ? MODE_RESTRICTED_COLLECTION[collectionName][0]
+          : DEFAULT_MODE;
+      const ImportOption =
+        IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
+      initialOptionsMap[collectionName] = new ImportOption(
+        collectionName,
+        initialMode,
+      );
     });
     });
     updateOptionsMap(initialOptionsMap);
     updateOptionsMap(initialOptionsMap);
   }, [allCollectionNames, updateOptionsMap]);
   }, [allCollectionNames, updateOptionsMap]);
 
 
   useEffect(() => {
   useEffect(() => {
     setInitialOptionsMap();
     setInitialOptionsMap();
-  }, []);
+  }, [setInitialOptionsMap]);
 
 
   return (
   return (
     <>
     <>
       <form className="mt-3 row row-cols-lg-auto g-3 align-items-center">
       <form className="mt-3 row row-cols-lg-auto g-3 align-items-center">
         <div className="col-12">
         <div className="col-12">
-          <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-            <span className="material-symbols-outlined">check_box</span>, {t('admin:export_management.check_all')}
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary me-2"
+            onClick={checkAll}
+          >
+            <span className="material-symbols-outlined">check_box</span>,{' '}
+            {t('admin:export_management.check_all')}
           </button>
           </button>
         </div>
         </div>
         <div className="col-12">
         <div className="col-12">
-          <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-            <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary me-2"
+            onClick={uncheckAll}
+          >
+            <span className="material-symbols-outlined">
+              check_box_outline_blank
+            </span>{' '}
+            {t('admin:export_management.uncheck_all')}
           </button>
           </button>
         </div>
         </div>
       </form>
       </form>
 
 
       <div className="card custom-card small my-4">
       <div className="card custom-card small my-4">
         <ul>
         <ul>
-          <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
+          <li>
+            {t(
+              'admin:importer_management.growi_settings.description_of_import_mode.about',
+            )}
+          </li>
           <ul>
           <ul>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.insert')}</li>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}</li>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}</li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.insert',
+              )}
+            </li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.upsert',
+              )}
+            </li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert',
+              )}
+            </li>
           </ul>
           </ul>
         </ul>
         </ul>
       </div>
       </div>
 
 
       {/* TODO: エラー追加 */}
       {/* TODO: エラー追加 */}
-      <GroupImportItems groupList={GROUPS_PAGE} groupName="Page" errors={[]} />
-      <GroupImportItems groupList={GROUPS_USER} groupName="User" errors={[]} />
-      <GroupImportItems groupList={GROUPS_CONFIG} groupName="Config" errors={[]} />
-      <OtherImportItems />
+      <GroupImportItems
+        groupList={GROUPS_PAGE}
+        groupName="Page"
+        errors={[]}
+        allCollectionNames={allCollectionNames}
+        selectedCollections={selectedCollections}
+        optionsMap={optionsMap}
+        onToggleCollection={toggleCheckbox}
+        onOptionChange={updateOption}
+        onConfigButtonClicked={openConfigurationModal}
+      />
+      <GroupImportItems
+        groupList={GROUPS_USER}
+        groupName="User"
+        errors={[]}
+        allCollectionNames={allCollectionNames}
+        selectedCollections={selectedCollections}
+        optionsMap={optionsMap}
+        onToggleCollection={toggleCheckbox}
+        onOptionChange={updateOption}
+        onConfigButtonClicked={openConfigurationModal}
+      />
+      <GroupImportItems
+        groupList={GROUPS_CONFIG}
+        groupName="Config"
+        errors={[]}
+        allCollectionNames={allCollectionNames}
+        selectedCollections={selectedCollections}
+        optionsMap={optionsMap}
+        onToggleCollection={toggleCheckbox}
+        onOptionChange={updateOption}
+        onConfigButtonClicked={openConfigurationModal}
+      />
+      <OtherImportItems
+        allCollectionNames={allCollectionNames}
+        selectedCollections={selectedCollections}
+        optionsMap={optionsMap}
+        onToggleCollection={toggleCheckbox}
+        onOptionChange={updateOption}
+        onConfigButtonClicked={openConfigurationModal}
+      />
 
 
       {configurationModal}
       {configurationModal}
     </>
     </>

+ 49 - 10
apps/app/src/client/components/Admin/G2GDataTransferStatusIcon.tsx

@@ -1,46 +1,85 @@
 import React, { type ComponentPropsWithoutRef, type JSX } from 'react';
 import React, { type ComponentPropsWithoutRef, type JSX } from 'react';
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
 
-import { G2G_PROGRESS_STATUS, type G2GProgressStatus } from '~/interfaces/g2g-transfer';
-
+import {
+  G2G_PROGRESS_STATUS,
+  type G2GProgressStatus,
+} from '~/interfaces/g2g-transfer';
 
 
 /**
 /**
  * Props for {@link G2GDataTransferStatusIcon}
  * Props for {@link G2GDataTransferStatusIcon}
  */
  */
-interface Props extends ComponentPropsWithoutRef<'span'>{
+interface Props extends ComponentPropsWithoutRef<'span'> {
   status: G2GProgressStatus;
   status: G2GProgressStatus;
 }
 }
 
 
 /**
 /**
  * Icon for G2G transfer status
  * Icon for G2G transfer status
  */
  */
-const G2GDataTransferStatusIcon = ({ status, className, ...props }: Props): JSX.Element => {
+const G2GDataTransferStatusIcon = ({
+  status,
+  className,
+  ...props
+}: Props): JSX.Element => {
   if (status === G2G_PROGRESS_STATUS.IN_PROGRESS) {
   if (status === G2G_PROGRESS_STATUS.IN_PROGRESS) {
     return (
     return (
-      <LoadingSpinner className={`${className}`} aria-label="in progress" {...props} />
+      <LoadingSpinner
+        className={`${className}`}
+        aria-label="in progress"
+        {...props}
+      />
     );
     );
   }
   }
 
 
   if (status === G2G_PROGRESS_STATUS.COMPLETED) {
   if (status === G2G_PROGRESS_STATUS.COMPLETED) {
     return (
     return (
-      <span className={`material-symbols-outlined text-info ${className}`} aria-label="completed" {...props}>check_circle</span>
+      <span
+        className={`material-symbols-outlined text-info ${className}`}
+        role="img"
+        aria-label="completed"
+        {...props}
+      >
+        check_circle
+      </span>
     );
     );
   }
   }
 
 
   if (status === G2G_PROGRESS_STATUS.ERROR) {
   if (status === G2G_PROGRESS_STATUS.ERROR) {
     return (
     return (
-      <span className={`material-symbols-outlined text-danger ${className}`} aria-label="error" {...props}>error</span>
+      <span
+        className={`material-symbols-outlined text-danger ${className}`}
+        role="img"
+        aria-label="error"
+        {...props}
+      >
+        error
+      </span>
     );
     );
   }
   }
 
 
   if (status === G2G_PROGRESS_STATUS.SKIPPED) {
   if (status === G2G_PROGRESS_STATUS.SKIPPED) {
     return (
     return (
-      <span className={`material-symbols-outlined ${className}`} aria-label="skipped" {...props}>block</span>
+      <span
+        className={`material-symbols-outlined ${className}`}
+        role="img"
+        aria-label="skipped"
+        {...props}
+      >
+        block
+      </span>
     );
     );
   }
   }
 
 
-  return <span className={`material-symbols-outlined ${className}`} aria-label="pending" {...props}>circle</span>;
+  return (
+    <span
+      className={`material-symbols-outlined ${className}`}
+      role="img"
+      aria-label="pending"
+      {...props}
+    >
+      circle
+    </span>
+  );
 };
 };
 
 
 export default G2GDataTransferStatusIcon;
 export default G2GDataTransferStatusIcon;

+ 12 - 7
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx

@@ -1,12 +1,11 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 
 type ErrorViewerProps = {
 type ErrorViewerProps = {
-  isOpen: boolean,
-  errors: any[],
-  onClose: () => void,
-}
+  isOpen: boolean;
+  errors: any[];
+  onClose: () => void;
+};
 
 
 const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
 const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
   const { errors } = props;
   const { errors } = props;
@@ -25,7 +24,13 @@ const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
         Errors
         Errors
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        <textarea className="form-control" rows={8} readOnly wrap="off" defaultValue={value}></textarea>
+        <textarea
+          className="form-control"
+          rows={8}
+          readOnly
+          wrap="off"
+          defaultValue={value}
+        ></textarea>
       </ModalBody>
       </ModalBody>
     </Modal>
     </Modal>
   );
   );

+ 106 - 36
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -1,23 +1,15 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 
 
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-  ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
 
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 // import { toastSuccess, toastError } from '~/client/util/toastr';
 
 
-
 class ImportCollectionConfigurationModal extends React.Component {
 class ImportCollectionConfigurationModal extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -46,9 +38,7 @@ class ImportCollectionConfigurationModal extends React.Component {
   }
   }
 
 
   updateOption() {
   updateOption() {
-    const {
-      collectionName, onOptionChange, onClose,
-    } = this.props;
+    const { collectionName, onOptionChange, onClose } = this.props;
 
 
     if (onOptionChange != null) {
     if (onOptionChange != null) {
       onOptionChange(collectionName, this.state.option);
       onOptionChange(collectionName, this.state.option);
@@ -61,7 +51,8 @@ class ImportCollectionConfigurationModal extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     const { option } = this.state;
     const { option } = this.state;
 
 
-    const translationBase = 'admin:importer_management.growi_settings.configuration.pages';
+    const translationBase =
+      'admin:importer_management.growi_settings.configuration.pages';
 
 
     /* eslint-disable react/no-unescaped-entities */
     /* eslint-disable react/no-unescaped-entities */
     return (
     return (
@@ -72,11 +63,22 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             type="checkbox"
             className="form-check-input"
             className="form-check-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
+            onChange={() =>
+              this.changeHandler({
+                isOverwriteAuthorWithCurrentUser:
+                  !option.isOverwriteAuthorWithCurrentUser,
+              })
+            }
           />
           />
           <label htmlFor="cbOpt4" className="form-label form-check-label">
           <label htmlFor="cbOpt4" className="form-label form-check-label">
             {t(`${translationBase}.overwrite_author.label`)}
             {t(`${translationBase}.overwrite_author.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
+            <p
+              className="form-text text-muted mt-0"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.overwrite_author.desc`),
+              }}
+            />
           </label>
           </label>
         </div>
         </div>
         <div className="form-check form-check-warning">
         <div className="form-check form-check-warning">
@@ -85,13 +87,24 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             type="checkbox"
             className="form-check-input"
             className="form-check-input"
             checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant2: !option.makePublicForGrant2 })}
+            onChange={() =>
+              this.changeHandler({
+                makePublicForGrant2: !option.makePublicForGrant2,
+              })
+            }
           />
           />
           <label htmlFor="cbOpt1" className="form-label form-check-label">
           <label htmlFor="cbOpt1" className="form-label form-check-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Anyone with the link') })}
+            {t(`${translationBase}.set_public_to_page.label`, {
+              from: t('Anyone with the link'),
+            })}
             <p
             <p
               className="form-text text-muted mt-0"
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Anyone with the link') }) }}
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.set_public_to_page.desc`, {
+                  from: t('Anyone with the link'),
+                }),
+              }}
             />
             />
           </label>
           </label>
         </div>
         </div>
@@ -101,13 +114,24 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             type="checkbox"
             className="form-check-input"
             className="form-check-input"
             checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
+            onChange={() =>
+              this.changeHandler({
+                makePublicForGrant4: !option.makePublicForGrant4,
+              })
+            }
           />
           />
           <label htmlFor="cbOpt2" className="form-label form-check-label">
           <label htmlFor="cbOpt2" className="form-label form-check-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only me') })}
+            {t(`${translationBase}.set_public_to_page.label`, {
+              from: t('Only me'),
+            })}
             <p
             <p
               className="form-text text-muted mt-0"
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only me') }) }}
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.set_public_to_page.desc`, {
+                  from: t('Only me'),
+                }),
+              }}
             />
             />
           </label>
           </label>
         </div>
         </div>
@@ -117,13 +141,24 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             type="checkbox"
             className="form-check-input"
             className="form-check-input"
             checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ makePublicForGrant5: !option.makePublicForGrant5 })}
+            onChange={() =>
+              this.changeHandler({
+                makePublicForGrant5: !option.makePublicForGrant5,
+              })
+            }
           />
           />
           <label htmlFor="cbOpt3" className="form-label form-check-label">
           <label htmlFor="cbOpt3" className="form-label form-check-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only inside the group') })}
+            {t(`${translationBase}.set_public_to_page.label`, {
+              from: t('Only inside the group'),
+            })}
             <p
             <p
               className="form-text text-muted mt-0"
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only inside the group') }) }}
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.set_public_to_page.desc`, {
+                  from: t('Only inside the group'),
+                }),
+              }}
             />
             />
           </label>
           </label>
         </div>
         </div>
@@ -133,11 +168,21 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             type="checkbox"
             className="form-check-input"
             className="form-check-input"
             checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })}
+            onChange={() =>
+              this.changeHandler({
+                initPageMetadatas: !option.initPageMetadatas,
+              })
+            }
           />
           />
           <label htmlFor="cbOpt5" className="form-label form-check-label">
           <label htmlFor="cbOpt5" className="form-label form-check-label">
             {t(`${translationBase}.initialize_meta_datas.label`)}
             {t(`${translationBase}.initialize_meta_datas.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
+            <p
+              className="form-text text-muted mt-0"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.initialize_meta_datas.desc`),
+              }}
+            />
           </label>
           </label>
         </div>
         </div>
       </>
       </>
@@ -149,7 +194,8 @@ class ImportCollectionConfigurationModal extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     const { option } = this.state;
     const { option } = this.state;
 
 
-    const translationBase = 'admin:importer_management.growi_settings.configuration.revisions';
+    const translationBase =
+      'admin:importer_management.growi_settings.configuration.revisions';
 
 
     /* eslint-disable react/no-unescaped-entities */
     /* eslint-disable react/no-unescaped-entities */
     return (
     return (
@@ -160,11 +206,22 @@ class ImportCollectionConfigurationModal extends React.Component {
             type="checkbox"
             type="checkbox"
             className="form-check-input"
             className="form-check-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
+            onChange={() =>
+              this.changeHandler({
+                isOverwriteAuthorWithCurrentUser:
+                  !option.isOverwriteAuthorWithCurrentUser,
+              })
+            }
           />
           />
           <label htmlFor="cbOpt1" className="form-label form-check-label">
           <label htmlFor="cbOpt1" className="form-label form-check-label">
             {t(`${translationBase}.overwrite_author.label`)}
             {t(`${translationBase}.overwrite_author.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
+            <p
+              className="form-text text-muted mt-0"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+              dangerouslySetInnerHTML={{
+                __html: t(`${translationBase}.overwrite_author.desc`),
+              }}
+            />
           </label>
           </label>
         </div>
         </div>
       </>
       </>
@@ -189,23 +246,36 @@ class ImportCollectionConfigurationModal extends React.Component {
     }
     }
 
 
     return (
     return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} onEnter={this.initialize}>
+      <Modal
+        isOpen={this.props.isOpen}
+        toggle={this.props.onClose}
+        onEnter={this.initialize}
+      >
         <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
         <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
           {`'${collectionName}'`} Configuration
           {`'${collectionName}'`} Configuration
         </ModalHeader>
         </ModalHeader>
 
 
-        <ModalBody>
-          {contents}
-        </ModalBody>
+        <ModalBody>{contents}</ModalBody>
 
 
         <ModalFooter>
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('Cancel')}</button>
-          <button type="button" className="btn btn-sm btn-primary" onClick={this.updateOption}>{t('Update')}</button>
+          <button
+            type="button"
+            className="btn btn-sm btn-outline-secondary"
+            onClick={this.props.onClose}
+          >
+            {t('Cancel')}
+          </button>
+          <button
+            type="button"
+            className="btn btn-sm btn-primary"
+            onClick={this.updateOption}
+          >
+            {t('Update')}
+          </button>
         </ModalFooter>
         </ModalFooter>
       </Modal>
       </Modal>
     );
     );
   }
   }
-
 }
 }
 
 
 ImportCollectionConfigurationModal.propTypes = {
 ImportCollectionConfigurationModal.propTypes = {

+ 86 - 34
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -1,17 +1,23 @@
 import React from 'react';
 import React from 'react';
-
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import {
 import {
-  Progress, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  Progress,
+  UncontrolledDropdown,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
 
-
 const MODE_ATTR_MAP = {
 const MODE_ATTR_MAP = {
   insert: { color: 'info', icon: 'add_circle', label: 'Insert' },
   insert: { color: 'info', icon: 'add_circle', label: 'Insert' },
   upsert: { color: 'success', icon: 'add_circle', label: 'Upsert' },
   upsert: { color: 'success', icon: 'add_circle', label: 'Upsert' },
-  flushAndInsert: { color: 'danger', icon: 'autorenew', label: 'Flush and Insert' },
+  flushAndInsert: {
+    color: 'danger',
+    icon: 'autorenew',
+    label: 'Flush and Insert',
+  },
 };
 };
 
 
 export const DEFAULT_MODE = 'insert';
 export const DEFAULT_MODE = 'insert';
@@ -23,13 +29,13 @@ export const MODE_RESTRICTED_COLLECTION = {
 };
 };
 
 
 export default class ImportCollectionItem extends React.Component {
 export default class ImportCollectionItem extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.changeHandler = this.changeHandler.bind(this);
     this.changeHandler = this.changeHandler.bind(this);
     this.modeSelectedHandler = this.modeSelectedHandler.bind(this);
     this.modeSelectedHandler = this.modeSelectedHandler.bind(this);
-    this.configButtonClickedHandler = this.configButtonClickedHandler.bind(this);
+    this.configButtonClickedHandler =
+      this.configButtonClickedHandler.bind(this);
     this.errorLinkClickedHandler = this.errorLinkClickedHandler.bind(this);
     this.errorLinkClickedHandler = this.errorLinkClickedHandler.bind(this);
   }
   }
 
 
@@ -76,13 +82,16 @@ export default class ImportCollectionItem extends React.Component {
   renderModeLabel(mode, isColorized = false) {
   renderModeLabel(mode, isColorized = false) {
     const attrMap = MODE_ATTR_MAP[mode];
     const attrMap = MODE_ATTR_MAP[mode];
     const className = isColorized ? `text-${attrMap.color}` : '';
     const className = isColorized ? `text-${attrMap.color}` : '';
-    return <span className={`text-nowrap ${className}`}><span className="material-symbols-outlined">{attrMap.icon}</span> {attrMap.label}</span>;
+    return (
+      <span className={`text-nowrap ${className}`}>
+        <span className="material-symbols-outlined">{attrMap.icon}</span>{' '}
+        {attrMap.label}
+      </span>
+    );
   }
   }
 
 
   renderCheckbox() {
   renderCheckbox() {
-    const {
-      collectionName, isSelected, isImporting,
-    } = this.props;
+    const { collectionName, isSelected, isImporting } = this.props;
 
 
     return (
     return (
       <div className="form-check form-check-info my-0">
       <div className="form-check form-check-info my-0">
@@ -96,7 +105,10 @@ export default class ImportCollectionItem extends React.Component {
           disabled={isImporting}
           disabled={isImporting}
           onChange={this.changeHandler}
           onChange={this.changeHandler}
         />
         />
-        <label className="form-label text-capitalize form-check-label" htmlFor={collectionName}>
+        <label
+          className="form-label text-capitalize form-check-label"
+          htmlFor={collectionName}
+        >
           {collectionName}
           {collectionName}
         </label>
         </label>
       </div>
       </div>
@@ -104,22 +116,26 @@ export default class ImportCollectionItem extends React.Component {
   }
   }
 
 
   renderModeSelector() {
   renderModeSelector() {
-    const {
-      collectionName, option, isImporting,
-    } = this.props;
+    const { collectionName, option, isImporting } = this.props;
     const currentMode = option?.mode || 'insert';
     const currentMode = option?.mode || 'insert';
     const attrMap = MODE_ATTR_MAP[currentMode];
     const attrMap = MODE_ATTR_MAP[currentMode];
-    const modes = MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
+    const modes =
+      MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
 
 
     return (
     return (
       <span className="d-inline-flex align-items-center">
       <span className="d-inline-flex align-items-center">
         Mode:&nbsp;
         Mode:&nbsp;
         <UncontrolledDropdown size="sm" className="d-inline-block">
         <UncontrolledDropdown size="sm" className="d-inline-block">
-          <DropdownToggle color={attrMap.color} caret disabled={isImporting} id={`ddmMode-${collectionName}`}>
+          <DropdownToggle
+            color={attrMap.color}
+            caret
+            disabled={isImporting}
+            id={`ddmMode-${collectionName}`}
+          >
             {this.renderModeLabel(currentMode)}
             {this.renderModeLabel(currentMode)}
           </DropdownToggle>
           </DropdownToggle>
           <DropdownMenu>
           <DropdownMenu>
-            {modes.map(mode => (
+            {modes.map((mode) => (
               <DropdownItem
               <DropdownItem
                 key={`buttonMode_${mode}`}
                 key={`buttonMode_${mode}`}
                 onClick={() => this.modeSelectedHandler(mode)}
                 onClick={() => this.modeSelectedHandler(mode)}
@@ -141,7 +157,9 @@ export default class ImportCollectionItem extends React.Component {
         type="button"
         type="button"
         className="btn btn-outline-secondary btn-sm p-1 ms-2"
         className="btn btn-outline-secondary btn-sm p-1 ms-2"
         disabled={isImporting || !isConfigButtonAvailable}
         disabled={isImporting || !isConfigButtonAvailable}
-        onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
+        onClick={
+          isConfigButtonAvailable ? this.configButtonClickedHandler : null
+        }
       >
       >
         <span className="material-symbols-outlined">settings</span>
         <span className="material-symbols-outlined">settings</span>
       </button>
       </button>
@@ -149,17 +167,37 @@ export default class ImportCollectionItem extends React.Component {
   }
   }
 
 
   renderProgressBar() {
   renderProgressBar() {
-    const {
-      isImporting, insertedCount, modifiedCount, errorsCount,
-    } = this.props;
+    const { isImporting, insertedCount, modifiedCount, errorsCount } =
+      this.props;
 
 
     const total = insertedCount + modifiedCount + errorsCount;
     const total = insertedCount + modifiedCount + errorsCount;
 
 
     return (
     return (
       <Progress multi className="mb-0">
       <Progress multi className="mb-0">
-        <Progress bar max={total} color="info" striped={isImporting} animated={isImporting} value={insertedCount} />
-        <Progress bar max={total} color="success" striped={isImporting} animated={isImporting} value={modifiedCount} />
-        <Progress bar max={total} color="danger" striped={isImporting} animated={isImporting} value={errorsCount} />
+        <Progress
+          bar
+          max={total}
+          color="info"
+          striped={isImporting}
+          animated={isImporting}
+          value={insertedCount}
+        />
+        <Progress
+          bar
+          max={total}
+          color="success"
+          striped={isImporting}
+          animated={isImporting}
+          value={modifiedCount}
+        />
+        <Progress
+          bar
+          max={total}
+          color="danger"
+          striped={isImporting}
+          animated={isImporting}
+          value={errorsCount}
+        />
       </Progress>
       </Progress>
     );
     );
   }
   }
@@ -174,20 +212,35 @@ export default class ImportCollectionItem extends React.Component {
     const { insertedCount, modifiedCount, errorsCount } = this.props;
     const { insertedCount, modifiedCount, errorsCount } = this.props;
     return (
     return (
       <div className="w-100 text-center">
       <div className="w-100 text-center">
-        <span className="text-info"><strong>{insertedCount}</strong> Inserted</span>,&nbsp;
-        <span className="text-success"><strong>{modifiedCount}</strong> Modified</span>,&nbsp;
-        { errorsCount > 0
-          ? <a className="text-danger" role="button" onClick={this.errorLinkClickedHandler}><u><strong>{errorsCount}</strong> Failed</u></a>
-          : <span className="text-muted"><strong>0</strong> Failed</span>
-        }
+        <span className="text-info">
+          <strong>{insertedCount}</strong> Inserted
+        </span>
+        ,&nbsp;
+        <span className="text-success">
+          <strong>{modifiedCount}</strong> Modified
+        </span>
+        ,&nbsp;
+        {errorsCount > 0 ? (
+          <button
+            type="button"
+            className="btn btn-link text-danger p-0"
+            onClick={this.errorLinkClickedHandler}
+          >
+            <u>
+              <strong>{errorsCount}</strong> Failed
+            </u>
+          </button>
+        ) : (
+          <span className="text-muted">
+            <strong>0</strong> Failed
+          </span>
+        )}
       </div>
       </div>
     );
     );
   }
   }
 
 
   render() {
   render() {
-    const {
-      isSelected, isHideProgress,
-    } = this.props;
+    const { isSelected, isHideProgress } = this.props;
 
 
     return (
     return (
       <div className="card border-light">
       <div className="card border-light">
@@ -211,7 +264,6 @@ export default class ImportCollectionItem extends React.Component {
       </div>
       </div>
     );
     );
   }
   }
-
 }
 }
 
 
 ImportCollectionItem.propTypes = {
 ImportCollectionItem.propTypes = {

+ 164 - 76
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -1,31 +1,31 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { useAdminSocket } from '~/features/admin/states/socket-io';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForPages } from '~/models/admin/import-option-for-pages';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 import { ImportOptionForRevisions } from '~/models/admin/import-option-for-revisions';
 
 
-
 import ErrorViewer from './ErrorViewer';
 import ErrorViewer from './ErrorViewer';
 import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
 import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
+import ImportCollectionItem, {
+  DEFAULT_MODE,
+  MODE_RESTRICTED_COLLECTION,
+} from './ImportCollectionItem';
 
 
-
-const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
-];
+const GROUPS_PAGE = ['pages', 'revisions', 'tags', 'pagetagrelations'];
 const GROUPS_USER = [
 const GROUPS_USER = [
-  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
+  'users',
+  'externalaccounts',
+  'usergroups',
+  'usergrouprelations',
 ];
 ];
-const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
-];
-const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+const GROUPS_CONFIG = ['configs', 'updateposts', 'globalnotificationsettings'];
+const ALL_GROUPED_COLLECTIONS =
+  GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 
 
 /** @type Record<string, typeof GrowiArchiveImportOption> */
 /** @type Record<string, typeof GrowiArchiveImportOption> */
 const IMPORT_OPTION_CLASS_MAPPING = {
 const IMPORT_OPTION_CLASS_MAPPING = {
@@ -34,7 +34,6 @@ const IMPORT_OPTION_CLASS_MAPPING = {
 };
 };
 
 
 class ImportForm extends React.Component {
 class ImportForm extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -69,12 +68,17 @@ class ImportForm extends React.Component {
       this.initialState.collectionNameToFileNameMap[collectionName] = fileName;
       this.initialState.collectionNameToFileNameMap[collectionName] = fileName;
 
 
       // determine initial mode
       // determine initial mode
-      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
-        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
-        : DEFAULT_MODE;
+      const initialMode =
+        MODE_RESTRICTED_COLLECTION[collectionName] != null
+          ? MODE_RESTRICTED_COLLECTION[collectionName][0]
+          : DEFAULT_MODE;
       // create GrowiArchiveImportOption instance
       // create GrowiArchiveImportOption instance
-      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
-      this.initialState.optionsMap[collectionName] = new ImportOption(collectionName, initialMode);
+      const ImportOption =
+        IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
+      this.initialState.optionsMap[collectionName] = new ImportOption(
+        collectionName,
+        initialMode,
+      );
     });
     });
 
 
     this.state = this.initialState;
     this.state = this.initialState;
@@ -93,6 +97,7 @@ class ImportForm extends React.Component {
     return Object.keys(this.state.collectionNameToFileNameMap);
     return Object.keys(this.state.collectionNameToFileNameMap);
   }
   }
 
 
+  // biome-ignore lint/correctness/noNestedComponentDefinitions: lifecycle method on a class component
   UNSAFE_componentWillMount() {
   UNSAFE_componentWillMount() {
     this.setupWebsocketEventHandler();
     this.setupWebsocketEventHandler();
   }
   }
@@ -106,21 +111,24 @@ class ImportForm extends React.Component {
 
 
     // websocket event
     // websocket event
     // eslint-disable-next-line object-curly-newline
     // eslint-disable-next-line object-curly-newline
-    socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
-      const { progressMap, errorsMap } = this.state;
-      progressMap[collectionName] = collectionProgress;
-
-      if (appendedErrors != null) {
-        const errors = errorsMap[collectionName] || [];
-        errorsMap[collectionName] = errors.concat(appendedErrors);
-      }
-
-      this.setState({
-        isImporting: true,
-        progressMap,
-        errorsMap,
-      });
-    });
+    socket.on(
+      'admin:onProgressForImport',
+      ({ collectionName, collectionProgress, appendedErrors }) => {
+        const { progressMap, errorsMap } = this.state;
+        progressMap[collectionName] = collectionProgress;
+
+        if (appendedErrors != null) {
+          const errors = errorsMap[collectionName] || [];
+          errorsMap[collectionName] = errors.concat(appendedErrors);
+        }
+
+        this.setState({
+          isImporting: true,
+          progressMap,
+          errorsMap,
+        });
+      },
+    );
 
 
     // websocket event
     // websocket event
     socket.on('admin:onTerminateForImport', () => {
     socket.on('admin:onTerminateForImport', () => {
@@ -154,8 +162,7 @@ class ImportForm extends React.Component {
     const selectedCollections = new Set(this.state.selectedCollections);
     const selectedCollections = new Set(this.state.selectedCollections);
     if (bool) {
     if (bool) {
       selectedCollections.add(collectionName);
       selectedCollections.add(collectionName);
-    }
-    else {
+    } else {
       selectedCollections.delete(collectionName);
       selectedCollections.delete(collectionName);
     }
     }
 
 
@@ -165,7 +172,9 @@ class ImportForm extends React.Component {
   }
   }
 
 
   async checkAll() {
   async checkAll() {
-    await this.setState({ selectedCollections: new Set(this.allCollectionNames) });
+    await this.setState({
+      selectedCollections: new Set(this.allCollectionNames),
+    });
     this.validate();
     this.validate();
   }
   }
 
 
@@ -186,11 +195,17 @@ class ImportForm extends React.Component {
   }
   }
 
 
   openConfigurationModal(collectionName) {
   openConfigurationModal(collectionName) {
-    this.setState({ isConfigurationModalOpen: true, collectionNameForConfiguration: collectionName });
+    this.setState({
+      isConfigurationModalOpen: true,
+      collectionNameForConfiguration: collectionName,
+    });
   }
   }
 
 
   showErrorsViewer(collectionName) {
   showErrorsViewer(collectionName) {
-    this.setState({ isErrorsViewerOpen: true, collectionNameForErrorsViewer: collectionName });
+    this.setState({
+      isErrorsViewerOpen: true,
+      collectionNameForErrorsViewer: collectionName,
+    });
   }
   }
 
 
   async validate() {
   async validate() {
@@ -224,7 +239,9 @@ class ImportForm extends React.Component {
     const { warnForOtherGroups, selectedCollections } = this.state;
     const { warnForOtherGroups, selectedCollections } = this.state;
 
 
     if (selectedCollections.size === 0) {
     if (selectedCollections.size === 0) {
-      warnForOtherGroups.push(t('admin:importer_management.growi_settings.errors.at_least_one'));
+      warnForOtherGroups.push(
+        t('admin:importer_management.growi_settings.errors.at_least_one'),
+      );
     }
     }
 
 
     this.setState({ warnForOtherGroups });
     this.setState({ warnForOtherGroups });
@@ -234,13 +251,20 @@ class ImportForm extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     const { warnForPageGroups, selectedCollections } = this.state;
     const { warnForPageGroups, selectedCollections } = this.state;
 
 
-    const pageRelatedCollectionsLength = ['pages', 'revisions'].filter((collectionName) => {
-      return selectedCollections.has(collectionName);
-    }).length;
+    const pageRelatedCollectionsLength = ['pages', 'revisions'].filter(
+      (collectionName) => {
+        return selectedCollections.has(collectionName);
+      },
+    ).length;
 
 
     // MUST be included both or neither when importing
     // MUST be included both or neither when importing
-    if (pageRelatedCollectionsLength !== 0 && pageRelatedCollectionsLength !== 2) {
-      warnForPageGroups.push(t('admin:importer_management.growi_settings.errors.page_and_revision'));
+    if (
+      pageRelatedCollectionsLength !== 0 &&
+      pageRelatedCollectionsLength !== 2
+    ) {
+      warnForPageGroups.push(
+        t('admin:importer_management.growi_settings.errors.page_and_revision'),
+      );
     }
     }
 
 
     this.setState({ warnForPageGroups });
     this.setState({ warnForPageGroups });
@@ -253,7 +277,12 @@ class ImportForm extends React.Component {
     // MUST include also 'users' if 'externalaccounts' is selected
     // MUST include also 'users' if 'externalaccounts' is selected
     if (selectedCollections.has('externalaccounts')) {
     if (selectedCollections.has('externalaccounts')) {
       if (!selectedCollections.has('users')) {
       if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Externalaccounts' }));
+        warnForUserGroups.push(
+          t('admin:importer_management.growi_settings.errors.depends', {
+            target: 'Users',
+            condition: 'Externalaccounts',
+          }),
+        );
       }
       }
     }
     }
 
 
@@ -267,7 +296,12 @@ class ImportForm extends React.Component {
     // MUST include also 'users' if 'usergroups' is selected
     // MUST include also 'users' if 'usergroups' is selected
     if (selectedCollections.has('usergroups')) {
     if (selectedCollections.has('usergroups')) {
       if (!selectedCollections.has('users')) {
       if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Usergroups' }));
+        warnForUserGroups.push(
+          t('admin:importer_management.growi_settings.errors.depends', {
+            target: 'Users',
+            condition: 'Usergroups',
+          }),
+        );
       }
       }
     }
     }
 
 
@@ -281,7 +315,12 @@ class ImportForm extends React.Component {
     // MUST include also 'usergroups' if 'usergrouprelations' is selected
     // MUST include also 'usergroups' if 'usergrouprelations' is selected
     if (selectedCollections.has('usergrouprelations')) {
     if (selectedCollections.has('usergrouprelations')) {
       if (!selectedCollections.has('usergroups')) {
       if (!selectedCollections.has('usergroups')) {
-        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Usergroups', condition: 'Usergrouprelations' }));
+        warnForUserGroups.push(
+          t('admin:importer_management.growi_settings.errors.depends', {
+            target: 'Usergroups',
+            condition: 'Usergrouprelations',
+          }),
+        );
       }
       }
     }
     }
 
 
@@ -289,9 +328,7 @@ class ImportForm extends React.Component {
   }
   }
 
 
   async import() {
   async import() {
-    const {
-      fileName, onPostImport, t,
-    } = this.props;
+    const { fileName, onPostImport, t } = this.props;
     const { selectedCollections, optionsMap } = this.state;
     const { selectedCollections, optionsMap } = this.state;
 
 
     // init progress data
     // init progress data
@@ -314,8 +351,7 @@ class ImportForm extends React.Component {
       }
       }
 
 
       toastSuccess(undefined, 'Import process has requested.');
       toastSuccess(undefined, 'Import process has requested.');
-    }
-    catch (err) {
+    } catch (err) {
       if (err.code === 'only_upsert_available') {
       if (err.code === 'only_upsert_available') {
         toastError(t('admin:importer_management.error.only_upsert_available'));
         toastError(t('admin:importer_management.error.only_upsert_available'));
       }
       }
@@ -331,9 +367,8 @@ class ImportForm extends React.Component {
     return (
     return (
       <div key={key} className="alert alert-warning">
       <div key={key} className="alert alert-warning">
         <ul>
         <ul>
-          {errors.map((error, index) => {
-            // eslint-disable-next-line react/no-array-index-key
-            return <li key={`${key}-${index}`}>{error}</li>;
+          {errors.map((error) => {
+            return <li key={`${key}-${String(error)}`}>{error}</li>;
           })}
           })}
         </ul>
         </ul>
       </div>
       </div>
@@ -363,7 +398,11 @@ class ImportForm extends React.Component {
       return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
       return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
     });
     });
 
 
-    return this.renderGroups(collectionNames, 'Other', this.state.warnForOtherGroups);
+    return this.renderGroups(
+      collectionNames,
+      'Other',
+      this.state.warnForOtherGroups,
+    );
   }
   }
 
 
   renderImportItems(collectionNames) {
   renderImportItems(collectionNames) {
@@ -382,15 +421,21 @@ class ImportForm extends React.Component {
         {collectionNames.map((collectionName) => {
         {collectionNames.map((collectionName) => {
           const collectionProgress = progressMap[collectionName];
           const collectionProgress = progressMap[collectionName];
           const errorsCount = errorsMap[collectionName]?.length ?? 0;
           const errorsCount = errorsMap[collectionName]?.length ?? 0;
-          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
+          const isConfigButtonAvailable = Object.keys(
+            IMPORT_OPTION_CLASS_MAPPING,
+          ).includes(collectionName);
 
 
           return (
           return (
             <div className="col-md-6 my-1" key={collectionName}>
             <div className="col-md-6 my-1" key={collectionName}>
               <ImportCollectionItem
               <ImportCollectionItem
                 isImporting={isImporting}
                 isImporting={isImporting}
                 isImported={collectionProgress ? isImported : false}
                 isImported={collectionProgress ? isImported : false}
-                insertedCount={collectionProgress ? collectionProgress.insertedCount : 0}
-                modifiedCount={collectionProgress ? collectionProgress.modifiedCount : 0}
+                insertedCount={
+                  collectionProgress ? collectionProgress.insertedCount : 0
+                }
+                modifiedCount={
+                  collectionProgress ? collectionProgress.modifiedCount : 0
+                }
                 errorsCount={errorsCount}
                 errorsCount={errorsCount}
                 collectionName={collectionName}
                 collectionName={collectionName}
                 isSelected={selectedCollections.has(collectionName)}
                 isSelected={selectedCollections.has(collectionName)}
@@ -410,7 +455,11 @@ class ImportForm extends React.Component {
   }
   }
 
 
   renderConfigurationModal() {
   renderConfigurationModal() {
-    const { isConfigurationModalOpen, collectionNameForConfiguration: collectionName, optionsMap } = this.state;
+    const {
+      isConfigurationModalOpen,
+      collectionNameForConfiguration: collectionName,
+      optionsMap,
+    } = this.state;
 
 
     if (collectionName == null) {
     if (collectionName == null) {
       return null;
       return null;
@@ -428,7 +477,8 @@ class ImportForm extends React.Component {
   }
   }
 
 
   renderErrorsViewer() {
   renderErrorsViewer() {
-    const { isErrorsViewerOpen, errorsMap, collectionNameForErrorsViewer } = this.state;
+    const { isErrorsViewerOpen, errorsMap, collectionNameForErrorsViewer } =
+      this.state;
     const errors = errorsMap[collectionNameForErrorsViewer];
     const errors = errorsMap[collectionNameForErrorsViewer];
 
 
     return (
     return (
@@ -443,32 +493,63 @@ class ImportForm extends React.Component {
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
     const {
     const {
-      canImport, isImporting,
-      warnForPageGroups, warnForUserGroups, warnForConfigGroups,
+      canImport,
+      isImporting,
+      warnForPageGroups,
+      warnForUserGroups,
+      warnForConfigGroups,
     } = this.state;
     } = this.state;
 
 
     return (
     return (
       <>
       <>
         <form className="row row-cols-lg-auto g-3 align-items-center">
         <form className="row row-cols-lg-auto g-3 align-items-center">
           <div className="col-12">
           <div className="col-12">
-            <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.checkAll}>
-              <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
+            <button
+              type="button"
+              className="btn btn-sm btn-outline-secondary me-2"
+              onClick={this.checkAll}
+            >
+              <span className="material-symbols-outlined">check_box</span>{' '}
+              {t('admin:export_management.check_all')}
             </button>
             </button>
           </div>
           </div>
           <div className="col-12">
           <div className="col-12">
-            <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.uncheckAll}>
-              <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
+            <button
+              type="button"
+              className="btn btn-sm btn-outline-secondary me-2"
+              onClick={this.uncheckAll}
+            >
+              <span className="material-symbols-outlined">
+                check_box_outline_blank
+              </span>{' '}
+              {t('admin:export_management.uncheck_all')}
             </button>
             </button>
           </div>
           </div>
         </form>
         </form>
 
 
         <div className="card custom-card small my-4">
         <div className="card custom-card small my-4">
           <ul>
           <ul>
-            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
+            <li>
+              {t(
+                'admin:importer_management.growi_settings.description_of_import_mode.about',
+              )}
+            </li>
             <ul>
             <ul>
-              <li>{t('admin:importer_management.growi_settings.description_of_import_mode.insert')}</li>
-              <li>{t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}</li>
-              <li>{t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}</li>
+              <li>
+                {t(
+                  'admin:importer_management.growi_settings.description_of_import_mode.insert',
+                )}
+              </li>
+              <li>
+                {t(
+                  'admin:importer_management.growi_settings.description_of_import_mode.upsert',
+                )}
+              </li>
+              <li>
+                {t(
+                  'admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert',
+                )}
+              </li>
             </ul>
             </ul>
           </ul>
           </ul>
         </div>
         </div>
@@ -479,10 +560,19 @@ class ImportForm extends React.Component {
         {this.renderOthers()}
         {this.renderOthers()}
 
 
         <div className="mt-4 text-center">
         <div className="mt-4 text-center">
-          <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
+          <button
+            type="button"
+            className="btn btn-outline-secondary mx-1"
+            onClick={this.props.onDiscard}
+          >
             {t('admin:importer_management.growi_settings.discard')}
             {t('admin:importer_management.growi_settings.discard')}
           </button>
           </button>
-          <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>
+          <button
+            type="button"
+            className="btn btn-primary mx-1"
+            onClick={this.import}
+            disabled={!canImport || isImporting}
+          >
             {t('admin:importer_management.import')}
             {t('admin:importer_management.import')}
           </button>
           </button>
         </div>
         </div>
@@ -492,7 +582,6 @@ class ImportForm extends React.Component {
       </>
       </>
     );
     );
   }
   }
-
 }
 }
 
 
 ImportForm.propTypes = {
 ImportForm.propTypes = {
@@ -516,5 +605,4 @@ const ImportFormWrapperFc = (props) => {
   return <ImportForm t={t} socket={socket} {...props} />;
   return <ImportForm t={t} socket={socket} {...props} />;
 };
 };
 
 
-
 export default ImportFormWrapperFc;
 export default ImportFormWrapperFc;

+ 21 - 15
apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
@@ -7,7 +6,6 @@ import { apiv3PostForm } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 
 
 class UploadForm extends React.Component {
 class UploadForm extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -33,14 +31,12 @@ class UploadForm extends React.Component {
     try {
     try {
       const { data } = await apiv3PostForm('/import/upload', formData);
       const { data } = await apiv3PostForm('/import/upload', formData);
       this.props.onUpload(data);
       this.props.onUpload(data);
-    }
-    catch (err) {
+    } catch (err) {
       if (err[0].code === 'versions-are-not-met') {
       if (err[0].code === 'versions-are-not-met') {
         if (this.props.onVersionMismatch !== null) {
         if (this.props.onVersionMismatch !== null) {
           this.props.onVersionMismatch(err[0].code);
           this.props.onVersionMismatch(err[0].code);
         }
         }
-      }
-      else {
+      } else {
         toastError(err);
         toastError(err);
       }
       }
     }
     }
@@ -48,9 +44,9 @@ class UploadForm extends React.Component {
 
 
   validateForm() {
   validateForm() {
     return (
     return (
-      this.inputRef.current // null check
-      && this.inputRef.current.files[0] // null check
-      && /\.zip$/.test(this.inputRef.current.files[0].name) // validate extension
+      this.inputRef.current && // null check
+      this.inputRef.current.files[0] && // null check
+      /\.zip$/.test(this.inputRef.current.files[0].name) // validate extension
     );
     );
   }
   }
 
 
@@ -61,7 +57,10 @@ class UploadForm extends React.Component {
       <form onSubmit={this.uploadZipFile}>
       <form onSubmit={this.uploadZipFile}>
         <fieldset>
         <fieldset>
           <div className="row">
           <div className="row">
-            <label htmlFor="file" className="col-md-3 col-form-label col-form-label-sm">
+            <label
+              htmlFor="file"
+              className="col-md-3 col-form-label col-form-label-sm"
+            >
               {t('admin:importer_management.growi_settings.growi_archive_file')}
               {t('admin:importer_management.growi_settings.growi_archive_file')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -76,12 +75,20 @@ class UploadForm extends React.Component {
           </div>
           </div>
           <div className="row">
           <div className="row">
             <div className="mt-4 text-center">
             <div className="mt-4 text-center">
-              { this.props.onDiscard && (
-                <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
+              {this.props.onDiscard && (
+                <button
+                  type="button"
+                  className="btn btn-outline-secondary mx-1"
+                  onClick={this.props.onDiscard}
+                >
                   {t('admin:importer_management.growi_settings.discard')}
                   {t('admin:importer_management.growi_settings.discard')}
                 </button>
                 </button>
-              ) }
-              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
+              )}
+              <button
+                type="submit"
+                className="btn btn-primary"
+                disabled={!this.validateForm()}
+              >
                 {t('admin:importer_management.growi_settings.upload')}
                 {t('admin:importer_management.growi_settings.upload')}
               </button>
               </button>
             </div>
             </div>
@@ -90,7 +97,6 @@ class UploadForm extends React.Component {
       </form>
       </form>
     );
     );
   }
   }
-
 }
 }
 
 
 UploadForm.propTypes = {
 UploadForm.propTypes = {

+ 25 - 24
apps/app/src/client/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -1,17 +1,14 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
-
 import ImportForm from './GrowiArchive/ImportForm';
 import ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
 import UploadForm from './GrowiArchive/UploadForm';
 
 
 class GrowiArchiveSection extends React.Component {
 class GrowiArchiveSection extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -27,9 +24,11 @@ class GrowiArchiveSection extends React.Component {
     this.discardData = this.discardData.bind(this);
     this.discardData = this.discardData.bind(this);
     this.resetState = this.resetState.bind(this);
     this.resetState = this.resetState.bind(this);
     this.handleMismatchedVersions = this.handleMismatchedVersions.bind(this);
     this.handleMismatchedVersions = this.handleMismatchedVersions.bind(this);
-    this.renderDefferentVersionAlert = this.renderDefferentVersionAlert.bind(this);
+    this.renderDefferentVersionAlert =
+      this.renderDefferentVersionAlert.bind(this);
   }
   }
 
 
+  // biome-ignore lint/correctness/noNestedComponentDefinitions: lifecycle method on a class component
   async UNSAFE_componentWillMount() {
   async UNSAFE_componentWillMount() {
     // get uploaded file status
     // get uploaded file status
     const res = await apiv3Get('/import/status');
     const res = await apiv3Get('/import/status');
@@ -42,9 +41,7 @@ class GrowiArchiveSection extends React.Component {
     }
     }
   }
   }
 
 
-  handleUpload({
-    meta, fileName, innerFileStats,
-  }) {
+  handleUpload({ meta, fileName, innerFileStats }) {
     this.setState({
     this.setState({
       fileName,
       fileName,
       innerFileStats,
       innerFileStats,
@@ -59,18 +56,15 @@ class GrowiArchiveSection extends React.Component {
       this.resetState();
       this.resetState();
 
 
       toastSuccess(`Deleted ${fileName}`);
       toastSuccess(`Deleted ${fileName}`);
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }
   }
 
 
-
   handleMismatchedVersions(err) {
   handleMismatchedVersions(err) {
     this.setState({
     this.setState({
       isTheSameVersion: false,
       isTheSameVersion: false,
     });
     });
-
   }
   }
 
 
   renderDefferentVersionAlert() {
   renderDefferentVersionAlert() {
@@ -92,17 +86,24 @@ class GrowiArchiveSection extends React.Component {
 
 
     return (
     return (
       <Fragment>
       <Fragment>
-        <h2 className="mb-3">{t('importer_management.import_growi_archive')}</h2>
+        <h2 className="mb-3">
+          {t('importer_management.import_growi_archive')}
+        </h2>
         <div className="card custom-card bg-body-tertiary mb-4 small">
         <div className="card custom-card bg-body-tertiary mb-4 small">
           <ul>
           <ul>
-            <li>{t('importer_management.skip_username_and_email_when_overlapped')}</li>
-            <li>{t('importer_management.prepare_new_account_for_migration')}</li>
+            <li>
+              {t('importer_management.skip_username_and_email_when_overlapped')}
+            </li>
+            <li>
+              {t('importer_management.prepare_new_account_for_migration')}
+            </li>
             <li>
             <li>
               <a
               <a
                 href={`${t('importer_management.admin_archive_data_import_guide_url')}`}
                 href={`${t('importer_management.admin_archive_data_import_guide_url')}`}
                 target="_blank"
                 target="_blank"
                 rel="noopener noreferrer"
                 rel="noopener noreferrer"
-              >{t('importer_management.archive_data_import_detail')}
+              >
+                {t('importer_management.archive_data_import_detail')}
               </a>
               </a>
             </li>
             </li>
           </ul>
           </ul>
@@ -117,18 +118,18 @@ class GrowiArchiveSection extends React.Component {
               onDiscard={this.discardData}
               onDiscard={this.discardData}
             />
             />
           </div>
           </div>
-        )
-          : (
-            <UploadForm
-              onUpload={this.handleUpload}
-              onDiscard={this.state.fileName != null ? this.discardData : undefined}
-              onVersionMismatch={this.handleMismatchedVersions}
-            />
-          )}
+        ) : (
+          <UploadForm
+            onUpload={this.handleUpload}
+            onDiscard={
+              this.state.fileName != null ? this.discardData : undefined
+            }
+            onVersionMismatch={this.handleMismatchedVersions}
+          />
+        )}
       </Fragment>
       </Fragment>
     );
     );
   }
   }
-
 }
 }
 
 
 GrowiArchiveSection.propTypes = {
 GrowiArchiveSection.propTypes = {

+ 4 - 2
apps/app/src/client/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -1,5 +1,4 @@
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import GrowiArchiveSection from './GrowiArchiveSection';
 import GrowiArchiveSection from './GrowiArchiveSection';
 
 
 const ImportDataPageContents = () => {
 const ImportDataPageContents = () => {
@@ -13,6 +12,9 @@ const ImportDataPageContents = () => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ImportDataPageContentsWrapper = withUnstatedContainers(ImportDataPageContents, []);
+const ImportDataPageContentsWrapper = withUnstatedContainers(
+  ImportDataPageContents,
+  [],
+);
 
 
 export default ImportDataPageContentsWrapper;
 export default ImportDataPageContentsWrapper;

+ 25 - 15
apps/app/src/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -1,5 +1,4 @@
 import React, { useEffect } from 'react';
 import React, { useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
@@ -9,8 +8,6 @@ import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import SlackConfiguration from './SlackConfiguration';
 import SlackConfiguration from './SlackConfiguration';
 
 
 const logger = loggerFactory('growi:NotificationSetting');
 const logger = loggerFactory('growi:NotificationSetting');
@@ -19,39 +16,47 @@ const LegacySlackIntegration = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { adminSlackIntegrationLegacyContainer } = props;
   const { adminSlackIntegrationLegacyContainer } = props;
 
 
-
   useEffect(() => {
   useEffect(() => {
-    const fetchLegacySlackIntegrationData = async() => {
+    const fetchLegacySlackIntegrationData = async () => {
       await adminSlackIntegrationLegacyContainer.retrieveData();
       await adminSlackIntegrationLegacyContainer.retrieveData();
     };
     };
 
 
     try {
     try {
       fetchLegacySlackIntegrationData();
       fetchLegacySlackIntegrationData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
       logger.error(errs);
       logger.error(errs);
     }
     }
   }, [adminSlackIntegrationLegacyContainer]);
   }, [adminSlackIntegrationLegacyContainer]);
 
 
-
-  const isDisabled = adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
+  const isDisabled =
+    adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
 
 
   return (
   return (
     <div data-testid="admin-slack-integration-legacy">
     <div data-testid="admin-slack-integration-legacy">
-      { isDisabled && (
+      {isDisabled && (
         <div className="alert alert-danger">
         <div className="alert alert-danger">
           <span className="material-symbols-outlined">remove</span>
           <span className="material-symbols-outlined">remove</span>
           {/* eslint-disable-next-line react/no-danger */}
           {/* eslint-disable-next-line react/no-danger */}
-          <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_disabled') }}></span>
+          <span
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+            dangerouslySetInnerHTML={{
+              __html: t('admin:slack_integration_legacy.alert_disabled'),
+            }}
+          ></span>
         </div>
         </div>
-      ) }
+      )}
 
 
       <div className="alert alert-warning">
       <div className="alert alert-warning">
         <span className="material-symbols-outlined">info</span>
         <span className="material-symbols-outlined">info</span>
         {/* eslint-disable-next-line react/no-danger */}
         {/* eslint-disable-next-line react/no-danger */}
-        <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_deplicated') }}></span>
+        <span
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+          dangerouslySetInnerHTML={{
+            __html: t('admin:slack_integration_legacy.alert_deplicated'),
+          }}
+        ></span>
       </div>
       </div>
 
 
       <SlackConfiguration />
       <SlackConfiguration />
@@ -59,10 +64,15 @@ const LegacySlackIntegration = (props) => {
   );
   );
 };
 };
 
 
-const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(LegacySlackIntegration, [AdminSlackIntegrationLegacyContainer]);
+const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(
+  LegacySlackIntegration,
+  [AdminSlackIntegrationLegacyContainer],
+);
 
 
 LegacySlackIntegration.propTypes = {
 LegacySlackIntegration.propTypes = {
-  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
+  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(
+    AdminSlackIntegrationLegacyContainer,
+  ).isRequired,
 };
 };
 
 
 export default LegacySlackIntegrationWithUnstatedContainer;
 export default LegacySlackIntegrationWithUnstatedContainer;

+ 151 - 64
apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -1,11 +1,10 @@
 import React, { useCallback, useEffect } from 'react';
 import React, { useCallback, useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -15,7 +14,8 @@ const logger = loggerFactory('growi:slackAppConfiguration');
 
 
 const SlackConfiguration = (props) => {
 const SlackConfiguration = (props) => {
   const { t, adminSlackIntegrationLegacyContainer } = props;
   const { t, adminSlackIntegrationLegacyContainer } = props;
-  const { webhookUrl, slackToken, retrieveError } = adminSlackIntegrationLegacyContainer.state;
+  const { webhookUrl, slackToken, retrieveError } =
+    adminSlackIntegrationLegacyContainer.state;
 
 
   const { register, handleSubmit, reset } = useForm();
   const { register, handleSubmit, reset } = useForm();
 
 
@@ -27,18 +27,24 @@ const SlackConfiguration = (props) => {
     });
     });
   }, [reset, webhookUrl, slackToken]);
   }, [reset, webhookUrl, slackToken]);
 
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminSlackIntegrationLegacyContainer.changeWebhookUrl(data.webhookUrl ?? '');
-      await adminSlackIntegrationLegacyContainer.changeSlackToken(data.slackToken ?? '');
-      await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
-      toastSuccess(t('notification_settings.updated_slackApp'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [adminSlackIntegrationLegacyContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminSlackIntegrationLegacyContainer.changeWebhookUrl(
+          data.webhookUrl ?? '',
+        );
+        await adminSlackIntegrationLegacyContainer.changeSlackToken(
+          data.slackToken ?? '',
+        );
+        await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
+        toastSuccess(t('notification_settings.updated_slackApp'));
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [adminSlackIntegrationLegacyContainer, t],
+  );
 
 
   return (
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
     <form onSubmit={handleSubmit(onClickSubmit)}>
@@ -56,23 +62,54 @@ const SlackConfiguration = (props) => {
               >
               >
                 {`Slack ${adminSlackIntegrationLegacyContainer.state.selectSlackOption}`}
                 {`Slack ${adminSlackIntegrationLegacyContainer.state.selectSlackOption}`}
               </button>
               </button>
-              <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <button className="dropdown-item" type="button" onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}>
+              <div
+                className="dropdown-menu"
+                role="menu"
+                aria-labelledby="dropdownMenuButton"
+              >
+                <button
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() =>
+                    adminSlackIntegrationLegacyContainer.switchSlackOption(
+                      'Incoming Webhooks',
+                    )
+                  }
+                >
                   Slack Incoming Webhooks
                   Slack Incoming Webhooks
                 </button>
                 </button>
-                <button className="dropdown-item" type="button" onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('App')}>Slack App</button>
+                <button
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() =>
+                    adminSlackIntegrationLegacyContainer.switchSlackOption(
+                      'App',
+                    )
+                  }
+                >
+                  Slack App
+                </button>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-        {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
+        {adminSlackIntegrationLegacyContainer.state.selectSlackOption ===
+        'Incoming Webhooks' ? (
           <React.Fragment>
           <React.Fragment>
-            <h2 className="border-bottom mb-5">{t('notification_settings.slack_incoming_configuration')}</h2>
+            <h2 className="border-bottom mb-5">
+              {t('notification_settings.slack_incoming_configuration')}
+            </h2>
 
 
             <div className="row mb-3">
             <div className="row mb-3">
-              <label className="form-label col-md-3 text-start text-md-end">Webhook URL</label>
+              <label
+                className="form-label col-md-3 text-start text-md-end"
+                htmlFor="webhookUrl"
+              >
+                Webhook URL
+              </label>
               <div className="col-md-6">
               <div className="col-md-6">
                 <input
                 <input
+                  id="webhookUrl"
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   {...register('webhookUrl')}
                   {...register('webhookUrl')}
@@ -87,10 +124,18 @@ const SlackConfiguration = (props) => {
                     type="checkbox"
                     type="checkbox"
                     className="form-check-input"
                     className="form-check-input"
                     id="cbPrioritizeIWH"
                     id="cbPrioritizeIWH"
-                    checked={adminSlackIntegrationLegacyContainer.state.isIncomingWebhookPrioritized || false}
-                    onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }}
+                    checked={
+                      adminSlackIntegrationLegacyContainer.state
+                        .isIncomingWebhookPrioritized || false
+                    }
+                    onChange={() => {
+                      adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized();
+                    }}
                   />
                   />
-                  <label className="form-label form-check-label" htmlFor="cbPrioritizeIWH">
+                  <label
+                    className="form-label form-check-label"
+                    htmlFor="cbPrioritizeIWH"
+                  >
                     {t('notification_settings.prioritize_webhook')}
                     {t('notification_settings.prioritize_webhook')}
                   </label>
                   </label>
                 </div>
                 </div>
@@ -100,40 +145,61 @@ const SlackConfiguration = (props) => {
               </div>
               </div>
             </div>
             </div>
           </React.Fragment>
           </React.Fragment>
-        )
-          : (
-            <React.Fragment>
-              <h2 className="border-bottom mb-3">{t('notification_settings.slack_app_configuration')}</h2>
-
-              <div className="card custom-card bg-danger-subtle">
-                <span className="text-danger"><span className="material-symbols-outlined">error</span>NOT RECOMMENDED</span>
-                <br />
-                {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.slack_app_configuration_desc') }} />
-                <br />
-                <a
-                  href="#slack-incoming-webhooks"
-                  data-bs-toggle="tab"
-                  onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
-                >
-                  {t('notification_settings.use_instead')}
-                </a>
-              </div>
+        ) : (
+          <React.Fragment>
+            <h2 className="border-bottom mb-3">
+              {t('notification_settings.slack_app_configuration')}
+            </h2>
 
 
-              <div className="row mb-5 mt-4">
-                <label className="form-label col-md-3 text-start text-md-end">OAuth access token</label>
-                <div className="col-md-6">
-                  <input
-                    className="form-control"
-                    type="text"
-                    {...register('slackToken')}
-                  />
-                </div>
-              </div>
+            <div className="card custom-card bg-danger-subtle">
+              <span className="text-danger">
+                <span className="material-symbols-outlined">error</span>NOT
+                RECOMMENDED
+              </span>
+              <br />
+              {/* eslint-disable-next-line react/no-danger */}
+              <span
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'notification_settings.slack_app_configuration_desc',
+                  ),
+                }}
+              />
+              <br />
+              <button
+                type="button"
+                className="btn btn-link p-0"
+                data-bs-toggle="tab"
+                data-bs-target="#slack-incoming-webhooks"
+                onClick={() =>
+                  adminSlackIntegrationLegacyContainer.switchSlackOption(
+                    'Incoming Webhooks',
+                  )
+                }
+              >
+                {t('notification_settings.use_instead')}
+              </button>
+            </div>
 
 
-            </React.Fragment>
-          )
-        }
+            <div className="row mb-5 mt-4">
+              <label
+                className="form-label col-md-3 text-start text-md-end"
+                htmlFor="slackToken"
+              >
+                OAuth access token
+              </label>
+              <div className="col-md-6">
+                <input
+                  id="slackToken"
+                  className="form-control"
+                  type="text"
+                  {...register('slackToken')}
+                />
+              </div>
+            </div>
+          </React.Fragment>
+        )}
 
 
         <AdminUpdateButtonRow
         <AdminUpdateButtonRow
           disabled={retrieveError != null}
           disabled={retrieveError != null}
@@ -143,16 +209,28 @@ const SlackConfiguration = (props) => {
         <hr />
         <hr />
 
 
         <h3>
         <h3>
-          <span className="material-symbols-outlined" aria-hidden="true">help</span>{' '}
-          <a href="#collapseHelpForIwh" data-bs-toggle="collapse">{t('notification_settings.how_to.header')}</a>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            help
+          </span>{' '}
+          <a href="#collapseHelpForIwh" data-bs-toggle="collapse">
+            {t('notification_settings.how_to.header')}
+          </a>
         </h3>
         </h3>
 
 
-        <ol id="collapseHelpForIwh" className="collapse card custom-card bg-body-tertiary">
+        <ol
+          id="collapseHelpForIwh"
+          className="collapse card custom-card bg-body-tertiary"
+        >
           <li className="ms-3">
           <li className="ms-3">
             {t('notification_settings.how_to.workspace')}
             {t('notification_settings.how_to.workspace')}
             <ol>
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
-              <li dangerouslySetInnerHTML={{ __html: t('notification_settings.how_to.workspace_desc1') }} />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.how_to.workspace_desc1'),
+                }}
+              />
               <li>{t('notification_settings.how_to.workspace_desc2')}</li>
               <li>{t('notification_settings.how_to.workspace_desc2')}</li>
               <li>{t('notification_settings.how_to.workspace_desc3')}</li>
               <li>{t('notification_settings.how_to.workspace_desc3')}</li>
             </ol>
             </ol>
@@ -161,11 +239,15 @@ const SlackConfiguration = (props) => {
             {t('notification_settings.how_to.at_growi')}
             {t('notification_settings.how_to.at_growi')}
             <ol>
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
-              <li dangerouslySetInnerHTML={{ __html: t('notification_settings.how_to.at_growi_desc') }} />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.how_to.at_growi_desc'),
+                }}
+              />
             </ol>
             </ol>
           </li>
           </li>
         </ol>
         </ol>
-
       </React.Fragment>
       </React.Fragment>
     </form>
     </form>
   );
   );
@@ -173,7 +255,9 @@ const SlackConfiguration = (props) => {
 
 
 SlackConfiguration.propTypes = {
 SlackConfiguration.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
+  adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(
+    AdminSlackIntegrationLegacyContainer,
+  ).isRequired,
 };
 };
 
 
 const SlackConfigurationWrapperFc = (props) => {
 const SlackConfigurationWrapperFc = (props) => {
@@ -182,6 +266,9 @@ const SlackConfigurationWrapperFc = (props) => {
   return <SlackConfiguration t={t} {...props} />;
   return <SlackConfiguration t={t} {...props} />;
 };
 };
 
 
-const SlackConfigurationWrapper = withUnstatedContainers(SlackConfigurationWrapperFc, [AdminSlackIntegrationLegacyContainer]);
+const SlackConfigurationWrapper = withUnstatedContainers(
+  SlackConfigurationWrapperFc,
+  [AdminSlackIntegrationLegacyContainer],
+);
 
 
 export default SlackConfigurationWrapper;
 export default SlackConfigurationWrapper;

+ 32 - 27
apps/app/src/client/components/Admin/ManageExternalAccount.tsx

@@ -1,34 +1,38 @@
-import React, { useCallback, useEffect, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
+import React, { type JSX, useCallback, useEffect } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
 
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 
 
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-
 import ExternalAccountTable from './Users/ExternalAccountTable';
 import ExternalAccountTable from './Users/ExternalAccountTable';
 
 
 type ManageExternalAccountProps = {
 type ManageExternalAccountProps = {
-  adminExternalAccountsContainer: AdminExternalAccountsContainer,
-}
-
-const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element => {
+  adminExternalAccountsContainer: AdminExternalAccountsContainer;
+};
 
 
+const ManageExternalAccount = (
+  props: ManageExternalAccountProps,
+): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { adminExternalAccountsContainer } = props;
   const { adminExternalAccountsContainer } = props;
-  const { activePage, totalAccounts, pagingLimit } = adminExternalAccountsContainer.state;
+  const { activePage, totalAccounts, pagingLimit } =
+    adminExternalAccountsContainer.state;
 
 
-  const externalAccountPageHandler = useCallback(async(selectedPage) => {
-    try {
-      await adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(selectedPage);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminExternalAccountsContainer]);
+  const externalAccountPageHandler = useCallback(
+    async (selectedPage) => {
+      try {
+        await adminExternalAccountsContainer.retrieveExternalAccountsByPagingNum(
+          selectedPage,
+        );
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminExternalAccountsContainer],
+  );
 
 
   // for Next routing
   // for Next routing
   useEffect(() => {
   useEffect(() => {
@@ -54,28 +58,29 @@ const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element =
           prefetch={false}
           prefetch={false}
           className="btn btn-outline-secondary"
           className="btn btn-outline-secondary"
         >
         >
-          <span className="material-symbols-outlined" aria-hidden="true">arrow_back</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            arrow_back
+          </span>
           {t('admin:user_management.back_to_user_management')}
           {t('admin:user_management.back_to_user_management')}
         </Link>
         </Link>
       </p>
       </p>
       <h2>{t('admin:user_management.external_account_list')}</h2>
       <h2>{t('admin:user_management.external_account_list')}</h2>
-      {(totalAccounts !== 0) ? (
+      {totalAccounts !== 0 ? (
         <>
         <>
           {pager}
           {pager}
           <ExternalAccountTable />
           <ExternalAccountTable />
           {pager}
           {pager}
         </>
         </>
-      )
-        : (
-          <>
-            { t('admin:user_management.external_account_none') }
-          </>
-        )
-      }
+      ) : (
+        <>{t('admin:user_management.external_account_none')}</>
+      )}
     </>
     </>
   );
   );
 };
 };
 
 
-const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccount, [AdminExternalAccountsContainer]);
+const ManageExternalAccountWrapper = withUnstatedContainers(
+  ManageExternalAccount,
+  [AdminExternalAccountsContainer],
+);
 
 
 export default ManageExternalAccountWrapper;
 export default ManageExternalAccountWrapper;

+ 60 - 25
apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx

@@ -1,13 +1,15 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
-  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+  UncontrolledDropdown,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -15,24 +17,30 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 const logger = loggerFactory('growi:importer');
 const logger = loggerFactory('growi:importer');
 
 
-
 type Props = {
 type Props = {
   adminMarkDownContainer: AdminMarkDownContainer;
   adminMarkDownContainer: AdminMarkDownContainer;
-}
+};
 
 
 const IndentForm = (props: Props) => {
 const IndentForm = (props: Props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
-  const onClickSubmit = useCallback(async(props) => {
-    try {
-      await props.adminMarkDownContainer.updateIndentSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.indent_header'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [t]);
+  const onClickSubmit = useCallback(
+    async (props) => {
+      try {
+        await props.adminMarkDownContainer.updateIndentSetting();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('markdown_settings.indent_header'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [t],
+  );
 
 
   const renderIndentSizeOption = (props) => {
   const renderIndentSizeOption = (props) => {
     const { adminMarkDownContainer } = props;
     const { adminMarkDownContainer } = props;
@@ -41,9 +49,14 @@ const IndentForm = (props: Props) => {
     return (
     return (
       <div className="col">
       <div className="col">
         <div>
         <div>
-          <label htmlFor="adminPreferredIndentSize" className="form-label">{t('markdown_settings.indent_options.indentSize')}</label>
+          <label htmlFor="adminPreferredIndentSize" className="form-label">
+            {t('markdown_settings.indent_options.indentSize')}
+          </label>
           <UncontrolledDropdown id="adminPreferredIndentSize">
           <UncontrolledDropdown id="adminPreferredIndentSize">
-            <DropdownToggle caret className="col-3 col-sm-2 col-md-5 col-lg-5 col-xl-3 text-end">
+            <DropdownToggle
+              caret
+              className="col-3 col-sm-2 col-md-5 col-lg-5 col-xl-3 text-end"
+            >
               <span className="float-start">
               <span className="float-start">
                 {adminPreferredIndentSize || 4}
                 {adminPreferredIndentSize || 4}
               </span>
               </span>
@@ -51,8 +64,14 @@ const IndentForm = (props: Props) => {
             <DropdownMenu className="dropdown-menu" role="menu">
             <DropdownMenu className="dropdown-menu" role="menu">
               {[2, 4].map((num) => {
               {[2, 4].map((num) => {
                 return (
                 return (
-                  <DropdownItem key={num} role="presentation" onClick={() => adminMarkDownContainer.setAdminPreferredIndentSize(num)}>
-                    <a role="menuitem">{num}</a>
+                  <DropdownItem
+                    key={num}
+                    role="presentation"
+                    onClick={() =>
+                      adminMarkDownContainer.setAdminPreferredIndentSize(num)
+                    }
+                  >
+                    <span>{num}</span>
                   </DropdownItem>
                   </DropdownItem>
                 );
                 );
               })}
               })}
@@ -70,7 +89,9 @@ const IndentForm = (props: Props) => {
     const { adminMarkDownContainer } = props;
     const { adminMarkDownContainer } = props;
     const { isIndentSizeForced } = adminMarkDownContainer.state;
     const { isIndentSizeForced } = adminMarkDownContainer.state;
 
 
-    const helpIndentInComment = { __html: t('markdown_settings.indent_options.disallow_indent_change_desc') };
+    const helpIndentInComment = {
+      __html: t('markdown_settings.indent_options.disallow_indent_change_desc'),
+    };
 
 
     return (
     return (
       <div className="col">
       <div className="col">
@@ -81,14 +102,23 @@ const IndentForm = (props: Props) => {
             id="isIndentSizeForced"
             id="isIndentSizeForced"
             checked={isIndentSizeForced || false}
             checked={isIndentSizeForced || false}
             onChange={() => {
             onChange={() => {
-              adminMarkDownContainer.setState({ isIndentSizeForced: !isIndentSizeForced });
+              adminMarkDownContainer.setState({
+                isIndentSizeForced: !isIndentSizeForced,
+              });
             }}
             }}
           />
           />
-          <label className="form-label form-check-label" htmlFor="isIndentSizeForced">
+          <label
+            className="form-label form-check-label"
+            htmlFor="isIndentSizeForced"
+          >
             {t('markdown_settings.indent_options.disallow_indent_change')}
             {t('markdown_settings.indent_options.disallow_indent_change')}
           </label>
           </label>
         </div>
         </div>
-        <p className="form-text text-muted" dangerouslySetInnerHTML={helpIndentInComment} />
+        <p
+          className="form-text text-muted"
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+          dangerouslySetInnerHTML={helpIndentInComment}
+        />
       </div>
       </div>
     );
     );
   };
   };
@@ -101,7 +131,10 @@ const IndentForm = (props: Props) => {
         {renderIndentSizeOption(props)}
         {renderIndentSizeOption(props)}
         {renderIndentForceOption(props)}
         {renderIndentForceOption(props)}
       </fieldset>
       </fieldset>
-      <AdminUpdateButtonRow onClick={() => onClickSubmit(props)} disabled={adminMarkDownContainer.state.retrieveError != null} />
+      <AdminUpdateButtonRow
+        onClick={() => onClickSubmit(props)}
+        disabled={adminMarkDownContainer.state.retrieveError != null}
+      />
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };
@@ -109,6 +142,8 @@ const IndentForm = (props: Props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const IndentFormWrapper = withUnstatedContainers(IndentForm, [AdminMarkDownContainer]);
+const IndentFormWrapper = withUnstatedContainers(IndentForm, [
+  AdminMarkDownContainer,
+]);
 
 
 export default IndentFormWrapper;
 export default IndentFormWrapper;

+ 57 - 21
apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -1,11 +1,10 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -14,22 +13,24 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 const logger = loggerFactory('growi:importer');
 const logger = loggerFactory('growi:importer');
 
 
 class LineBreakForm extends React.Component {
 class LineBreakForm extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
-
   async onClickSubmit() {
   async onClickSubmit() {
     const { t } = this.props;
     const { t } = this.props;
 
 
     try {
     try {
       await this.props.adminMarkDownContainer.updateLineBreakSetting();
       await this.props.adminMarkDownContainer.updateLineBreakSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.lineBreak_header'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('markdown_settings.lineBreak_header'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
@@ -39,7 +40,9 @@ class LineBreakForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { t, adminMarkDownContainer } = this.props;
     const { isEnabledLinebreaks } = adminMarkDownContainer.state;
     const { isEnabledLinebreaks } = adminMarkDownContainer.state;
 
 
-    const helpLineBreak = { __html: t('markdown_settings.lineBreak_options.enable_lineBreak_desc') };
+    const helpLineBreak = {
+      __html: t('markdown_settings.lineBreak_options.enable_lineBreak_desc'),
+    };
 
 
     return (
     return (
       <div className="col">
       <div className="col">
@@ -49,13 +52,24 @@ class LineBreakForm extends React.Component {
             className="form-check-input"
             className="form-check-input"
             id="isEnabledLinebreaks"
             id="isEnabledLinebreaks"
             checked={isEnabledLinebreaks}
             checked={isEnabledLinebreaks}
-            onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
+            onChange={() => {
+              adminMarkDownContainer.setState({
+                isEnabledLinebreaks: !isEnabledLinebreaks,
+              });
+            }}
           />
           />
-          <label className="form-label form-check-label" htmlFor="isEnabledLinebreaks">
-            {t('markdown_settings.lineBreak_options.enable_lineBreak') }
+          <label
+            className="form-label form-check-label"
+            htmlFor="isEnabledLinebreaks"
+          >
+            {t('markdown_settings.lineBreak_options.enable_lineBreak')}
           </label>
           </label>
         </div>
         </div>
-        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreak} />
+        <p
+          className="form-text text-muted"
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+          dangerouslySetInnerHTML={helpLineBreak}
+        />
       </div>
       </div>
     );
     );
   }
   }
@@ -64,7 +78,11 @@ class LineBreakForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { t, adminMarkDownContainer } = this.props;
     const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
     const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
 
 
-    const helpLineBreakInComment = { __html: t('markdown_settings.lineBreak_options.enable_lineBreak_for_comment_desc') };
+    const helpLineBreakInComment = {
+      __html: t(
+        'markdown_settings.lineBreak_options.enable_lineBreak_for_comment_desc',
+      ),
+    };
 
 
     return (
     return (
       <div className="col">
       <div className="col">
@@ -74,13 +92,26 @@ class LineBreakForm extends React.Component {
             className="form-check-input"
             className="form-check-input"
             id="isEnabledLinebreaksInComments"
             id="isEnabledLinebreaksInComments"
             checked={isEnabledLinebreaksInComments}
             checked={isEnabledLinebreaksInComments}
-            onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
+            onChange={() => {
+              adminMarkDownContainer.setState({
+                isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments,
+              });
+            }}
           />
           />
-          <label className="form-label form-check-label" htmlFor="isEnabledLinebreaksInComments">
-            {t('markdown_settings.lineBreak_options.enable_lineBreak_for_comment') }
+          <label
+            className="form-label form-check-label"
+            htmlFor="isEnabledLinebreaksInComments"
+          >
+            {t(
+              'markdown_settings.lineBreak_options.enable_lineBreak_for_comment',
+            )}
           </label>
           </label>
         </div>
         </div>
-        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreakInComment} />
+        <p
+          className="form-text text-muted"
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
+          dangerouslySetInnerHTML={helpLineBreakInComment}
+        />
       </div>
       </div>
     );
     );
   }
   }
@@ -94,11 +125,13 @@ class LineBreakForm extends React.Component {
           {this.renderLineBreakOption()}
           {this.renderLineBreakOption()}
           {this.renderLineBreakInCommentOption()}
           {this.renderLineBreakInCommentOption()}
         </fieldset>
         </fieldset>
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
+        <AdminUpdateButtonRow
+          onClick={this.onClickSubmit}
+          disabled={adminMarkDownContainer.state.retrieveError != null}
+        />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
-
 }
 }
 
 
 const LineBreakFormFC = (props) => {
 const LineBreakFormFC = (props) => {
@@ -109,11 +142,14 @@ const LineBreakFormFC = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [AdminMarkDownContainer]);
+const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [
+  AdminMarkDownContainer,
+]);
 
 
 LineBreakForm.propTypes = {
 LineBreakForm.propTypes = {
   t: PropTypes.func.isRequired,
   t: PropTypes.func.isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer)
+    .isRequired,
 };
 };
 
 
 export default LineBreakFormWrapper;
 export default LineBreakFormWrapper;

+ 28 - 18
apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -1,5 +1,4 @@
-import React, { useEffect, type JSX } from 'react';
-
+import React, { type JSX, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
@@ -9,30 +8,28 @@ import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import IndentForm from './IndentForm';
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
 import LineBreakForm from './LineBreakForm';
 import XssForm from './XssForm';
 import XssForm from './XssForm';
 
 
 const logger = loggerFactory('growi:MarkDown');
 const logger = loggerFactory('growi:MarkDown');
 
 
-type Props ={
-  adminMarkDownContainer: AdminMarkDownContainer
-}
+type Props = {
+  adminMarkDownContainer: AdminMarkDownContainer;
+};
 
 
 const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
 const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { adminMarkDownContainer } = props;
   const { adminMarkDownContainer } = props;
 
 
   useEffect(() => {
   useEffect(() => {
-    const fetchMarkdownData = async() => {
+    const fetchMarkdownData = async () => {
       await adminMarkDownContainer.retrieveMarkdownData();
       await adminMarkDownContainer.retrieveMarkdownData();
     };
     };
 
 
     try {
     try {
       fetchMarkdownData();
       fetchMarkdownData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
       logger.error(errs);
       logger.error(errs);
@@ -42,23 +39,35 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   return (
   return (
     <div data-testid="admin-markdown" className="mb-5">
     <div data-testid="admin-markdown" className="mb-5">
       {/* Line Break Setting */}
       {/* Line Break Setting */}
-      <h2 className="admin-setting-header">{t('markdown_settings.lineBreak_header')}</h2>
+      <h2 className="admin-setting-header">
+        {t('markdown_settings.lineBreak_header')}
+      </h2>
       <Card className="card custom-card bg-body-tertiary my-3">
       <Card className="card custom-card bg-body-tertiary my-3">
-        <CardBody className="px-0 py-2">{ t('markdown_settings.lineBreak_desc') }</CardBody>
+        <CardBody className="px-0 py-2">
+          {t('markdown_settings.lineBreak_desc')}
+        </CardBody>
       </Card>
       </Card>
       <LineBreakForm />
       <LineBreakForm />
 
 
       {/* Indent Setting */}
       {/* Indent Setting */}
-      <h2 className="admin-setting-header mt-5">{t('markdown_settings.indent_header')}</h2>
+      <h2 className="admin-setting-header mt-5">
+        {t('markdown_settings.indent_header')}
+      </h2>
       <Card className="card custom-card bg-body-tertiary my-3">
       <Card className="card custom-card bg-body-tertiary my-3">
-        <CardBody className="px-0 py-2">{t('markdown_settings.indent_desc') }</CardBody>
+        <CardBody className="px-0 py-2">
+          {t('markdown_settings.indent_desc')}
+        </CardBody>
       </Card>
       </Card>
       <IndentForm />
       <IndentForm />
 
 
       {/* XSS Setting */}
       {/* XSS Setting */}
-      <h2 className="admin-setting-header mt-5">{ t('markdown_settings.xss_header') }</h2>
+      <h2 className="admin-setting-header mt-5">
+        {t('markdown_settings.xss_header')}
+      </h2>
       <Card className="card custom-card bg-body-tertiary my-3">
       <Card className="card custom-card bg-body-tertiary my-3">
-        <CardBody className="px-0 py-2">{ t('markdown_settings.xss_desc') }</CardBody>
+        <CardBody className="px-0 py-2">
+          {t('markdown_settings.xss_desc')}
+        </CardBody>
       </Card>
       </Card>
       <XssForm />
       <XssForm />
     </div>
     </div>
@@ -66,8 +75,9 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
 });
 });
 MarkDownSettingContents.displayName = 'MarkDownSettingContents';
 MarkDownSettingContents.displayName = 'MarkDownSettingContents';
 
 
-
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkDownSettingContents, [AdminMarkDownContainer]);
-
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(
+  MarkDownSettingContents,
+  [AdminMarkDownContainer],
+);
 
 
 export default MarkdownSettingWithUnstatedContainer;
 export default MarkdownSettingWithUnstatedContainer;

+ 33 - 19
apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx

@@ -1,24 +1,25 @@
-import { useCallback, type JSX } from 'react';
-
+import { type JSX, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import type { UseFormRegister, UseFormSetValue } from 'react-hook-form';
 import type { UseFormRegister, UseFormSetValue } from 'react-hook-form';
 
 
 import type AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import type AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
+import {
+  attributes as recommendedAttributes,
+  tagNames as recommendedTagNames,
+} from '~/services/renderer/recommended-whitelist';
 
 
 type FormValues = {
 type FormValues = {
-  tagWhitelist: string,
-  attrWhitelist: string,
-}
+  tagWhitelist: string;
+  attrWhitelist: string;
+};
 
 
-type Props ={
-  adminMarkDownContainer: AdminMarkDownContainer,
-  register: UseFormRegister<FormValues>,
-  setValue: UseFormSetValue<FormValues>,
-}
+type Props = {
+  adminMarkDownContainer: AdminMarkDownContainer;
+  register: UseFormRegister<FormValues>;
+  setValue: UseFormSetValue<FormValues>;
+};
 
 
 export const WhitelistInput = (props: Props): JSX.Element => {
 export const WhitelistInput = (props: Props): JSX.Element => {
-
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { adminMarkDownContainer, register, setValue } = props;
   const { adminMarkDownContainer, register, setValue } = props;
 
 
@@ -39,9 +40,16 @@ export const WhitelistInput = (props: Props): JSX.Element => {
       <div className="mt-4">
       <div className="mt-4">
         <div className="d-flex justify-content-between">
         <div className="d-flex justify-content-between">
           {t('markdown_settings.xss_options.tag_names')}
           {t('markdown_settings.xss_options.tag_names')}
-          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendTagButtonHandler}>
-            {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
-          </p>
+          <button
+            type="button"
+            id="btn-import-tags"
+            className="btn btn-sm btn-primary"
+            onClick={clickRecommendTagButtonHandler}
+          >
+            {t('markdown_settings.xss_options.import_recommended', {
+              target: 'Tags',
+            })}
+          </button>
         </div>
         </div>
         <textarea
         <textarea
           className="form-control xss-list"
           className="form-control xss-list"
@@ -53,9 +61,16 @@ export const WhitelistInput = (props: Props): JSX.Element => {
       <div className="mt-4">
       <div className="mt-4">
         <div className="d-flex justify-content-between">
         <div className="d-flex justify-content-between">
           {t('markdown_settings.xss_options.tag_attributes')}
           {t('markdown_settings.xss_options.tag_attributes')}
-          <p id="btn-import-tags" className="btn btn-sm btn-primary" onClick={clickRecommendAttrButtonHandler}>
-            {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
-          </p>
+          <button
+            type="button"
+            id="btn-import-attrs"
+            className="btn btn-sm btn-primary"
+            onClick={clickRecommendAttrButtonHandler}
+          >
+            {t('markdown_settings.xss_options.import_recommended', {
+              target: 'Attrs',
+            })}
+          </button>
         </div>
         </div>
         <textarea
         <textarea
           className="form-control xss-list"
           className="form-control xss-list"
@@ -66,5 +81,4 @@ export const WhitelistInput = (props: Props): JSX.Element => {
       </div>
       </div>
     </>
     </>
   );
   );
-
 };
 };

+ 70 - 37
apps/app/src/client/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,31 +1,29 @@
 import React, { useCallback, useEffect } from 'react';
 import React, { useCallback, useEffect } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
-import { tagNames as recommendedTagNames, attributes as recommendedAttributes } from '~/services/renderer/recommended-whitelist';
+import {
+  attributes as recommendedAttributes,
+  tagNames as recommendedTagNames,
+} from '~/services/renderer/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import { WhitelistInput } from './WhitelistInput';
 import { WhitelistInput } from './WhitelistInput';
 
 
 const logger = loggerFactory('growi:importer');
 const logger = loggerFactory('growi:importer');
 
 
 const XssForm = (props) => {
 const XssForm = (props) => {
   const { t, adminMarkDownContainer } = props;
   const { t, adminMarkDownContainer } = props;
-  const {
-    xssOption, tagWhitelist, attrWhitelist, retrieveError,
-  } = adminMarkDownContainer.state;
+  const { xssOption, tagWhitelist, attrWhitelist, retrieveError } =
+    adminMarkDownContainer.state;
 
 
-  const {
-    register, handleSubmit, reset, setValue,
-  } = useForm();
+  const { register, handleSubmit, reset, setValue } = useForm();
 
 
   // Sync form with container state
   // Sync form with container state
   useEffect(() => {
   useEffect(() => {
@@ -35,28 +33,37 @@ const XssForm = (props) => {
     });
     });
   }, [reset, tagWhitelist, attrWhitelist]);
   }, [reset, tagWhitelist, attrWhitelist]);
 
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminMarkDownContainer.setState({ tagWhitelist: data.tagWhitelist ?? '' });
-      await adminMarkDownContainer.setState({ attrWhitelist: data.attrWhitelist ?? '' });
-      await adminMarkDownContainer.updateXssSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.xss_header'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [adminMarkDownContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminMarkDownContainer.setState({
+          tagWhitelist: data.tagWhitelist ?? '',
+        });
+        await adminMarkDownContainer.setState({
+          attrWhitelist: data.attrWhitelist ?? '',
+        });
+        await adminMarkDownContainer.updateXssSetting();
+        toastSuccess(
+          t('toaster.update_successed', {
+            target: t('markdown_settings.xss_header'),
+            ns: 'commons',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [adminMarkDownContainer, t],
+  );
 
 
   const xssOptions = useCallback(() => {
   const xssOptions = useCallback(() => {
-
     const rehypeRecommendedTags = recommendedTagNames.join(',');
     const rehypeRecommendedTags = recommendedTagNames.join(',');
     const rehypeRecommendedAttributes = JSON.stringify(recommendedAttributes);
     const rehypeRecommendedAttributes = JSON.stringify(recommendedAttributes);
 
 
     return (
     return (
       <div className="col-12 mt-3">
       <div className="col-12 mt-3">
         <div className="row">
         <div className="row">
-
           <div className="col-md-6 col-sm-12 align-self-start">
           <div className="col-md-6 col-sm-12 align-self-start">
             <div className="form-check">
             <div className="form-check">
               <input
               <input
@@ -65,10 +72,19 @@ const XssForm = (props) => {
                 id="xssOption1"
                 id="xssOption1"
                 name="XssOption"
                 name="XssOption"
                 checked={xssOption === RehypeSanitizeType.RECOMMENDED}
                 checked={xssOption === RehypeSanitizeType.RECOMMENDED}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.RECOMMENDED }) }}
+                onChange={() => {
+                  adminMarkDownContainer.setState({
+                    xssOption: RehypeSanitizeType.RECOMMENDED,
+                  });
+                }}
               />
               />
-              <label className="form-label form-check-label w-100" htmlFor="xssOption1">
-                <p className="fw-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
+              <label
+                className="form-label form-check-label w-100"
+                htmlFor="xssOption1"
+              >
+                <p className="fw-bold">
+                  {t('markdown_settings.xss_options.recommended_setting')}
+                </p>
                 <div className="mt-4">
                 <div className="mt-4">
                   <div className="d-flex justify-content-between">
                   <div className="d-flex justify-content-between">
                     {t('markdown_settings.xss_options.tag_names')}
                     {t('markdown_settings.xss_options.tag_names')}
@@ -107,11 +123,24 @@ const XssForm = (props) => {
                 id="xssOption2"
                 id="xssOption2"
                 name="XssOption"
                 name="XssOption"
                 checked={xssOption === RehypeSanitizeType.CUSTOM}
                 checked={xssOption === RehypeSanitizeType.CUSTOM}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeType.CUSTOM }) }}
+                onChange={() => {
+                  adminMarkDownContainer.setState({
+                    xssOption: RehypeSanitizeType.CUSTOM,
+                  });
+                }}
               />
               />
-              <label className="form-label form-check-label w-100" htmlFor="xssOption2">
-                <p className="fw-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
-                <WhitelistInput adminMarkDownContainer={adminMarkDownContainer} register={register} setValue={setValue} />
+              <label
+                className="form-label form-check-label w-100"
+                htmlFor="xssOption2"
+              >
+                <p className="fw-bold">
+                  {t('markdown_settings.xss_options.custom_whitelist')}
+                </p>
+                <WhitelistInput
+                  adminMarkDownContainer={adminMarkDownContainer}
+                  register={register}
+                  setValue={setValue}
+                />
               </label>
               </label>
             </div>
             </div>
           </div>
           </div>
@@ -137,16 +166,17 @@ const XssForm = (props) => {
                   checked={isEnabledXss}
                   checked={isEnabledXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                 />
                 />
-                <label className="form-label form-check-label w-100" htmlFor="XssEnable">
+                <label
+                  className="form-label form-check-label w-100"
+                  htmlFor="XssEnable"
+                >
                   {t('markdown_settings.xss_options.enable_xss_prevention')}
                   {t('markdown_settings.xss_options.enable_xss_prevention')}
                 </label>
                 </label>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
 
 
-          <div className="col-12">
-            {isEnabledXss && xssOptions()}
-          </div>
+          <div className="col-12">{isEnabledXss && xssOptions()}</div>
         </fieldset>
         </fieldset>
         <AdminUpdateButtonRow
         <AdminUpdateButtonRow
           disabled={retrieveError != null}
           disabled={retrieveError != null}
@@ -159,7 +189,8 @@ const XssForm = (props) => {
 
 
 XssForm.propTypes = {
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer)
+    .isRequired,
 };
 };
 
 
 const XssFormWrapperFC = (props) => {
 const XssFormWrapperFC = (props) => {
@@ -168,6 +199,8 @@ const XssFormWrapperFC = (props) => {
   return <XssForm t={t} {...props} />;
   return <XssForm t={t} {...props} />;
 };
 };
 
 
-const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AdminMarkDownContainer]);
+const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [
+  AdminMarkDownContainer,
+]);
 
 
 export default XssFormWrapper;
 export default XssFormWrapper;

+ 1 - 4
apps/app/src/client/components/Admin/NotFoundPage.tsx

@@ -1,11 +1,8 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 export const AdminNotFoundPage = (): JSX.Element => {
 export const AdminNotFoundPage = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
 
 
-  return (
-    <h1 className="title">{t('not_found_page.page_not_exist')}</h1>
-  );
+  return <h1 className="title">{t('not_found_page.page_not_exist')}</h1>;
 };
 };

+ 71 - 24
apps/app/src/client/components/Admin/Notification/GlobalNotification.jsx

@@ -1,30 +1,31 @@
 import { React, useCallback } from 'react';
 import { React, useCallback } from 'react';
-
-import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import GlobalNotificationList from './GlobalNotificationList';
 import GlobalNotificationList from './GlobalNotificationList';
 
 
 const logger = loggerFactory('growi:GlobalNotification');
 const logger = loggerFactory('growi:GlobalNotification');
 
 
 const GlobalNotification = (props) => {
 const GlobalNotification = (props) => {
-
   const { adminNotificationContainer } = props;
   const { adminNotificationContainer } = props;
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
-  const onClickSubmit = useCallback(async() => {
+  const onClickSubmit = useCallback(async () => {
     try {
     try {
       await adminNotificationContainer.updateGlobalNotificationForPages();
       await adminNotificationContainer.updateGlobalNotificationForPages();
-      toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification'), ns: 'commons' }));
-    }
-    catch (err) {
+      toastSuccess(
+        t('toaster.update_successed', {
+          target: t('external_notification.external_notification'),
+          ns: 'commons',
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
@@ -34,11 +35,18 @@ const GlobalNotification = (props) => {
   const { globalNotifications } = adminNotificationContainer.state;
   const { globalNotifications } = adminNotificationContainer.state;
   return (
   return (
     <>
     <>
-      <h2 className="border-bottom my-4">{t('notification_settings.valid_page')}</h2>
+      <h2 className="border-bottom my-4">
+        {t('notification_settings.valid_page')}
+      </h2>
 
 
       <p className="card custom-card bg-body-tertiary">
       <p className="card custom-card bg-body-tertiary">
         {/* eslint-disable-next-line react/no-danger */}
         {/* eslint-disable-next-line react/no-danger */}
-        <span dangerouslySetInnerHTML={{ __html: t('notification_settings.link_notification_help') }} />
+        <span
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+          dangerouslySetInnerHTML={{
+            __html: t('notification_settings.link_notification_help'),
+          }}
+        />
       </p>
       </p>
       <div className="row mb-4">
       <div className="row mb-4">
         <div className="col-md-8 offset-md-2">
         <div className="col-md-8 offset-md-2">
@@ -47,12 +55,25 @@ const GlobalNotification = (props) => {
               id="isNotificationForOwnerPageEnabled"
               id="isNotificationForOwnerPageEnabled"
               className="form-check-input"
               className="form-check-input"
               type="checkbox"
               type="checkbox"
-              checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
-              onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
+              checked={
+                adminNotificationContainer.state
+                  .isNotificationForOwnerPageEnabled || false
+              }
+              onChange={() => {
+                adminNotificationContainer.switchIsNotificationForOwnerPageEnabled();
+              }}
             />
             />
-            <label className="form-label form-check-label" htmlFor="isNotificationForOwnerPageEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isNotificationForOwnerPageEnabled"
+            >
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: t('notification_settings.just_me_notification_help') }} />
+              <span
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.just_me_notification_help'),
+                }}
+              />
             </label>
             </label>
           </div>
           </div>
         </div>
         </div>
@@ -65,12 +86,25 @@ const GlobalNotification = (props) => {
               id="isNotificationForGroupPageEnabled"
               id="isNotificationForGroupPageEnabled"
               className="form-check-input"
               className="form-check-input"
               type="checkbox"
               type="checkbox"
-              checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
-              onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
+              checked={
+                adminNotificationContainer.state
+                  .isNotificationForGroupPageEnabled || false
+              }
+              onChange={() => {
+                adminNotificationContainer.switchIsNotificationForGroupPageEnabled();
+              }}
             />
             />
-            <label className="form-label form-check-label" htmlFor="isNotificationForGroupPageEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isNotificationForGroupPageEnabled"
+            >
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: t('notification_settings.group_notification_help') }} />
+              <span
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.group_notification_help'),
+                }}
+              />
             </label>
             </label>
           </div>
           </div>
         </div>
         </div>
@@ -82,7 +116,8 @@ const GlobalNotification = (props) => {
             className="btn btn-primary"
             className="btn btn-primary"
             onClick={onClickSubmit}
             onClick={onClickSubmit}
             disabled={adminNotificationContainer.state.retrieveError}
             disabled={adminNotificationContainer.state.retrieveError}
-          >{t('Update')}
+          >
+            {t('Update')}
           </button>
           </button>
         </div>
         </div>
       </div>
       </div>
@@ -94,14 +129,23 @@ const GlobalNotification = (props) => {
         className="btn btn-outline-secondary mb-3"
         className="btn btn-outline-secondary mb-3"
         type="button"
         type="button"
         onClick={() => router.push('/admin/global-notification/new')}
         onClick={() => router.push('/admin/global-notification/new')}
-      >{t('notification_settings.add_notification')}
+      >
+        {t('notification_settings.add_notification')}
       </button>
       </button>
       <table className="table table-bordered">
       <table className="table table-bordered">
         <thead>
         <thead>
           <tr>
           <tr>
             <th>ON/OFF</th>
             <th>ON/OFF</th>
             {/* eslint-disable-next-line react/no-danger */}
             {/* eslint-disable-next-line react/no-danger */}
-            <th>{t('notification_settings.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help') }} /></th>
+            <th>
+              {t('notification_settings.trigger_path')}{' '}
+              <span
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('notification_settings.trigger_path_help'),
+                }}
+              />
+            </th>
             <th>{t('notification_settings.trigger_events')}</th>
             <th>{t('notification_settings.trigger_events')}</th>
             <th>{t('notification_settings.notify_to')}</th>
             <th>{t('notification_settings.notify_to')}</th>
             <th></th>
             <th></th>
@@ -118,9 +162,12 @@ const GlobalNotification = (props) => {
 };
 };
 
 
 GlobalNotification.propTypes = {
 GlobalNotification.propTypes = {
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 };
 
 
-const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotification, [AdminNotificationContainer]);
+const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotification, [
+  AdminNotificationContainer,
+]);
 
 
 export default GlobalNotificationWrapper;
 export default GlobalNotificationWrapper;

+ 90 - 40
apps/app/src/client/components/Admin/Notification/GlobalNotificationList.jsx

@@ -1,24 +1,20 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import NotificationDeleteModal from './NotificationDeleteModal';
 import NotificationDeleteModal from './NotificationDeleteModal';
 import { NotificationTypeIcon } from './NotificationTypeIcon';
 import { NotificationTypeIcon } from './NotificationTypeIcon';
 
 
-
 const logger = loggerFactory('growi:GolobalNotificationList');
 const logger = loggerFactory('growi:GolobalNotificationList');
 
 
 class GlobalNotificationList extends React.Component {
 class GlobalNotificationList extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -36,34 +32,52 @@ class GlobalNotificationList extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     const isEnabled = !notification.isEnabled;
     const isEnabled = !notification.isEnabled;
     try {
     try {
-      await apiv3Put(`/notification-setting/global-notification/${notification._id}/enabled`, {
-        isEnabled,
-      });
-      toastSuccess(t('notification_settings.toggle_notification', { path: notification.triggerPath }));
+      await apiv3Put(
+        `/notification-setting/global-notification/${notification._id}/enabled`,
+        {
+          isEnabled,
+        },
+      );
+      toastSuccess(
+        t('notification_settings.toggle_notification', {
+          path: notification.triggerPath,
+        }),
+      );
       await this.props.adminNotificationContainer.retrieveNotificationData();
       await this.props.adminNotificationContainer.retrieveNotificationData();
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
   }
   }
 
 
   openConfirmationModal(notification) {
   openConfirmationModal(notification) {
-    this.setState({ isConfirmationModalOpen: true, notificationForConfiguration: notification });
+    this.setState({
+      isConfirmationModalOpen: true,
+      notificationForConfiguration: notification,
+    });
   }
   }
 
 
   closeConfirmationModal() {
   closeConfirmationModal() {
-    this.setState({ isConfirmationModalOpen: false, notificationForConfiguration: null });
+    this.setState({
+      isConfirmationModalOpen: false,
+      notificationForConfiguration: null,
+    });
   }
   }
 
 
   async onClickSubmit() {
   async onClickSubmit() {
     const { t, adminNotificationContainer } = this.props;
     const { t, adminNotificationContainer } = this.props;
 
 
     try {
     try {
-      const deletedNotificaton = await adminNotificationContainer.deleteGlobalNotificationPattern(this.state.notificationForConfiguration._id);
-      toastSuccess(t('notification_settings.delete_notification_pattern', { path: deletedNotificaton.triggerPath }));
-    }
-    catch (err) {
+      const deletedNotificaton =
+        await adminNotificationContainer.deleteGlobalNotificationPattern(
+          this.state.notificationForConfiguration._id,
+        );
+      toastSuccess(
+        t('notification_settings.delete_notification_pattern', {
+          path: deletedNotificaton.triggerPath,
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
@@ -88,50 +102,67 @@ class GlobalNotificationList extends React.Component {
                     defaultChecked={notification.isEnabled}
                     defaultChecked={notification.isEnabled}
                     onClick={() => this.toggleIsEnabled(notification)}
                     onClick={() => this.toggleIsEnabled(notification)}
                   />
                   />
-                  <label className="form-label form-check-label" htmlFor={notification._id} />
+                  <label
+                    className="form-label form-check-label"
+                    htmlFor={notification._id}
+                  >
+                    <span className="visually-hidden">{t('Enable')}</span>
+                  </label>
                 </div>
                 </div>
               </td>
               </td>
-              <td>
-                {notification.triggerPath}
-              </td>
+              <td>{notification.triggerPath}</td>
               <td>
               <td>
                 <ul className="list-inline mb-0">
                 <ul className="list-inline mb-0">
                   {notification.triggerEvents.includes('pageCreate') && (
                   {notification.triggerEvents.includes('pageCreate') && (
                     <li className="list-inline-item badge rounded-pill bg-success">
                     <li className="list-inline-item badge rounded-pill bg-success">
-                      <span className=" material-symbols-outlined">description</span> CREATE
+                      <span className=" material-symbols-outlined">
+                        description
+                      </span>{' '}
+                      CREATE
                     </li>
                     </li>
                   )}
                   )}
                   {notification.triggerEvents.includes('pageEdit') && (
                   {notification.triggerEvents.includes('pageEdit') && (
                     <li className="list-inline-item badge rounded-pill bg-warning text-dark">
                     <li className="list-inline-item badge rounded-pill bg-warning text-dark">
-                      <span className="material-symbols-outlined">edit</span> EDIT
+                      <span className="material-symbols-outlined">edit</span>{' '}
+                      EDIT
                     </li>
                     </li>
                   )}
                   )}
                   {notification.triggerEvents.includes('pageMove') && (
                   {notification.triggerEvents.includes('pageMove') && (
                     <li className="list-inline-item badge rounded-pill bg-pink">
                     <li className="list-inline-item badge rounded-pill bg-pink">
-                      <span className="material-symbols-outlined">redo</span> MOVE
+                      <span className="material-symbols-outlined">redo</span>{' '}
+                      MOVE
                     </li>
                     </li>
                   )}
                   )}
                   {notification.triggerEvents.includes('pageDelete') && (
                   {notification.triggerEvents.includes('pageDelete') && (
                     <li className="list-inline-item badge rounded-pill bg-danger">
                     <li className="list-inline-item badge rounded-pill bg-danger">
-                      <span className="material-symbols-outlined">delete_forever</span>DELETE
+                      <span className="material-symbols-outlined">
+                        delete_forever
+                      </span>
+                      DELETE
                     </li>
                     </li>
                   )}
                   )}
                   {notification.triggerEvents.includes('pageLike') && (
                   {notification.triggerEvents.includes('pageLike') && (
                     <li className="list-inline-item badge rounded-pill bg-info">
                     <li className="list-inline-item badge rounded-pill bg-info">
-                      <span className="material-symbols-outlined">favorite</span> LIKE
+                      <span className="material-symbols-outlined">
+                        favorite
+                      </span>{' '}
+                      LIKE
                     </li>
                     </li>
                   )}
                   )}
                   {notification.triggerEvents.includes('comment') && (
                   {notification.triggerEvents.includes('comment') && (
                     <li className="list-inline-item badge rounded-pill bg-primary">
                     <li className="list-inline-item badge rounded-pill bg-primary">
-                      <span className="material-symbols-outlined">bubble_chart</span> POST
+                      <span className="material-symbols-outlined">
+                        bubble_chart
+                      </span>{' '}
+                      POST
                     </li>
                     </li>
                   )}
                   )}
                 </ul>
                 </ul>
               </td>
               </td>
               <td>
               <td>
                 <NotificationTypeIcon notification={notification} />
                 <NotificationTypeIcon notification={notification} />
-                { notification.__t === 'mail' && notification.toEmail }
-                { notification.__t === 'slack' && notification.slackChannels }
+                {notification.__t === 'mail' && notification.toEmail}
+                {notification.__t === 'slack' && notification.slackChannels}
               </td>
               </td>
               <td className="td-abs-center">
               <td className="td-abs-center">
                 <div className="dropdown">
                 <div className="dropdown">
@@ -143,14 +174,29 @@ class GlobalNotificationList extends React.Component {
                     aria-haspopup="true"
                     aria-haspopup="true"
                     aria-expanded="false"
                     aria-expanded="false"
                   >
                   >
-                    <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
+                    <span className="material-symbols-outlined">settings</span>{' '}
+                    <span className="caret"></span>
                   </button>
                   </button>
-                  <div className="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
-                    <a className="dropdown-item" href={urljoin('/admin/global-notification/', notification._id)}>
-                      <span className="material-symbols-outlined">note</span> {t('Edit')}
+                  <div className="dropdown-menu dropdown-menu-right">
+                    <a
+                      className="dropdown-item"
+                      href={urljoin(
+                        '/admin/global-notification/',
+                        notification._id,
+                      )}
+                    >
+                      <span className="material-symbols-outlined">note</span>{' '}
+                      {t('Edit')}
                     </a>
                     </a>
-                    <button className="dropdown-item" type="button" onClick={() => this.openConfirmationModal(notification)}>
-                      <span className="material-symbols-outlined text-danger">delete_forever</span> {t('Delete')}
+                    <button
+                      className="dropdown-item"
+                      type="button"
+                      onClick={() => this.openConfirmationModal(notification)}
+                    >
+                      <span className="material-symbols-outlined text-danger">
+                        delete_forever
+                      </span>{' '}
+                      {t('Delete')}
                     </button>
                     </button>
                   </div>
                   </div>
                 </div>
                 </div>
@@ -163,19 +209,20 @@ class GlobalNotificationList extends React.Component {
             isOpen={this.state.isConfirmationModalOpen}
             isOpen={this.state.isConfirmationModalOpen}
             onClose={this.closeConfirmationModal}
             onClose={this.closeConfirmationModal}
             onClickSubmit={this.onClickSubmit}
             onClickSubmit={this.onClickSubmit}
-            notificationForConfiguration={this.state.notificationForConfiguration}
+            notificationForConfiguration={
+              this.state.notificationForConfiguration
+            }
           />
           />
         )}
         )}
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
-
 }
 }
 
 
 GlobalNotificationList.propTypes = {
 GlobalNotificationList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
-
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 };
 
 
 const GlobalNotificationListWrapperFC = (props) => {
 const GlobalNotificationListWrapperFC = (props) => {
@@ -184,6 +231,9 @@ const GlobalNotificationListWrapperFC = (props) => {
   return <GlobalNotificationList t={t} {...props} />;
   return <GlobalNotificationList t={t} {...props} />;
 };
 };
 
 
-const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AdminNotificationContainer]);
+const GlobalNotificationListWrapper = withUnstatedContainers(
+  GlobalNotificationListWrapperFC,
+  [AdminNotificationContainer],
+);
 
 
 export default GlobalNotificationListWrapper;
 export default GlobalNotificationListWrapper;

+ 160 - 102
apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx

@@ -1,45 +1,49 @@
 import React, {
 import React, {
-  useCallback, useMemo, useEffect, useState, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
 } from 'react';
 } from 'react';
-
-import { useAtomValue } from 'jotai';
-import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { useAtomValue } from 'jotai';
+import { useTranslation } from 'next-i18next';
 
 
-import { NotifyType, TriggerEventType } from '~/client/interfaces/global-notification';
+import {
+  NotifyType,
+  TriggerEventType,
+} from '~/client/interfaces/global-notification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { isMailerSetupAtom } from '~/states/server-configurations';
 import { isMailerSetupAtom } from '~/states/server-configurations';
 import { useSWRxGlobalNotification } from '~/stores/global-notification';
 import { useSWRxGlobalNotification } from '~/stores/global-notification';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
 import TriggerEventCheckBox from './TriggerEventCheckBox';
 import TriggerEventCheckBox from './TriggerEventCheckBox';
 
 
-
 const logger = loggerFactory('growi:manageGlobalNotification');
 const logger = loggerFactory('growi:manageGlobalNotification');
 
 
-
 type Props = {
 type Props = {
-  globalNotificationId?: string,
-}
+  globalNotificationId?: string;
+};
 
 
 const ManageGlobalNotification = (props: Props): JSX.Element => {
 const ManageGlobalNotification = (props: Props): JSX.Element => {
-
   const [triggerPath, setTriggerPath] = useState('');
   const [triggerPath, setTriggerPath] = useState('');
   const [notifyType, setNotifyType] = useState<NotifyType>(NotifyType.Email);
   const [notifyType, setNotifyType] = useState<NotifyType>(NotifyType.Email);
   const [emailToSend, setEmailToSend] = useState('');
   const [emailToSend, setEmailToSend] = useState('');
   const [slackChannelToSend, setSlackChannelToSend] = useState('');
   const [slackChannelToSend, setSlackChannelToSend] = useState('');
-  const [triggerEvents, setTriggerEvents] = useState(new Set());
-  const { data: globalNotificationData, update: updateGlobalNotification } = useSWRxGlobalNotification(props.globalNotificationId || '');
-  const globalNotification = useMemo(() => globalNotificationData?.globalNotification, [globalNotificationData?.globalNotification]);
+  const [triggerEvents, setTriggerEvents] = useState(new Set<string>());
+  const { data: globalNotificationData, update: updateGlobalNotification } =
+    useSWRxGlobalNotification(props.globalNotificationId || '');
+  const globalNotification = useMemo(
+    () => globalNotificationData?.globalNotification,
+    [globalNotificationData?.globalNotification],
+  );
 
 
   const router = useRouter();
   const router = useRouter();
 
 
-
   useEffect(() => {
   useEffect(() => {
     if (globalNotification != null) {
     if (globalNotification != null) {
       const notifyType = globalNotification.__t;
       const notifyType = globalNotification.__t;
@@ -50,15 +54,15 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
 
 
       if (notifyType === NotifyType.Email) {
       if (notifyType === NotifyType.Email) {
         setEmailToSend(globalNotification.toEmail);
         setEmailToSend(globalNotification.toEmail);
-      }
-      else {
+      } else {
         setSlackChannelToSend(globalNotification.slackChannels);
         setSlackChannelToSend(globalNotification.slackChannels);
       }
       }
     }
     }
   }, [globalNotification]);
   }, [globalNotification]);
 
 
   const isLoading = globalNotificationData === undefined;
   const isLoading = globalNotificationData === undefined;
-  const notExistsGlobalNotification = !isLoading && globalNotificationData == null;
+  const notExistsGlobalNotification =
+    !isLoading && globalNotificationData == null;
 
 
   useEffect(() => {
   useEffect(() => {
     if (notExistsGlobalNotification) {
     if (notExistsGlobalNotification) {
@@ -66,22 +70,24 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
     }
     }
   }, [notExistsGlobalNotification, router]);
   }, [notExistsGlobalNotification, router]);
 
 
+  const onChangeTriggerEvents = useCallback(
+    (triggerEvent: string) => {
+      let newTriggerEvents: string[];
+
+      if (triggerEvents.has(triggerEvent)) {
+        newTriggerEvents = [...triggerEvents].filter(
+          (item) => item !== triggerEvent,
+        );
+        setTriggerEvents(new Set(newTriggerEvents));
+      } else {
+        newTriggerEvents = [...triggerEvents, triggerEvent];
+        setTriggerEvents(new Set(newTriggerEvents));
+      }
+    },
+    [triggerEvents],
+  );
 
 
-  const onChangeTriggerEvents = useCallback((triggerEvent) => {
-    let newTriggerEvents;
-
-    if (triggerEvents.has(triggerEvent)) {
-      newTriggerEvents = ([...triggerEvents].filter(item => item !== triggerEvent));
-      setTriggerEvents(new Set(newTriggerEvents));
-    }
-    else {
-      newTriggerEvents = [...triggerEvents, triggerEvent];
-      setTriggerEvents(new Set(newTriggerEvents));
-    }
-  }, [triggerEvents]);
-
-
-  const updateButtonClickedHandler = useCallback(async() => {
+  const updateButtonClickedHandler = useCallback(async () => {
     const requestParams = {
     const requestParams = {
       triggerPath,
       triggerPath,
       notifyType,
       notifyType,
@@ -94,18 +100,27 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
       if (props.globalNotificationId != null) {
       if (props.globalNotificationId != null) {
         await updateGlobalNotification(requestParams);
         await updateGlobalNotification(requestParams);
         router.push('/admin/notification');
         router.push('/admin/notification');
-      }
-      else {
-        await apiv3Post('/notification-setting/global-notification', requestParams);
+      } else {
+        await apiv3Post(
+          '/notification-setting/global-notification',
+          requestParams,
+        );
         router.push('/admin/notification');
         router.push('/admin/notification');
       }
       }
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
-  }, [emailToSend, notifyType, props.globalNotificationId, router, slackChannelToSend, triggerEvents, triggerPath, updateGlobalNotification]);
-
+  }, [
+    emailToSend,
+    notifyType,
+    props.globalNotificationId,
+    router,
+    slackChannelToSend,
+    triggerEvents,
+    triggerPath,
+    updateGlobalNotification,
+  ]);
 
 
   // Mailer setup status (unused yet but kept for potential conditional logic)
   // Mailer setup status (unused yet but kept for potential conditional logic)
   const isMailerSetup = useAtomValue(isMailerSetupAtom);
   const isMailerSetup = useAtomValue(isMailerSetupAtom);
@@ -115,22 +130,34 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
     <>
     <>
       <div className="my-3">
       <div className="my-3">
         <Link href="/admin/notification" className="btn btn-outline-secondary">
         <Link href="/admin/notification" className="btn btn-outline-secondary">
-          <span className="material-symbols-outlined" aria-hidden="true">arrow_left_alt</span>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            arrow_left_alt
+          </span>
           {t('notification_settings.back_to_list')}
           {t('notification_settings.back_to_list')}
         </Link>
         </Link>
       </div>
       </div>
 
 
-
       <div className="row">
       <div className="row">
         <div className="form-box col-md-12">
         <div className="form-box col-md-12">
-          <h2 className="border-bottom mb-5">{t('notification_settings.notification_detail')}</h2>
+          <h2 className="border-bottom mb-5">
+            {t('notification_settings.notification_detail')}
+          </h2>
         </div>
         </div>
 
 
         <div className="col-sm-4">
         <div className="col-sm-4">
           <h3>
           <h3>
-            <label htmlFor="triggerPath" className="form-label">{t('notification_settings.trigger_path')}
+            <label htmlFor="triggerPath" className="form-label">
+              {t('notification_settings.trigger_path')}
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
-              <small dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help', '<code>*</code>') }} />
+              <small
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'notification_settings.trigger_path_help',
+                    '<code>*</code>',
+                  ),
+                }}
+              />
             </label>
             </label>
           </h3>
           </h3>
           <div>
           <div>
@@ -139,7 +166,9 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
               type="text"
               type="text"
               name="triggerPath"
               name="triggerPath"
               value={triggerPath}
               value={triggerPath}
-              onChange={(e) => { setTriggerPath(e.target.value) }}
+              onChange={(e) => {
+                setTriggerPath(e.target.value);
+              }}
               required
               required
             />
             />
           </div>
           </div>
@@ -154,7 +183,9 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 name="notifyType"
                 name="notifyType"
                 value="mail"
                 value="mail"
                 checked={notifyType === NotifyType.Email}
                 checked={notifyType === NotifyType.Email}
-                onChange={() => { setNotifyType(NotifyType.Email) }}
+                onChange={() => {
+                  setNotifyType(NotifyType.Email);
+                }}
               />
               />
               <label className="form-label form-check-label" htmlFor="mail">
               <label className="form-label form-check-label" htmlFor="mail">
                 <p className="fw-bold">Email</p>
                 <p className="fw-bold">Email</p>
@@ -168,7 +199,9 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 name="notifyType"
                 name="notifyType"
                 value="slack"
                 value="slack"
                 checked={notifyType === NotifyType.SLACK}
                 checked={notifyType === NotifyType.SLACK}
-                onChange={() => { setNotifyType(NotifyType.SLACK) }}
+                onChange={() => {
+                  setNotifyType(NotifyType.SLACK);
+                }}
               />
               />
               <label className="form-label form-check-label" htmlFor="slack">
               <label className="form-label form-check-label" htmlFor="slack">
                 <p className="fw-bold">Slack</p>
                 <p className="fw-bold">Slack</p>
@@ -176,57 +209,77 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
             </div>
             </div>
           </div>
           </div>
 
 
-          {notifyType === NotifyType.Email
-            ? (
-              <>
-                <div className="input-group notify-to-option" id="mail-input">
-                  <div>
-                    <span className="input-group-text" id="mail-addon"></span><span className="material-symbols-outlined">mail</span>
-                  </div>
-                  <input
-                    className="form-control"
-                    type="text"
-                    aria-describedby="mail-addon"
-                    name="toEmail"
-                    placeholder="Email"
-                    value={emailToSend}
-                    onChange={(e) => { setEmailToSend(e.target.value) }}
-                  />
-
+          {notifyType === NotifyType.Email ? (
+            <>
+              <div className="input-group notify-to-option" id="mail-input">
+                <div>
+                  <span className="input-group-text" id="mail-addon"></span>
+                  <span className="material-symbols-outlined">mail</span>
                 </div>
                 </div>
-
-                <p className="p-2">
-                  {/* eslint-disable-next-line react/no-danger */}
-                  {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
-                  <b>Hint: </b>
-                  <a href="https://ifttt.com/create" target="blank">{t('notification_settings.email.ifttt_link')}
-                    <span className="material-symbols-outlined">share</span>
-                  </a>
-                </p>
-              </>
-            )
-            : (
-              <>
-                <div className="input-group notify-to-option" id="slack-input">
-                  <div>
-                    <span className="input-group-text" id="slack-channel-addon"></span><span className="material-symbols-outlined">tag</span>
-                  </div>
-                  <input
-                    className="form-control"
-                    type="text"
-                    aria-describedby="slack-channel-addon"
-                    name="notificationGlobal[slackChannels]"
-                    placeholder="Slack Channel"
-                    value={slackChannelToSend}
-                    onChange={(e) => { setSlackChannelToSend(e.target.value) }}
+                <input
+                  className="form-control"
+                  type="text"
+                  aria-describedby="mail-addon"
+                  name="toEmail"
+                  placeholder="Email"
+                  value={emailToSend}
+                  onChange={(e) => {
+                    setEmailToSend(e.target.value);
+                  }}
+                />
+              </div>
+
+              <p className="p-2">
+                {/* eslint-disable-next-line react/no-danger */}
+                {!isMailerSetup && (
+                  <span
+                    className="form-text text-muted"
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('admin:mailer_setup_required'),
+                    }}
                   />
                   />
+                )}
+                <b>Hint: </b>
+                <a href="https://ifttt.com/create" target="blank">
+                  {t('notification_settings.email.ifttt_link')}
+                  <span className="material-symbols-outlined">share</span>
+                </a>
+              </p>
+            </>
+          ) : (
+            <>
+              <div className="input-group notify-to-option" id="slack-input">
+                <div>
+                  <span
+                    className="input-group-text"
+                    id="slack-channel-addon"
+                  ></span>
+                  <span className="material-symbols-outlined">tag</span>
                 </div>
                 </div>
-                <p className="p-2">
-                  {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
-                </p>
-              </>
-            )}
+                <input
+                  className="form-control"
+                  type="text"
+                  aria-describedby="slack-channel-addon"
+                  name="notificationGlobal[slackChannels]"
+                  placeholder="Slack Channel"
+                  value={slackChannelToSend}
+                  onChange={(e) => {
+                    setSlackChannelToSend(e.target.value);
+                  }}
+                />
+              </div>
+              <p className="p-2">
+                {/* eslint-disable-next-line react/no-danger */}
+                <span
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('notification_settings.channel_desc'),
+                  }}
+                />
+              </p>
+            </>
+          )}
         </div>
         </div>
 
 
         <div className="offset-1 col-sm-5">
         <div className="offset-1 col-sm-5">
@@ -240,7 +293,8 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.CREATE)}
                 onChange={() => onChangeTriggerEvents(TriggerEventType.CREATE)}
               >
               >
                 <span className="badge rounded-pill bg-success">
                 <span className="badge rounded-pill bg-success">
-                  <span className="material-symbols-outlined">edit_note</span> CREATE
+                  <span className="material-symbols-outlined">edit_note</span>{' '}
+                  CREATE
                 </span>
                 </span>
               </TriggerEventCheckBox>
               </TriggerEventCheckBox>
             </div>
             </div>
@@ -276,7 +330,10 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.DELETE)}
                 onChange={() => onChangeTriggerEvents(TriggerEventType.DELETE)}
               >
               >
                 <span className="badge rounded-pill bg-danger">
                 <span className="badge rounded-pill bg-danger">
-                  <span className="material-symbols-outlined">delete_forever</span>DELETE
+                  <span className="material-symbols-outlined">
+                    delete_forever
+                  </span>
+                  DELETE
                 </span>
                 </span>
               </TriggerEventCheckBox>
               </TriggerEventCheckBox>
             </div>
             </div>
@@ -288,7 +345,8 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.LIKE)}
                 onChange={() => onChangeTriggerEvents(TriggerEventType.LIKE)}
               >
               >
                 <span className="badge rounded-pill bg-info">
                 <span className="badge rounded-pill bg-info">
-                  <span className="material-symbols-outlined">favorite</span>LIKE
+                  <span className="material-symbols-outlined">favorite</span>
+                  LIKE
                 </span>
                 </span>
               </TriggerEventCheckBox>
               </TriggerEventCheckBox>
             </div>
             </div>
@@ -300,11 +358,11 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.POST)}
                 onChange={() => onChangeTriggerEvents(TriggerEventType.POST)}
               >
               >
                 <span className="badge rounded-pill bg-primary">
                 <span className="badge rounded-pill bg-primary">
-                  <span className="material-symbols-outlined">language</span>POST
+                  <span className="material-symbols-outlined">language</span>
+                  POST
                 </span>
                 </span>
               </TriggerEventCheckBox>
               </TriggerEventCheckBox>
             </div>
             </div>
-
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 18 - 11
apps/app/src/client/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -1,37 +1,44 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 
 class NotificationDeleteModal extends React.PureComponent {
 class NotificationDeleteModal extends React.PureComponent {
-
   render() {
   render() {
     const { t, notificationForConfiguration } = this.props;
     const { t, notificationForConfiguration } = this.props;
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-danger">
-          <span className="material-symbols-outlined">delete_forever</span>Delete Global Notification Setting
+        <ModalHeader
+          tag="h4"
+          toggle={this.props.onClose}
+          className="text-danger"
+        >
+          <span className="material-symbols-outlined">delete_forever</span>
+          Delete Global Notification Setting
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>
           <p>
           <p>
-            {t('notification_settings.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
+            {t('notification_settings.delete_notification_pattern_desc1', {
+              path: notificationForConfiguration.triggerPath,
+            })}
           </p>
           </p>
           <p className="text-danger">
           <p className="text-danger">
             {t('notification_settings.delete_notification_pattern_desc2')}
             {t('notification_settings.delete_notification_pattern_desc2')}
           </p>
           </p>
         </ModalBody>
         </ModalBody>
         <ModalFooter>
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-danger" onClick={this.props.onClickSubmit}>
-            <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
+          <button
+            type="button"
+            className="btn btn-sm btn-danger"
+            onClick={this.props.onClickSubmit}
+          >
+            <span className="material-symbols-outlined">delete_forever</span>{' '}
+            {t('Delete')}
           </button>
           </button>
         </ModalFooter>
         </ModalFooter>
       </Modal>
       </Modal>
     );
     );
   }
   }
-
 }
 }
 
 
 NotificationDeleteModal.propTypes = {
 NotificationDeleteModal.propTypes = {

+ 93 - 54
apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx

@@ -1,13 +1,8 @@
-import React, {
-  useCallback, useEffect, useMemo, useState,
-} from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import {
-  TabContent, TabPane,
-} from 'reactstrap';
+import { TabContent, TabPane } from 'reactstrap';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
@@ -16,8 +11,6 @@ import loggerFactory from '~/utils/logger';
 
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import CustomNav from '../../CustomNavigation/CustomNav';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import GlobalNotification from './GlobalNotification';
 import GlobalNotification from './GlobalNotification';
 import UserTriggerNotification from './UserTriggerNotification';
 import UserTriggerNotification from './UserTriggerNotification';
 
 
@@ -25,14 +18,34 @@ const logger = loggerFactory('growi:NotificationSetting');
 
 
 let retrieveErrors = null;
 let retrieveErrors = null;
 
 
+const SettingsIcon = () => (
+  <span className="material-symbols-outlined">settings</span>
+);
+
+const navTabMapping = {
+  user_trigger_notification: {
+    Icon: SettingsIcon,
+    i18n: 'User trigger notification',
+  },
+  global_notification: {
+    Icon: SettingsIcon,
+    i18n: 'Global notification',
+  },
+};
 
 
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const Badge = ({ isEnabled }) => {
 const Badge = ({ isEnabled }) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
-  return isEnabled
-    ? <span className="badge text-bg-success">{t('external_notification.enabled')}</span>
-    : <span className="badge text-bg-primary">{t('external_notification.disabled')}</span>;
+  return isEnabled ? (
+    <span className="badge text-bg-success">
+      {t('external_notification.enabled')}
+    </span>
+  ) : (
+    <span className="badge text-bg-primary">
+      {t('external_notification.disabled')}
+    </span>
+  );
 };
 };
 
 
 const SkeletonListItem = () => (
 const SkeletonListItem = () => (
@@ -48,20 +61,32 @@ const SkeletonListItem = () => (
 const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
 const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
-  const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
+  const isCautionVisible =
+    currentBotType === SlackbotType.OFFICIAL ||
+    currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
 
 
   return (
   return (
-    <li data-testid="slack-integration-list-item" className="list-group-item bg-body-tertiary">
+    <li
+      data-testid="slack-integration-list-item"
+      className="list-group-item bg-body-tertiary"
+    >
       <h4>
       <h4>
         <Badge isEnabled={isEnabled} />
         <Badge isEnabled={isEnabled} />
-        <a href="/admin/slack-integration" className="ms-2">{t('slack_integration.slack_integration')}</a>
+        <a href="/admin/slack-integration" className="ms-2">
+          {t('slack_integration.slack_integration')}
+        </a>
       </h4>
       </h4>
-      { isCautionVisible && (
+      {isCautionVisible && (
         <ul className="mt-2 ps-4">
         <ul className="mt-2 ps-4">
           {/* eslint-disable-next-line react/no-danger */}
           {/* eslint-disable-next-line react/no-danger */}
-          <li dangerouslySetInnerHTML={{ __html: t('external_notification.caution_enabled') }} />
+          <li
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+            dangerouslySetInnerHTML={{
+              __html: t('external_notification.caution_enabled'),
+            }}
+          />
         </ul>
         </ul>
-      ) }
+      )}
     </li>
     </li>
   );
   );
 };
 };
@@ -74,16 +99,24 @@ const LegacySlackIntegrationListItem = ({ isEnabled }) => {
     <li className="list-group-item">
     <li className="list-group-item">
       <h4>
       <h4>
         <Badge isEnabled={isEnabled} />
         <Badge isEnabled={isEnabled} />
-        <a href="/admin/slack-integration-legacy" className="ms-2">{t('slack_integration_legacy.slack_integration_legacy')}</a>
+        <a href="/admin/slack-integration-legacy" className="ms-2">
+          {t('slack_integration_legacy.slack_integration_legacy')}
+        </a>
       </h4>
       </h4>
-      { isEnabled && (
+      {isEnabled && (
         <ul className="mt-2 ps-4">
         <ul className="mt-2 ps-4">
           <li>
           <li>
             {/* eslint-disable-next-line react/no-danger */}
             {/* eslint-disable-next-line react/no-danger */}
-            <span className="text-danger" dangerouslySetInnerHTML={{ __html: t('slack_integration_legacy.alert_deplicated') }}></span>
+            <span
+              className="text-danger"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+              dangerouslySetInnerHTML={{
+                __html: t('slack_integration_legacy.alert_deplicated'),
+              }}
+            ></span>
           </li>
           </li>
         </ul>
         </ul>
-      ) }
+      )}
     </li>
     </li>
   );
   );
 };
 };
@@ -95,24 +128,24 @@ function NotificationSetting(props) {
 
 
   const [isMounted, setMounted] = useState(false);
   const [isMounted, setMounted] = useState(false);
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
-  const [activeComponents, setActiveComponents] = useState(new Set(['user_trigger_notification']));
+  const [activeComponents, setActiveComponents] = useState(
+    new Set(['user_trigger_notification']),
+  );
 
 
   const switchActiveTab = (selectedTab) => {
   const switchActiveTab = (selectedTab) => {
     setActiveTab(selectedTab);
     setActiveTab(selectedTab);
     setActiveComponents(activeComponents.add(selectedTab));
     setActiveComponents(activeComponents.add(selectedTab));
   };
   };
 
 
-  const fetchData = useCallback(async() => {
+  const fetchData = useCallback(async () => {
     try {
     try {
       await adminNotificationContainer.retrieveNotificationData();
       await adminNotificationContainer.retrieveNotificationData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
       logger.error(errs);
       logger.error(errs);
       retrieveErrors = errs;
       retrieveErrors = errs;
-    }
-    finally {
+    } finally {
       setMounted(true);
       setMounted(true);
     }
     }
   }, [adminNotificationContainer]);
   }, [adminNotificationContainer]);
@@ -121,39 +154,37 @@ function NotificationSetting(props) {
     fetchData();
     fetchData();
   }, [fetchData]);
   }, [fetchData]);
 
 
-  const navTabMapping = useMemo(() => {
-    return {
-      user_trigger_notification: {
-        Icon: () => <span className="material-symbols-outlined">settings</span>,
-        i18n: 'User trigger notification',
-      },
-      global_notification: {
-        Icon: () => <span className="material-symbols-outlined">settings</span>,
-        i18n: 'Global notification',
-      },
-    };
-  }, []);
-
-  const { isSlackbotConfigured, isSlackLegacyConfigured, currentBotType } = adminNotificationContainer.state;
+  const { isSlackbotConfigured, isSlackLegacyConfigured, currentBotType } =
+    adminNotificationContainer.state;
   const isSlackEnabled = isSlackbotConfigured;
   const isSlackEnabled = isSlackbotConfigured;
   const isSlackLegacyEnabled = !isSlackbotConfigured && isSlackLegacyConfigured;
   const isSlackLegacyEnabled = !isSlackbotConfigured && isSlackLegacyConfigured;
 
 
   return (
   return (
     <div data-testid="admin-notification">
     <div data-testid="admin-notification">
-      <h2 className="admin-setting-header">{t('external_notification.header_status')}</h2>
+      <h2 className="admin-setting-header">
+        {t('external_notification.header_status')}
+      </h2>
       <ul className="list-group">
       <ul className="list-group">
-        { !isMounted && <SkeletonListItem />}
-        { isMounted && (
+        {!isMounted && <SkeletonListItem />}
+        {isMounted && (
           <>
           <>
-            <SlackIntegrationListItem isEnabled={isSlackEnabled} currentBotType={currentBotType} />
+            <SlackIntegrationListItem
+              isEnabled={isSlackEnabled}
+              currentBotType={currentBotType}
+            />
             {/* Legacy Slack Integration become visible only when new Slack Integration is disabled */}
             {/* Legacy Slack Integration become visible only when new Slack Integration is disabled */}
-            { !isSlackEnabled && <LegacySlackIntegrationListItem isEnabled={isSlackLegacyEnabled} /> }
+            {!isSlackEnabled && (
+              <LegacySlackIntegrationListItem
+                isEnabled={isSlackLegacyEnabled}
+              />
+            )}
           </>
           </>
-        ) }
+        )}
       </ul>
       </ul>
 
 
-
-      <h2 className="admin-setting-header mt-5">{t('notification_settings.notification_settings')}</h2>
+      <h2 className="admin-setting-header mt-5">
+        {t('notification_settings.notification_settings')}
+      </h2>
 
 
       <CustomNav
       <CustomNav
         activeTab={activeTab}
         activeTab={activeTab}
@@ -165,20 +196,28 @@ function NotificationSetting(props) {
 
 
       <TabContent activeTab={activeTab} className="p-5">
       <TabContent activeTab={activeTab} className="p-5">
         <TabPane tabId="user_trigger_notification">
         <TabPane tabId="user_trigger_notification">
-          {activeComponents.has('user_trigger_notification') && <UserTriggerNotification />}
+          {activeComponents.has('user_trigger_notification') && (
+            <UserTriggerNotification />
+          )}
         </TabPane>
         </TabPane>
         <TabPane tabId="global_notification">
         <TabPane tabId="global_notification">
-          {activeComponents.has('global_notification') && <GlobalNotification />}
+          {activeComponents.has('global_notification') && (
+            <GlobalNotification />
+          )}
         </TabPane>
         </TabPane>
       </TabContent>
       </TabContent>
     </div>
     </div>
   );
   );
 }
 }
 
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(
+  NotificationSetting,
+  [AdminNotificationContainer],
+);
 
 
 NotificationSetting.propTypes = {
 NotificationSetting.propTypes = {
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 };
 
 
 export default NotificationSettingWithUnstatedContainer;
 export default NotificationSettingWithUnstatedContainer;

+ 8 - 6
apps/app/src/client/components/Admin/Notification/NotificationTypeIcon.tsx

@@ -1,18 +1,18 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import type { INotificationType } from '~/client/interfaces/notification';
 import type { INotificationType } from '~/client/interfaces/notification';
 
 
-
 type NotificationTypeIconProps = {
 type NotificationTypeIconProps = {
   // supports 2 types:
   // supports 2 types:
   //   User trigger notification -> has 'provider: slack'
   //   User trigger notification -> has 'provider: slack'
   //   Global notification -> has '__t: slack|mail'
   //   Global notification -> has '__t: slack|mail'
-  notification: INotificationType
-}
+  notification: INotificationType;
+};
 
 
-export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Element => {
+export const NotificationTypeIcon = (
+  props: NotificationTypeIconProps,
+): JSX.Element => {
   const { __t, _id, provider } = props.notification;
   const { __t, _id, provider } = props.notification;
 
 
   const type = __t != null && __t === 'mail' ? 'mail' : 'slack';
   const type = __t != null && __t === 'mail' ? 'mail' : 'slack';
@@ -28,7 +28,9 @@ export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Elem
 
 
   return (
   return (
     <>
     <>
-      <span id={elemId} className="material-symbols-outlined me-1">{iconName}</span>
+      <span id={elemId} className="material-symbols-outlined me-1">
+        {iconName}
+      </span>
       <UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip>
       <UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip>
     </>
     </>
   );
   );

+ 5 - 5
apps/app/src/client/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -1,5 +1,4 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
@@ -15,15 +14,16 @@ const TriggerEventCheckBox = (props) => {
         checked={props.checked}
         checked={props.checked}
         onChange={props.onChange}
         onChange={props.onChange}
       />
       />
-      <label className="form-label form-check-label" htmlFor={`trigger-event-${props.event}`}>
-        {props.children}{' '}
-        {t(`notification_settings.event_${props.event}`)}
+      <label
+        className="form-label form-check-label"
+        htmlFor={`trigger-event-${props.event}`}
+      >
+        {props.children} {t(`notification_settings.event_${props.event}`)}
       </label>
       </label>
     </div>
     </div>
   );
   );
 };
 };
 
 
-
 TriggerEventCheckBox.propTypes = {
 TriggerEventCheckBox.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
 
 

+ 18 - 13
apps/app/src/client/components/Admin/Notification/UserNotificationRow.jsx

@@ -1,16 +1,13 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import { NotificationTypeIcon } from './NotificationTypeIcon';
 import { NotificationTypeIcon } from './NotificationTypeIcon';
 
 
 class UserNotificationRow extends React.PureComponent {
 class UserNotificationRow extends React.PureComponent {
-
   render() {
   render() {
     const { t, notification } = this.props;
     const { t, notification } = this.props;
     const id = `user-notification-${notification._id}`;
     const id = `user-notification-${notification._id}`;
@@ -18,26 +15,32 @@ class UserNotificationRow extends React.PureComponent {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
         <tr className="admin-notif-row" key={id}>
         <tr className="admin-notif-row" key={id}>
+          <td className="px-4">{notification.pathPattern}</td>
           <td className="px-4">
           <td className="px-4">
-            {notification.pathPattern}
-          </td>
-          <td className="px-4">
-            <NotificationTypeIcon notification={notification} />{notification.channel}
+            <NotificationTypeIcon notification={notification} />
+            {notification.channel}
           </td>
           </td>
           <td>
           <td>
-            <button type="submit" className="btn btn-outline-danger" onClick={() => { this.props.onClickDeleteBtn(notification._id) }}>{t('Delete')}</button>
+            <button
+              type="submit"
+              className="btn btn-outline-danger"
+              onClick={() => {
+                this.props.onClickDeleteBtn(notification._id);
+              }}
+            >
+              {t('Delete')}
+            </button>
           </td>
           </td>
         </tr>
         </tr>
       </React.Fragment>
       </React.Fragment>
     );
     );
-
   }
   }
-
 }
 }
 
 
 UserNotificationRow.propTypes = {
 UserNotificationRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 
 
   notification: PropTypes.object.isRequired,
   notification: PropTypes.object.isRequired,
   onClickDeleteBtn: PropTypes.func.isRequired,
   onClickDeleteBtn: PropTypes.func.isRequired,
@@ -50,7 +53,9 @@ const UserNotificationRowWrapperWrapperFC = (props) => {
   return <UserNotificationRow t={t} {...props} />;
   return <UserNotificationRow t={t} {...props} />;
 };
 };
 
 
-const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AdminNotificationContainer]);
-
+const UserNotificationRowWrapper = withUnstatedContainers(
+  UserNotificationRowWrapperWrapperFC,
+  [AdminNotificationContainer],
+);
 
 
 export default UserNotificationRowWrapper;
 export default UserNotificationRowWrapper;

+ 66 - 29
apps/app/src/client/components/Admin/Notification/UserTriggerNotification.jsx

@@ -1,20 +1,17 @@
 import React from 'react';
 import React from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import UserNotificationRow from './UserNotificationRow';
 import UserNotificationRow from './UserNotificationRow';
 
 
 const logger = loggerFactory('growi:slackAppConfiguration');
 const logger = loggerFactory('growi:slackAppConfiguration');
 
 
 class UserTriggerNotification extends React.Component {
 class UserTriggerNotification extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -28,7 +25,6 @@ class UserTriggerNotification extends React.Component {
     this.validateForm = this.validateForm.bind(this);
     this.validateForm = this.validateForm.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickDeleteBtn = this.onClickDeleteBtn.bind(this);
     this.onClickDeleteBtn = this.onClickDeleteBtn.bind(this);
-
   }
   }
 
 
   /**
   /**
@@ -53,11 +49,13 @@ class UserTriggerNotification extends React.Component {
     const { t, adminNotificationContainer } = this.props;
     const { t, adminNotificationContainer } = this.props;
 
 
     try {
     try {
-      await adminNotificationContainer.addNotificationPattern(this.state.pathPattern, this.state.channel);
+      await adminNotificationContainer.addNotificationPattern(
+        this.state.pathPattern,
+        this.state.channel,
+      );
       toastSuccess(t('notification_settings.add_notification_pattern'));
       toastSuccess(t('notification_settings.add_notification_pattern'));
       this.setState({ pathPattern: '', channel: '' });
       this.setState({ pathPattern: '', channel: '' });
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
@@ -67,10 +65,16 @@ class UserTriggerNotification extends React.Component {
     const { t, adminNotificationContainer } = this.props;
     const { t, adminNotificationContainer } = this.props;
 
 
     try {
     try {
-      const deletedNotificaton = await adminNotificationContainer.deleteUserTriggerNotificationPattern(notificationIdForDelete);
-      toastSuccess(t('notification_settings.delete_notification_pattern', { path: deletedNotificaton.pathPattern }));
-    }
-    catch (err) {
+      const deletedNotificaton =
+        await adminNotificationContainer.deleteUserTriggerNotificationPattern(
+          notificationIdForDelete,
+        );
+      toastSuccess(
+        t('notification_settings.delete_notification_pattern', {
+          path: deletedNotificaton.pathPattern,
+        }),
+      );
+    } catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
@@ -78,11 +82,14 @@ class UserTriggerNotification extends React.Component {
 
 
   render() {
   render() {
     const { t, adminNotificationContainer } = this.props;
     const { t, adminNotificationContainer } = this.props;
-    const userNotifications = adminNotificationContainer.state.userNotifications || [];
+    const userNotifications =
+      adminNotificationContainer.state.userNotifications || [];
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="border-bottom my-4">{t('notification_settings.user_trigger_notification_header')}</h2>
+        <h2 className="border-bottom my-4">
+          {t('notification_settings.user_trigger_notification_header')}
+        </h2>
 
 
         <table className="table table-bordered">
         <table className="table table-bordered">
           <thead>
           <thead>
@@ -101,18 +108,27 @@ class UserTriggerNotification extends React.Component {
                   name="pathPattern"
                   name="pathPattern"
                   value={this.state.pathPattern}
                   value={this.state.pathPattern}
                   placeholder="e.g. /projects/xxx/MTG/*"
                   placeholder="e.g. /projects/xxx/MTG/*"
-                  onChange={(e) => { this.changePathPattern(e.target.value) }}
+                  onChange={(e) => {
+                    this.changePathPattern(e.target.value);
+                  }}
                 />
                 />
                 <p className="p-2 mb-0">
                 <p className="p-2 mb-0">
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.pattern_desc') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('notification_settings.pattern_desc'),
+                    }}
+                  />
                 </p>
                 </p>
               </td>
               </td>
 
 
               <td>
               <td>
                 <div className="input-group notify-to-option" id="slack-input">
                 <div className="input-group notify-to-option" id="slack-input">
                   <div>
                   <div>
-                    <span className="input-group-text"><span className="material-symbols-outlined">tag</span></span>
+                    <span className="input-group-text">
+                      <span className="material-symbols-outlined">tag</span>
+                    </span>
                   </div>
                   </div>
                   <input
                   <input
                     className="form-control"
                     className="form-control"
@@ -120,35 +136,53 @@ class UserTriggerNotification extends React.Component {
                     name="channel"
                     name="channel"
                     value={this.state.channel}
                     value={this.state.channel}
                     placeholder="e.g. project-xxx"
                     placeholder="e.g. project-xxx"
-                    onChange={(e) => { this.changeChannel(e.target.value) }}
+                    onChange={(e) => {
+                      this.changeChannel(e.target.value);
+                    }}
                   />
                   />
                 </div>
                 </div>
                 <p className="p-2 mb-0">
                 <p className="p-2 mb-0">
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('notification_settings.channel_desc'),
+                    }}
+                  />
                 </p>
                 </p>
               </td>
               </td>
               <td>
               <td>
-                <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('commons:Add')}</button>
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={!this.validateForm()}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('commons:Add')}
+                </button>
               </td>
               </td>
             </tr>
             </tr>
-            {userNotifications.length > 0 && userNotifications.map((notification) => {
-              return <UserNotificationRow notification={notification} onClickDeleteBtn={this.onClickDeleteBtn} key={notification._id} />;
-            })}
+            {userNotifications.length > 0 &&
+              userNotifications.map((notification) => {
+                return (
+                  <UserNotificationRow
+                    notification={notification}
+                    onClickDeleteBtn={this.onClickDeleteBtn}
+                    key={notification._id}
+                  />
+                );
+              })}
           </tbody>
           </tbody>
         </table>
         </table>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
-
-
 }
 }
 
 
-
 UserTriggerNotification.propTypes = {
 UserTriggerNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
-
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer)
+    .isRequired,
 };
 };
 
 
 const UserTriggerNotificationWrapperFC = (props) => {
 const UserTriggerNotificationWrapperFC = (props) => {
@@ -157,6 +191,9 @@ const UserTriggerNotificationWrapperFC = (props) => {
   return <UserTriggerNotification t={t} {...props} />;
   return <UserTriggerNotification t={t} {...props} />;
 };
 };
 
 
-const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AdminNotificationContainer]);
+const UserTriggerNotificationWrapper = withUnstatedContainers(
+  UserTriggerNotificationWrapperFC,
+  [AdminNotificationContainer],
+);
 
 
 export default UserTriggerNotificationWrapper;
 export default UserTriggerNotificationWrapper;

+ 7 - 10
apps/app/src/client/components/Admin/Security/DeleteAllShareLinksModal.jsx

@@ -1,10 +1,7 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import {
-  Button, Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
 
 const DeleteAllShareLinksModal = React.memo((props) => {
 const DeleteAllShareLinksModal = React.memo((props) => {
   const { t, onClickDeleteButton, onClose } = props;
   const { t, onClickDeleteButton, onClose } = props;
@@ -19,16 +16,18 @@ const DeleteAllShareLinksModal = React.memo((props) => {
   }, [onClose]);
   }, [onClose]);
 
 
   return (
   return (
-    <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
+    <Modal
+      isOpen={props.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>
         <span>
           <span className="material-symbols-outlined">delete_forever</span>
           <span className="material-symbols-outlined">delete_forever</span>
           {t('security_settings.delete_all_share_links')}
           {t('security_settings.delete_all_share_links')}
         </span>
         </span>
       </ModalHeader>
       </ModalHeader>
-      <ModalBody>
-        { t('security_settings.share_link_notice')}
-      </ModalBody>
+      <ModalBody>{t('security_settings.share_link_notice')}</ModalBody>
       <ModalFooter>
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteAllLinkHandler}>
         <Button color="danger" onClick={deleteAllLinkHandler}>
@@ -38,11 +37,9 @@ const DeleteAllShareLinksModal = React.memo((props) => {
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>
   );
   );
-
 });
 });
 DeleteAllShareLinksModal.displayName = 'DeleteAllShareLinksModal';
 DeleteAllShareLinksModal.displayName = 'DeleteAllShareLinksModal';
 
 
-
 DeleteAllShareLinksModal.propTypes = {
 DeleteAllShareLinksModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
 
 

+ 11 - 13
apps/app/src/client/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
@@ -7,18 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
 import GitHubSecuritySettingContents from './GitHubSecuritySettingContents';
 import GitHubSecuritySettingContents from './GitHubSecuritySettingContents';
 
 
 const GitHubSecurityManagement = (props) => {
 const GitHubSecurityManagement = (props) => {
   const { adminGitHubSecurityContainer } = props;
   const { adminGitHubSecurityContainer } = props;
 
 
-  const fetchGitHubSecuritySettingsData = useCallback(async() => {
+  const fetchGitHubSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminGitHubSecurityContainer.retrieveSecurityData();
       await adminGitHubSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
@@ -26,18 +22,20 @@ const GitHubSecurityManagement = (props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     fetchGitHubSecuritySettingsData();
     fetchGitHubSecuritySettingsData();
-  }, [adminGitHubSecurityContainer, fetchGitHubSecuritySettingsData]);
+  }, [fetchGitHubSecuritySettingsData]);
 
 
   return <GitHubSecuritySettingContents />;
   return <GitHubSecuritySettingContents />;
 };
 };
 
 
-
 GitHubSecurityManagement.propTypes = {
 GitHubSecurityManagement.propTypes = {
-  adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
+  adminGitHubSecurityContainer: PropTypes.instanceOf(
+    AdminGitHubSecurityContainer,
+  ).isRequired,
 };
 };
 
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
-  AdminGitHubSecurityContainer,
-]);
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  GitHubSecurityManagement,
+  [AdminGitHubSecurityContainer],
+);
 
 
 export default GitHubSecurityManagementWithUnstatedContainer;
 export default GitHubSecurityManagementWithUnstatedContainer;

+ 162 - 58
apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.tsx

@@ -1,35 +1,35 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 import React, { useCallback, useEffect } from 'react';
 import React, { useCallback, useEffect } from 'react';
-
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 type Props = {
 type Props = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
-  adminGitHubSecurityContainer: AdminGitHubSecurityContainer
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+  adminGitHubSecurityContainer: AdminGitHubSecurityContainer;
 };
 };
 
 
 const GitHubSecurityManagementContents = (props: Props) => {
 const GitHubSecurityManagementContents = (props: Props) => {
-  const {
-    adminGeneralSecurityContainer, adminGitHubSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminGitHubSecurityContainer } = props;
 
 
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
   const siteUrl = useSiteUrlWithEmptyValueWarn();
 
 
   const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
   const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
-  const { githubClientId, githubClientSecret, retrieveError } = adminGitHubSecurityContainer.state;
-  const gitHubCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/github/callback');
+  const { githubClientId, githubClientSecret, retrieveError } =
+    adminGitHubSecurityContainer.state;
+  const gitHubCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/github/callback',
+  );
 
 
   const { register, handleSubmit, reset } = useForm();
   const { register, handleSubmit, reset } = useForm();
 
 
@@ -41,32 +41,37 @@ const GitHubSecurityManagementContents = (props: Props) => {
     });
     });
   }, [reset, githubClientId, githubClientSecret]);
   }, [reset, githubClientId, githubClientSecret]);
 
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminGitHubSecurityContainer.updateGitHubSetting({
-        githubClientId: data.githubClientId ?? '',
-        githubClientSecret: data.githubClientSecret ?? '',
-        isSameUsernameTreatedAsIdenticalUser: adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.GitHub.updated_github'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGitHubSecurityContainer, adminGeneralSecurityContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminGitHubSecurityContainer.updateGitHubSetting({
+          githubClientId: data.githubClientId ?? '',
+          githubClientSecret: data.githubClientSecret ?? '',
+          isSameUsernameTreatedAsIdenticalUser:
+            adminGitHubSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.OAuth.GitHub.updated_github'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGitHubSecurityContainer, adminGeneralSecurityContainer, t],
+  );
 
 
   return (
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
     <form onSubmit={handleSubmit(onClickSubmit)}>
       <React.Fragment>
       <React.Fragment>
-
         <h2 className="alert-anchor border-bottom">
         <h2 className="alert-anchor border-bottom">
           {t('security_settings.OAuth.GitHub.name')}
           {t('security_settings.OAuth.GitHub.name')}
         </h2>
         </h2>
 
 
         {retrieveError != null && (
         {retrieveError != null && (
           <div className="alert alert-danger">
           <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {retrieveError}</p>
+            <p>
+              {t('Error occurred')} : {retrieveError}
+            </p>
           </div>
           </div>
         )}
         )}
 
 
@@ -77,47 +82,81 @@ const GitHubSecurityManagementContents = (props: Props) => {
                 id="isGitHubEnabled"
                 id="isGitHubEnabled"
                 className="form-check-input"
                 className="form-check-input"
                 type="checkbox"
                 type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isGitHubEnabled || false}
-                onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
+                checked={
+                  adminGeneralSecurityContainer.state.isGitHubEnabled || false
+                }
+                onChange={() => {
+                  adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled();
+                }}
               />
               />
-              <label className="form-label form-check-label" htmlFor="isGitHubEnabled">
+              <label
+                className="form-label form-check-label"
+                htmlFor="isGitHubEnabled"
+              >
                 {t('security_settings.OAuth.GitHub.enable_github')}
                 {t('security_settings.OAuth.GitHub.enable_github')}
               </label>
               </label>
             </div>
             </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+            {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+              'github',
+            ) &&
+              isGitHubEnabled && (
+                <div className="badge text-bg-warning">
+                  {t('security_settings.setup_is_not_yet_complete')}
+                </div>
+              )}
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mb-4">
         <div className="row mb-4">
-          <label className="form-label col-12 col-md-3 text-start text-md-end py-2">{t('security_settings.callback_URL')}</label>
+          <label
+            className="form-label col-12 col-md-3 text-start text-md-end py-2"
+            htmlFor="gitHubCallbackUrl"
+          >
+            {t('security_settings.callback_URL')}
+          </label>
           <div className="col-12 col-md-6">
           <div className="col-12 col-md-6">
             <input
             <input
+              id="gitHubCallbackUrl"
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               value={gitHubCallbackUrl}
               value={gitHubCallbackUrl}
               readOnly
               readOnly
             />
             />
-            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">
+              {t('security_settings.desc_of_callback_URL', {
+                AuthName: 'OAuth',
+              })}
+            </p>
             {(siteUrl == null || siteUrl === '') && (
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <span className="material-symbols-outlined">error</span>
                 <span className="material-symbols-outlined">error</span>
                 <span // eslint-disable-next-line max-len
                 <span // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('alert.siteUrl_is_not_set', {
+                      link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                      ns: 'commons',
+                    }),
+                  }}
                 />
                 />
               </div>
               </div>
             )}
             )}
           </div>
           </div>
         </div>
         </div>
 
 
-
         {isGitHubEnabled && (
         {isGitHubEnabled && (
           <React.Fragment>
           <React.Fragment>
-
-            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">
+              {t('security_settings.configuration')}
+            </h3>
 
 
             <div className="row mb-4">
             <div className="row mb-4">
-              <label htmlFor="githubClientId" className="col-3 text-end py-2 form-label">{t('security_settings.clientID')}</label>
+              <label
+                htmlFor="githubClientId"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.clientID')}
+              </label>
               <div className="col-6">
               <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
@@ -125,13 +164,25 @@ const GitHubSecurityManagementContents = (props: Props) => {
                   {...register('githubClientId')}
                   {...register('githubClientId')}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
+                  <small
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GITHUB_CLIENT_ID',
+                      }),
+                    }}
+                  />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-3">
             <div className="row mb-3">
-              <label htmlFor="githubClientSecret" className="col-3 text-end py-2 form-label">{t('security_settings.client_secret')}</label>
+              <label
+                htmlFor="githubClientSecret"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.client_secret')}
+              </label>
               <div className="col-6">
               <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
@@ -139,7 +190,14 @@ const GitHubSecurityManagementContents = (props: Props) => {
                   {...register('githubClientSecret')}
                   {...register('githubClientSecret')}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
+                  <small
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GITHUB_CLIENT_SECRET',
+                      }),
+                    }}
+                  />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -151,29 +209,52 @@ const GitHubSecurityManagementContents = (props: Props) => {
                     id="bindByUserNameGitHub"
                     id="bindByUserNameGitHub"
                     className="form-check-input"
                     className="form-check-input"
                     type="checkbox"
                     type="checkbox"
-                    checked={adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                    checked={
+                      adminGitHubSecurityContainer.state
+                        .isSameUsernameTreatedAsIdenticalUser || false
+                    }
+                    onChange={() => {
+                      adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                    }}
                   />
                   />
                   <label
                   <label
                     className="form-check-label"
                     className="form-check-label"
                     htmlFor="bindByUserNameGitHub"
                     htmlFor="bindByUserNameGitHub"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-                  />
+                  >
+                    <span
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.Treat email matching as identical',
+                        ),
+                      }}
+                    />
+                  </label>
                 </div>
                 </div>
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+                  <small
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical_warn',
+                      ),
+                    }}
+                  />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-4">
             <div className="row mb-4">
               <div className="offset-3 col-5">
               <div className="offset-3 col-5">
-                <button type="submit" className="btn btn-primary" disabled={retrieveError != null}>
+                <button
+                  type="submit"
+                  className="btn btn-primary"
+                  disabled={retrieveError != null}
+                >
                   {t('Update')}
                   {t('Update')}
                 </button>
                 </button>
               </div>
               </div>
             </div>
             </div>
-
           </React.Fragment>
           </React.Fragment>
         )}
         )}
 
 
@@ -181,19 +262,42 @@ const GitHubSecurityManagementContents = (props: Props) => {
 
 
         <div style={{ minHeight: '300px' }}>
         <div style={{ minHeight: '300px' }}>
           <h4>
           <h4>
-            <span className="material-symbols-outlined" aria-hidden="true">help</span>
-            <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.github')}</a>
+            <span className="material-symbols-outlined" aria-hidden="true">
+              help
+            </span>
+            <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse">
+              {' '}
+              {t('security_settings.OAuth.how_to.github')}
+            </a>
           </h4>
           </h4>
           <div className="card custom-card bg-body-tertiary">
           <div className="card custom-card bg-body-tertiary">
             <ol id="collapseHelpForGitHubOauth" className="collapse mb-0">
             <ol id="collapseHelpForGitHubOauth" className="collapse mb-0">
               {/* eslint-disable-next-line max-len */}
               {/* eslint-disable-next-line max-len */}
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_3') }} />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.GitHub.register_1', {
+                    link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>',
+                  }),
+                }}
+              />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.GitHub.register_2', {
+                    url: gitHubCallbackUrl,
+                  }),
+                }}
+              />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.GitHub.register_3'),
+                }}
+              />
             </ol>
             </ol>
           </div>
           </div>
         </div>
         </div>
-
       </React.Fragment>
       </React.Fragment>
     </form>
     </form>
   );
   );
@@ -202,9 +306,9 @@ const GitHubSecurityManagementContents = (props: Props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const GitHubSecurityManagementContentsWrapper = withUnstatedContainers(GitHubSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminGitHubSecurityContainer,
-]);
+const GitHubSecurityManagementContentsWrapper = withUnstatedContainers(
+  GitHubSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminGitHubSecurityContainer],
+);
 
 
 export default GitHubSecurityManagementContentsWrapper;
 export default GitHubSecurityManagementContentsWrapper;

+ 11 - 13
apps/app/src/client/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
@@ -7,37 +6,36 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
 
 
 const GoogleSecurityManagement = (props) => {
 const GoogleSecurityManagement = (props) => {
   const { adminGoogleSecurityContainer } = props;
   const { adminGoogleSecurityContainer } = props;
 
 
-  const fetchGoogleSecuritySettingsData = useCallback(async() => {
+  const fetchGoogleSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminGoogleSecurityContainer.retrieveSecurityData();
       await adminGoogleSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
   }, [adminGoogleSecurityContainer]);
   }, [adminGoogleSecurityContainer]);
 
 
-
   useEffect(() => {
   useEffect(() => {
     fetchGoogleSecuritySettingsData();
     fetchGoogleSecuritySettingsData();
-  }, [adminGoogleSecurityContainer, fetchGoogleSecuritySettingsData]);
+  }, [fetchGoogleSecuritySettingsData]);
 
 
   return <GoogleSecurityManagementContents />;
   return <GoogleSecurityManagementContents />;
 };
 };
 
 
-
 GoogleSecurityManagement.propTypes = {
 GoogleSecurityManagement.propTypes = {
-  adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
+  adminGoogleSecurityContainer: PropTypes.instanceOf(
+    AdminGoogleSecurityContainer,
+  ).isRequired,
 };
 };
 
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
-  AdminGoogleSecurityContainer,
-]);
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  GoogleSecurityManagement,
+  [AdminGoogleSecurityContainer],
+);
 
 
 export default GoogleSecurityManagementWithUnstatedContainer;
 export default GoogleSecurityManagementWithUnstatedContainer;

+ 175 - 60
apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.tsx

@@ -1,6 +1,5 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 import React, { useCallback, useEffect } from 'react';
 import React, { useCallback, useEffect } from 'react';
-
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
@@ -8,27 +7,29 @@ import urljoin from 'url-join';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 type Props = {
 type Props = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
-  adminGoogleSecurityContainer: AdminGoogleSecurityContainer
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+  adminGoogleSecurityContainer: AdminGoogleSecurityContainer;
 };
 };
 
 
 const GoogleSecurityManagementContents = (props: Props) => {
 const GoogleSecurityManagementContents = (props: Props) => {
-  const {
-    adminGeneralSecurityContainer, adminGoogleSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminGoogleSecurityContainer } = props;
 
 
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
   const siteUrl = useSiteUrlWithEmptyValueWarn();
 
 
   const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
   const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
-  const { googleClientId, googleClientSecret, retrieveError } = adminGoogleSecurityContainer.state;
-  const googleCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/google/callback');
+  const { googleClientId, googleClientSecret, retrieveError } =
+    adminGoogleSecurityContainer.state;
+  const googleCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/google/callback',
+  );
 
 
   const { register, handleSubmit, reset } = useForm();
   const { register, handleSubmit, reset } = useForm();
 
 
@@ -40,32 +41,37 @@ const GoogleSecurityManagementContents = (props: Props) => {
     });
     });
   }, [reset, googleClientId, googleClientSecret]);
   }, [reset, googleClientId, googleClientSecret]);
 
 
-  const onClickSubmit = useCallback(async(data) => {
-    try {
-      await adminGoogleSecurityContainer.updateGoogleSetting({
-        googleClientId: data.googleClientId ?? '',
-        googleClientSecret: data.googleClientSecret ?? '',
-        isSameEmailTreatedAsIdenticalUser: adminGoogleSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.Google.updated_google'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGoogleSecurityContainer, adminGeneralSecurityContainer, t]);
+  const onClickSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminGoogleSecurityContainer.updateGoogleSetting({
+          googleClientId: data.googleClientId ?? '',
+          googleClientSecret: data.googleClientSecret ?? '',
+          isSameEmailTreatedAsIdenticalUser:
+            adminGoogleSecurityContainer.state
+              .isSameEmailTreatedAsIdenticalUser,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.OAuth.Google.updated_google'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGoogleSecurityContainer, adminGeneralSecurityContainer, t],
+  );
 
 
   return (
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
     <form onSubmit={handleSubmit(onClickSubmit)}>
       <React.Fragment>
       <React.Fragment>
-
         <h2 className="alert-anchor border-bottom">
         <h2 className="alert-anchor border-bottom">
           {t('security_settings.OAuth.Google.name')}
           {t('security_settings.OAuth.Google.name')}
         </h2>
         </h2>
 
 
         {retrieveError != null && (
         {retrieveError != null && (
           <div className="alert alert-danger">
           <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {retrieveError}</p>
+            <p>
+              {t('Error occurred')} : {retrieveError}
+            </p>
           </div>
           </div>
         )}
         )}
 
 
@@ -76,48 +82,82 @@ const GoogleSecurityManagementContents = (props: Props) => {
                 id="isGoogleEnabled"
                 id="isGoogleEnabled"
                 className="form-check-input"
                 className="form-check-input"
                 type="checkbox"
                 type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isGoogleEnabled || false}
-                onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
+                checked={
+                  adminGeneralSecurityContainer.state.isGoogleEnabled || false
+                }
+                onChange={() => {
+                  adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled();
+                }}
               />
               />
-              <label className="form-label form-check-label" htmlFor="isGoogleEnabled">
+              <label
+                className="form-label form-check-label"
+                htmlFor="isGoogleEnabled"
+              >
                 {t('security_settings.OAuth.Google.enable_google')}
                 {t('security_settings.OAuth.Google.enable_google')}
               </label>
               </label>
             </div>
             </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+            {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+              'google',
+            ) &&
+              isGoogleEnabled && (
+                <div className="badge text-bg-warning">
+                  {t('security_settings.setup_is_not_yet_complete')}
+                </div>
+              )}
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <label className="form-label col-12 col-md-3 text-start text-md-end py-2">{t('security_settings.callback_URL')}</label>
+          <label
+            className="form-label col-12 col-md-3 text-start text-md-end py-2"
+            htmlFor="googleCallbackUrl"
+          >
+            {t('security_settings.callback_URL')}
+          </label>
           <div className="col-12 col-md-6">
           <div className="col-12 col-md-6">
             <input
             <input
+              id="googleCallbackUrl"
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               value={googleCallbackUrl}
               value={googleCallbackUrl}
               readOnly
               readOnly
             />
             />
-            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">
+              {t('security_settings.desc_of_callback_URL', {
+                AuthName: 'OAuth',
+              })}
+            </p>
             {(siteUrl == null || siteUrl === '') && (
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <span className="material-symbols-outlined">error</span>
                 <span className="material-symbols-outlined">error</span>
                 <span
                 <span
-                // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                  // eslint-disable-next-line max-len
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('alert.siteUrl_is_not_set', {
+                      link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                      ns: 'commons',
+                    }),
+                  }}
                 />
                 />
               </div>
               </div>
             )}
             )}
           </div>
           </div>
         </div>
         </div>
 
 
-
         {isGoogleEnabled && (
         {isGoogleEnabled && (
           <React.Fragment>
           <React.Fragment>
-
-            <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+            <h3 className="border-bottom mb-4">
+              {t('security_settings.configuration')}
+            </h3>
 
 
             <div className="row mb-4">
             <div className="row mb-4">
-              <label htmlFor="googleClientId" className="col-3 text-end py-2 form-label">{t('security_settings.clientID')}</label>
+              <label
+                htmlFor="googleClientId"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.clientID')}
+              </label>
               <div className="col-6">
               <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
@@ -125,13 +165,25 @@ const GoogleSecurityManagementContents = (props: Props) => {
                   {...register('googleClientId')}
                   {...register('googleClientId')}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
+                  <small
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GOOGLE_CLIENT_ID',
+                      }),
+                    }}
+                  />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-4">
             <div className="row mb-4">
-              <label htmlFor="googleClientSecret" className="col-3 text-end py-2 form-label">{t('security_settings.client_secret')}</label>
+              <label
+                htmlFor="googleClientSecret"
+                className="col-3 text-end py-2 form-label"
+              >
+                {t('security_settings.client_secret')}
+              </label>
               <div className="col-6">
               <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
@@ -139,7 +191,14 @@ const GoogleSecurityManagementContents = (props: Props) => {
                   {...register('googleClientSecret')}
                   {...register('googleClientSecret')}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
+                  <small
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.Use env var if empty', {
+                        env: 'OAUTH_GOOGLE_CLIENT_SECRET',
+                      }),
+                    }}
+                  />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -151,29 +210,52 @@ const GoogleSecurityManagementContents = (props: Props) => {
                     id="bindByUserNameGoogle"
                     id="bindByUserNameGoogle"
                     className="form-check-input"
                     className="form-check-input"
                     type="checkbox"
                     type="checkbox"
-                    checked={adminGoogleSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                    onChange={() => { adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                    checked={
+                      adminGoogleSecurityContainer.state
+                        .isSameEmailTreatedAsIdenticalUser || false
+                    }
+                    onChange={() => {
+                      adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                    }}
                   />
                   />
                   <label
                   <label
                     className="form-check-label"
                     className="form-check-label"
                     htmlFor="bindByUserNameGoogle"
                     htmlFor="bindByUserNameGoogle"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-                  />
+                  >
+                    <span
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.Treat email matching as identical',
+                        ),
+                      }}
+                    />
+                  </label>
                 </div>
                 </div>
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+                  <small
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical_warn',
+                      ),
+                    }}
+                  />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-4">
             <div className="row mb-4">
               <div className="offset-3 col-5">
               <div className="offset-3 col-5">
-                <button type="submit" className="btn btn-primary" disabled={retrieveError != null}>
+                <button
+                  type="submit"
+                  className="btn btn-primary"
+                  disabled={retrieveError != null}
+                >
                   {t('Update')}
                   {t('Update')}
                 </button>
                 </button>
               </div>
               </div>
             </div>
             </div>
-
           </React.Fragment>
           </React.Fragment>
         )}
         )}
 
 
@@ -181,29 +263,62 @@ const GoogleSecurityManagementContents = (props: Props) => {
 
 
         <div style={{ minHeight: '300px' }}>
         <div style={{ minHeight: '300px' }}>
           <h4>
           <h4>
-            <span className="material-symbols-outlined" aria-hidden="true">help</span>
-            <a href="#collapseHelpForGoogleOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.google')}</a>
+            <span className="material-symbols-outlined" aria-hidden="true">
+              help
+            </span>
+            <a href="#collapseHelpForGoogleOauth" data-bs-toggle="collapse">
+              {' '}
+              {t('security_settings.OAuth.how_to.google')}
+            </a>
           </h4>
           </h4>
           <div className="card custom-card bg-body-tertiary">
           <div className="card custom-card bg-body-tertiary">
             <ol id="collapseHelpForGoogleOauth" className="collapse mb-0">
             <ol id="collapseHelpForGoogleOauth" className="collapse mb-0">
               {/* eslint-disable-next-line max-len */}
               {/* eslint-disable-next-line max-len */}
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_2') }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_3') }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
-              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_5') }} />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_1', {
+                    link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>',
+                  }),
+                }}
+              />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_2'),
+                }}
+              />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_3'),
+                }}
+              />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_4', {
+                    url: googleCallbackUrl,
+                  }),
+                }}
+              />
+              <li
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('security_settings.OAuth.Google.register_5'),
+                }}
+              />
             </ol>
             </ol>
           </div>
           </div>
         </div>
         </div>
-
       </React.Fragment>
       </React.Fragment>
     </form>
     </form>
   );
   );
 };
 };
 
 
-const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(GoogleSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminGoogleSecurityContainer,
-]);
+const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(
+  GoogleSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminGoogleSecurityContainer],
+);
 
 
 export default GoogleSecurityManagementContentsWrapper;
 export default GoogleSecurityManagementContentsWrapper;

+ 51 - 31
apps/app/src/client/components/Admin/Security/LdapAuthTest.tsx

@@ -1,25 +1,22 @@
-import React, { useState, type JSX } from 'react';
-
+import React, { type JSX, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IResTestLdap } from '~/interfaces/ldap';
 import type { IResTestLdap } from '~/interfaces/ldap';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
 
 
 type LdapAuthTestProps = {
 type LdapAuthTestProps = {
-  username: string,
-  password: string,
-  onChangeUsername: (username: string) => void,
-  onChangePassword: (password: string) => void,
-}
+  username: string;
+  password: string;
+  onChangeUsername: (username: string) => void;
+  onChangePassword: (password: string) => void;
+};
 
 
 export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
 export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
-  const {
-    username, password, onChangeUsername, onChangePassword,
-  } = props;
+  const { username, password, onChangeUsername, onChangePassword } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [logs, setLogs] = useState('');
   const [logs, setLogs] = useState('');
   const [errorMessage, setErrorMessage] = useState('');
   const [errorMessage, setErrorMessage] = useState('');
@@ -36,7 +33,7 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
   /**
   /**
    * Test ldap auth
    * Test ldap auth
    */
    */
-  const testLdapCredentials = async() => {
+  const testLdapCredentials = async () => {
     try {
     try {
       const response = await apiPost<IResTestLdap>('/login/testLdap', {
       const response = await apiPost<IResTestLdap>('/login/testLdap', {
         loginForm: {
         loginForm: {
@@ -45,9 +42,8 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
         },
         },
       });
       });
 
 
-      const {
-        err, message, status, ldapConfiguration, ldapAccountInfo,
-      } = response;
+      const { err, message, status, ldapConfiguration, ldapAccountInfo } =
+        response;
 
 
       // add logs
       // add logs
       if (err) {
       if (err) {
@@ -68,62 +64,86 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
       }
       }
 
 
       if (ldapConfiguration) {
       if (ldapConfiguration) {
-        const prettified = JSON.stringify(ldapConfiguration.server, undefined, 4);
+        const prettified = JSON.stringify(
+          ldapConfiguration.server,
+          undefined,
+          4,
+        );
         addLogs(`LDAP Configuration : ${prettified}`);
         addLogs(`LDAP Configuration : ${prettified}`);
       }
       }
       if (ldapAccountInfo) {
       if (ldapAccountInfo) {
         const prettified = JSON.stringify(ldapAccountInfo, undefined, 4);
         const prettified = JSON.stringify(ldapAccountInfo, undefined, 4);
         addLogs(`Retrieved LDAP Account : ${prettified}`);
         addLogs(`Retrieved LDAP Account : ${prettified}`);
       }
       }
-
-    }
-    // Catch server communication error
-    catch (err) {
+    } catch (err) {
+      // Catch server communication error
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
   };
   };
 
 
-
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-      {successMessage !== '' && <div className="alert alert-success">{successMessage}</div>}
-      {errorMessage !== '' && <div className="alert alert-warning">{errorMessage}</div>}
+      {successMessage !== '' && (
+        <div className="alert alert-success">{successMessage}</div>
+      )}
+      {errorMessage !== '' && (
+        <div className="alert alert-warning">{errorMessage}</div>
+      )}
       <div className="row mt-3">
       <div className="row mt-3">
-        <label htmlFor="username" className="col-3 col-form-label text-end">{t('username')}</label>
+        <label htmlFor="username" className="col-3 col-form-label text-end">
+          {t('username')}
+        </label>
         <div className="col-6">
         <div className="col-6">
           <input
           <input
             className="form-control"
             className="form-control"
             name="username"
             name="username"
             value={username}
             value={username}
-            onChange={(e) => { onChangeUsername(e.target.value) }}
+            onChange={(e) => {
+              onChangeUsername(e.target.value);
+            }}
             autoComplete="off"
             autoComplete="off"
           />
           />
         </div>
         </div>
       </div>
       </div>
       <div className="row mt-3">
       <div className="row mt-3">
-        <label htmlFor="password" className="col-3 col-form-label text-end">{t('Password')}</label>
+        <label htmlFor="password" className="col-3 col-form-label text-end">
+          {t('Password')}
+        </label>
         <div className="col-6">
         <div className="col-6">
           <input
           <input
             className="form-control"
             className="form-control"
             type="password"
             type="password"
             name="password"
             name="password"
             value={password}
             value={password}
-            onChange={(e) => { onChangePassword(e.target.value) }}
+            onChange={(e) => {
+              onChangePassword(e.target.value);
+            }}
             autoComplete="off"
             autoComplete="off"
           />
           />
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="mt-4">
       <div className="mt-4">
-        <label className="form-label"><h5>Logs</h5></label>
-        <textarea id="taLogs" className="col form-control" rows={4} value={logs} readOnly />
+        <h5 className="form-label">Logs</h5>
+        <textarea
+          id="taLogs"
+          className="col form-control"
+          rows={4}
+          value={logs}
+          readOnly
+        />
       </div>
       </div>
 
 
       <div className="mt-4">
       <div className="mt-4">
-        <button type="button" className="btn btn-outline-secondary offset-5 col-2" onClick={testLdapCredentials}>Test</button>
+        <button
+          type="button"
+          className="btn btn-outline-secondary offset-5 col-2"
+          onClick={testLdapCredentials}
+        >
+          Test
+        </button>
       </div>
       </div>
     </React.Fragment>
     </React.Fragment>
-
   );
   );
 };
 };

+ 1 - 12
apps/app/src/client/components/Admin/Security/LdapAuthTestModal.jsx

@@ -1,19 +1,11 @@
 import React from 'react';
 import React from 'react';
-
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import { LdapAuthTest } from './LdapAuthTest';
 import { LdapAuthTest } from './LdapAuthTest';
 
 
-
 class LdapAuthTestModal extends React.Component {
 class LdapAuthTestModal extends React.Component {
-
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -41,7 +33,6 @@ class LdapAuthTestModal extends React.Component {
   }
   }
 
 
   render() {
   render() {
-
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
         <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
         <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
@@ -58,10 +49,8 @@ class LdapAuthTestModal extends React.Component {
       </Modal>
       </Modal>
     );
     );
   }
   }
-
 }
 }
 
 
-
 LdapAuthTestModal.propTypes = {
 LdapAuthTestModal.propTypes = {
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,

+ 10 - 11
apps/app/src/client/components/Admin/Security/LdapSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
@@ -7,17 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import LdapSecuritySettingContents from './LdapSecuritySettingContents';
 import LdapSecuritySettingContents from './LdapSecuritySettingContents';
 
 
 const LdapSecuritySetting = (props) => {
 const LdapSecuritySetting = (props) => {
   const { adminLdapSecurityContainer } = props;
   const { adminLdapSecurityContainer } = props;
 
 
-  const fetchLdapSecuritySettingsData = useCallback(async() => {
+  const fetchLdapSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminLdapSecurityContainer.retrieveSecurityData();
       await adminLdapSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
@@ -25,17 +22,19 @@ const LdapSecuritySetting = (props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     fetchLdapSecuritySettingsData();
     fetchLdapSecuritySettingsData();
-  }, [adminLdapSecurityContainer, fetchLdapSecuritySettingsData]);
+  }, [fetchLdapSecuritySettingsData]);
 
 
   return <LdapSecuritySettingContents />;
   return <LdapSecuritySettingContents />;
 };
 };
 
 
 LdapSecuritySetting.propTypes = {
 LdapSecuritySetting.propTypes = {
-  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
+  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer)
+    .isRequired,
 };
 };
 
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
-  AdminLdapSecurityContainer,
-]);
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(
+  LdapSecuritySetting,
+  [AdminLdapSecurityContainer],
+);
 
 
 export default LdapSecuritySettingWithUnstatedContainer;
 export default LdapSecuritySettingWithUnstatedContainer;

+ 323 - 136
apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.tsx

@@ -1,19 +1,14 @@
-import React, {
-  useState, useEffect, useCallback,
-} from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import LdapAuthTestModal from './LdapAuthTestModal';
 import LdapAuthTestModal from './LdapAuthTestModal';
 
 
-
 type Props = {
 type Props = {
   adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
   adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
   adminLdapSecurityContainer: AdminLdapSecurityContainer;
   adminLdapSecurityContainer: AdminLdapSecurityContainer;
@@ -26,12 +21,20 @@ const LdapSecuritySettingContents = (props: Props) => {
 
 
   const { isLdapEnabled } = adminGeneralSecurityContainer.state;
   const { isLdapEnabled } = adminGeneralSecurityContainer.state;
   const {
   const {
-    serverUrl, ldapBindDN, ldapBindDNPassword, ldapSearchFilter,
-    ldapAttrMapUsername, ldapAttrMapMail, ldapAttrMapName,
-    ldapGroupSearchBase, ldapGroupSearchFilter, ldapGroupDnProperty,
+    serverUrl,
+    ldapBindDN,
+    ldapBindDNPassword,
+    ldapSearchFilter,
+    ldapAttrMapUsername,
+    ldapAttrMapMail,
+    ldapAttrMapName,
+    ldapGroupSearchBase,
+    ldapGroupSearchFilter,
+    ldapGroupDnProperty,
   } = adminLdapSecurityContainer.state;
   } = adminLdapSecurityContainer.state;
 
 
-  const [isLdapAuthTestModalShown, setIsLdapAuthTestModalShown] = useState(false);
+  const [isLdapAuthTestModalShown, setIsLdapAuthTestModalShown] =
+    useState(false);
 
 
   const { register, handleSubmit, reset } = useForm();
   const { register, handleSubmit, reset } = useForm();
 
 
@@ -49,34 +52,46 @@ const LdapSecuritySettingContents = (props: Props) => {
       ldapGroupDnProperty,
       ldapGroupDnProperty,
     });
     });
   }, [
   }, [
-    reset, serverUrl, ldapBindDN, ldapBindDNPassword, ldapSearchFilter,
-    ldapAttrMapUsername, ldapAttrMapMail, ldapAttrMapName,
-    ldapGroupSearchBase, ldapGroupSearchFilter, ldapGroupDnProperty,
+    reset,
+    serverUrl,
+    ldapBindDN,
+    ldapBindDNPassword,
+    ldapSearchFilter,
+    ldapAttrMapUsername,
+    ldapAttrMapMail,
+    ldapAttrMapName,
+    ldapGroupSearchBase,
+    ldapGroupSearchFilter,
+    ldapGroupDnProperty,
   ]);
   ]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminLdapSecurityContainer.updateLdapSetting({
-        serverUrl: data.serverUrl,
-        isUserBind: adminLdapSecurityContainer.state.isUserBind,
-        ldapBindDN: data.ldapBindDN,
-        ldapBindDNPassword: data.ldapBindDNPassword,
-        ldapSearchFilter: data.ldapSearchFilter,
-        ldapAttrMapUsername: data.ldapAttrMapUsername,
-        isSameUsernameTreatedAsIdenticalUser: adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-        ldapAttrMapMail: data.ldapAttrMapMail,
-        ldapAttrMapName: data.ldapAttrMapName,
-        ldapGroupSearchBase: data.ldapGroupSearchBase,
-        ldapGroupSearchFilter: data.ldapGroupSearchFilter,
-        ldapGroupDnProperty: data.ldapGroupDnProperty,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.ldap.updated_ldap'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminLdapSecurityContainer, adminGeneralSecurityContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminLdapSecurityContainer.updateLdapSetting({
+          serverUrl: data.serverUrl,
+          isUserBind: adminLdapSecurityContainer.state.isUserBind,
+          ldapBindDN: data.ldapBindDN,
+          ldapBindDNPassword: data.ldapBindDNPassword,
+          ldapSearchFilter: data.ldapSearchFilter,
+          ldapAttrMapUsername: data.ldapAttrMapUsername,
+          isSameUsernameTreatedAsIdenticalUser:
+            adminLdapSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+          ldapAttrMapMail: data.ldapAttrMapMail,
+          ldapAttrMapName: data.ldapAttrMapName,
+          ldapGroupSearchBase: data.ldapGroupSearchBase,
+          ldapGroupSearchFilter: data.ldapGroupSearchFilter,
+          ldapGroupDnProperty: data.ldapGroupDnProperty,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.ldap.updated_ldap'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminLdapSecurityContainer, adminGeneralSecurityContainer],
+  );
 
 
   const openLdapAuthTestModal = useCallback(() => {
   const openLdapAuthTestModal = useCallback(() => {
     setIsLdapAuthTestModalShown(true);
     setIsLdapAuthTestModalShown(true);
@@ -88,10 +103,7 @@ const LdapSecuritySettingContents = (props: Props) => {
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-
-      <h2 className="alert-anchor border-bottom mb-4">
-        LDAP
-      </h2>
+      <h2 className="alert-anchor border-bottom mb-4">LDAP</h2>
 
 
       <div className="row my-4">
       <div className="row my-4">
         <div className="col-6 offset-3">
         <div className="col-6 offset-3">
@@ -101,25 +113,39 @@ const LdapSecuritySettingContents = (props: Props) => {
               className="form-check-input"
               className="form-check-input"
               type="checkbox"
               type="checkbox"
               checked={isLdapEnabled}
               checked={isLdapEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsLdapEnabled();
+              }}
             />
             />
-            <label className="form-label form-check-label" htmlFor="isLdapEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isLdapEnabled"
+            >
               {t('security_settings.ldap.enable_ldap')}
               {t('security_settings.ldap.enable_ldap')}
             </label>
             </label>
           </div>
           </div>
-          {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'ldap',
+          ) &&
+            isLdapEnabled && (
+              <div className="badge text-bg-warning">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
         </div>
       </div>
       </div>
 
 
-
       {isLdapEnabled && (
       {isLdapEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
         <form onSubmit={handleSubmit(onSubmit)}>
-
-          <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+          <h3 className="border-bottom mb-4">
+            {t('security_settings.configuration')}
+          </h3>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label htmlFor="serverUrl" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="serverUrl"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               Server URL
               Server URL
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
@@ -132,17 +158,23 @@ const LdapSecuritySettingContents = (props: Props) => {
                 <p
                 <p
                   className="form-text text-muted"
                   className="form-text text-muted"
                   // eslint-disable-next-line react/no-danger
                   // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.server_url_detail') }}
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.ldap.server_url_detail'),
+                  }}
                 />
                 />
-                {t('security_settings.example')}: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
+                {t('security_settings.example')}:{' '}
+                <code>
+                  ldaps://ldap.company.com/ou=people,dc=company,dc=com
+                </code>
               </small>
               </small>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label">
+            <span className="form-label text-start text-md-end col-md-3 col-form-label">
               <strong>{t('security_settings.ldap.bind_mode')}</strong>
               <strong>{t('security_settings.ldap.bind_mode')}</strong>
-            </label>
+            </span>
             <div className="col-md-9">
             <div className="col-md-9">
               <div className="dropdown">
               <div className="dropdown">
                 <button
                 <button
@@ -153,15 +185,33 @@ const LdapSecuritySettingContents = (props: Props) => {
                   aria-haspopup="true"
                   aria-haspopup="true"
                   aria-expanded="true"
                   aria-expanded="true"
                 >
                 >
-                  {adminLdapSecurityContainer.state.isUserBind
-                    ? <span className="pull-left">{t('security_settings.ldap.bind_user')}</span>
-                    : <span className="pull-left">{t('security_settings.ldap.bind_manager')}</span>}
+                  {adminLdapSecurityContainer.state.isUserBind ? (
+                    <span className="pull-left">
+                      {t('security_settings.ldap.bind_user')}
+                    </span>
+                  ) : (
+                    <span className="pull-left">
+                      {t('security_settings.ldap.bind_manager')}
+                    </span>
+                  )}
                 </button>
                 </button>
-                <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                  <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
+                <div className="dropdown-menu">
+                  <button
+                    className="dropdown-item"
+                    type="button"
+                    onClick={() => {
+                      adminLdapSecurityContainer.changeLdapBindMode(true);
+                    }}
+                  >
                     {t('security_settings.ldap.bind_user')}
                     {t('security_settings.ldap.bind_user')}
                   </button>
                   </button>
-                  <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
+                  <button
+                    className="dropdown-item"
+                    type="button"
+                    onClick={() => {
+                      adminLdapSecurityContainer.changeLdapBindMode(false);
+                    }}
+                  >
                     {t('security_settings.ldap.bind_manager')}
                     {t('security_settings.ldap.bind_manager')}
                   </button>
                   </button>
                 </div>
                 </div>
@@ -170,89 +220,135 @@ const LdapSecuritySettingContents = (props: Props) => {
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="ldapBindDN"
+            >
               <strong>Bind DN</strong>
               <strong>Bind DN</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
               <input
               <input
+                id="ldapBindDN"
                 className="form-control"
                 className="form-control"
                 type="text"
                 type="text"
                 {...register('ldapBindDN')}
                 {...register('ldapBindDN')}
               />
               />
-              {(adminLdapSecurityContainer.state.isUserBind === true) ? (
+              {adminLdapSecurityContainer.state.isUserBind === true ? (
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
                   <small>
                   <small>
-                    {t('security_settings.ldap.bind_DN_user_detail1')}<br />
+                    {t('security_settings.ldap.bind_DN_user_detail1')}
+                    <br />
                     {/* eslint-disable-next-line react/no-danger */}
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.bind_DN_user_detail2') }} /><br />
-                    {t('security_settings.example')}1: <code>uid={'{{ username }}'},dc=domain,dc=com</code><br />
-                    {t('security_settings.example')}2: <code>{'{{ username }}'}@domain.com</code>
+                    <span
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.ldap.bind_DN_user_detail2',
+                        ),
+                      }}
+                    />
+                    <br />
+                    {t('security_settings.example')}1:{' '}
+                    <code>uid={'{{ username }}'},dc=domain,dc=com</code>
+                    <br />
+                    {t('security_settings.example')}2:{' '}
+                    <code>{'{{ username }}'}@domain.com</code>
                   </small>
                   </small>
                 </p>
                 </p>
-              )
-                : (
-                  <p className="form-text text-muted">
-                    <small>
-                      {t('security_settings.ldap.bind_DN_manager_detail')}<br />
-                      {t('security_settings.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
-                      {t('security_settings.example')}2: <code>admin@domain.com</code>
-                    </small>
-                  </p>
-                )}
+              ) : (
+                <p className="form-text text-muted">
+                  <small>
+                    {t('security_settings.ldap.bind_DN_manager_detail')}
+                    <br />
+                    {t('security_settings.example')}1:{' '}
+                    <code>uid=admin,dc=domain,dc=com</code>
+                    <br />
+                    {t('security_settings.example')}2:{' '}
+                    <code>admin@domain.com</code>
+                  </small>
+                </p>
+              )}
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="text-start text-md-end col-md-3 col-form-label" htmlFor="bindDNPassword">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+              htmlFor="bindDNPassword"
+            >
               <strong>{t('security_settings.ldap.bind_DN_password')}</strong>
               <strong>{t('security_settings.ldap.bind_DN_password')}</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
-              {(adminLdapSecurityContainer.state.isUserBind) ? (
+              {adminLdapSecurityContainer.state.isUserBind ? (
                 <p className="card custom-card">
                 <p className="card custom-card">
                   <small>
                   <small>
                     {t('security_settings.ldap.bind_DN_password_user_detail')}
                     {t('security_settings.ldap.bind_DN_password_user_detail')}
                   </small>
                   </small>
                 </p>
                 </p>
-              )
-                : (
-                  <>
-                    <input
-                      className="form-control"
-                      type="password"
-                      {...register('ldapBindDNPassword')}
-                    />
-                    <p className="form-text text-muted">
-                      <small>{t('security_settings.ldap.bind_DN_password_manager_detail')}</small>
-                    </p>
-                  </>
-                )}
+              ) : (
+                <>
+                  <input
+                    className="form-control"
+                    type="password"
+                    {...register('ldapBindDNPassword')}
+                  />
+                  <p className="form-text text-muted">
+                    <small>
+                      {t(
+                        'security_settings.ldap.bind_DN_password_manager_detail',
+                      )}
+                    </small>
+                  </p>
+                </>
+              )}
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="ldapSearchFilter"
+            >
               <strong>{t('security_settings.ldap.search_filter')}</strong>
               <strong>{t('security_settings.ldap.search_filter')}</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
               <input
               <input
+                id="ldapSearchFilter"
                 className="form-control"
                 className="form-control"
                 type="text"
                 type="text"
                 {...register('ldapSearchFilter')}
                 {...register('ldapSearchFilter')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 <small>
                 <small>
-                  {t('security_settings.ldap.search_filter_detail1')}<br />
+                  {t('security_settings.ldap.search_filter_detail1')}
+                  <br />
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.search_filter_detail2') }} /><br />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.ldap.search_filter_detail2'),
+                    }}
+                  />
+                  <br />
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.search_filter_detail3') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('security_settings.ldap.search_filter_detail3'),
+                    }}
+                  />
                 </small>
                 </small>
               </p>
               </p>
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 <small>
                 <small>
-                  {t('security_settings.example')}1 - {t('security_settings.ldap.search_filter_example1')}:
-                  <code>(|(uid={'{{username}}'})(mail={'{{username}}'}))</code><br />
-                  {t('security_settings.example')}2 - {t('security_settings.ldap.search_filter_example2')}:
+                  {t('security_settings.example')}1 -{' '}
+                  {t('security_settings.ldap.search_filter_example1')}:
+                  <code>
+                    (|(uid={'{{username}}'})(mail={'{{username}}'}))
+                  </code>
+                  <br />
+                  {t('security_settings.example')}2 -{' '}
+                  {t('security_settings.ldap.search_filter_example2')}:
                   <code>(sAMAccountName={'{{username}}'})</code>
                   <code>(sAMAccountName={'{{username}}'})</code>
                 </small>
                 </small>
               </p>
               </p>
@@ -264,7 +360,10 @@ const LdapSecuritySettingContents = (props: Props) => {
           </h3>
           </h3>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="attrMapUsername">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="attrMapUsername"
+            >
               <strong>{t('username')}</strong>
               <strong>{t('username')}</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
@@ -276,7 +375,12 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.username_detail') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.ldap.username_detail'),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
@@ -288,25 +392,48 @@ const LdapSecuritySettingContents = (props: Props) => {
                   type="checkbox"
                   type="checkbox"
                   className="form-check-input"
                   className="form-check-input"
                   id="isSameUsernameTreatedAsIdenticalUser"
                   id="isSameUsernameTreatedAsIdenticalUser"
-                  checked={adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
-                  onChange={() => { adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  checked={
+                    adminLdapSecurityContainer.state
+                      .isSameUsernameTreatedAsIdenticalUser
+                  }
+                  onChange={() => {
+                    adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                  }}
                 />
                 />
                 <label
                 <label
                   className="form-check-label"
                   className="form-check-label"
                   htmlFor="isSameUsernameTreatedAsIdenticalUser"
                   htmlFor="isSameUsernameTreatedAsIdenticalUser"
-                  // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
-                />
+                >
+                  <span
+                    // eslint-disable-next-line react/no-danger
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat username matching as identical',
+                      ),
+                    }}
+                  />
+                </label>
               </div>
               </div>
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical_warn',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="attrMapMail">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="attrMapMail"
+            >
               <strong>{t('Email')}</strong>
               <strong>{t('Email')}</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
@@ -317,15 +444,16 @@ const LdapSecuritySettingContents = (props: Props) => {
                 {...register('ldapAttrMapMail')}
                 {...register('ldapAttrMapMail')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small>
-                  {t('security_settings.ldap.mail_detail')}
-                </small>
+                <small>{t('security_settings.ldap.mail_detail')}</small>
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="attrMapName">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="attrMapName"
+            >
               <strong>{t('Name')}</strong>
               <strong>{t('Name')}</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
@@ -335,21 +463,23 @@ const LdapSecuritySettingContents = (props: Props) => {
                 {...register('ldapAttrMapName')}
                 {...register('ldapAttrMapName')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small>
-                  {t('security_settings.ldap.name_detail')}
-                </small>
+                <small>{t('security_settings.ldap.name_detail')}</small>
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
-
           <h3 className="alert-anchor border-bottom mb-4">
           <h3 className="alert-anchor border-bottom mb-4">
             {t('security_settings.ldap.group_search_filter')} ({t('optional')})
             {t('security_settings.ldap.group_search_filter')} ({t('optional')})
           </h3>
           </h3>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="groupSearchBase">
-              <strong>{t('security_settings.ldap.group_search_base_DN')}</strong>
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="groupSearchBase"
+            >
+              <strong>
+                {t('security_settings.ldap.group_search_base_DN')}
+              </strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
               <input
               <input
@@ -360,15 +490,27 @@ const LdapSecuritySettingContents = (props: Props) => {
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 <small>
                 <small>
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_base_DN_detail') }} /><br />
-                  {t('security_settings.example')}: <code>ou=groups,dc=domain,dc=com</code>
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_base_DN_detail',
+                      ),
+                    }}
+                  />
+                  <br />
+                  {t('security_settings.example')}:{' '}
+                  <code>ou=groups,dc=domain,dc=com</code>
                 </small>
                 </small>
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="groupSearchFilter">
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="groupSearchFilter"
+            >
               <strong>{t('security_settings.ldap.group_search_filter')}</strong>
               <strong>{t('security_settings.ldap.group_search_filter')}</strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
@@ -380,9 +522,32 @@ const LdapSecuritySettingContents = (props: Props) => {
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 <small>
                 <small>
                   {/* eslint-disable react/no-danger */}
                   {/* eslint-disable react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail1') }} /><br />
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail2') }} /><br />
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail3') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail1',
+                      ),
+                    }}
+                  />
+                  <br />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail2',
+                      ),
+                    }}
+                  />
+                  <br />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail3',
+                      ),
+                    }}
+                  />
                   {/* eslint-enable react/no-danger */}
                   {/* eslint-enable react/no-danger */}
                 </small>
                 </small>
               </p>
               </p>
@@ -390,15 +555,27 @@ const LdapSecuritySettingContents = (props: Props) => {
                 <small>
                 <small>
                   {t('security_settings.example')}:
                   {t('security_settings.example')}:
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail4') }} />
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.ldap.group_search_filter_detail4',
+                      ),
+                    }}
+                  />
                 </small>
                 </small>
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label" htmlFor="groupDnProperty">
-              <strong>{t('security_settings.ldap.group_search_user_DN_property')}</strong>
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="groupDnProperty"
+            >
+              <strong>
+                {t('security_settings.ldap.group_search_user_DN_property')}
+              </strong>
             </label>
             </label>
             <div className="col-md-9">
             <div className="col-md-9">
               <input
               <input
@@ -409,7 +586,14 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_user_DN_property_detail') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.ldap.group_search_user_DN_property_detail',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
@@ -418,7 +602,9 @@ const LdapSecuritySettingContents = (props: Props) => {
               <button
               <button
                 type="submit"
                 type="submit"
                 className="btn btn-primary"
                 className="btn btn-primary"
-                disabled={adminLdapSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminLdapSecurityContainer.state.retrieveError != null
+                }
               >
               >
                 {t('Update')}
                 {t('Update')}
               </button>
               </button>
@@ -426,24 +612,25 @@ const LdapSecuritySettingContents = (props: Props) => {
                 type="button"
                 type="button"
                 className="btn btn-outline-secondary ms-2"
                 className="btn btn-outline-secondary ms-2"
                 onClick={openLdapAuthTestModal}
                 onClick={openLdapAuthTestModal}
-              >{t('security_settings.ldap.test_config')}
+              >
+                {t('security_settings.ldap.test_config')}
               </button>
               </button>
             </div>
             </div>
           </div>
           </div>
-
         </form>
         </form>
       )}
       )}
 
 
-
-      <LdapAuthTestModal isOpen={isLdapAuthTestModalShown} onClose={closeLdapAuthTestModal} />
-
+      <LdapAuthTestModal
+        isOpen={isLdapAuthTestModalShown}
+        onClose={closeLdapAuthTestModal}
+      />
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };
 
 
-const LdapSecuritySettingContentsWrapper = withUnstatedContainers(LdapSecuritySettingContents, [
-  AdminGeneralSecurityContainer,
-  AdminLdapSecurityContainer,
-]);
+const LdapSecuritySettingContentsWrapper = withUnstatedContainers(
+  LdapSecuritySettingContents,
+  [AdminGeneralSecurityContainer, AdminLdapSecurityContainer],
+);
 
 
 export default LdapSecuritySettingContentsWrapper;
 export default LdapSecuritySettingContentsWrapper;

+ 10 - 12
apps/app/src/client/components/Admin/Security/LocalSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
@@ -7,36 +6,35 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import LocalSecuritySettingContents from './LocalSecuritySettingContents';
 import LocalSecuritySettingContents from './LocalSecuritySettingContents';
 
 
 const LocalSecuritySetting = (props) => {
 const LocalSecuritySetting = (props) => {
   const { adminLocalSecurityContainer } = props;
   const { adminLocalSecurityContainer } = props;
 
 
-  const fetchLocalSecuritySettingsData = useCallback(async() => {
+  const fetchLocalSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminLocalSecurityContainer.retrieveSecurityData();
       await adminLocalSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
   }, [adminLocalSecurityContainer]);
   }, [adminLocalSecurityContainer]);
 
 
-
   useEffect(() => {
   useEffect(() => {
     fetchLocalSecuritySettingsData();
     fetchLocalSecuritySettingsData();
-  }, [adminLocalSecurityContainer, fetchLocalSecuritySettingsData]);
+  }, [fetchLocalSecuritySettingsData]);
 
 
   return <LocalSecuritySettingContents />;
   return <LocalSecuritySettingContents />;
 };
 };
 
 
 LocalSecuritySetting.propTypes = {
 LocalSecuritySetting.propTypes = {
-  adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
+  adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer)
+    .isRequired,
 };
 };
 
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
-  AdminLocalSecurityContainer,
-]);
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(
+  LocalSecuritySetting,
+  [AdminLocalSecurityContainer],
+);
 
 
 export default LocalSecuritySettingWithUnstatedContainer;
 export default LocalSecuritySettingWithUnstatedContainer;

+ 117 - 55
apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.tsx

@@ -1,12 +1,12 @@
 import React, { useCallback, useEffect } from 'react';
 import React, { useCallback, useEffect } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
-import { useTranslation } from 'next-i18next';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
+import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { isMailerSetupAtom } from '~/states/server-configurations';
 import { isMailerSetupAtom } from '~/states/server-configurations';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -17,58 +17,70 @@ type Props = {
 };
 };
 
 
 const LocalSecuritySettingContents = (props: Props): JSX.Element => {
 const LocalSecuritySettingContents = (props: Props): JSX.Element => {
-  const {
-    adminGeneralSecurityContainer,
-    adminLocalSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminLocalSecurityContainer } = props;
 
 
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const isMailerSetup = useAtomValue(isMailerSetupAtom);
   const isMailerSetup = useAtomValue(isMailerSetupAtom);
 
 
   const { register, handleSubmit, reset } = useForm();
   const { register, handleSubmit, reset } = useForm();
 
 
-  const { registrationMode, isPasswordResetEnabled, isEmailAuthenticationEnabled } = adminLocalSecurityContainer.state;
+  const {
+    registrationMode,
+    isPasswordResetEnabled,
+    isEmailAuthenticationEnabled,
+  } = adminLocalSecurityContainer.state;
   const { isLocalEnabled } = adminGeneralSecurityContainer.state;
   const { isLocalEnabled } = adminGeneralSecurityContainer.state;
 
 
   useEffect(() => {
   useEffect(() => {
     reset({
     reset({
-      registrationWhitelist: adminLocalSecurityContainer.state.registrationWhitelist.join('\n'),
+      registrationWhitelist:
+        adminLocalSecurityContainer.state.registrationWhitelist.join('\n'),
     });
     });
   }, [reset, adminLocalSecurityContainer.state.registrationWhitelist]);
   }, [reset, adminLocalSecurityContainer.state.registrationWhitelist]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminLocalSecurityContainer.updateLocalSecuritySetting({
-        registrationMode: adminLocalSecurityContainer.state.registrationMode,
-        registrationWhitelist: data.registrationWhitelist.split('\n'),
-        isPasswordResetEnabled: adminLocalSecurityContainer.state.isPasswordResetEnabled,
-        isEmailAuthenticationEnabled: adminLocalSecurityContainer.state.isEmailAuthenticationEnabled,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.updated_general_security_setting'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminGeneralSecurityContainer, adminLocalSecurityContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminLocalSecurityContainer.updateLocalSecuritySetting({
+          registrationMode: adminLocalSecurityContainer.state.registrationMode,
+          registrationWhitelist: data.registrationWhitelist.split('\n'),
+          isPasswordResetEnabled:
+            adminLocalSecurityContainer.state.isPasswordResetEnabled,
+          isEmailAuthenticationEnabled:
+            adminLocalSecurityContainer.state.isEmailAuthenticationEnabled,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.updated_general_security_setting'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminGeneralSecurityContainer, adminLocalSecurityContainer],
+  );
 
 
   return (
   return (
     <>
     <>
       {adminLocalSecurityContainer.state.retrieveError != null && (
       {adminLocalSecurityContainer.state.retrieveError != null && (
         <div className="alert alert-danger">
         <div className="alert alert-danger">
           <p>
           <p>
-            {t('Error occurred')} : {adminLocalSecurityContainer.state.retrieveError}
+            {t('Error occurred')} :{' '}
+            {adminLocalSecurityContainer.state.retrieveError}
           </p>
           </p>
         </div>
         </div>
       )}
       )}
-      <h2 className="alert-anchor border-bottom">{t('security_settings.Local.name')}</h2>
+      <h2 className="alert-anchor border-bottom">
+        {t('security_settings.Local.name')}
+      </h2>
 
 
       {adminLocalSecurityContainer.state.useOnlyEnvVars && (
       {adminLocalSecurityContainer.state.useOnlyEnvVars && (
         <p
         <p
           className="alert alert-info"
           className="alert alert-info"
           // eslint-disable-next-line max-len
           // eslint-disable-next-line max-len
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
           dangerouslySetInnerHTML={{
           dangerouslySetInnerHTML={{
-            __html: t('security_settings.Local.note for the only env option', { env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }),
+            __html: t('security_settings.Local.note for the only env option', {
+              env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
+            }),
           }}
           }}
         />
         />
       )}
       )}
@@ -81,22 +93,34 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
               className="form-check-input"
               className="form-check-input"
               id="isLocalEnabled"
               id="isLocalEnabled"
               checked={isLocalEnabled}
               checked={isLocalEnabled}
-              onChange={() => adminGeneralSecurityContainer.switchIsLocalEnabled()}
+              onChange={() =>
+                adminGeneralSecurityContainer.switchIsLocalEnabled()
+              }
               disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
               disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
             />
             />
-            <label className="form-label form-check-label" htmlFor="isLocalEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isLocalEnabled"
+            >
               {t('security_settings.Local.enable_local')}
               {t('security_settings.Local.enable_local')}
             </label>
             </label>
           </div>
           </div>
-          {!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled && (
-            <div className="badge bg-warning text-dark">{t('security_settings.setup_is_not_yet_complete')}</div>
-          )}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'local',
+          ) &&
+            isLocalEnabled && (
+              <div className="badge bg-warning text-dark">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
         </div>
       </div>
       </div>
 
 
       {isLocalEnabled && (
       {isLocalEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
         <form onSubmit={handleSubmit(onSubmit)}>
-          <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
+          <h3 className="border-bottom">
+            {t('security_settings.configuration')}
+          </h3>
 
 
           <div className="row">
           <div className="row">
             <div className="col-12 col-md-4 text-start text-md-end py-2">
             <div className="col-12 col-md-4 text-start text-md-end py-2">
@@ -112,16 +136,21 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                   aria-haspopup="true"
                   aria-haspopup="true"
                   aria-expanded="true"
                   aria-expanded="true"
                 >
                 >
-                  {registrationMode === 'Open' && t('security_settings.registration_mode.open')}
-                  {registrationMode === 'Restricted' && t('security_settings.registration_mode.restricted')}
-                  {registrationMode === 'Closed' && t('security_settings.registration_mode.closed')}
+                  {registrationMode === 'Open' &&
+                    t('security_settings.registration_mode.open')}
+                  {registrationMode === 'Restricted' &&
+                    t('security_settings.registration_mode.restricted')}
+                  {registrationMode === 'Closed' &&
+                    t('security_settings.registration_mode.closed')}
                 </button>
                 </button>
-                <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                <div className="dropdown-menu">
                   <button
                   <button
                     className="dropdown-item"
                     className="dropdown-item"
                     type="button"
                     type="button"
                     onClick={() => {
                     onClick={() => {
-                      adminLocalSecurityContainer.changeRegistrationMode('Open');
+                      adminLocalSecurityContainer.changeRegistrationMode(
+                        'Open',
+                      );
                     }}
                     }}
                   >
                   >
                     {t('security_settings.registration_mode.open')}
                     {t('security_settings.registration_mode.open')}
@@ -130,7 +159,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                     className="dropdown-item"
                     className="dropdown-item"
                     type="button"
                     type="button"
                     onClick={() => {
                     onClick={() => {
-                      adminLocalSecurityContainer.changeRegistrationMode('Restricted');
+                      adminLocalSecurityContainer.changeRegistrationMode(
+                        'Restricted',
+                      );
                     }}
                     }}
                   >
                   >
                     {t('security_settings.registration_mode.restricted')}
                     {t('security_settings.registration_mode.restricted')}
@@ -139,19 +170,30 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                     className="dropdown-item"
                     className="dropdown-item"
                     type="button"
                     type="button"
                     onClick={() => {
                     onClick={() => {
-                      adminLocalSecurityContainer.changeRegistrationMode('Closed');
+                      adminLocalSecurityContainer.changeRegistrationMode(
+                        'Closed',
+                      );
                     }}
                     }}
                   >
                   >
                     {t('security_settings.registration_mode.closed')}
                     {t('security_settings.registration_mode.closed')}
                   </button>
                   </button>
                 </div>
                 </div>
               </div>
               </div>
-              <p className="form-text text-muted small">{t('security_settings.register_limitation_desc')}</p>
+              <p className="form-text text-muted small">
+                {t('security_settings.register_limitation_desc')}
+              </p>
             </div>
             </div>
           </div>
           </div>
           <div className="row">
           <div className="row">
             <div className="col-12 col-md-4 text-start text-md-end">
             <div className="col-12 col-md-4 text-start text-md-end">
-              <strong dangerouslySetInnerHTML={{ __html: t('security_settings.The whitelist of registration permission E-mail address') }} />
+              <strong
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.The whitelist of registration permission E-mail address',
+                  ),
+                }}
+              />
             </div>
             </div>
             <div className="col-12 col-md-8">
             <div className="col-12 col-md-8">
               <textarea
               <textarea
@@ -171,7 +213,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
           </div>
           </div>
 
 
           <div className="row">
           <div className="row">
-            <label className="col-12 col-md-4 text-start text-md-end  col-form-label">{t('security_settings.Local.password_reset_by_users')}</label>
+            <span className="col-12 col-md-4 text-start text-md-end col-form-label">
+              {t('security_settings.Local.password_reset_by_users')}
+            </span>
             <div className="col-12 col-md-8">
             <div className="col-12 col-md-8">
               <div className="form-check form-switch form-check-success">
               <div className="form-check form-switch form-check-success">
                 <input
                 <input
@@ -179,17 +223,25 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                   className="form-check-input"
                   className="form-check-input"
                   id="isPasswordResetEnabled"
                   id="isPasswordResetEnabled"
                   checked={isPasswordResetEnabled}
                   checked={isPasswordResetEnabled}
-                  onChange={() => adminLocalSecurityContainer.switchIsPasswordResetEnabled()}
+                  onChange={() =>
+                    adminLocalSecurityContainer.switchIsPasswordResetEnabled()
+                  }
                 />
                 />
-                <label className="form-label form-check-label" htmlFor="isPasswordResetEnabled">
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="isPasswordResetEnabled"
+                >
                   {t('security_settings.Local.enable_password_reset_by_users')}
                   {t('security_settings.Local.enable_password_reset_by_users')}
                 </label>
                 </label>
               </div>
               </div>
               {!isMailerSetup && (
               {!isMailerSetup && (
                 <div className="alert alert-warning p-2 my-1 small d-inline-block">
                 <div className="alert alert-warning p-2 my-1 small d-inline-block">
-                  <span>{t('commons:alert.password_reset_please_enable_mailer')}</span>
+                  <span>
+                    {t('commons:alert.password_reset_please_enable_mailer')}
+                  </span>
                   <Link href="/admin/app#mail-settings">
                   <Link href="/admin/app#mail-settings">
-                    <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
+                    <span className="material-symbols-outlined">link</span>{' '}
+                    {t('app_setting.mail_settings')}
                   </Link>
                   </Link>
                 </div>
                 </div>
               )}
               )}
@@ -200,7 +252,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
           </div>
           </div>
 
 
           <div className="row">
           <div className="row">
-            <label className="col-12 col-md-4 text-start text-md-end  col-form-label">{t('security_settings.Local.email_authentication')}</label>
+            <span className="col-12 col-md-4 text-start text-md-end col-form-label">
+              {t('security_settings.Local.email_authentication')}
+            </span>
             <div className="col-12 col-md-8">
             <div className="col-12 col-md-8">
               <div className="form-check form-switch form-check-success">
               <div className="form-check form-switch form-check-success">
                 <input
                 <input
@@ -208,9 +262,14 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                   className="form-check-input"
                   className="form-check-input"
                   id="isEmailAuthenticationEnabled"
                   id="isEmailAuthenticationEnabled"
                   checked={isEmailAuthenticationEnabled}
                   checked={isEmailAuthenticationEnabled}
-                  onChange={() => adminLocalSecurityContainer.switchIsEmailAuthenticationEnabled()}
+                  onChange={() =>
+                    adminLocalSecurityContainer.switchIsEmailAuthenticationEnabled()
+                  }
                 />
                 />
-                <label className="form-label form-check-label" htmlFor="isEmailAuthenticationEnabled">
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="isEmailAuthenticationEnabled"
+                >
                   {t('security_settings.Local.enable_email_authentication')}
                   {t('security_settings.Local.enable_email_authentication')}
                 </label>
                 </label>
               </div>
               </div>
@@ -218,7 +277,8 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
                 <div className="alert alert-warning p-2 my-1 small d-inline-block">
                 <div className="alert alert-warning p-2 my-1 small d-inline-block">
                   <span>{t('commons:alert.please_enable_mailer')}</span>
                   <span>{t('commons:alert.please_enable_mailer')}</span>
                   <Link href="/admin/app#mail-settings">
                   <Link href="/admin/app#mail-settings">
-                    <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
+                    <span className="material-symbols-outlined">link</span>{' '}
+                    {t('app_setting.mail_settings')}
                   </Link>
                   </Link>
                 </div>
                 </div>
               )}
               )}
@@ -233,7 +293,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
               <button
               <button
                 type="submit"
                 type="submit"
                 className="btn btn-primary"
                 className="btn btn-primary"
-                disabled={adminLocalSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminLocalSecurityContainer.state.retrieveError != null
+                }
               >
               >
                 {t('Update')}
                 {t('Update')}
               </button>
               </button>
@@ -245,9 +307,9 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
   );
   );
 };
 };
 
 
-const LocalSecuritySettingContentsWrapper = withUnstatedContainers(LocalSecuritySettingContents, [
-  AdminGeneralSecurityContainer,
-  AdminLocalSecurityContainer,
-]);
+const LocalSecuritySettingContentsWrapper = withUnstatedContainers(
+  LocalSecuritySettingContents,
+  [AdminGeneralSecurityContainer, AdminLocalSecurityContainer],
+);
 
 
 export default LocalSecuritySettingContentsWrapper;
 export default LocalSecuritySettingContentsWrapper;

+ 10 - 11
apps/app/src/client/components/Admin/Security/OidcSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
@@ -7,17 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import OidcSecurityManagementContents from './OidcSecuritySettingContents';
 import OidcSecurityManagementContents from './OidcSecuritySettingContents';
 
 
 const OidcSecurityManagement = (props) => {
 const OidcSecurityManagement = (props) => {
   const { adminOidcSecurityContainer } = props;
   const { adminOidcSecurityContainer } = props;
 
 
-  const fetchOidcSecuritySettingsData = useCallback(async() => {
+  const fetchOidcSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminOidcSecurityContainer.retrieveSecurityData();
       await adminOidcSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
@@ -25,17 +22,19 @@ const OidcSecurityManagement = (props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     fetchOidcSecuritySettingsData();
     fetchOidcSecuritySettingsData();
-  }, [adminOidcSecurityContainer, fetchOidcSecuritySettingsData]);
+  }, [fetchOidcSecuritySettingsData]);
 
 
   return <OidcSecurityManagementContents />;
   return <OidcSecurityManagementContents />;
 };
 };
 
 
 OidcSecurityManagement.propTypes = {
 OidcSecurityManagement.propTypes = {
-  adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
+  adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer)
+    .isRequired,
 };
 };
 
 
-const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(OidcSecurityManagement, [
-  AdminOidcSecurityContainer,
-]);
+const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  OidcSecurityManagement,
+  [AdminOidcSecurityContainer],
+);
 
 
 export default OidcSecurityManagementWithUnstatedContainer;
 export default OidcSecurityManagementWithUnstatedContainer;

+ 398 - 108
apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.tsx

@@ -1,14 +1,12 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -22,18 +20,31 @@ const OidcSecurityManagementContents = (props: Props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
   const siteUrl = useSiteUrlWithEmptyValueWarn();
 
 
-  const {
-    adminGeneralSecurityContainer, adminOidcSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminOidcSecurityContainer } = props;
   const { isOidcEnabled } = adminGeneralSecurityContainer.state;
   const { isOidcEnabled } = adminGeneralSecurityContainer.state;
   const {
   const {
-    oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret,
-    oidcAuthorizationEndpoint, oidcTokenEndpoint, oidcRevocationEndpoint, oidcIntrospectionEndpoint,
-    oidcUserInfoEndpoint, oidcEndSessionEndpoint, oidcRegistrationEndpoint, oidcJWKSUri,
-    oidcAttrMapId, oidcAttrMapUserName, oidcAttrMapName, oidcAttrMapEmail,
+    oidcProviderName,
+    oidcIssuerHost,
+    oidcClientId,
+    oidcClientSecret,
+    oidcAuthorizationEndpoint,
+    oidcTokenEndpoint,
+    oidcRevocationEndpoint,
+    oidcIntrospectionEndpoint,
+    oidcUserInfoEndpoint,
+    oidcEndSessionEndpoint,
+    oidcRegistrationEndpoint,
+    oidcJWKSUri,
+    oidcAttrMapId,
+    oidcAttrMapUserName,
+    oidcAttrMapName,
+    oidcAttrMapEmail,
   } = adminOidcSecurityContainer.state;
   } = adminOidcSecurityContainer.state;
 
 
-  const oidcCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/oidc/callback');
+  const oidcCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/oidc/callback',
+  );
 
 
   const { register, handleSubmit, reset } = useForm();
   const { register, handleSubmit, reset } = useForm();
 
 
@@ -57,41 +68,59 @@ const OidcSecurityManagementContents = (props: Props) => {
       oidcAttrMapEmail,
       oidcAttrMapEmail,
     });
     });
   }, [
   }, [
-    reset, oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret,
-    oidcAuthorizationEndpoint, oidcTokenEndpoint, oidcRevocationEndpoint, oidcIntrospectionEndpoint,
-    oidcUserInfoEndpoint, oidcEndSessionEndpoint, oidcRegistrationEndpoint, oidcJWKSUri,
-    oidcAttrMapId, oidcAttrMapUserName, oidcAttrMapName, oidcAttrMapEmail,
+    reset,
+    oidcProviderName,
+    oidcIssuerHost,
+    oidcClientId,
+    oidcClientSecret,
+    oidcAuthorizationEndpoint,
+    oidcTokenEndpoint,
+    oidcRevocationEndpoint,
+    oidcIntrospectionEndpoint,
+    oidcUserInfoEndpoint,
+    oidcEndSessionEndpoint,
+    oidcRegistrationEndpoint,
+    oidcJWKSUri,
+    oidcAttrMapId,
+    oidcAttrMapUserName,
+    oidcAttrMapName,
+    oidcAttrMapEmail,
   ]);
   ]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminOidcSecurityContainer.updateOidcSetting({
-        oidcProviderName: data.oidcProviderName,
-        oidcIssuerHost: data.oidcIssuerHost,
-        oidcClientId: data.oidcClientId,
-        oidcClientSecret: data.oidcClientSecret,
-        oidcAuthorizationEndpoint: data.oidcAuthorizationEndpoint,
-        oidcTokenEndpoint: data.oidcTokenEndpoint,
-        oidcRevocationEndpoint: data.oidcRevocationEndpoint,
-        oidcIntrospectionEndpoint: data.oidcIntrospectionEndpoint,
-        oidcUserInfoEndpoint: data.oidcUserInfoEndpoint,
-        oidcEndSessionEndpoint: data.oidcEndSessionEndpoint,
-        oidcRegistrationEndpoint: data.oidcRegistrationEndpoint,
-        oidcJWKSUri: data.oidcJWKSUri,
-        oidcAttrMapId: data.oidcAttrMapId,
-        oidcAttrMapUserName: data.oidcAttrMapUserName,
-        oidcAttrMapName: data.oidcAttrMapName,
-        oidcAttrMapEmail: data.oidcAttrMapEmail,
-        isSameUsernameTreatedAsIdenticalUser: adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser: adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
-      });
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.OIDC.updated_oidc'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminOidcSecurityContainer, adminGeneralSecurityContainer]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminOidcSecurityContainer.updateOidcSetting({
+          oidcProviderName: data.oidcProviderName,
+          oidcIssuerHost: data.oidcIssuerHost,
+          oidcClientId: data.oidcClientId,
+          oidcClientSecret: data.oidcClientSecret,
+          oidcAuthorizationEndpoint: data.oidcAuthorizationEndpoint,
+          oidcTokenEndpoint: data.oidcTokenEndpoint,
+          oidcRevocationEndpoint: data.oidcRevocationEndpoint,
+          oidcIntrospectionEndpoint: data.oidcIntrospectionEndpoint,
+          oidcUserInfoEndpoint: data.oidcUserInfoEndpoint,
+          oidcEndSessionEndpoint: data.oidcEndSessionEndpoint,
+          oidcRegistrationEndpoint: data.oidcRegistrationEndpoint,
+          oidcJWKSUri: data.oidcJWKSUri,
+          oidcAttrMapId: data.oidcAttrMapId,
+          oidcAttrMapUserName: data.oidcAttrMapUserName,
+          oidcAttrMapName: data.oidcAttrMapName,
+          oidcAttrMapEmail: data.oidcAttrMapEmail,
+          isSameUsernameTreatedAsIdenticalUser:
+            adminOidcSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+          isSameEmailTreatedAsIdenticalUser:
+            adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
+        });
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+        toastSuccess(t('security_settings.OAuth.OIDC.updated_oidc'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [t, adminOidcSecurityContainer, adminGeneralSecurityContainer],
+  );
 
 
   return (
   return (
     <>
     <>
@@ -107,33 +136,58 @@ const OidcSecurityManagementContents = (props: Props) => {
               className="form-check-input"
               className="form-check-input"
               type="checkbox"
               type="checkbox"
               checked={adminGeneralSecurityContainer.state.isOidcEnabled}
               checked={adminGeneralSecurityContainer.state.isOidcEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsOidcEnabled();
+              }}
             />
             />
-            <label className="form-label form-check-label" htmlFor="isOidcEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isOidcEnabled"
+            >
               {t('security_settings.OAuth.enable_oidc')}
               {t('security_settings.OAuth.enable_oidc')}
             </label>
             </label>
           </div>
           </div>
-          {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'oidc',
+          ) &&
+            isOidcEnabled && (
+              <div className="badge text-bg-warning">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="row mb-5">
       <div className="row mb-5">
-        <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
+        <label
+          className="text-start text-md-end col-md-3 col-form-label"
+          htmlFor="oidcCallbackUrl"
+        >
+          {t('security_settings.callback_URL')}
+        </label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
+            id="oidcCallbackUrl"
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             value={oidcCallbackUrl}
             value={oidcCallbackUrl}
             readOnly
             readOnly
           />
           />
-          <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+          <p className="form-text text-muted small">
+            {t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}
+          </p>
           {(siteUrl == null || siteUrl === '') && (
           {(siteUrl == null || siteUrl === '') && (
             <div className="alert alert-danger">
             <div className="alert alert-danger">
               <span className="material-symbols-outlined">error</span>
               <span className="material-symbols-outlined">error</span>
               <span
               <span
                 // eslint-disable-next-line max-len
                 // eslint-disable-next-line max-len
-                dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('alert.siteUrl_is_not_set', {
+                    link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                    ns: 'commons',
+                  }),
+                }}
               />
               />
             </div>
             </div>
           )}
           )}
@@ -142,11 +196,17 @@ const OidcSecurityManagementContents = (props: Props) => {
 
 
       {isOidcEnabled && (
       {isOidcEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
         <form onSubmit={handleSubmit(onSubmit)}>
-
-          <h3 className="border-bottom mb-4">{t('security_settings.configuration')}</h3>
+          <h3 className="border-bottom mb-4">
+            {t('security_settings.configuration')}
+          </h3>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcProviderName" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.providerName')}</label>
+            <label
+              htmlFor="oidcProviderName"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.providerName')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -157,7 +217,12 @@ const OidcSecurityManagementContents = (props: Props) => {
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcIssuerHost" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.issuerHost')}</label>
+            <label
+              htmlFor="oidcIssuerHost"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.issuerHost')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -165,13 +230,25 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcIssuerHost')}
                 {...register('oidcIssuerHost')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_OIDC_ISSUER_HOST',
+                    }),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcClientId" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.clientID')}</label>
+            <label
+              htmlFor="oidcClientId"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.clientID')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -179,13 +256,25 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcClientId')}
                 {...register('oidcClientId')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_OIDC_CLIENT_ID',
+                    }),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcClientSecret" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.client_secret')}</label>
+            <label
+              htmlFor="oidcClientSecret"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.client_secret')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -193,13 +282,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcClientSecret')}
                 {...register('oidcClientSecret')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_OIDC_CLIENT_SECRET',
+                    }),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcAuthorizationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcAuthorizationEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.authorization_endpoint')}
               {t('security_settings.authorization_endpoint')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -209,13 +308,25 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAuthorizationEndpoint')}
                 {...register('oidcAuthorizationEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcTokenEndpoint" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.token_endpoint')}</label>
+            <label
+              htmlFor="oidcTokenEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.token_endpoint')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -223,13 +334,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcTokenEndpoint')}
                 {...register('oidcTokenEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcRevocationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcRevocationEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.revocation_endpoint')}
               {t('security_settings.revocation_endpoint')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -239,13 +360,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcRevocationEndpoint')}
                 {...register('oidcRevocationEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcIntrospectionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcIntrospectionEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.introspection_endpoint')}
               {t('security_settings.introspection_endpoint')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -255,13 +386,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcIntrospectionEndpoint')}
                 {...register('oidcIntrospectionEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcUserInfoEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcUserInfoEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.userinfo_endpoint')}
               {t('security_settings.userinfo_endpoint')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -271,13 +412,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcUserInfoEndpoint')}
                 {...register('oidcUserInfoEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcEndSessionEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcEndSessionEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.end_session_endpoint')}
               {t('security_settings.end_session_endpoint')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -287,13 +438,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcEndSessionEndpoint')}
                 {...register('oidcEndSessionEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcRegistrationEndpoint" className="text-start text-md-end col-md-3 col-form-label">
+            <label
+              htmlFor="oidcRegistrationEndpoint"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
               {t('security_settings.registration_endpoint')}
               {t('security_settings.registration_endpoint')}
             </label>
             </label>
             <div className="col-md-6">
             <div className="col-md-6">
@@ -303,13 +464,25 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcRegistrationEndpoint')}
                 {...register('oidcRegistrationEndpoint')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcJWKSUri" className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.jwks_uri')}</label>
+            <label
+              htmlFor="oidcJWKSUri"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('security_settings.jwks_uri')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -317,7 +490,14 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcJWKSUri')}
                 {...register('oidcJWKSUri')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.OAuth.OIDC.Use discovered URL if empty',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
@@ -327,7 +507,12 @@ const OidcSecurityManagementContents = (props: Props) => {
           </h3>
           </h3>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapId" className="text-start text-md-end col-md-3 col-form-label">Identifier</label>
+            <label
+              htmlFor="oidcAttrMapId"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              Identifier
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -335,13 +520,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapId')}
                 {...register('oidcAttrMapId')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.id_detail') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.id_detail'),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapUserName" className="text-start text-md-end col-md-3 col-form-label">{t('username')}</label>
+            <label
+              htmlFor="oidcAttrMapUserName"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('username')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -349,13 +544,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapUserName')}
                 {...register('oidcAttrMapUserName')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.username_detail') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.username_detail'),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapName" className="text-start text-md-end col-md-3 col-form-label">{t('Name')}</label>
+            <label
+              htmlFor="oidcAttrMapName"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('Name')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -363,13 +568,23 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapName')}
                 {...register('oidcAttrMapName')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.name_detail') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.name_detail'),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label htmlFor="oidcAttrMapEmail" className="text-start text-md-end col-md-3 col-form-label">{t('Email')}</label>
+            <label
+              htmlFor="oidcAttrMapEmail"
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+              {t('Email')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
                 className="form-control"
                 className="form-control"
@@ -377,27 +592,50 @@ const OidcSecurityManagementContents = (props: Props) => {
                 {...register('oidcAttrMapEmail')}
                 {...register('oidcAttrMapEmail')}
               />
               />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_settings.OAuth.OIDC.mapping_detail', {
+                      target: t('Email'),
+                    }),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row mb-4">
           <div className="row mb-4">
-            <label className="form-label text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
+            <label
+              className="form-label text-start text-md-end col-md-3 col-form-label"
+              htmlFor="oidcCallbackUrlPreview"
+            >
+              {t('security_settings.callback_URL')}
+            </label>
             <div className="col-md-6">
             <div className="col-md-6">
               <input
               <input
+                id="oidcCallbackUrlPreview"
                 className="form-control"
                 className="form-control"
                 type="text"
                 type="text"
                 defaultValue={oidcCallbackUrl}
                 defaultValue={oidcCallbackUrl}
                 readOnly
                 readOnly
               />
               />
-              <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+              <p className="form-text text-muted small">
+                {t('security_settings.desc_of_callback_URL', {
+                  AuthName: 'OAuth',
+                })}
+              </p>
               {(siteUrl == null || siteUrl === '') && (
               {(siteUrl == null || siteUrl === '') && (
                 <div className="alert alert-danger">
                 <div className="alert alert-danger">
                   <span className="material-symbols-outlined">error</span>
                   <span className="material-symbols-outlined">error</span>
                   <span
                   <span
                     // eslint-disable-next-line max-len
                     // eslint-disable-next-line max-len
-                    dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t('alert.siteUrl_is_not_set', {
+                        link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                        ns: 'commons',
+                      }),
+                    }}
                   />
                   />
                 </div>
                 </div>
               )}
               )}
@@ -411,17 +649,37 @@ const OidcSecurityManagementContents = (props: Props) => {
                   id="bindByUserName-oidc"
                   id="bindByUserName-oidc"
                   className="form-check-input"
                   className="form-check-input"
                   type="checkbox"
                   type="checkbox"
-                  checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
-                  onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  checked={
+                    adminOidcSecurityContainer.state
+                      .isSameUsernameTreatedAsIdenticalUser
+                  }
+                  onChange={() => {
+                    adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                  }}
                 />
                 />
                 <label
                 <label
                   className="form-label form-check-label"
                   className="form-label form-check-label"
                   htmlFor="bindByUserName-oidc"
                   htmlFor="bindByUserName-oidc"
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
-                />
+                >
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat username matching as identical',
+                      ),
+                    }}
+                  />
+                </label>
               </div>
               </div>
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical_warn',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
@@ -433,17 +691,37 @@ const OidcSecurityManagementContents = (props: Props) => {
                   id="bindByEmail-oidc"
                   id="bindByEmail-oidc"
                   className="form-check-input"
                   className="form-check-input"
                   type="checkbox"
                   type="checkbox"
-                  checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                  onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                  checked={
+                    adminOidcSecurityContainer.state
+                      .isSameEmailTreatedAsIdenticalUser || false
+                  }
+                  onChange={() => {
+                    adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                  }}
                 />
                 />
                 <label
                 <label
                   className="form-label form-check-label"
                   className="form-label form-check-label"
                   htmlFor="bindByEmail-oidc"
                   htmlFor="bindByEmail-oidc"
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-                />
+                >
+                  <span
+                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                    dangerouslySetInnerHTML={{
+                      __html: t(
+                        'security_settings.Treat email matching as identical',
+                      ),
+                    }}
+                  />
+                </label>
               </div>
               </div>
               <p className="form-text text-muted">
               <p className="form-text text-muted">
-                <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat email matching as identical_warn',
+                    ),
+                  }}
+                />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
@@ -453,7 +731,9 @@ const OidcSecurityManagementContents = (props: Props) => {
               <button
               <button
                 type="submit"
                 type="submit"
                 className="btn btn-primary"
                 className="btn btn-primary"
-                disabled={adminOidcSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminOidcSecurityContainer.state.retrieveError != null
+                }
               >
               >
                 {t('Update')}
                 {t('Update')}
               </button>
               </button>
@@ -462,30 +742,40 @@ const OidcSecurityManagementContents = (props: Props) => {
         </form>
         </form>
       )}
       )}
 
 
-
       <hr />
       <hr />
 
 
       <div style={{ minHeight: '300px' }}>
       <div style={{ minHeight: '300px' }}>
         <h4>
         <h4>
-          <span className="material-symbols-outlined" aria-hidden="true">help</span>
-          <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.oidc')}</a>
+          <span className="material-symbols-outlined" aria-hidden="true">
+            help
+          </span>
+          <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse">
+            {' '}
+            {t('security_settings.OAuth.how_to.oidc')}
+          </a>
         </h4>
         </h4>
         <div className=" card custom-card bg-body-tertiary">
         <div className=" card custom-card bg-body-tertiary">
           <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
           <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
             <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
             <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.register_2', { url: oidcCallbackUrl }) }} />
+            <li
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+              dangerouslySetInnerHTML={{
+                __html: t('security_settings.OAuth.OIDC.register_2', {
+                  url: oidcCallbackUrl,
+                }),
+              }}
+            />
             <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
             <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
           </ol>
           </ol>
         </div>
         </div>
       </div>
       </div>
-
     </>
     </>
   );
   );
 };
 };
 
 
-const OidcSecurityManagementContentsWrapper = withUnstatedContainers(OidcSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminOidcSecurityContainer,
-]);
+const OidcSecurityManagementContentsWrapper = withUnstatedContainers(
+  OidcSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminOidcSecurityContainer],
+);
 
 
 export default OidcSecurityManagementContentsWrapper;
 export default OidcSecurityManagementContentsWrapper;

+ 10 - 11
apps/app/src/client/components/Admin/Security/SamlSecuritySetting.jsx

@@ -1,5 +1,4 @@
-import React, { useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
@@ -7,17 +6,15 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
 
 const SamlSecurityManagement = (props) => {
 const SamlSecurityManagement = (props) => {
   const { adminSamlSecurityContainer } = props;
   const { adminSamlSecurityContainer } = props;
 
 
-  const fetchSamlSecuritySettingsData = useCallback(async() => {
+  const fetchSamlSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminSamlSecurityContainer.retrieveSecurityData();
       await adminSamlSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
@@ -25,17 +22,19 @@ const SamlSecurityManagement = (props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     fetchSamlSecuritySettingsData();
     fetchSamlSecuritySettingsData();
-  }, [adminSamlSecurityContainer, fetchSamlSecuritySettingsData]);
+  }, [fetchSamlSecuritySettingsData]);
 
 
   return <SamlSecuritySettingContents />;
   return <SamlSecuritySettingContents />;
 };
 };
 
 
 SamlSecurityManagement.propTypes = {
 SamlSecurityManagement.propTypes = {
-  adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
+  adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer)
+    .isRequired,
 };
 };
 
 
-const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(SamlSecurityManagement, [
-  AdminSamlSecurityContainer,
-]);
+const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  SamlSecurityManagement,
+  [AdminSamlSecurityContainer],
+);
 
 
 export default SamlSecurityManagementWithUnstatedContainer;
 export default SamlSecurityManagementWithUnstatedContainer;

+ 369 - 116
apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx

@@ -1,16 +1,14 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { useState, useEffect, useCallback } from 'react';
-
+import React, { useCallback, useEffect, useState } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 import { useSiteUrlWithEmptyValueWarn } from '~/states/global';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -21,9 +19,7 @@ type Props = {
 };
 };
 
 
 const SamlSecurityManagementContents = (props: Props) => {
 const SamlSecurityManagementContents = (props: Props) => {
-  const {
-    adminGeneralSecurityContainer, adminSamlSecurityContainer,
-  } = props;
+  const { adminGeneralSecurityContainer, adminSamlSecurityContainer } = props;
 
 
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const siteUrl = useSiteUrlWithEmptyValueWarn();
   const siteUrl = useSiteUrlWithEmptyValueWarn();
@@ -37,51 +33,60 @@ const SamlSecurityManagementContents = (props: Props) => {
       samlIssuer: adminSamlSecurityContainer.state.samlIssuer || '',
       samlIssuer: adminSamlSecurityContainer.state.samlIssuer || '',
       samlCert: adminSamlSecurityContainer.state.samlCert || '',
       samlCert: adminSamlSecurityContainer.state.samlCert || '',
       samlAttrMapId: adminSamlSecurityContainer.state.samlAttrMapId || '',
       samlAttrMapId: adminSamlSecurityContainer.state.samlAttrMapId || '',
-      samlAttrMapUsername: adminSamlSecurityContainer.state.samlAttrMapUsername || '',
+      samlAttrMapUsername:
+        adminSamlSecurityContainer.state.samlAttrMapUsername || '',
       samlAttrMapMail: adminSamlSecurityContainer.state.samlAttrMapMail || '',
       samlAttrMapMail: adminSamlSecurityContainer.state.samlAttrMapMail || '',
-      samlAttrMapFirstName: adminSamlSecurityContainer.state.samlAttrMapFirstName || '',
-      samlAttrMapLastName: adminSamlSecurityContainer.state.samlAttrMapLastName || '',
+      samlAttrMapFirstName:
+        adminSamlSecurityContainer.state.samlAttrMapFirstName || '',
+      samlAttrMapLastName:
+        adminSamlSecurityContainer.state.samlAttrMapLastName || '',
       samlABLCRule: adminSamlSecurityContainer.state.samlABLCRule || '',
       samlABLCRule: adminSamlSecurityContainer.state.samlABLCRule || '',
     });
     });
   }, [adminSamlSecurityContainer.state, reset]);
   }, [adminSamlSecurityContainer.state, reset]);
 
 
-  const onSubmit = useCallback(async(data) => {
-    try {
-      await adminSamlSecurityContainer.updateSamlSetting({
-        samlEntryPoint: data.samlEntryPoint,
-        samlIssuer: data.samlIssuer,
-        samlCert: data.samlCert,
-        samlAttrMapId: data.samlAttrMapId,
-        samlAttrMapUsername: data.samlAttrMapUsername,
-        samlAttrMapMail: data.samlAttrMapMail,
-        samlAttrMapFirstName: data.samlAttrMapFirstName,
-        samlAttrMapLastName: data.samlAttrMapLastName,
-        isSameUsernameTreatedAsIdenticalUser: adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser: adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
-        samlABLCRule: data.samlABLCRule,
-      });
-      toastSuccess(t('security_settings.SAML.updated_saml'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-    try {
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminSamlSecurityContainer, adminGeneralSecurityContainer, t]);
+  const onSubmit = useCallback(
+    async (data) => {
+      try {
+        await adminSamlSecurityContainer.updateSamlSetting({
+          samlEntryPoint: data.samlEntryPoint,
+          samlIssuer: data.samlIssuer,
+          samlCert: data.samlCert,
+          samlAttrMapId: data.samlAttrMapId,
+          samlAttrMapUsername: data.samlAttrMapUsername,
+          samlAttrMapMail: data.samlAttrMapMail,
+          samlAttrMapFirstName: data.samlAttrMapFirstName,
+          samlAttrMapLastName: data.samlAttrMapLastName,
+          isSameUsernameTreatedAsIdenticalUser:
+            adminSamlSecurityContainer.state
+              .isSameUsernameTreatedAsIdenticalUser,
+          isSameEmailTreatedAsIdenticalUser:
+            adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser,
+          samlABLCRule: data.samlABLCRule,
+        });
+        toastSuccess(t('security_settings.SAML.updated_saml'));
+      } catch (err) {
+        toastError(err);
+      }
+
+      try {
+        await adminGeneralSecurityContainer.retrieveSetupStratedies();
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminSamlSecurityContainer, adminGeneralSecurityContainer, t],
+  );
 
 
   const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
   const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
   const { isSamlEnabled } = adminGeneralSecurityContainer.state;
   const { isSamlEnabled } = adminGeneralSecurityContainer.state;
 
 
-  const samlCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/saml/callback');
+  const samlCallbackUrl = urljoin(
+    pathUtils.removeTrailingSlash(siteUrl),
+    '/passport/saml/callback',
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-
       <h2 className="alert-anchor border-bottom">
       <h2 className="alert-anchor border-bottom">
         {t('security_settings.SAML.name')}
         {t('security_settings.SAML.name')}
       </h2>
       </h2>
@@ -89,7 +94,12 @@ const SamlSecurityManagementContents = (props: Props) => {
       {useOnlyEnvVars && (
       {useOnlyEnvVars && (
         <p
         <p
           className="alert alert-info"
           className="alert alert-info"
-          dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+          dangerouslySetInnerHTML={{
+            __html: t('security_settings.SAML.note for the only env option', {
+              env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS',
+            }),
+          }}
         />
         />
       )}
       )}
 
 
@@ -101,34 +111,61 @@ const SamlSecurityManagementContents = (props: Props) => {
               className="form-check-input"
               className="form-check-input"
               type="checkbox"
               type="checkbox"
               checked={adminGeneralSecurityContainer.state.isSamlEnabled}
               checked={adminGeneralSecurityContainer.state.isSamlEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsSamlEnabled();
+              }}
               disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
               disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
             />
             />
-            <label className="form-label form-check-label" htmlFor="isSamlEnabled">
+            <label
+              className="form-label form-check-label"
+              htmlFor="isSamlEnabled"
+            >
               {t('security_settings.SAML.enable_saml')}
               {t('security_settings.SAML.enable_saml')}
             </label>
             </label>
           </div>
           </div>
-          {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
-              && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
+          {!adminGeneralSecurityContainer.state.setupStrategies.includes(
+            'saml',
+          ) &&
+            isSamlEnabled && (
+              <div className="badge text-bg-warning">
+                {t('security_settings.setup_is_not_yet_complete')}
+              </div>
+            )}
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="row mb-5">
       <div className="row mb-5">
-        <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
+        <label
+          className="text-start text-md-end col-md-3 col-form-label"
+          htmlFor="samlCallbackUrl"
+        >
+          {t('security_settings.callback_URL')}
+        </label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
+            id="samlCallbackUrl"
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             defaultValue={samlCallbackUrl}
             defaultValue={samlCallbackUrl}
             readOnly
             readOnly
           />
           />
-          <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
+          <p className="form-text text-muted small">
+            {t('security_settings.desc_of_callback_URL', {
+              AuthName: 'SAML Identity',
+            })}
+          </p>
           {(siteUrl == null || siteUrl === '') && (
           {(siteUrl == null || siteUrl === '') && (
             <div className="alert alert-danger">
             <div className="alert alert-danger">
               <span className="material-symbols-outlined">error</span>
               <span className="material-symbols-outlined">error</span>
               <span
               <span
                 // eslint-disable-next-line max-len
                 // eslint-disable-next-line max-len
-                dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t('alert.siteUrl_is_not_set', {
+                    link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`,
+                    ns: 'commons',
+                  }),
+                }}
               />
               />
             </div>
             </div>
           )}
           )}
@@ -137,32 +174,44 @@ const SamlSecurityManagementContents = (props: Props) => {
 
 
       {isSamlEnabled && (
       {isSamlEnabled && (
         <form onSubmit={handleSubmit(onSubmit)}>
         <form onSubmit={handleSubmit(onSubmit)}>
-
-          {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
+          {adminSamlSecurityContainer.state.missingMandatoryConfigKeys
+            .length !== 0 && (
             <div className="alert alert-danger">
             <div className="alert alert-danger">
               {t('security_settings.missing mandatory configs')}
               {t('security_settings.missing mandatory configs')}
               <ul className="mb-0">
               <ul className="mb-0">
-                {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
-                  const key = configKey.replace('security:passport-saml:', '');
-                  return <li key={configKey}>{t(`security_settings.form_item_name.${key}`)}</li>;
-                })}
+                {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map(
+                  (configKey) => {
+                    const key = configKey.replace(
+                      'security:passport-saml:',
+                      '',
+                    );
+                    return (
+                      <li key={configKey}>
+                        {t(`security_settings.form_item_name.${key}`)}
+                      </li>
+                    );
+                  },
+                )}
               </ul>
               </ul>
             </div>
             </div>
           )}
           )}
 
 
+          <h3 className="alert-anchor border-bottom mb-3">Basic Settings</h3>
 
 
-          <h3 className="alert-anchor border-bottom mb-3">
-            Basic Settings
-          </h3>
-
-          <table className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}>
+          <table
+            className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}
+          >
             <colgroup>
             <colgroup>
               <col className="item-name" />
               <col className="item-name" />
               <col className="from-db" />
               <col className="from-db" />
               <col className="from-env-vars" />
               <col className="from-env-vars" />
             </colgroup>
             </colgroup>
             <thead>
             <thead>
-              <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              <tr>
+                <th></th>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
               <tr>
               <tr>
@@ -183,7 +232,15 @@ const SamlSecurityManagementContents = (props: Props) => {
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ENTRY_POINT' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
@@ -205,7 +262,15 @@ const SamlSecurityManagementContents = (props: Props) => {
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ISSUER' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
@@ -219,14 +284,13 @@ const SamlSecurityManagementContents = (props: Props) => {
                     {...register('samlCert')}
                     {...register('samlCert')}
                   />
                   />
                   <p>
                   <p>
-                    <small>
-                      {t('security_settings.SAML.cert_detail')}
-                    </small>
+                    <small>{t('security_settings.SAML.cert_detail')}</small>
                   </p>
                   </p>
                   <div>
                   <div>
                     <small>
                     <small>
                       e.g.
                       e.g.
-                      <pre className="card custom-card">{`-----BEGIN CERTIFICATE-----
+                      <pre className="card custom-card">
+                        {`-----BEGIN CERTIFICATE-----
 MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
 MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
 UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
 UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
 ...
 ...
@@ -246,7 +310,15 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     value={adminSamlSecurityContainer.state.envCert || ''}
                     value={adminSamlSecurityContainer.state.envCert || ''}
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_CERT' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
@@ -264,7 +336,11 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <col className="from-env-vars" />
               <col className="from-env-vars" />
             </colgroup>
             </colgroup>
             <thead>
             <thead>
-              <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              <tr>
+                <th></th>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
               <tr>
               <tr>
@@ -276,9 +352,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapId')}
                     {...register('samlAttrMapId')}
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small>
-                      {t('security_settings.SAML.id_detail')}
-                    </small>
+                    <small>{t('security_settings.SAML.id_detail')}</small>
                   </p>
                   </p>
                 </td>
                 </td>
                 <td>
                 <td>
@@ -289,7 +363,15 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ATTR_MAPPING_ID' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
@@ -302,18 +384,33 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapUsername')}
                     {...register('samlAttrMapUsername')}
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.username_detail') }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.username_detail'),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
                 <td>
                 <td>
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapUsername || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapUsername || ''
+                    }
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ATTR_MAPPING_USERNAME' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
@@ -326,23 +423,42 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapMail')}
                     {...register('samlAttrMapMail')}
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: 'Email' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.mapping_detail', {
+                          target: 'Email',
+                        }),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
                 <td>
                 <td>
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapMail || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapMail || ''
+                    }
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ATTR_MAPPING_MAIL' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
               <tr>
               <tr>
-                <th>{t('security_settings.form_item_name.attrMapFirstName')}</th>
+                <th>
+                  {t('security_settings.form_item_name.attrMapFirstName')}
+                </th>
                 <td>
                 <td>
                   <input
                   <input
                     className="form-control"
                     className="form-control"
@@ -351,21 +467,48 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
                     {/* eslint-disable-next-line max-len */}
                     {/* eslint-disable-next-line max-len */}
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapFirstName') }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.mapping_detail', {
+                          target: t(
+                            'security_settings.form_item_name.attrMapFirstName',
+                          ),
+                        }),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
                 <td>
                 <td>
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapFirstName || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapFirstName || ''
+                    }
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
                     <small>
                     <small>
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
+                      <span
+                        // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.SAML.Use env var if empty',
+                            { env: 'SAML_ATTR_MAPPING_FIRST_NAME' },
+                          ),
+                        }}
+                      />
                       <br />
                       <br />
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'firstName' }) }} />
+                      <span
+                        // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.Use default if both are empty',
+                            { target: 'firstName' },
+                          ),
+                        }}
+                      />
                     </small>
                     </small>
                   </p>
                   </p>
                 </td>
                 </td>
@@ -380,21 +523,48 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
                     {/* eslint-disable-next-line max-len */}
                     {/* eslint-disable-next-line max-len */}
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapLastName') }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t('security_settings.SAML.mapping_detail', {
+                          target: t(
+                            'security_settings.form_item_name.attrMapLastName',
+                          ),
+                        }),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
                 <td>
                 <td>
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"
-                    value={adminSamlSecurityContainer.state.envAttrMapLastName || ''}
+                    value={
+                      adminSamlSecurityContainer.state.envAttrMapLastName || ''
+                    }
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
                     <small>
                     <small>
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
+                      <span
+                        // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.SAML.Use env var if empty',
+                            { env: 'SAML_ATTR_MAPPING_LAST_NAME' },
+                          ),
+                        }}
+                      />
                       <br />
                       <br />
-                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'lastName' }) }} />
+                      <span
+                        // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                        dangerouslySetInnerHTML={{
+                          __html: t(
+                            'security_settings.Use default if both are empty',
+                            { target: 'lastName' },
+                          ),
+                        }}
+                      />
                     </small>
                     </small>
                   </p>
                   </p>
                 </td>
                 </td>
@@ -412,17 +582,37 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                 id="bindByUserName-SAML"
                 id="bindByUserName-SAML"
                 className="form-check-input"
                 className="form-check-input"
                 type="checkbox"
                 type="checkbox"
-                checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                checked={
+                  adminSamlSecurityContainer.state
+                    .isSameUsernameTreatedAsIdenticalUser || false
+                }
+                onChange={() => {
+                  adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                }}
               />
               />
               <label
               <label
                 className="form-label form-check-label"
                 className="form-label form-check-label"
                 htmlFor="bindByUserName-SAML"
                 htmlFor="bindByUserName-SAML"
-                dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
-              />
+              >
+                <span
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat username matching as identical',
+                    ),
+                  }}
+                />
+              </label>
             </div>
             </div>
             <p className="form-text text-muted">
             <p className="form-text text-muted">
-              <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
+              <small
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.Treat username matching as identical_warn',
+                  ),
+                }}
+              />
             </p>
             </p>
           </div>
           </div>
 
 
@@ -432,17 +622,37 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                 id="bindByEmail-SAML"
                 id="bindByEmail-SAML"
                 className="form-check-input"
                 className="form-check-input"
                 type="checkbox"
                 type="checkbox"
-                checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
-                onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                checked={
+                  adminSamlSecurityContainer.state
+                    .isSameEmailTreatedAsIdenticalUser || false
+                }
+                onChange={() => {
+                  adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                }}
               />
               />
               <label
               <label
                 className="form-label form-check-label"
                 className="form-label form-check-label"
                 htmlFor="bindByEmail-SAML"
                 htmlFor="bindByEmail-SAML"
-                dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-              />
+              >
+                <span
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat email matching as identical',
+                    ),
+                  }}
+                />
+              </label>
             </div>
             </div>
             <p className="form-text text-muted">
             <p className="form-text text-muted">
-              <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
+              <small
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                dangerouslySetInnerHTML={{
+                  __html: t(
+                    'security_settings.Treat email matching as identical_warn',
+                  ),
+                }}
+              />
             </p>
             </p>
           </div>
           </div>
 
 
@@ -451,7 +661,14 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
           </h3>
           </h3>
 
 
           <p className="form-text text-muted">
           <p className="form-text text-muted">
-            <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_detail') }} />
+            <small
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+              dangerouslySetInnerHTML={{
+                __html: t(
+                  'security_settings.SAML.attr_based_login_control_detail',
+                ),
+              }}
+            />
           </p>
           </p>
 
 
           <table className="table settings-table">
           <table className="table settings-table">
@@ -461,13 +678,15 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <col className="from-env-vars" />
               <col className="from-env-vars" />
             </colgroup>
             </colgroup>
             <thead>
             <thead>
-              <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              <tr>
+                <th></th>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
               <tr>
               <tr>
-                <th>
-                  { t('security_settings.form_item_name.ABLCRule') }
-                </th>
+                <th>{t('security_settings.form_item_name.ABLCRule')}</th>
                 <td>
                 <td>
                   <textarea
                   <textarea
                     className="form-control"
                     className="form-control"
@@ -481,8 +700,12 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                         target="_blank"
                         target="_blank"
                         rel="noreferer noreferrer"
                         rel="noreferer noreferrer"
                       >
                       >
-                        Apache Lucene - Query Parser Syntax <span className="growi-custom-icons">external_link</span>
-                      </a>.
+                        Apache Lucene - Query Parser Syntax{' '}
+                        <span className="growi-custom-icons">
+                          external_link
+                        </span>
+                      </a>
+                      .
                     </p>
                     </p>
                     <div className="accordion" id="accordionId">
                     <div className="accordion" id="accordionId">
                       <div className="accordion-item p-1">
                       <div className="accordion-item p-1">
@@ -496,14 +719,36 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                           >
                           >
                             <span className="material-symbols-outlined me-1">
                             <span className="material-symbols-outlined me-1">
                               {isHelpOpened ? 'expand_more' : 'chevron_right'}
                               {isHelpOpened ? 'expand_more' : 'chevron_right'}
-                            </span> Show more...
+                            </span>{' '}
+                            Show more...
                           </button>
                           </button>
                         </h2>
                         </h2>
                         <Collapse isOpen={isHelpOpened}>
                         <Collapse isOpen={isHelpOpened}>
                           <div className="accordion-body">
                           <div className="accordion-body">
-                            <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_help') }} />
-                            <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example1') }} />
-                            <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example2') }} />
+                            <p
+                              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                              dangerouslySetInnerHTML={{
+                                __html: t(
+                                  'security_settings.SAML.attr_based_login_control_rule_help',
+                                ),
+                              }}
+                            />
+                            <p
+                              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                              dangerouslySetInnerHTML={{
+                                __html: t(
+                                  'security_settings.SAML.attr_based_login_control_rule_example1',
+                                ),
+                              }}
+                            />
+                            <p
+                              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                              dangerouslySetInnerHTML={{
+                                __html: t(
+                                  'security_settings.SAML.attr_based_login_control_rule_example2',
+                                ),
+                              }}
+                            />
                           </div>
                           </div>
                         </Collapse>
                         </Collapse>
                       </div>
                       </div>
@@ -517,7 +762,15 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     readOnly
                     readOnly
                   />
                   />
                   <p className="form-text text-muted">
                   <p className="form-text text-muted">
-                    <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
+                    <small
+                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                      dangerouslySetInnerHTML={{
+                        __html: t(
+                          'security_settings.SAML.Use env var if empty',
+                          { env: 'SAML_ABLC_RULE' },
+                        ),
+                      }}
+                    />
                   </p>
                   </p>
                 </td>
                 </td>
               </tr>
               </tr>
@@ -529,23 +782,23 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <button
               <button
                 type="submit"
                 type="submit"
                 className="btn btn-primary"
                 className="btn btn-primary"
-                disabled={adminSamlSecurityContainer.state.retrieveError != null}
+                disabled={
+                  adminSamlSecurityContainer.state.retrieveError != null
+                }
               >
               >
                 {t('Update')}
                 {t('Update')}
               </button>
               </button>
             </div>
             </div>
           </div>
           </div>
-
         </form>
         </form>
       )}
       )}
-
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };
 
 
-const SamlSecurityManagementContentsWrapper = withUnstatedContainers(SamlSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminSamlSecurityContainer,
-]);
+const SamlSecurityManagementContentsWrapper = withUnstatedContainers(
+  SamlSecurityManagementContents,
+  [AdminGeneralSecurityContainer, AdminSamlSecurityContainer],
+);
 
 
 export default SamlSecurityManagementContentsWrapper;
 export default SamlSecurityManagementContentsWrapper;

+ 10 - 9
apps/app/src/client/components/Admin/Security/SecurityManagement.tsx

@@ -1,25 +1,23 @@
-import React, { useEffect, useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import SecurityManagementContents from './SecurityManagementContents';
 import SecurityManagementContents from './SecurityManagementContents';
 
 
 type Props = {
 type Props = {
-  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
-}
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+};
 
 
 const SecurityManagement = (props: Props) => {
 const SecurityManagement = (props: Props) => {
   const { adminGeneralSecurityContainer } = props;
   const { adminGeneralSecurityContainer } = props;
 
 
-  const fetchGeneralSecuritySettingsData = useCallback(async() => {
+  const fetchGeneralSecuritySettingsData = useCallback(async () => {
     try {
     try {
       await adminGeneralSecurityContainer.retrieveSecurityData();
       await adminGeneralSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
+    } catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
       toastError(errs);
       toastError(errs);
     }
     }
@@ -27,11 +25,14 @@ const SecurityManagement = (props: Props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     fetchGeneralSecuritySettingsData();
     fetchGeneralSecuritySettingsData();
-  }, [adminGeneralSecurityContainer, fetchGeneralSecuritySettingsData]);
+  }, [fetchGeneralSecuritySettingsData]);
 
 
   return <SecurityManagementContents />;
   return <SecurityManagementContents />;
 };
 };
 
 
-const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);
+const SecurityManagementWithUnstatedContainer = withUnstatedContainers(
+  SecurityManagement,
+  [AdminGeneralSecurityContainer],
+);
 
 
 export default SecurityManagementWithUnstatedContainer;
 export default SecurityManagementWithUnstatedContainer;

+ 65 - 42
apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx

@@ -1,11 +1,9 @@
-import React, { useMemo, useState } from 'react';
-
-import { useTranslation } from 'next-i18next';
+import React, { useState } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 import { TabContent, TabPane } from 'reactstrap';
 
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import CustomNav from '../../CustomNavigation/CustomNav';
-
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
@@ -15,47 +13,65 @@ import SamlSecuritySetting from './SamlSecuritySetting';
 import { SecuritySetting } from './SecuritySetting';
 import { SecuritySetting } from './SecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
 import ShareLinkSetting from './ShareLinkSetting';
 
 
+const PassportLocalIcon = () => (
+  <span className="material-symbols-outlined">groups</span>
+);
+const PassportLdapIcon = () => (
+  <span className="material-symbols-outlined">network_node</span>
+);
+const PassportSamlIcon = () => (
+  <span className="material-symbols-outlined">key</span>
+);
+const PassportOidcIcon = () => (
+  <span className="material-symbols-outlined">key</span>
+);
+const PassportGoogleIcon = () => (
+  <span className="growi-custom-icons align-bottom">google</span>
+);
+const PassportGitHubIcon = () => (
+  <span className="growi-custom-icons align-bottom">github</span>
+);
+
+const navTabMapping = {
+  passport_local: {
+    Icon: PassportLocalIcon,
+    i18n: 'ID/Pass',
+  },
+  passport_ldap: {
+    Icon: PassportLdapIcon,
+    i18n: 'LDAP',
+  },
+  passport_saml: {
+    Icon: PassportSamlIcon,
+    i18n: 'SAML',
+  },
+  passport_oidc: {
+    Icon: PassportOidcIcon,
+    i18n: 'OIDC',
+  },
+  passport_google: {
+    Icon: PassportGoogleIcon,
+    i18n: 'Google',
+  },
+  passport_github: {
+    Icon: PassportGitHubIcon,
+    i18n: 'GitHub',
+  },
+};
+
 const SecurityManagementContents = () => {
 const SecurityManagementContents = () => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
   const [activeTab, setActiveTab] = useState('passport_local');
   const [activeTab, setActiveTab] = useState('passport_local');
-  const [activeComponents, setActiveComponents] = useState(new Set(['passport_local']));
+  const [activeComponents, setActiveComponents] = useState(
+    new Set(['passport_local']),
+  );
 
 
   const switchActiveTab = (selectedTab) => {
   const switchActiveTab = (selectedTab) => {
     setActiveTab(selectedTab);
     setActiveTab(selectedTab);
     setActiveComponents(activeComponents.add(selectedTab));
     setActiveComponents(activeComponents.add(selectedTab));
   };
   };
 
 
-  const navTabMapping = useMemo(() => {
-    return {
-      passport_local: {
-        Icon: () => <span className="material-symbols-outlined">groups</span>,
-        i18n: 'ID/Pass',
-      },
-      passport_ldap: {
-        Icon: () => <span className="material-symbols-outlined">network_node</span>,
-        i18n: 'LDAP',
-      },
-      passport_saml: {
-        Icon: () => <span className="material-symbols-outlined">key</span>,
-        i18n: 'SAML',
-      },
-      passport_oidc: {
-        Icon: () => <span className="material-symbols-outlined">key</span>,
-        i18n: 'OIDC',
-      },
-      passport_google: {
-        Icon: () => <span className="growi-custom-icons align-bottom">google</span>,
-        i18n: 'Google',
-      },
-      passport_github: {
-        Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
-        i18n: 'GitHub',
-      },
-    };
-  }, []);
-
-
   return (
   return (
     <div data-testid="admin-security">
     <div data-testid="admin-security">
       <div className="mb-5">
       <div className="mb-5">
@@ -67,22 +83,26 @@ const SecurityManagementContents = () => {
         <ShareLinkSetting />
         <ShareLinkSetting />
       </div>
       </div>
 
 
-
       {/* XSS configuration link */}
       {/* XSS configuration link */}
       <div className="mb-5">
       <div className="mb-5">
-        <h2 className="border-bottom pb-2">{t('security_settings.xss_prevent_setting')}</h2>
+        <h2 className="border-bottom pb-2">
+          {t('security_settings.xss_prevent_setting')}
+        </h2>
         <div className="mt-4">
         <div className="mt-4">
           <Link
           <Link
             href="/admin/markdown/#preventXSS"
             href="/admin/markdown/#preventXSS"
             style={{ fontSize: 'large' }}
             style={{ fontSize: 'large' }}
           >
           >
-            <span className="material-symbols-outlined me-1">login</span> {t('security_settings.xss_prevent_setting_link')}
+            <span className="material-symbols-outlined me-1">login</span>{' '}
+            {t('security_settings.xss_prevent_setting_link')}
           </Link>
           </Link>
         </div>
         </div>
       </div>
       </div>
 
 
       <div className="auth-mechanism-configurations">
       <div className="auth-mechanism-configurations">
-        <h2 className="border-bottom pb-2">{t('security_settings.Authentication mechanism settings')}</h2>
+        <h2 className="border-bottom pb-2">
+          {t('security_settings.Authentication mechanism settings')}
+        </h2>
         <CustomNav
         <CustomNav
           activeTab={activeTab}
           activeTab={activeTab}
           navTabMapping={navTabMapping}
           navTabMapping={navTabMapping}
@@ -104,16 +124,19 @@ const SecurityManagementContents = () => {
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
           </TabPane>
           </TabPane>
           <TabPane tabId="passport_google">
           <TabPane tabId="passport_google">
-            {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
+            {activeComponents.has('passport_google') && (
+              <GoogleSecuritySetting />
+            )}
           </TabPane>
           </TabPane>
           <TabPane tabId="passport_github">
           <TabPane tabId="passport_github">
-            {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
+            {activeComponents.has('passport_github') && (
+              <GitHubSecuritySetting />
+            )}
           </TabPane>
           </TabPane>
         </TabContent>
         </TabContent>
       </div>
       </div>
     </div>
     </div>
   );
   );
-
 };
 };
 
 
 export default SecurityManagementContents;
 export default SecurityManagementContents;

+ 20 - 7
apps/app/src/client/components/Admin/Security/SecuritySetting/CommentManageRightsSettings.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import type React from 'react';
 
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 
@@ -7,7 +7,10 @@ type Props = {
   t: (key: string) => string;
   t: (key: string) => string;
 };
 };
 
 
-export const CommentManageRightsSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const CommentManageRightsSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   const { isRomUserAllowedToComment } = adminGeneralSecurityContainer.state;
   const { isRomUserAllowedToComment } = adminGeneralSecurityContainer.state;
 
 
   return (
   return (
@@ -30,22 +33,32 @@ export const CommentManageRightsSettings: React.FC<Props> = ({ adminGeneralSecur
               aria-expanded="true"
               aria-expanded="true"
             >
             >
               <span className="float-start">
               <span className="float-start">
-                {isRomUserAllowedToComment === true && t('security_settings.read_only_users_comment.accept')}
-                {isRomUserAllowedToComment === false && t('security_settings.read_only_users_comment.deny')}
+                {isRomUserAllowedToComment === true &&
+                  t('security_settings.read_only_users_comment.accept')}
+                {isRomUserAllowedToComment === false &&
+                  t('security_settings.read_only_users_comment.deny')}
               </span>
               </span>
             </button>
             </button>
-            <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+            <div className="dropdown-menu">
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(false) }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(
+                    false,
+                  );
+                }}
               >
               >
                 {t('security_settings.read_only_users_comment.deny')}
                 {t('security_settings.read_only_users_comment.deny')}
               </button>
               </button>
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(true) }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(
+                    true,
+                  );
+                }}
               >
               >
                 {t('security_settings.read_only_users_comment.accept')}
                 {t('security_settings.read_only_users_comment.accept')}
               </button>
               </button>

+ 19 - 7
apps/app/src/client/components/Admin/Security/SecuritySetting/PageAccessRightsSettings.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React from 'react';
+import type React from 'react';
 
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 
@@ -8,7 +8,10 @@ type Props = {
   t: (key: string, options?: Record<string, unknown>) => string;
   t: (key: string, options?: Record<string, unknown>) => string;
 };
 };
 
 
-export const PageAccessRightsSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const PageAccessRightsSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   const { currentRestrictGuestMode } = adminGeneralSecurityContainer.state;
   const { currentRestrictGuestMode } = adminGeneralSecurityContainer.state;
 
 
   return (
   return (
@@ -31,22 +34,30 @@ export const PageAccessRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
               aria-expanded="true"
               aria-expanded="true"
             >
             >
               <span className="float-start">
               <span className="float-start">
-                {currentRestrictGuestMode === 'Deny' && t('security_settings.guest_mode.deny')}
-                {currentRestrictGuestMode === 'Readonly' && t('security_settings.guest_mode.readonly')}
+                {currentRestrictGuestMode === 'Deny' &&
+                  t('security_settings.guest_mode.deny')}
+                {currentRestrictGuestMode === 'Readonly' &&
+                  t('security_settings.guest_mode.readonly')}
               </span>
               </span>
             </button>
             </button>
-            <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+            <div className="dropdown-menu">
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.changeRestrictGuestMode('Deny');
+                }}
               >
               >
                 {t('security_settings.guest_mode.deny')}
                 {t('security_settings.guest_mode.deny')}
               </button>
               </button>
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}
+                onClick={() => {
+                  adminGeneralSecurityContainer.changeRestrictGuestMode(
+                    'Readonly',
+                  );
+                }}
               >
               >
                 {t('security_settings.guest_mode.readonly')}
                 {t('security_settings.guest_mode.readonly')}
               </button>
               </button>
@@ -59,6 +70,7 @@ export const PageAccessRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
               <br />
               <br />
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
               <b
               <b
+                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
                 dangerouslySetInnerHTML={{
                   __html: t('security_settings.Fixed by env var', {
                   __html: t('security_settings.Fixed by env var', {
                     key: 'FORCE_WIKI_MODE',
                     key: 'FORCE_WIKI_MODE',

+ 321 - 170
apps/app/src/client/components/Admin/Security/SecuritySetting/PageDeleteRightsSettings.tsx

@@ -1,20 +1,23 @@
-import React, { useCallback } from 'react';
-
+import type React from 'react';
+import { useCallback } from 'react';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import {
 import {
-  PageDeleteConfigValue,
   type IPageDeleteConfigValue,
   type IPageDeleteConfigValue,
   type IPageDeleteConfigValueToProcessValidation,
   type IPageDeleteConfigValueToProcessValidation,
+  PageDeleteConfigValue,
 } from '~/interfaces/page-delete-config';
 } from '~/interfaces/page-delete-config';
-import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
+import {
+  prepareDeleteConfigValuesForCalc,
+  validateDeleteConfigs,
+} from '~/utils/page-delete-config';
 
 
 import {
 import {
   DeletionType,
   DeletionType,
   type DeletionTypeValue,
   type DeletionTypeValue,
-  getDeletionTypeForT,
   getDeleteConfigValueForT,
   getDeleteConfigValueForT,
+  getDeletionTypeForT,
   isRecursiveDeletion,
   isRecursiveDeletion,
   isTypeDeletion,
   isTypeDeletion,
 } from './types';
 } from './types';
@@ -24,7 +27,10 @@ type Props = {
   t: (key: string) => string;
   t: (key: string) => string;
 };
 };
 
 
-export const PageDeleteRightsSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const PageDeleteRightsSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   const {
   const {
     currentPageDeletionAuthority,
     currentPageDeletionAuthority,
     currentPageCompleteDeletionAuthority,
     currentPageCompleteDeletionAuthority,
@@ -32,189 +38,291 @@ export const PageDeleteRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
     currentPageRecursiveCompleteDeletionAuthority,
     currentPageRecursiveCompleteDeletionAuthority,
   } = adminGeneralSecurityContainer.state;
   } = adminGeneralSecurityContainer.state;
 
 
-  const getRecursiveDeletionConfigState = useCallback((deletionType: DeletionTypeValue) => {
-    if (isTypeDeletion(deletionType)) {
+  const getRecursiveDeletionConfigState = useCallback(
+    (deletionType: DeletionTypeValue) => {
+      if (isTypeDeletion(deletionType)) {
+        return [
+          adminGeneralSecurityContainer.state
+            .currentPageRecursiveDeletionAuthority,
+          adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
+        ] as const;
+      }
+
       return [
       return [
-        adminGeneralSecurityContainer.state.currentPageRecursiveDeletionAuthority,
-        adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
+        adminGeneralSecurityContainer.state
+          .currentPageRecursiveCompleteDeletionAuthority,
+        adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
       ] as const;
       ] as const;
-    }
-
-    return [
-      adminGeneralSecurityContainer.state.currentPageRecursiveCompleteDeletionAuthority,
-      adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
-    ] as const;
-  }, [adminGeneralSecurityContainer]);
+    },
+    [adminGeneralSecurityContainer],
+  );
 
 
-  const previousPageRecursiveAuthorityState = useCallback((deletionType: DeletionTypeValue) => {
-    return isTypeDeletion(deletionType)
-      ? adminGeneralSecurityContainer.state.previousPageRecursiveDeletionAuthority
-      : adminGeneralSecurityContainer.state.previousPageRecursiveCompleteDeletionAuthority;
-  }, [adminGeneralSecurityContainer]);
+  const previousPageRecursiveAuthorityState = useCallback(
+    (deletionType: DeletionTypeValue) => {
+      return isTypeDeletion(deletionType)
+        ? adminGeneralSecurityContainer.state
+            .previousPageRecursiveDeletionAuthority
+        : adminGeneralSecurityContainer.state
+            .previousPageRecursiveCompleteDeletionAuthority;
+    },
+    [adminGeneralSecurityContainer],
+  );
 
 
-  const setPagePreviousRecursiveAuthorityState = useCallback((deletionType: DeletionTypeValue, previousState: IPageDeleteConfigValue | null) => {
-    if (isTypeDeletion(deletionType)) {
-      adminGeneralSecurityContainer.changePreviousPageRecursiveDeletionAuthority(previousState);
-      return;
-    }
+  const setPagePreviousRecursiveAuthorityState = useCallback(
+    (
+      deletionType: DeletionTypeValue,
+      previousState: IPageDeleteConfigValue | null,
+    ) => {
+      if (isTypeDeletion(deletionType)) {
+        adminGeneralSecurityContainer.changePreviousPageRecursiveDeletionAuthority(
+          previousState,
+        );
+        return;
+      }
 
 
-    adminGeneralSecurityContainer.changePreviousPageRecursiveCompleteDeletionAuthority(previousState);
-  }, [adminGeneralSecurityContainer]);
+      adminGeneralSecurityContainer.changePreviousPageRecursiveCompleteDeletionAuthority(
+        previousState,
+      );
+    },
+    [adminGeneralSecurityContainer],
+  );
 
 
-  const expandDeleteOptionsState = useCallback((deletionType: DeletionTypeValue) => {
-    return isTypeDeletion(deletionType)
-      ? adminGeneralSecurityContainer.state.expandOtherOptionsForDeletion
-      : adminGeneralSecurityContainer.state.expandOtherOptionsForCompleteDeletion;
-  }, [adminGeneralSecurityContainer]);
+  const expandDeleteOptionsState = useCallback(
+    (deletionType: DeletionTypeValue) => {
+      return isTypeDeletion(deletionType)
+        ? adminGeneralSecurityContainer.state.expandOtherOptionsForDeletion
+        : adminGeneralSecurityContainer.state
+            .expandOtherOptionsForCompleteDeletion;
+    },
+    [adminGeneralSecurityContainer],
+  );
 
 
-  const setExpandOtherDeleteOptionsState = useCallback((deletionType: DeletionTypeValue, bool: boolean) => {
-    if (isTypeDeletion(deletionType)) {
-      adminGeneralSecurityContainer.switchExpandOtherOptionsForDeletion(bool);
-      return;
-    }
-    adminGeneralSecurityContainer.switchExpandOtherOptionsForCompleteDeletion(bool);
-  }, [adminGeneralSecurityContainer]);
+  const setExpandOtherDeleteOptionsState = useCallback(
+    (deletionType: DeletionTypeValue, bool: boolean) => {
+      if (isTypeDeletion(deletionType)) {
+        adminGeneralSecurityContainer.switchExpandOtherOptionsForDeletion(bool);
+        return;
+      }
+      adminGeneralSecurityContainer.switchExpandOtherOptionsForCompleteDeletion(
+        bool,
+      );
+    },
+    [adminGeneralSecurityContainer],
+  );
 
 
-  const setDeletionConfigState = useCallback((
+  const setDeletionConfigState = useCallback(
+    (
       newState: IPageDeleteConfigValue,
       newState: IPageDeleteConfigValue,
       setState: (value: IPageDeleteConfigValue) => void,
       setState: (value: IPageDeleteConfigValue) => void,
       deletionType: DeletionTypeValue,
       deletionType: DeletionTypeValue,
-  ) => {
-    setState(newState);
+    ) => {
+      setState(newState);
 
 
-    if (previousPageRecursiveAuthorityState(deletionType) !== null) {
-      setPagePreviousRecursiveAuthorityState(deletionType, null);
-    }
+      if (previousPageRecursiveAuthorityState(deletionType) !== null) {
+        setPagePreviousRecursiveAuthorityState(deletionType, null);
+      }
 
 
-    if (isRecursiveDeletion(deletionType)) {
-      return;
-    }
+      if (isRecursiveDeletion(deletionType)) {
+        return;
+      }
 
 
-    const [recursiveState, setRecursiveState] = getRecursiveDeletionConfigState(deletionType);
+      const [recursiveState, setRecursiveState] =
+        getRecursiveDeletionConfigState(deletionType);
 
 
-    const calculableValue = prepareDeleteConfigValuesForCalc(
-      newState as IPageDeleteConfigValueToProcessValidation,
-      recursiveState as IPageDeleteConfigValueToProcessValidation,
-    );
-    const shouldForceUpdate = !validateDeleteConfigs(calculableValue[0], calculableValue[1]);
-    if (shouldForceUpdate) {
-      setRecursiveState(newState);
-      setPagePreviousRecursiveAuthorityState(deletionType, recursiveState);
-      setExpandOtherDeleteOptionsState(deletionType, true);
-    }
-  }, [
-    getRecursiveDeletionConfigState,
-    previousPageRecursiveAuthorityState,
-    setPagePreviousRecursiveAuthorityState,
-    setExpandOtherDeleteOptionsState,
-  ]);
+      const calculableValue = prepareDeleteConfigValuesForCalc(
+        newState as IPageDeleteConfigValueToProcessValidation,
+        recursiveState as IPageDeleteConfigValueToProcessValidation,
+      );
+      const shouldForceUpdate = !validateDeleteConfigs(
+        calculableValue[0],
+        calculableValue[1],
+      );
+      if (shouldForceUpdate) {
+        setRecursiveState(newState);
+        setPagePreviousRecursiveAuthorityState(deletionType, recursiveState);
+        setExpandOtherDeleteOptionsState(deletionType, true);
+      }
+    },
+    [
+      getRecursiveDeletionConfigState,
+      previousPageRecursiveAuthorityState,
+      setPagePreviousRecursiveAuthorityState,
+      setExpandOtherDeleteOptionsState,
+    ],
+  );
 
 
-  const renderPageDeletePermissionDropdown = useCallback((
+  const renderPageDeletePermissionDropdown = useCallback(
+    (
       currentState: IPageDeleteConfigValue,
       currentState: IPageDeleteConfigValue,
       setState: (value: IPageDeleteConfigValue) => void,
       setState: (value: IPageDeleteConfigValue) => void,
       deletionType: DeletionTypeValue,
       deletionType: DeletionTypeValue,
       isButtonDisabled: boolean,
       isButtonDisabled: boolean,
-  ) => {
-    return (
-      <div className="dropdown">
-        <button
-          className="btn btn-outline-secondary dropdown-toggle text-end"
-          type="button"
-          id="dropdownMenuButton"
-          data-bs-toggle="dropdown"
-          aria-haspopup="true"
-          aria-expanded="true"
-        >
-          <span className="float-start">{t(getDeleteConfigValueForT(currentState))}</span>
-        </button>
-        <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-          {isRecursiveDeletion(deletionType)
-            ? (
+    ) => {
+      return (
+        <div className="dropdown">
+          <button
+            className="btn btn-outline-secondary dropdown-toggle text-end"
+            type="button"
+            id="dropdownMenuButton"
+            data-bs-toggle="dropdown"
+            aria-haspopup="true"
+            aria-expanded="true"
+          >
+            <span className="float-start">
+              {t(getDeleteConfigValueForT(currentState))}
+            </span>
+          </button>
+          <div className="dropdown-menu">
+            {isRecursiveDeletion(deletionType) ? (
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { setDeletionConfigState(PageDeleteConfigValue.Inherit, setState, deletionType) }}
+                onClick={() => {
+                  setDeletionConfigState(
+                    PageDeleteConfigValue.Inherit,
+                    setState,
+                    deletionType,
+                  );
+                }}
               >
               >
                 {t('security_settings.inherit')}
                 {t('security_settings.inherit')}
               </button>
               </button>
-            )
-            : (
+            ) : (
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { setDeletionConfigState(PageDeleteConfigValue.Anyone, setState, deletionType) }}
+                onClick={() => {
+                  setDeletionConfigState(
+                    PageDeleteConfigValue.Anyone,
+                    setState,
+                    deletionType,
+                  );
+                }}
               >
               >
                 {t('security_settings.anyone')}
                 {t('security_settings.anyone')}
               </button>
               </button>
             )}
             )}
-          <button
-            className={`dropdown-item ${isButtonDisabled ? 'disabled' : ''}`}
-            type="button"
-            onClick={() => { setDeletionConfigState(PageDeleteConfigValue.AdminAndAuthor, setState, deletionType) }}
-          >
-            {t('security_settings.admin_and_author')}
-          </button>
-          <button
-            className="dropdown-item"
-            type="button"
-            onClick={() => { setDeletionConfigState(PageDeleteConfigValue.AdminOnly, setState, deletionType) }}
-          >
-            {t('security_settings.admin_only')}
-          </button>
+            <button
+              className={`dropdown-item ${isButtonDisabled ? 'disabled' : ''}`}
+              type="button"
+              onClick={() => {
+                setDeletionConfigState(
+                  PageDeleteConfigValue.AdminAndAuthor,
+                  setState,
+                  deletionType,
+                );
+              }}
+            >
+              {t('security_settings.admin_and_author')}
+            </button>
+            <button
+              className="dropdown-item"
+              type="button"
+              onClick={() => {
+                setDeletionConfigState(
+                  PageDeleteConfigValue.AdminOnly,
+                  setState,
+                  deletionType,
+                );
+              }}
+            >
+              {t('security_settings.admin_only')}
+            </button>
+          </div>
+          <p className="form-text text-muted small">
+            {t(
+              `security_settings.${getDeletionTypeForT(deletionType)}_explanation`,
+            )}
+          </p>
         </div>
         </div>
-        <p className="form-text text-muted small">{t(`security_settings.${getDeletionTypeForT(deletionType)}_explanation`)}</p>
-      </div>
-    );
-  }, [t, setDeletionConfigState]);
+      );
+    },
+    [t, setDeletionConfigState],
+  );
 
 
-  const renderPageDeletePermission = useCallback((
+  const renderPageDeletePermission = useCallback(
+    (
       currentState: IPageDeleteConfigValue,
       currentState: IPageDeleteConfigValue,
       setState: (value: IPageDeleteConfigValue) => void,
       setState: (value: IPageDeleteConfigValue) => void,
       deletionType: DeletionTypeValue,
       deletionType: DeletionTypeValue,
       isButtonDisabled: boolean,
       isButtonDisabled: boolean,
-  ) => {
-    const expandDeleteOptions = expandDeleteOptionsState(deletionType);
+    ) => {
+      const expandDeleteOptions = expandDeleteOptionsState(deletionType);
 
 
-    return (
-      <div key={`page-delete-permission-dropdown-${deletionType}`} className="row">
-        <div className="col-md-4 text-md-end">
-          {!isRecursiveDeletion(deletionType) && isTypeDeletion(deletionType) && <strong>{t('security_settings.page_delete')}</strong>}
-          {!isRecursiveDeletion(deletionType) && !isTypeDeletion(deletionType) && <strong>{t('security_settings.page_delete_completely')}</strong>}
-        </div>
+      return (
+        <div
+          key={`page-delete-permission-dropdown-${deletionType}`}
+          className="row"
+        >
+          <div className="col-md-4 text-md-end">
+            {!isRecursiveDeletion(deletionType) &&
+              isTypeDeletion(deletionType) && (
+                <strong>{t('security_settings.page_delete')}</strong>
+              )}
+            {!isRecursiveDeletion(deletionType) &&
+              !isTypeDeletion(deletionType) && (
+                <strong>{t('security_settings.page_delete_completely')}</strong>
+              )}
+          </div>
 
 
-        <div className="col-md-8">
-          {!isRecursiveDeletion(deletionType)
-            ? (
+          <div className="col-md-8">
+            {!isRecursiveDeletion(deletionType) ? (
               <>
               <>
-                {renderPageDeletePermissionDropdown(currentState, setState, deletionType, isButtonDisabled)}
-                {currentState === PageDeleteConfigValue.Anyone && deletionType === DeletionType.CompleteDeletion && (
-                  <>
-                    <input
-                      id="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox"
-                      className="form-check-input"
-                      type="checkbox"
-                      checked={adminGeneralSecurityContainer.state.isAllGroupMembershipRequiredForPageCompleteDeletion}
-                      onChange={() => { adminGeneralSecurityContainer.switchIsAllGroupMembershipRequiredForPageCompleteDeletion() }}
-                    />
-                    <label className="form-check-label" htmlFor="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox">
-                      {t('security_settings.is_all_group_membership_required_for_page_complete_deletion')}
-                    </label>
-                    <p className="form-text text-muted small mt-2">
-                      {t('security_settings.is_all_group_membership_required_for_page_complete_deletion_explanation')}
-                    </p>
-                  </>
+                {renderPageDeletePermissionDropdown(
+                  currentState,
+                  setState,
+                  deletionType,
+                  isButtonDisabled,
                 )}
                 )}
+                {currentState === PageDeleteConfigValue.Anyone &&
+                  deletionType === DeletionType.CompleteDeletion && (
+                    <>
+                      <input
+                        id="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox"
+                        className="form-check-input"
+                        type="checkbox"
+                        checked={
+                          adminGeneralSecurityContainer.state
+                            .isAllGroupMembershipRequiredForPageCompleteDeletion
+                        }
+                        onChange={() => {
+                          adminGeneralSecurityContainer.switchIsAllGroupMembershipRequiredForPageCompleteDeletion();
+                        }}
+                      />
+                      <label
+                        className="form-check-label"
+                        htmlFor="isAllGroupMembershipRequiredForPageCompleteDeletionCheckbox"
+                      >
+                        {t(
+                          'security_settings.is_all_group_membership_required_for_page_complete_deletion',
+                        )}
+                      </label>
+                      <p className="form-text text-muted small mt-2">
+                        {t(
+                          'security_settings.is_all_group_membership_required_for_page_complete_deletion_explanation',
+                        )}
+                      </p>
+                    </>
+                  )}
               </>
               </>
-            )
-            : (
+            ) : (
               <>
               <>
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-link p-0 mb-4"
                   className="btn btn-link p-0 mb-4"
                   aria-expanded="false"
                   aria-expanded="false"
-                  onClick={() => setExpandOtherDeleteOptionsState(deletionType, !expandDeleteOptions)}
+                  onClick={() =>
+                    setExpandOtherDeleteOptionsState(
+                      deletionType,
+                      !expandDeleteOptions,
+                    )
+                  }
                 >
                 >
-                  <span className={`material-symbols-outlined me-1 ${expandDeleteOptions ? 'rotate-90' : ''}`}>navigate_next</span>
+                  <span
+                    className={`material-symbols-outlined me-1 ${expandDeleteOptions ? 'rotate-90' : ''}`}
+                  >
+                    navigate_next
+                  </span>
                   {t('security_settings.other_options')}
                   {t('security_settings.other_options')}
                 </button>
                 </button>
                 <Collapse isOpen={expandDeleteOptions}>
                 <Collapse isOpen={expandDeleteOptions}>
@@ -223,67 +331,110 @@ export const PageDeleteRightsSettings: React.FC<Props> = ({ adminGeneralSecurity
                       <span className="text-warning">
                       <span className="text-warning">
                         <span className="material-symbols-outlined">info</span>
                         <span className="material-symbols-outlined">info</span>
                         {/* eslint-disable-next-line react/no-danger */}
                         {/* eslint-disable-next-line react/no-danger */}
-                        <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
+                        <span
+                          // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                          dangerouslySetInnerHTML={{
+                            __html: t(
+                              'security_settings.page_delete_rights_caution',
+                            ),
+                          }}
+                        />
                       </span>
                       </span>
                     </p>
                     </p>
-                    {previousPageRecursiveAuthorityState(deletionType) !== null && (
+                    {previousPageRecursiveAuthorityState(deletionType) !==
+                      null && (
                       <div className="mb-3">
                       <div className="mb-3">
-                        <strong>{t('security_settings.forced_update_desc')}</strong>
-                        <code>{t(getDeleteConfigValueForT(previousPageRecursiveAuthorityState(deletionType)))}</code>
+                        <strong>
+                          {t('security_settings.forced_update_desc')}
+                        </strong>
+                        <code>
+                          {t(
+                            getDeleteConfigValueForT(
+                              previousPageRecursiveAuthorityState(deletionType),
+                            ),
+                          )}
+                        </code>
                       </div>
                       </div>
                     )}
                     )}
-                    {renderPageDeletePermissionDropdown(currentState, setState, deletionType, isButtonDisabled)}
+                    {renderPageDeletePermissionDropdown(
+                      currentState,
+                      setState,
+                      deletionType,
+                      isButtonDisabled,
+                    )}
                   </div>
                   </div>
                 </Collapse>
                 </Collapse>
               </>
               </>
             )}
             )}
+          </div>
         </div>
         </div>
-      </div>
-    );
-  }, [
-    adminGeneralSecurityContainer,
-    expandDeleteOptionsState,
-    previousPageRecursiveAuthorityState,
-    renderPageDeletePermissionDropdown,
-    setExpandOtherDeleteOptionsState,
-    t,
-  ]);
+      );
+    },
+    [
+      adminGeneralSecurityContainer,
+      expandDeleteOptionsState,
+      previousPageRecursiveAuthorityState,
+      renderPageDeletePermissionDropdown,
+      setExpandOtherDeleteOptionsState,
+      t,
+    ],
+  );
 
 
-  const isButtonDisabledForDeletion = !validateDeleteConfigs(currentPageDeletionAuthority, PageDeleteConfigValue.AdminAndAuthor);
+  const isButtonDisabledForDeletion = !validateDeleteConfigs(
+    currentPageDeletionAuthority,
+    PageDeleteConfigValue.AdminAndAuthor,
+  );
 
 
-  const isButtonDisabledForCompleteDeletion = !validateDeleteConfigs(currentPageCompleteDeletionAuthority, PageDeleteConfigValue.AdminAndAuthor);
+  const isButtonDisabledForCompleteDeletion = !validateDeleteConfigs(
+    currentPageCompleteDeletionAuthority,
+    PageDeleteConfigValue.AdminAndAuthor,
+  );
 
 
   return (
   return (
     <>
     <>
       <h4 className="mb-3">{t('security_settings.page_delete_rights')}</h4>
       <h4 className="mb-3">{t('security_settings.page_delete_rights')}</h4>
       {[
       {[
-        [currentPageDeletionAuthority, adminGeneralSecurityContainer.changePageDeletionAuthority, DeletionType.Deletion, false],
+        [
+          currentPageDeletionAuthority,
+          adminGeneralSecurityContainer.changePageDeletionAuthority,
+          DeletionType.Deletion,
+          false,
+        ],
         [
         [
           currentPageRecursiveDeletionAuthority,
           currentPageRecursiveDeletionAuthority,
           adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
           adminGeneralSecurityContainer.changePageRecursiveDeletionAuthority,
           DeletionType.RecursiveDeletion,
           DeletionType.RecursiveDeletion,
           isButtonDisabledForDeletion,
           isButtonDisabledForDeletion,
         ],
         ],
-      ].map(arr => renderPageDeletePermission(
-        arr[0] as IPageDeleteConfigValue,
-        arr[1] as (value: IPageDeleteConfigValue) => void,
-        arr[2] as DeletionTypeValue,
-        arr[3] as boolean,
-      ))}
+      ].map((arr) =>
+        renderPageDeletePermission(
+          arr[0] as IPageDeleteConfigValue,
+          arr[1] as (value: IPageDeleteConfigValue) => void,
+          arr[2] as DeletionTypeValue,
+          arr[3] as boolean,
+        ),
+      )}
       {[
       {[
-        [currentPageCompleteDeletionAuthority, adminGeneralSecurityContainer.changePageCompleteDeletionAuthority, DeletionType.CompleteDeletion, false],
+        [
+          currentPageCompleteDeletionAuthority,
+          adminGeneralSecurityContainer.changePageCompleteDeletionAuthority,
+          DeletionType.CompleteDeletion,
+          false,
+        ],
         [
         [
           currentPageRecursiveCompleteDeletionAuthority,
           currentPageRecursiveCompleteDeletionAuthority,
           adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
           adminGeneralSecurityContainer.changePageRecursiveCompleteDeletionAuthority,
           DeletionType.RecursiveCompleteDeletion,
           DeletionType.RecursiveCompleteDeletion,
           isButtonDisabledForCompleteDeletion,
           isButtonDisabledForCompleteDeletion,
         ],
         ],
-      ].map(arr => renderPageDeletePermission(
-        arr[0] as IPageDeleteConfigValue,
-        arr[1] as (value: IPageDeleteConfigValue) => void,
-        arr[2] as DeletionTypeValue,
-        arr[3] as boolean,
-      ))}
+      ].map((arr) =>
+        renderPageDeletePermission(
+          arr[0] as IPageDeleteConfigValue,
+          arr[1] as (value: IPageDeleteConfigValue) => void,
+          arr[2] as DeletionTypeValue,
+          arr[3] as boolean,
+        ),
+      )}
     </>
     </>
   );
   );
 };
 };

+ 39 - 16
apps/app/src/client/components/Admin/Security/SecuritySetting/PageListDisplaySettings.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import type React from 'react';
 
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 
@@ -7,7 +7,10 @@ type Props = {
   t: (key: string) => string;
   t: (key: string) => string;
 };
 };
 
 
-export const PageListDisplaySettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const PageListDisplaySettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   return (
   return (
     <>
     <>
       <h4 className="alert-anchor">
       <h4 className="alert-anchor">
@@ -47,24 +50,34 @@ export const PageListDisplaySettings: React.FC<Props> = ({ adminGeneralSecurityC
                     aria-expanded="true"
                     aria-expanded="true"
                   >
                   >
                     <span className="float-start">
                     <span className="float-start">
-                      {adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Displayed'
-                        && t('security_settings.always_displayed')}
-                      {adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Hidden'
-                        && t('security_settings.always_hidden')}
+                      {adminGeneralSecurityContainer.state
+                        .currentOwnerRestrictionDisplayMode === 'Displayed' &&
+                        t('security_settings.always_displayed')}
+                      {adminGeneralSecurityContainer.state
+                        .currentOwnerRestrictionDisplayMode === 'Hidden' &&
+                        t('security_settings.always_hidden')}
                     </span>
                     </span>
                   </button>
                   </button>
-                  <div className="dropdown-menu" aria-labelledby="isShowRestrictedByOwner">
+                  <div className="dropdown-menu">
                     <button
                     <button
                       className="dropdown-item"
                       className="dropdown-item"
                       type="button"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode('Displayed') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode(
+                          'Displayed',
+                        );
+                      }}
                     >
                     >
                       {t('security_settings.always_displayed')}
                       {t('security_settings.always_displayed')}
                     </button>
                     </button>
                     <button
                     <button
                       className="dropdown-item"
                       className="dropdown-item"
                       type="button"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode('Hidden') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode(
+                          'Hidden',
+                        );
+                      }}
                     >
                     >
                       {t('security_settings.always_hidden')}
                       {t('security_settings.always_hidden')}
                     </button>
                     </button>
@@ -84,24 +97,34 @@ export const PageListDisplaySettings: React.FC<Props> = ({ adminGeneralSecurityC
                     aria-expanded="true"
                     aria-expanded="true"
                   >
                   >
                     <span className="float-start">
                     <span className="float-start">
-                      {adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Displayed'
-                        && t('security_settings.always_displayed')}
-                      {adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Hidden'
-                        && t('security_settings.always_hidden')}
+                      {adminGeneralSecurityContainer.state
+                        .currentGroupRestrictionDisplayMode === 'Displayed' &&
+                        t('security_settings.always_displayed')}
+                      {adminGeneralSecurityContainer.state
+                        .currentGroupRestrictionDisplayMode === 'Hidden' &&
+                        t('security_settings.always_hidden')}
                     </span>
                     </span>
                   </button>
                   </button>
-                  <div className="dropdown-menu" aria-labelledby="isShowRestrictedByGroup">
+                  <div className="dropdown-menu">
                     <button
                     <button
                       className="dropdown-item"
                       className="dropdown-item"
                       type="button"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode('Displayed') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode(
+                          'Displayed',
+                        );
+                      }}
                     >
                     >
                       {t('security_settings.always_displayed')}
                       {t('security_settings.always_displayed')}
                     </button>
                     </button>
                     <button
                     <button
                       className="dropdown-item"
                       className="dropdown-item"
                       type="button"
                       type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode('Hidden') }}
+                      onClick={() => {
+                        adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode(
+                          'Hidden',
+                        );
+                      }}
                     >
                     >
                       {t('security_settings.always_hidden')}
                       {t('security_settings.always_hidden')}
                     </button>
                     </button>

+ 15 - 5
apps/app/src/client/components/Admin/Security/SecuritySetting/SessionMaxAgeSettings.tsx

@@ -1,5 +1,4 @@
-import React from 'react';
-
+import type React from 'react';
 import type { UseFormRegister } from 'react-hook-form';
 import type { UseFormRegister } from 'react-hook-form';
 
 
 type Props = {
 type Props = {
@@ -12,21 +11,32 @@ export const SessionMaxAgeSettings: React.FC<Props> = ({ register, t }) => {
     <>
     <>
       <h4>{t('security_settings.session')}</h4>
       <h4>{t('security_settings.session')}</h4>
       <div className="row">
       <div className="row">
-        <label className="text-start text-md-end col-md-3 col-form-label">
+        <label
+          className="text-start text-md-end col-md-3 col-form-label"
+          htmlFor="sessionMaxAge"
+        >
           {t('security_settings.max_age')}
           {t('security_settings.max_age')}
         </label>
         </label>
         <div className="col-md-8">
         <div className="col-md-8">
           <input
           <input
+            id="sessionMaxAge"
             className="form-control col-md-4"
             className="form-control col-md-4"
             type="text"
             type="text"
             {...register('sessionMaxAge')}
             {...register('sessionMaxAge')}
             placeholder="2592000000"
             placeholder="2592000000"
           />
           />
           {/* eslint-disable-next-line react/no-danger */}
           {/* eslint-disable-next-line react/no-danger */}
-          <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
+          <p
+            className="form-text text-muted"
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+            dangerouslySetInnerHTML={{
+              __html: t('security_settings.max_age_desc'),
+            }}
+          />
           <p className="card custom-card bg-warning-subtle">
           <p className="card custom-card bg-warning-subtle">
             <span className="text-warning">
             <span className="text-warning">
-              <span className="material-symbols-outlined">info</span> {t('security_settings.max_age_caution')}
+              <span className="material-symbols-outlined">info</span>{' '}
+              {t('security_settings.max_age_caution')}
             </span>
             </span>
           </p>
           </p>
         </div>
         </div>

+ 44 - 13
apps/app/src/client/components/Admin/Security/SecuritySetting/UserHomepageDeletionSettings.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React from 'react';
+import type React from 'react';
 
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 
@@ -8,10 +8,15 @@ type Props = {
   t: (key: string) => string;
   t: (key: string) => string;
 };
 };
 
 
-export const UserHomepageDeletionSettings: React.FC<Props> = ({ adminGeneralSecurityContainer, t }) => {
+export const UserHomepageDeletionSettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
   return (
   return (
     <>
     <>
-      <h4 className="mb-3">{t('security_settings.user_homepage_deletion.user_homepage_deletion')}</h4>
+      <h4 className="mb-3">
+        {t('security_settings.user_homepage_deletion.user_homepage_deletion')}
+      </h4>
       <div className="row mb-4">
       <div className="row mb-4">
         <div className="col-md-10 offset-md-2">
         <div className="col-md-10 offset-md-2">
           <div className="form-check form-switch form-check-success">
           <div className="form-check form-switch form-check-success">
@@ -19,11 +24,21 @@ export const UserHomepageDeletionSettings: React.FC<Props> = ({ adminGeneralSecu
               type="checkbox"
               type="checkbox"
               className="form-check-input"
               className="form-check-input"
               id="is-user-page-deletion-enabled"
               id="is-user-page-deletion-enabled"
-              checked={adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled}
-              onChange={() => { adminGeneralSecurityContainer.switchIsUsersHomepageDeletionEnabled() }}
+              checked={
+                adminGeneralSecurityContainer.state
+                  .isUsersHomepageDeletionEnabled
+              }
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsUsersHomepageDeletionEnabled();
+              }}
             />
             />
-            <label className="form-label form-check-label" htmlFor="is-user-page-deletion-enabled">
-              {t('security_settings.user_homepage_deletion.enable_user_homepage_deletion')}
+            <label
+              className="form-label form-check-label"
+              htmlFor="is-user-page-deletion-enabled"
+            >
+              {t(
+                'security_settings.user_homepage_deletion.enable_user_homepage_deletion',
+              )}
             </label>
             </label>
           </div>
           </div>
           <div className="custom-control custom-switch custom-checkbox-success mt-2">
           <div className="custom-control custom-switch custom-checkbox-success mt-2">
@@ -31,17 +46,33 @@ export const UserHomepageDeletionSettings: React.FC<Props> = ({ adminGeneralSecu
               type="checkbox"
               type="checkbox"
               className="form-check-input"
               className="form-check-input"
               id="is-force-delete-user-homepage-on-user-deletion"
               id="is-force-delete-user-homepage-on-user-deletion"
-              checked={adminGeneralSecurityContainer.state.isForceDeleteUserHomepageOnUserDeletion}
-              onChange={() => { adminGeneralSecurityContainer.switchIsForceDeleteUserHomepageOnUserDeletion() }}
-              disabled={!adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled}
+              checked={
+                adminGeneralSecurityContainer.state
+                  .isForceDeleteUserHomepageOnUserDeletion
+              }
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsForceDeleteUserHomepageOnUserDeletion();
+              }}
+              disabled={
+                !adminGeneralSecurityContainer.state
+                  .isUsersHomepageDeletionEnabled
+              }
             />
             />
-            <label className="form-check-label" htmlFor="is-force-delete-user-homepage-on-user-deletion">
-              {t('security_settings.user_homepage_deletion.enable_force_delete_user_homepage_on_user_deletion')}
+            <label
+              className="form-check-label"
+              htmlFor="is-force-delete-user-homepage-on-user-deletion"
+            >
+              {t(
+                'security_settings.user_homepage_deletion.enable_force_delete_user_homepage_on_user_deletion',
+              )}
             </label>
             </label>
           </div>
           </div>
           <p
           <p
             className="form-text text-muted small mt-2"
             className="form-text text-muted small mt-2"
-            dangerouslySetInnerHTML={{ __html: t('security_settings.user_homepage_deletion.desc') }}
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+            dangerouslySetInnerHTML={{
+              __html: t('security_settings.user_homepage_deletion.desc'),
+            }}
           />
           />
         </div>
         </div>
       </div>
       </div>

+ 81 - 37
apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx

@@ -1,13 +1,12 @@
-import React, { useCallback, useEffect } from 'react';
-
+import type React from 'react';
+import { useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../../UnstatedUtils';
 import { withUnstatedContainers } from '../../../UnstatedUtils';
-
 import { CommentManageRightsSettings } from './CommentManageRightsSettings';
 import { CommentManageRightsSettings } from './CommentManageRightsSettings';
 import { PageAccessRightsSettings } from './PageAccessRightsSettings';
 import { PageAccessRightsSettings } from './PageAccessRightsSettings';
 import { PageDeleteRightsSettings } from './PageDeleteRightsSettings';
 import { PageDeleteRightsSettings } from './PageDeleteRightsSettings';
@@ -23,7 +22,9 @@ type Props = {
   adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
   adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
 };
 };
 
 
-const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContainer }) => {
+const SecuritySettingComponent: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+}) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { register, handleSubmit, reset } = useForm<FormData>();
   const { register, handleSubmit, reset } = useForm<FormData>();
 
 
@@ -34,35 +35,56 @@ const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContain
     });
     });
   }, [reset, adminGeneralSecurityContainer.state.sessionMaxAge]);
   }, [reset, adminGeneralSecurityContainer.state.sessionMaxAge]);
 
 
-  const onSubmit = useCallback(async(data: FormData) => {
-    try {
-      // Save all security settings with form data
-      await adminGeneralSecurityContainer.updateGeneralSecuritySetting({
-        sessionMaxAge: data.sessionMaxAge,
-        restrictGuestMode: adminGeneralSecurityContainer.state.currentRestrictGuestMode,
-        pageDeletionAuthority: adminGeneralSecurityContainer.state.currentPageDeletionAuthority,
-        pageCompleteDeletionAuthority: adminGeneralSecurityContainer.state.currentPageCompleteDeletionAuthority,
-        pageRecursiveDeletionAuthority: adminGeneralSecurityContainer.state.currentPageRecursiveDeletionAuthority,
-        pageRecursiveCompleteDeletionAuthority: adminGeneralSecurityContainer.state.currentPageRecursiveCompleteDeletionAuthority,
-        isAllGroupMembershipRequiredForPageCompleteDeletion: adminGeneralSecurityContainer.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
-        hideRestrictedByGroup: adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Hidden',
-        hideRestrictedByOwner: adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Hidden',
-        isUsersHomepageDeletionEnabled: adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
-        isForceDeleteUserHomepageOnUserDeletion: adminGeneralSecurityContainer.state.isForceDeleteUserHomepageOnUserDeletion,
-        isRomUserAllowedToComment: adminGeneralSecurityContainer.state.isRomUserAllowedToComment,
-      });
-      toastSuccess(t('security_settings.updated_general_security_setting'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminGeneralSecurityContainer, t]);
+  const onSubmit = useCallback(
+    async (data: FormData) => {
+      try {
+        // Save all security settings with form data
+        await adminGeneralSecurityContainer.updateGeneralSecuritySetting({
+          sessionMaxAge: data.sessionMaxAge,
+          restrictGuestMode:
+            adminGeneralSecurityContainer.state.currentRestrictGuestMode,
+          pageDeletionAuthority:
+            adminGeneralSecurityContainer.state.currentPageDeletionAuthority,
+          pageCompleteDeletionAuthority:
+            adminGeneralSecurityContainer.state
+              .currentPageCompleteDeletionAuthority,
+          pageRecursiveDeletionAuthority:
+            adminGeneralSecurityContainer.state
+              .currentPageRecursiveDeletionAuthority,
+          pageRecursiveCompleteDeletionAuthority:
+            adminGeneralSecurityContainer.state
+              .currentPageRecursiveCompleteDeletionAuthority,
+          isAllGroupMembershipRequiredForPageCompleteDeletion:
+            adminGeneralSecurityContainer.state
+              .isAllGroupMembershipRequiredForPageCompleteDeletion,
+          hideRestrictedByGroup:
+            adminGeneralSecurityContainer.state
+              .currentGroupRestrictionDisplayMode === 'Hidden',
+          hideRestrictedByOwner:
+            adminGeneralSecurityContainer.state
+              .currentOwnerRestrictionDisplayMode === 'Hidden',
+          isUsersHomepageDeletionEnabled:
+            adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
+          isForceDeleteUserHomepageOnUserDeletion:
+            adminGeneralSecurityContainer.state
+              .isForceDeleteUserHomepageOnUserDeletion,
+          isRomUserAllowedToComment:
+            adminGeneralSecurityContainer.state.isRomUserAllowedToComment,
+        });
+        toastSuccess(t('security_settings.updated_general_security_setting'));
+      } catch (err) {
+        toastError(err);
+      }
+    },
+    [adminGeneralSecurityContainer, t],
+  );
 
 
   if (adminGeneralSecurityContainer.state.retrieveError != null) {
   if (adminGeneralSecurityContainer.state.retrieveError != null) {
     return (
     return (
       <div>
       <div>
         <p>
         <p>
-          {t('Error occurred')} : {adminGeneralSecurityContainer.state.retrieveError}
+          {t('Error occurred')} :{' '}
+          {adminGeneralSecurityContainer.state.retrieveError}
         </p>
         </p>
       </div>
       </div>
     );
     );
@@ -70,22 +92,41 @@ const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContain
 
 
   return (
   return (
     <div data-testid="admin-security-setting">
     <div data-testid="admin-security-setting">
-      <h2 className="border-bottom mb-5">{t('security_settings.security_settings')}</h2>
+      <h2 className="border-bottom mb-5">
+        {t('security_settings.security_settings')}
+      </h2>
 
 
       <form onSubmit={handleSubmit(onSubmit)}>
       <form onSubmit={handleSubmit(onSubmit)}>
         <div className="vstack gap-3">
         <div className="vstack gap-3">
-          <PageListDisplaySettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <PageAccessRightsSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <PageDeleteRightsSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <UserHomepageDeletionSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
-          <CommentManageRightsSettings adminGeneralSecurityContainer={adminGeneralSecurityContainer} t={t} />
+          <PageListDisplaySettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <PageAccessRightsSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <PageDeleteRightsSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <UserHomepageDeletionSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
+          <CommentManageRightsSettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
           <SessionMaxAgeSettings register={register} t={t} />
           <SessionMaxAgeSettings register={register} t={t} />
 
 
           <div className="text-center text-md-start offset-md-3 col-md-5">
           <div className="text-center text-md-start offset-md-3 col-md-5">
             <button
             <button
               type="submit"
               type="submit"
               className="btn btn-primary"
               className="btn btn-primary"
-              disabled={adminGeneralSecurityContainer.state.retrieveError != null}
+              disabled={
+                adminGeneralSecurityContainer.state.retrieveError != null
+              }
             >
             >
               {t('Update')}
               {t('Update')}
             </button>
             </button>
@@ -96,4 +137,7 @@ const SecuritySettingComponent: React.FC<Props> = ({ adminGeneralSecurityContain
   );
   );
 };
 };
 
 
-export const SecuritySetting = withUnstatedContainers(SecuritySettingComponent, [AdminGeneralSecurityContainer]);
+export const SecuritySetting = withUnstatedContainers(
+  SecuritySettingComponent,
+  [AdminGeneralSecurityContainer],
+);

Некоторые файлы не были показаны из-за большого количества измененных файлов