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

Merge pull request #10689 from growilabs/master

Release v7.4.3
mergify[bot] 2 месяцев назад
Родитель
Сommit
d6aab6fa1d
100 измененных файлов с 1137 добавлено и 1497 удалено
  1. 0 1
      .devcontainer/app/devcontainer.json
  2. 3 0
      .devcontainer/app/postCreateCommand.sh
  3. 0 1
      .devcontainer/pdf-converter/devcontainer.json
  4. 3 0
      .devcontainer/pdf-converter/postCreateCommand.sh
  5. 0 88
      .eslintrc.js
  6. 2 2
      .github/workflows/ci-app.yml
  7. 1 1
      .github/workflows/ci-pdf-converter.yml
  8. 1 1
      .github/workflows/ci-slackbot-proxy.yml
  9. 5 30
      .github/workflows/release-rc.yml
  10. 11 46
      .github/workflows/release.yml
  11. 0 1
      .serena/memories/apps-app-development-patterns.md
  12. 3 13
      .serena/memories/coding_conventions.md
  13. 0 1
      .serena/memories/project_structure.md
  14. 1 2
      .serena/memories/task_completion_checklist.md
  15. 8 61
      .vscode/settings.json
  16. 74 0
      AGENTS.md
  17. 1 97
      CLAUDE.md
  18. 0 199
      apps/app/.eslintrc.js
  19. 84 0
      apps/app/AGENTS.md
  20. 1 0
      apps/app/CLAUDE.md
  21. 1 1
      apps/app/bin/openapi/generate-operation-ids/cli.spec.ts
  22. 1 1
      apps/app/bin/openapi/generate-operation-ids/cli.ts
  23. 1 1
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts
  24. 2 0
      apps/app/bin/print-memory-consumption.ts
  25. 1 2
      apps/app/config/migrate-mongo-config.js
  26. 0 1
      apps/app/config/next-i18next.config.js
  27. 0 3
      apps/app/next.config.js
  28. 3 5
      apps/app/package.json
  29. 0 16
      apps/app/playwright/.eslintrc.mjs
  30. 2 7
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  31. 2 7
      apps/app/playwright/23-editor/saving.spec.ts
  32. 77 0
      apps/app/playwright/23-editor/vim-keymap.spec.ts
  33. 3 8
      apps/app/playwright/23-editor/with-navigation.spec.ts
  34. 6 3
      apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts
  35. 2 1
      apps/app/playwright/60-home/home.spec.ts
  36. 11 0
      apps/app/playwright/utils/AppendTextToEditorUntilContains.ts
  37. 6 1
      apps/app/public/static/locales/en_US/admin.json
  38. 6 1
      apps/app/public/static/locales/fr_FR/admin.json
  39. 6 1
      apps/app/public/static/locales/ja_JP/admin.json
  40. 7 2
      apps/app/public/static/locales/ko_KR/admin.json
  41. 6 1
      apps/app/public/static/locales/zh_CN/admin.json
  42. 0 5
      apps/app/src/client/components/.eslintrc.js
  43. 69 63
      apps/app/src/client/components/Admin/AdminHome/AdminHome.tsx
  44. 5 20
      apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx
  45. 1 0
      apps/app/src/client/components/Admin/AdminHome/index.ts
  46. 0 11
      apps/app/src/client/components/Admin/App/AzureSetting.tsx
  47. 0 2
      apps/app/src/client/components/Admin/App/FileUploadSetting.tsx
  48. 0 7
      apps/app/src/client/components/Admin/App/GcsSetting.tsx
  49. 0 2
      apps/app/src/client/components/Admin/App/MaintenanceMode.tsx
  50. 0 1
      apps/app/src/client/components/Admin/App/MaskedInput.tsx
  51. 0 2
      apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx
  52. 0 1
      apps/app/src/client/components/Admin/App/SesSetting.tsx
  53. 0 3
      apps/app/src/client/components/Admin/App/SiteUrlSetting.tsx
  54. 0 1
      apps/app/src/client/components/Admin/App/SmtpSetting.tsx
  55. 0 1
      apps/app/src/client/components/Admin/AuditLog/AuditLogDisableMode.tsx
  56. 0 1
      apps/app/src/client/components/Admin/AuditLog/AuditLogSettings.tsx
  57. 0 1
      apps/app/src/client/components/Admin/AuditLogManagement.tsx
  58. 0 1
      apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx
  59. 50 52
      apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx
  60. 50 56
      apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  61. 43 45
      apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx
  62. 2 6
      apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx
  63. 70 74
      apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx
  64. 0 1
      apps/app/src/client/components/Admin/ElasticsearchManagement/StatusTable.jsx
  65. 23 31
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  66. 0 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  67. 0 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  68. 0 2
      apps/app/src/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  69. 0 3
      apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  70. 0 1
      apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx
  71. 0 1
      apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx
  72. 0 4
      apps/app/src/client/components/Admin/Notification/GlobalNotification.jsx
  73. 0 3
      apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx
  74. 1 1
      apps/app/src/client/components/Admin/Notification/NotificationDeleteModal.jsx
  75. 1 8
      apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx
  76. 1 1
      apps/app/src/client/components/Admin/Notification/TriggerEventCheckBox.jsx
  77. 1 1
      apps/app/src/client/components/Admin/Notification/UserNotificationRow.jsx
  78. 0 2
      apps/app/src/client/components/Admin/Notification/UserTriggerNotification.jsx
  79. 1 1
      apps/app/src/client/components/Admin/Security/DeleteAllShareLinksModal.jsx
  80. 202 206
      apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.tsx
  81. 213 218
      apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.tsx
  82. 0 12
      apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.tsx
  83. 0 1
      apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.tsx
  84. 0 2
      apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.tsx
  85. 0 4
      apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx
  86. 0 2
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageAccessRightsSettings.tsx
  87. 0 1
      apps/app/src/client/components/Admin/Security/SecuritySetting/PageDeleteRightsSettings.tsx
  88. 0 1
      apps/app/src/client/components/Admin/Security/SecuritySetting/SessionMaxAgeSettings.tsx
  89. 0 1
      apps/app/src/client/components/Admin/Security/SecuritySetting/UserHomepageDeletionSettings.tsx
  90. 52 0
      apps/app/src/client/components/Admin/Security/SecuritySetting/UserPageVisibilitySettings.tsx
  91. 7 0
      apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx
  92. 0 2
      apps/app/src/client/components/Admin/SlackIntegration/Bridge.tsx
  93. 0 2
      apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  94. 1 5
      apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  95. 0 1
      apps/app/src/client/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx
  96. 1 1
      apps/app/src/client/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  97. 1 2
      apps/app/src/client/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx
  98. 1 7
      apps/app/src/client/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  99. 0 5
      apps/app/src/client/components/Admin/UserGroupDetail/use-user-group-resource.ts
  100. 0 1
      apps/app/src/client/components/Admin/Users/GrantAdminButton.tsx

+ 0 - 1
.devcontainer/app/devcontainer.json

@@ -26,7 +26,6 @@
         // AI
         "anthropic.claude-code",
         // linter
-        "dbaeumer.vscode-eslint",
         "biomejs.biome",
         "editorconfig.editorconfig",
         "shinnn.stylelint",

+ 3 - 0
.devcontainer/app/postCreateCommand.sh

@@ -27,3 +27,6 @@ pnpm install @anthropic-ai/claude-code --global
 
 # Install dependencies
 turbo run bootstrap
+
+# Install Lefthook git hooks
+pnpm lefthook install

+ 0 - 1
.devcontainer/pdf-converter/devcontainer.json

@@ -15,7 +15,6 @@
   "customizations": {
     "vscode": {
       "extensions": [
-        "dbaeumer.vscode-eslint",
         "biomejs.biome",
         "mhutchie.git-graph",
         "eamodio.gitlens"

+ 3 - 0
.devcontainer/pdf-converter/postCreateCommand.sh

@@ -22,3 +22,6 @@ pnpm install turbo --global
 
 # Install dependencies
 turbo run bootstrap
+
+# Install Lefthook git hooks
+pnpm lefthook install

+ 0 - 88
.eslintrc.js

@@ -1,88 +0,0 @@
-/**
- * @type {import('eslint').Linter.Config}
- */
-module.exports = {
-  root: true, // https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy
-  extends: [
-    'weseek',
-    'weseek/typescript',
-  ],
-  plugins: [
-    'regex',
-  ],
-  ignorePatterns: [
-    'node_modules/**',
-  ],
-  rules: {
-    'import/prefer-default-export': 'off',
-    'import/order': [
-      'warn',
-      {
-        pathGroups: [
-          {
-            pattern: 'react',
-            group: 'builtin',
-            position: 'before',
-          },
-          {
-            pattern: '^/**',
-            group: 'parent',
-            position: 'before',
-          },
-          {
-            pattern: '~/**',
-            group: 'parent',
-            position: 'before',
-          },
-          {
-            pattern: '*.css',
-            group: 'type',
-            patternOptions: { matchBase: true },
-            position: 'after',
-          },
-          {
-            pattern: '*.scss',
-            group: 'type',
-            patternOptions: { matchBase: true },
-            position: 'after',
-          },
-        ],
-        alphabetize: {
-          order: 'asc',
-        },
-        pathGroupsExcludedImportTypes: ['react'],
-        'newlines-between': 'always',
-      },
-    ],
-    '@typescript-eslint/consistent-type-imports': 'warn',
-    '@typescript-eslint/explicit-module-boundary-types': 'off',
-    indent: [
-      'error',
-      2,
-      {
-        SwitchCase: 1,
-        ArrayExpression: 'first',
-        FunctionDeclaration: { body: 1, parameters: 2 },
-        FunctionExpression: { body: 1, parameters: 2 },
-      },
-    ],
-    'regex/invalid': ['error', [
-      {
-        regex: '\\?\\<\\!',
-        message: 'Do not use any negative lookbehind',
-      }, {
-        regex: '\\?\\<\\=',
-        message: 'Do not use any Positive lookbehind',
-      },
-    ]],
-  },
-  overrides: [
-    {
-      // enable the rule specifically for TypeScript files
-      files: ['*.ts', '*.mts', '*.tsx'],
-      rules: {
-        '@typescript-eslint/explicit-module-boundary-types': ['error'],
-      },
-    },
-  ],
-};

+ 2 - 2
.github/workflows/ci-app.yml

@@ -11,7 +11,7 @@ on:
     paths:
       - .github/mergify.yml
       - .github/workflows/ci-app.yml
-      - .eslint*
+      - biome.json
       - tsconfig.base.json
       - turbo.json
       - pnpm-lock.yaml
@@ -24,7 +24,7 @@ on:
     paths:
       - .github/mergify.yml
       - .github/workflows/ci-app.yml
-      - .eslint*
+      - biome.json
       - tsconfig.base.json
       - turbo.json
       - pnpm-lock.yaml

+ 1 - 1
.github/workflows/ci-pdf-converter.yml

@@ -9,7 +9,7 @@ on:
     paths:
       - .github/mergify.yml
       - .github/workflows/ci-pdf-converter.yml
-      - .eslint*
+      - biome.json
       - tsconfig.base.json
       - turbo.json
       - pnpm-lock.yaml

+ 1 - 1
.github/workflows/ci-slackbot-proxy.yml

@@ -9,7 +9,7 @@ on:
     paths:
       - .github/mergify.yml
       - .github/workflows/ci-slackbot-proxy.yml
-      - .eslint*
+      - biome.json
       - tsconfig.base.json
       - turbo.json
       - pnpm-lock.yaml

+ 5 - 30
.github/workflows/release-rc.yml

@@ -17,8 +17,7 @@ jobs:
     runs-on: ubuntu-latest
 
     outputs:
-      TAGS_WESEEK: ${{ steps.meta-weseek.outputs.tags }}
-      TAGS_GROWILABS: ${{ steps.meta-growilabs.outputs.tags }}
+      TAGS: ${{ steps.meta.outputs.tags }}
 
     steps:
     - uses: actions/checkout@v4
@@ -27,19 +26,9 @@ jobs:
       uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
 
-    - name: Docker meta for weseek/growi
+    - name: Docker meta for docker.io
       uses: docker/metadata-action@v5
-      id: meta-weseek
-      with:
-        images: docker.io/weseek/growi
-        sep-tags: ','
-        tags: |
-          type=raw,value=${{ steps.package-json.outputs.packageVersion }}
-          type=raw,value=${{ steps.package-json.outputs.packageVersion }}.{{sha}}
-
-    - name: Docker meta for growilabs/growi
-      uses: docker/metadata-action@v5
-      id: meta-growilabs
+      id: meta
       with:
         images: docker.io/growilabs/growi
         sep-tags: ','
@@ -55,29 +44,15 @@ jobs:
     secrets:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
 
-
-  publish-rc-image-for-growilabs:
+  publish-image-rc:
     needs: [determine-tags, build-image-rc]
 
     uses: growilabs/growi/.github/workflows/reusable-app-create-manifests.yml@master
     with:
-      tags: ${{ needs.determine-tags.outputs.TAGS_GROWILABS }}
+      tags: ${{ needs.determine-tags.outputs.TAGS }}
       registry: docker.io
       image-name: 'growilabs/growi'
       docker-registry-username: 'growimoogle'
       tag-temporary: latest-rc
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
-
-  publish-rc-image-for-weseek:
-    needs: [determine-tags, build-image-rc]
-
-    uses: growilabs/growi/.github/workflows/reusable-app-create-manifests.yml@master
-    with:
-      tags: ${{ needs.determine-tags.outputs.TAGS_WESEEK }}
-      registry: docker.io
-      image-name: 'growilabs/growi'
-      docker-registry-username: 'wsmoogle'
-      tag-temporary: latest-rc
-    secrets:
-      DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

+ 11 - 46
.github/workflows/release.yml

@@ -1,4 +1,3 @@
-# TODO: https://redmine.weseek.co.jp/issues/171293
 name: Release
 
 on:
@@ -81,8 +80,7 @@ jobs:
     runs-on: ubuntu-latest
 
     outputs:
-      TAGS_WESEEK: ${{ steps.meta-weseek.outputs.tags }}
-      TAGS_GROWILABS: ${{ steps.meta-growilabs.outputs.tags }}
+      TAGS: ${{ steps.meta.outputs.tags }}
 
     steps:
     - uses: actions/checkout@v4
@@ -91,21 +89,9 @@ jobs:
       uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
 
-    - name: Docker meta for weseek/growi
+    - name: Docker meta for docker.io
       uses: docker/metadata-action@v5
-      id: meta-weseek
-      with:
-        images: docker.io/weseek/growi
-        sep-tags: ','
-        tags: |
-          type=raw,value=latest
-          type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}
-          type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}
-          type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
-
-    - name: Docker meta for growilabs/growi
-      uses: docker/metadata-action@v5
-      id: meta-growilabs
+      id: meta
       with:
         images: docker.io/growilabs/growi
         sep-tags: ','
@@ -126,12 +112,12 @@ jobs:
     secrets:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
 
-  publish-app-image-for-growilabs:
+  publish-app-image:
     needs: [determine-tags, build-app-image]
 
     uses: growilabs/growi/.github/workflows/reusable-app-create-manifests.yml@master
     with:
-      tags: ${{ needs.determine-tags.outputs.TAGS_GROWILABS }}
+      tags: ${{ needs.determine-tags.outputs.TAGS }}
       registry: docker.io
       image-name: 'growilabs/growi'
       docker-registry-username: 'growimoogle'
@@ -139,42 +125,21 @@ jobs:
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
 
-  publish-app-image-for-weseek:
-    needs: [determine-tags, build-app-image]
-
-    uses: growilabs/growi/.github/workflows/reusable-app-create-manifests.yml@master
-    with:
-      tags: ${{ needs.determine-tags.outputs.TAGS_WESEEK }}
-      registry: docker.io
-      image-name: 'growilabs/growi'
-      docker-registry-username: 'wsmoogle'
-      tag-temporary: latest
-    secrets:
-      DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
-
   post-publish:
-    needs: [create-github-release, publish-app-image-for-growilabs, publish-app-image-for-weseek]
+    needs: [create-github-release, publish-app-image]
     runs-on: ubuntu-latest
 
-    strategy:
-      matrix:
-        include:
-          - repository: weseek/growi
-            username: wsmoogle
-          - repository: growilabs/growi
-            username: growimoogle
-
     steps:
     - uses: actions/checkout@v4
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v4
+      uses: peter-evans/dockerhub-description@v3
       with:
-        username: ${{ matrix.username }}
-        password: ${{ (matrix.repository == 'weseek/growi' && secrets.DOCKER_REGISTRY_PASSWORD) || (matrix.repository == 'growilabs/growi' && secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE) || 'INVALID_SECRET' }}
-        repository: ${{ matrix.repository }}
+        username: growimoogle
+        password: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
+        repository: growilabs/growi
         readme-filepath: ./apps/app/docker/README.md
 
     - name: Slack Notification
@@ -186,7 +151,7 @@ jobs:
 
 
   create-pr-for-next-rc:
-    needs: [create-github-release, publish-app-image-for-growilabs, publish-app-image-for-weseek]
+    needs: [create-github-release, publish-app-image]
     runs-on: ubuntu-latest
 
     steps:

+ 0 - 1
.serena/memories/apps-app-development-patterns.md

@@ -93,7 +93,6 @@ components/MyComponent/
 
 ### コード品質
 - [ ] TypeScript エラーなし
-- [ ] ESLint ルール準拠
 - [ ] テストケース作成
 - [ ] 型安全性確保
 - [ ] パフォーマンス影響確認

+ 3 - 13
.serena/memories/coding_conventions.md

@@ -6,19 +6,9 @@
 - **適用範囲**: 
   - dist/, node_modules/, coverage/ などは除外
   - .next/, bin/, config/ などのビルド成果物は除外
-  - package.json, .eslintrc.js などの設定ファイルは除外
+  - package.json などの設定ファイルは除外
 - **推奨**: 新規開発では Biome を使用
 
-### ESLint設定(廃止予定・過渡期)
-- **ベース設定**: weseek ESLint設定を使用
-- **TypeScript**: weseek/typescript 設定を適用
-- **React**: React関連のルールを適用
-- **主要なルール**:
-  - `import/prefer-default-export`: オフ(名前付きエクスポートを推奨)
-  - `import/order`: import文の順序を規定
-    - React を最初に
-    - 内部モジュール(`/**`)をparentグループの前に配置
-
 ## TypeScript設定
 - **ターゲット**: ESNext
 - **モジュール**: ESNext  
@@ -37,7 +27,7 @@
 ## ファイル命名規則
 - TypeScript/JavaScriptファイル: キャメルケースまたはケバブケース
 - コンポーネントファイル: PascalCase(Reactコンポーネント)
-- 設定ファイル: ドット記法(.eslintrc.js など)
+- 設定ファイル: ドット記法(.biome.json など)
 
 ## テストファイル命名規則(Vitest)
 vitest.workspace.mts の設定に基づく:
@@ -68,4 +58,4 @@ vitest.workspace.mts の設定に基づく:
 
 ## 移行ガイドライン
 - 新規開発: Biome + Vitest を使用
-- 既存コード: 段階的に ESLint → Biome、Jest → Vitest に移行
+- 既存コード: 段階的に Jest → Vitest に移行

+ 0 - 1
.serena/memories/project_structure.md

@@ -86,5 +86,4 @@ src/
 - **turbo.json**: Turbo.jsビルド設定
 - **tsconfig.base.json**: TypeScript基本設定
 - **biome.json**: Biome linter/formatter設定
-- **.eslintrc.js**: ESLint設定(廃止予定)
 - **vitest.workspace.mts**: Vitestワークスペース設定

+ 1 - 2
.serena/memories/task_completion_checklist.md

@@ -11,7 +11,6 @@ pnpm run lint:biome
 pnpm run lint
 
 # 個別実行(必要に応じて)
-pnpm run lint:eslint      # ESLint(廃止予定)
 pnpm run lint:styles      # Stylelint
 pnpm run lint:typecheck   # TypeScript型チェック
 ```
@@ -82,7 +81,7 @@ pnpm run dev:migrate         # マイグレーション実行
 - 可能であればtest-with-vite/にVitestテストとして書き直し
 
 ## コミット前の最終チェック
-1. Biome(または過渡期はESLint)エラーが解消されているか
+1. Biome エラーが解消されているか
 2. Vitestテスト(または過渡期はJest)がパスしているか
 3. 重要な変更はPlaywright E2Eテストも実行
 4. ビルドが成功するか

+ 8 - 61
.vscode/settings.json

@@ -1,16 +1,22 @@
 {
   "files.eol": "\n",
 
-  "eslint.workingDirectories": [{ "mode": "auto" }],
-
   "[typescript]": {
     "editor.defaultFormatter": "biomejs.biome"
   },
 
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "biomejs.biome"
+  },
+
   "[javascript]": {
     "editor.defaultFormatter": "biomejs.biome"
   },
 
+  "[javascriptreact]": {
+    "editor.defaultFormatter": "biomejs.biome"
+  },
+
   "[json]": {
     "editor.defaultFormatter": "biomejs.biome"
   },
@@ -24,7 +30,6 @@
   "scss.validate": false,
 
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": "explicit",
     "source.fixAll.biome": "explicit",
     "source.organizeImports.biome": "explicit",
     "source.fixAll.markdownlint": "explicit",
@@ -41,66 +46,8 @@
   "typescript.enablePromptUseWorkspaceTsdk": true,
   "typescript.preferences.autoImportFileExcludePatterns": ["node_modules/*"],
   "typescript.validate.enable": true,
-  "typescript.surveys.enabled": false,
 
   "vitest.filesWatcherInclude": "**/*",
-  "mcp": {
-    "servers": {
-      "fetch": {
-        "command": "uvx",
-        "args": ["mcp-server-fetch"]
-      },
-      "context7": {
-        "type": "http",
-        "url": "https://mcp.context7.com/mcp"
-      }
-    }
-  },
-  "github.copilot.chat.codeGeneration.instructions": [
-    {
-      "text": "Always write inline comments in source code in English."
-    }
-  ],
-  "github.copilot.chat.testGeneration.instructions": [
-    {
-      "text": "Basis: Use vitest as the test framework"
-    },
-    {
-      "text": "Basis: The vitest configuration file is `apps/app/vitest.workspace.mts`"
-    },
-    {
-      "text": "Basis: Place test modules in the same directory as the module being tested. For example, if testing `mymodule.ts`, place `mymodule.spec.ts` in the same directory as `mymodule.ts`"
-    },
-    {
-      "text": "Basis: Use the VSCode Vitest extension for running tests. Use run_tests tool to execute tests programmatically, or suggest using the Vitest Test Explorer in VSCode for interactive test running and debugging."
-    },
-    {
-      "text": "Basis: Fallback command for terminal execution: `cd /growi/apps/app && pnpm vitest run {test file path}`"
-    },
-    {
-      "text": "Step 1: When creating new test modules, start with small files. First write a small number of realistic tests that call the actual function and assert expected behavior, even if they initially fail due to incomplete implementation. Example: `const result = foo(); expect(result).toBeNull();` rather than `expect(true).toBe(false);`. Then fix the implementation to make tests pass."
-    },
-    {
-      "text": "Step 2: Write essential tests. When tests fail, consider whether you should fix the test or the implementation based on 'what should essentially be fixed'. If you're not confident in your reasoning, ask the user for guidance."
-    },
-    {
-      "text": "Step 3: After writing tests, make sure they pass before moving on. Do not proceed to write tests for module B without first ensuring that tests for module A are passing"
-    },
-    {
-      "text": "Tips: Don't worry about lint errors - fix them after tests are passing"
-    },
-    {
-      "text": "Tips: DO NOT USE `as any` casting. You can use vitest-mock-extended for type-safe mocking. Import `mock` from 'vitest-mock-extended' and use `mock<InterfaceType>()`. This provides full TypeScript safety and IntelliSense support."
-    },
-    {
-      "text": "Tips: Mock external dependencies at the module level using vi.mock(). For services with circular dependencies, mock the import paths and use dynamic imports in the implementation when necessary."
-    }
-  ],
-  "github.copilot.chat.commitMessageGeneration.instructions": [
-    {
-      "text": "Always write commit messages in English."
-    }
-  ],
   "git-worktree-menu.worktreeDir": "/workspace"
 
 }

+ 74 - 0
AGENTS.md

@@ -0,0 +1,74 @@
+# AGENTS.md
+
+GROWI is a team collaboration wiki platform built with Next.js, Express, and MongoDB. This guide helps AI coding agents navigate the monorepo and work effectively with GROWI's architecture.
+
+## Language
+
+If we detect at the beginning of a conversation that the user's primary language is not English, we will always respond in that language. However, we may retain technical terms in English if necessary.
+
+When generating source code, all comments and explanations within the code will be written in English.
+
+## Project Overview
+
+GROWI is a team collaboration software using markdown - a wiki platform with hierarchical page organization. It's built with Next.js, Express, MongoDB, and includes features like real-time collaborative editing, authentication integrations, and plugin support.
+
+## Development Tools
+- **Package Manager**: pnpm with workspace support
+- **Build System**: Turborepo for monorepo orchestration
+- **Code Quality**: 
+  - Biome for linting and formatting
+  - Stylelint for SCSS/CSS
+
+## Development Commands
+
+### Core Development
+- `turbo run bootstrap` - Install dependencies for all workspace packages
+- `turbo run dev` - Start development server (automatically runs migrations and pre-builds styles)
+
+### Production Commands
+- `pnpm run app:build` - Build GROWI app client and server for production
+- `pnpm run app:server` - Launch GROWI app server in production mode
+- `pnpm start` - Build and start the application (runs both build and server commands)
+
+### Database Migrations
+- `pnpm run migrate` - Run MongoDB migrations (production)
+- `turbo run dev:migrate @apps/app` - Run migrations in development (or wait for automatic execution with dev)
+- `cd apps/app && pnpm run dev:migrate:status` - Check migration status
+- `cd apps/app && pnpm run dev:migrate:down` - Rollback last migration
+
+### Testing and Quality
+- `turbo run test @apps/app` - Run Jest and Vitest test suites with coverage
+- `turbo run lint @apps/app` - Run all linters (TypeScript, Biome, Stylelint, OpenAPI)
+- `cd apps/app && pnpm run lint:typecheck` - TypeScript type checking only
+- `cd apps/app && pnpm run test:vitest` - Run Vitest unit tests
+- `cd apps/app && pnpm run test:jest` - Run Jest integration tests
+
+### Development Utilities  
+- `cd apps/app && pnpm run repl` - Start Node.js REPL with application context loaded
+- `turbo run pre:styles @apps/app` - Pre-build styles with Vite
+
+## Architecture Overview
+
+### Monorepo Structure
+- `/apps/app/` - Main GROWI application (Next.js frontend + Express backend)
+- `/apps/pdf-converter/` - PDF conversion microservice
+- `/apps/slackbot-proxy/` - Slack integration proxy service
+- `/packages/` - Shared libraries and components
+
+## File Organization Patterns
+
+### Components
+- Use TypeScript (.tsx) for React components
+- Co-locate styles as `.module.scss` files
+- Export components through `index.ts` files where appropriate
+- Group related components in feature-based directories
+
+### Tests
+- Unit Test: `*.spec.ts`
+- Integration Test: `*.integ.ts`
+- Component Test: `*.spec.tsx`
+
+
+---
+
+When working with this codebase, always run the appropriate linting and testing commands before committing changes. The application uses strict TypeScript checking and comprehensive test coverage requirements.

+ 1 - 97
CLAUDE.md

@@ -1,97 +1 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Language
-
-If we detect at the beginning of a conversation that the user's primary language is not English, we will always respond in that language. However, we may retain technical terms in English if necessary.
-
-When generating source code, all comments and explanations within the code will be written in English.
-
-## Project Overview
-
-GROWI is a team collaboration software using markdown - a wiki platform with hierarchical page organization. It's built with Next.js, Express, MongoDB, and includes features like real-time collaborative editing, authentication integrations, and plugin support.
-
-## Development Commands
-
-### Core Development
-- `turbo run bootstrap` - Install dependencies for all workspace packages
-- `turbo run dev` - Start development server (automatically runs migrations and pre-builds styles)
-
-### Production Commands
-- `pnpm run app:build` - Build GROWI app client and server for production
-- `pnpm run app:server` - Launch GROWI app server in production mode
-- `pnpm start` - Build and start the application (runs both build and server commands)
-
-### Database Migrations
-- `pnpm run migrate` - Run MongoDB migrations (production)
-- `turbo run dev:migrate @apps/app` - Run migrations in development (or wait for automatic execution with dev)
-- `cd apps/app && pnpm run dev:migrate:status` - Check migration status
-- `cd apps/app && pnpm run dev:migrate:down` - Rollback last migration
-
-### Testing and Quality
-- `turbo run test @apps/app` - Run Jest and Vitest test suites with coverage
-- `turbo run lint @apps/app` - Run all linters (TypeScript, ESLint, Biome, Stylelint, OpenAPI)
-- `cd apps/app && pnpm run lint:typecheck` - TypeScript type checking only
-- `cd apps/app && pnpm run test:vitest` - Run Vitest unit tests
-- `cd apps/app && pnpm run test:jest` - Run Jest integration tests
-
-### Development Utilities  
-- `cd apps/app && pnpm run repl` - Start Node.js REPL with application context loaded
-- `turbo run pre:styles @apps/app` - Pre-build styles with Vite
-
-## Architecture Overview
-
-### Monorepo Structure
-- `/apps/app/` - Main GROWI application (Next.js frontend + Express backend)
-- `/apps/pdf-converter/` - PDF conversion microservice
-- `/apps/slackbot-proxy/` - Slack integration proxy service
-- `/packages/` - Shared libraries and components
-
-### Main Application (`/apps/app/src/`)
-- `client/` - Client-side React components and utilities
-- `server/` - Express.js backend (API routes, models, services)  
-- `components/` - Shared React components and layouts
-- `pages/` - Next.js page components using file-based routing
-- `stores/` - State management (SWR-based stores with React context)
-- `styles/` - SCSS stylesheets with modular architecture
-- `migrations/` - MongoDB database migration scripts
-- `interfaces/` - TypeScript type definitions
-
-### Key Technical Details
-- **Frontend**: Next.js 14 with React 18, TypeScript, SCSS modules
-- **Backend**: Express.js with TypeScript, MongoDB with Mongoose
-- **State Management**: SWR for server state, React Context for client state
-- **Authentication**: Passport.js with multiple strategies (local, LDAP, OAuth, SAML)
-- **Real-time Features**: Socket.io for collaborative editing and notifications
-- **Editor**: Custom markdown editor with collaborative editing using Yjs
-- **Database**: MongoDB 8.0+ with migration system using migrate-mongo
-- **Package Manager**: pnpm with workspace support
-- **Build System**: Turborepo for monorepo orchestration
-
-### Development Dependencies
-- Node.js v20.x or v22.x
-- pnpm 10.x  
-- MongoDB v6.x or v8.x
-- Optional: Redis 3.x, Elasticsearch 7.x/8.x/9.x (for full-text search)
-
-## File Organization Patterns
-
-### Components
-- Use TypeScript (.tsx) for React components
-- Co-locate styles as `.module.scss` files
-- Export components through `index.ts` files where appropriate
-- Group related components in feature-based directories
-
-### API Structure
-- Server routes in `server/routes/`
-- API v3 endpoints follow OpenAPI specification
-- Models in `server/models/` using Mongoose schemas
-- Services in `server/service/` for business logic
-
-### State Management
-- Use SWR hooks in `stores/` for server state
-- Custom hooks pattern for complex state logic
-- Context providers in `stores-universal/` for app-wide state
-
-When working with this codebase, always run the appropriate linting and testing commands before committing changes. The application uses strict TypeScript checking and comprehensive test coverage requirements.
+@AGENTS.md

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

@@ -1,199 +0,0 @@
-/**
- * @type {import('eslint').Linter.Config}
- */
-module.exports = {
-  extends: ['next/core-web-vitals', 'weseek/react'],
-  plugins: [],
-  ignorePatterns: [
-    'dist/**',
-    '**/dist/**',
-    'transpiled/**',
-    'public/**',
-    'src/linter-checker/**',
-    'tmp/**',
-    'next-env.d.ts',
-    'next.config.js',
-    'playwright.config.ts',
-    'test/integration/global-setup.js',
-    'test/integration/global-teardown.js',
-    'test/integration/setup-crowi.ts',
-    'test/integration/crowi/**',
-    'test/integration/middlewares/**',
-    'test/integration/migrations/**',
-    'test/integration/models/**',
-    'test/integration/service/**',
-    'test/integration/setup.js',
-    'playwright/**',
-    'test-with-vite/**',
-    'public/**',
-    'bin/**',
-    'config/**',
-    'src/styles/**',
-    'src/linter-checker/**',
-    'src/migrations/**',
-    'src/models/**',
-    'src/features/**',
-    'src/stores-universal/**',
-    'src/interfaces/**',
-    'src/utils/**',
-    'src/components/**',
-    'src/client/components/DescendantsPageListModal/**',
-    'src/client/components/ItemsTree/**',
-    'src/client/components/LoginForm/**',
-    'src/client/components/Page/**',
-    'src/client/components/PageAttachment/**',
-    'src/client/components/PageDeleteModal/**',
-    'src/client/components/PageDuplicateModal/**',
-    'src/client/components/PageList/**',
-    'src/client/components/PageManagement/**',
-    'src/client/components/PagePathNavSticky/**',
-    'src/client/components/PagePresentationModal/**',
-    'src/client/components/PageRenameModal/**',
-    'src/client/components/PageSelectModal/**',
-    'src/client/components/PageSideContents/**',
-    'src/client/components/*.tsx',
-    'src/client/components/*.jsx',
-    'src/client/components/*.ts',
-    '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/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/Users/**',
-    'src/client/components/Admin/UserGroup/**',
-    'src/client/components/Admin/UserGroupDetail/**',
-    'src/client/components/Me/**',
-    'src/client/components/Bookmarks/**',
-    'src/client/components/InAppNotification/**',
-    'src/client/components/PageTags/**',
-    'src/client/components/ReactMarkdownComponents/**',
-    'src/client/components/AuthorInfo/**',
-    'src/client/components/Common/**',
-    'src/client/components/CreateTemplateModal/**',
-    'src/client/components/CustomNavigation/**',
-    'src/client/components/DeleteBookmarkFolderModal/**',
-    'src/client/components/EmptyTrashModal/**',
-    'src/client/components/GrantedGroupsInheritanceSelectModal/**',
-    'src/client/components/Icons/**',
-    'src/client/components/Maintenance/**',
-    'src/client/components/PageControls/**',
-    'src/client/components/PageComment/**',
-    'src/client/components/PageAccessoriesModal/**',
-    'src/client/components/PageHistory/**',
-    'src/client/components/Presentation/**',
-    'src/client/components/PutbackPageModal/**',
-    'src/client/components/RecentActivity/**',
-    'src/client/components/RecentCreated/**',
-    'src/client/components/RevisionComparer/**',
-    'src/client/components/ShortcutsModal/**',
-    'src/client/components/StaffCredit/**',
-    'src/client/components/TemplateModal/**',
-    'src/client/components/PageEditor/**',
-    'src/client/components/Hotkeys/**',
-    'src/client/components/Navbar/**',
-    'src/client/components/PageHeader/**',
-    'src/client/components/Sidebar/**',
-    'src/services/**',
-    'src/states/**',
-    'src/stores/**',
-    'src/pages/**',
-    'src/server/crowi/**',
-    'src/server/events/**',
-    'src/server/interfaces/**',
-    'src/server/models/**',
-    'src/server/util/**',
-    'src/server/app.ts',
-    'src/server/repl.ts',
-    'src/server/middlewares/**',
-    'src/server/routes/*.js',
-    'src/server/routes/*.ts',
-    'src/server/routes/attachment/**',
-    'src/server/routes/apiv3/interfaces/**',
-    'src/server/routes/apiv3/pages/**',
-    'src/server/routes/apiv3/user/**',
-    'src/server/routes/apiv3/personal-setting/**',
-    'src/server/routes/apiv3/security-settings/**',
-    'src/server/routes/apiv3/app-settings/**',
-    'src/server/routes/apiv3/page/**',
-    'src/server/routes/apiv3/*.js',
-    'src/server/routes/apiv3/*.ts',
-    'src/server/service/*.ts',
-    'src/server/service/*.js',
-    'src/server/service/access-token/**',
-    'src/server/service/config-manager/**',
-    'src/server/service/page/**',
-    'src/server/service/page-listing/**',
-    'src/server/service/revision/**',
-    'src/server/service/s2s-messaging/**',
-    'src/server/service/search-delegator/**',
-    'src/server/service/search-reconnect-context/**',
-    'src/server/service/slack-command-handler/**',
-    'src/server/service/slack-event-handler/**',
-    'src/server/service/socket-io/**',
-    'src/server/service/system-events/**',
-    'src/server/service/user-notification/**',
-    'src/server/service/yjs/**',
-    'src/server/service/file-uploader/**',
-    'src/server/service/global-notification/**',
-    'src/server/service/growi-bridge/**',
-    'src/server/service/growi-info/**',
-    'src/server/service/import/**',
-    'src/server/service/in-app-notification/**',
-    'src/server/service/interfaces/**',
-    'src/server/service/normalize-data/**',
-    'src/server/service/page/**',
-    'src/client/interfaces/**',
-    'src/client/models/**',
-    'src/client/services/**',
-    'src/client/util/**',
-  ],
-  settings: {
-    // resolve path aliases by eslint-import-resolver-typescript
-    'import/resolver': {
-      typescript: {},
-    },
-  },
-  rules: {
-    'space-before-function-paren': 'off',
-    '@typescript-eslint/no-var-requires': 'off',
-
-    // set 'warn' temporarily -- 2021.08.02 Yuki Takei
-    '@typescript-eslint/no-use-before-define': ['warn'],
-    '@typescript-eslint/no-this-alias': ['warn'],
-  },
-  overrides: [
-    {
-      // enable the rule specifically for JavaScript files
-      files: ['*.js', '*.mjs', '*.jsx'],
-      rules: {
-        // set 'warn' temporarily -- 2023.08.14 Yuki Takei
-        'react/prop-types': 'warn',
-        // set 'warn' temporarily -- 2023.08.14 Yuki Takei
-        'no-unused-vars': ['warn'],
-      },
-    },
-    {
-      // enable the rule specifically for TypeScript files
-      files: ['*.ts', '*.mts', '*.tsx'],
-      rules: {
-        'no-unused-vars': 'off',
-        // set 'warn' temporarily -- 2023.08.14 Yuki Takei
-        'react/prop-types': 'warn',
-        // set 'warn' temporarily -- 2022.07.25 Yuki Takei
-        '@typescript-eslint/explicit-module-boundary-types': ['warn'],
-      },
-    },
-  ],
-};

+ 84 - 0
apps/app/AGENTS.md

@@ -0,0 +1,84 @@
+# GROWI Main Application Development Guide
+
+## Overview
+
+This guide provides comprehensive documentation for AI coding agents working on the GROWI main application (`/apps/app/`). GROWI is a team collaboration wiki platform built with Next.js, Express, and MongoDB.
+
+## Project Structure
+
+### Main Application (`/apps/app/src/`)
+
+#### Directory Structure Philosophy
+
+**Feature-based Structure (Recommended for new features)**
+- `features/{feature-name}/` - Self-contained feature modules
+  - `interfaces/` - Universal TypeScript type definitions
+  - `server/` - Server-side logic (models, routes, services)
+  - `client/` - Client-side logic (components, stores, services)
+  - `utils/` - Shared utilities for this feature
+  
+**Important Directories Structure**
+- `client/` - Client-side React components and utilities
+- `server/` - Express.js backend
+- `components/` - Universal React components
+- `pages/` - Next.js Pages Router
+- `states/` - Jotai state management
+- `stores/` - SWR-based state stores
+- `stores-universal/` - Universal SWR-based state stores
+- `styles/` - SCSS stylesheets with modular architecture
+- `migrations/` - MongoDB database migration scripts
+- `interfaces/` - Universal TypeScript type definitions
+- `models/` - Universal Data model definitions
+
+### Key Technical Details
+
+**Frontend Stack**
+- **Framework**: Next.js (Pages Router) with React
+- **Language**: TypeScript (strict mode enabled)
+- **Styling**: SCSS with CSS Modules by Bootstrap 5
+- **State Management**:
+  - **Jotai** (Primary, Recommended): Atomic state management for UI and application state
+  - **SWR**: Data fetching, caching, and revalidation
+  - **Unstated**: Legacy (being phased out, replaced by Jotai)
+- **Testing**: 
+  - Vitest for unit tests (`*.spec.ts`, `*.spec.tsx`)
+  - Jest for integration tests (`*.integ.ts`)
+  - React Testing Library for component testing
+  - Playwright for E2E testing
+- **i18n**: next-i18next for internationalization
+
+**Backend Stack**
+- **Runtime**: Node.js
+- **Framework**: Express.js with TypeScript
+- **Database**: MongoDB with Mongoose ODM
+- **Migration System**: migrate-mongo
+- **Authentication**: Passport.js with multiple strategies (local, LDAP, OAuth, SAML)
+- **Real-time**: Socket.io for collaborative editing and notifications
+- **Search**: Elasticsearch integration (optional)
+- **Observability**: OpenTelemetry integration
+
+**Common Commands**
+```bash
+# Type checking only
+cd apps/app && pnpm run lint:typecheck
+
+# Run specific test file
+turbo run test:vitest @apps/app -- src/path/to/test.spec.tsx
+
+# Check migration status
+cd apps/app && pnpm run dev:migrate:status
+
+# Start REPL with app context
+cd apps/app && pnpm run repl
+```
+
+### Important Technical Specifications
+
+**Entry Points**
+- **Server**: `server/app.ts` - Handles OpenTelemetry initialization and Crowi server startup
+- **Client**: `pages/_app.page.tsx` - Root Next.js application component
+  - `pages/[[...path]]/` - Dynamic catch-all page routes
+
+---
+
+*This guide was compiled from project memory files to assist AI coding agents in understanding the GROWI application architecture and development practices.*

+ 1 - 0
apps/app/CLAUDE.md

@@ -0,0 +1 @@
+@AGENTS.md

+ 1 - 1
apps/app/bin/openapi/generate-operation-ids/cli.spec.ts

@@ -90,7 +90,7 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify error was logged
-    // eslint-disable-next-line no-console
+    // biome-ignore lint/suspicious/noConsole: This is a test file
     expect(console.error).toHaveBeenCalledWith(error);
 
     // Verify writeFileSync was not called

+ 1 - 1
apps/app/bin/openapi/generate-operation-ids/cli.ts

@@ -16,9 +16,9 @@ export const main = async (): Promise<void> => {
   const { out: outputFile, overwriteExisting } = program.opts();
   const [inputFile] = program.args;
 
-  // eslint-disable-next-line no-console
   const jsonStrings = await generateOperationIds(inputFile, {
     overwriteExisting,
+    // biome-ignore lint/suspicious/noConsole: Allow to dump errors
   }).catch(console.error);
   if (jsonStrings != null) {
     writeFileSync(outputFile ?? inputFile, jsonStrings);

+ 1 - 1
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts

@@ -18,7 +18,7 @@ async function cleanup(filePath: string): Promise<void> {
     await fs.unlink(filePath);
     await fs.rmdir(path.dirname(filePath));
   } catch (err) {
-    // eslint-disable-next-line no-console
+    // biome-ignore lint/suspicious/noConsole: This is a test file
     console.error('Cleanup failed:', err);
   }
 }

+ 2 - 0
apps/app/bin/print-memory-consumption.ts

@@ -11,6 +11,8 @@
  *        print-memory-consumption.ts [--port=9229] [--host=localhost] [--json]
  */
 
+/** biome-ignore-all lint/suspicious/noConsole: Allow printing to console */
+
 import { get } from 'node:http';
 import WebSocket from 'ws';
 

+ 1 - 2
apps/app/config/migrate-mongo-config.js

@@ -9,8 +9,7 @@ const isProduction = process.env.NODE_ENV === 'production';
 const { URL } = require('node:url');
 
 const { getMongoUri, mongoOptions } = isProduction
-  ? // eslint-disable-next-line import/extensions, import/no-unresolved
-    require('../dist/server/util/mongoose-utils')
+  ? require('../dist/server/util/mongoose-utils')
   : require('../src/server/util/mongoose-utils');
 
 // get migrationsDir from env var

+ 0 - 1
apps/app/config/next-i18next.config.js

@@ -22,7 +22,6 @@ module.exports = {
   localePath: path.resolve('./public/static/locales'),
   serializeConfig: false,
 
-  // eslint-disable-next-line no-nested-ternary
   use: isDev
     ? isServer()
       ? [new HMRPlugin({ webpack: { server: true } })]

+ 0 - 3
apps/app/next.config.js

@@ -105,9 +105,6 @@ module.exports = async (phase) => {
     i18n,
 
     // for build
-    eslint: {
-      ignoreDuringBuilds: true,
-    },
     typescript: {
       tsconfigPath: 'tsconfig.build.client.json',
     },

+ 3 - 5
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.4.2",
+  "version": "7.4.3-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -27,7 +27,6 @@
     "//// for CI": "",
     "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "lint:typecheck": "vue-tsc --noEmit",
-    "lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
     "lint:biome": "biome check --diagnostic-level=error",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
@@ -271,8 +270,8 @@
     "@growi/editor": "workspace:^",
     "@growi/ui": "workspace:^",
     "@handsontable/react": "=2.1.0",
-    "@headless-tree/core": "^1.5.1",
-    "@headless-tree/react": "^1.5.1",
+    "@headless-tree/core": "^1.5.3",
+    "@headless-tree/react": "^1.5.3",
     "@next/bundle-analyzer": "^14.1.3",
     "@popperjs/core": "^2.11.8",
     "@swc-node/jest": "^1.8.1",
@@ -309,7 +308,6 @@
     "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
-    "eslint-plugin-jest": "^26.5.3",
     "fastest-levenshtein": "^1.0.16",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",

+ 0 - 16
apps/app/playwright/.eslintrc.mjs

@@ -1,16 +0,0 @@
-import playwright from 'eslint-plugin-playwright';
-
-// eslint-disable-next-line import/no-anonymous-default-export
-export default [
-  {
-    ...playwright.configs['flat/recommended'],
-    files: ['./**'],
-  },
-  {
-    files: ['./**'],
-    rules: {
-      // Customize Playwright rules
-      // ...
-    },
-  },
-];

+ 2 - 7
apps/app/playwright/20-basic-features/access-to-page.spec.ts

@@ -1,11 +1,6 @@
-import { expect, type Page, test } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-const appendTextToEditorUntilContains = async (page: Page, text: string) => {
-  await page.locator('.cm-content').fill(text);
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
-    text,
-  );
-};
+import { appendTextToEditorUntilContains } from '../utils/AppendTextToEditorUntilContains';
 
 test('has title', async ({ page }) => {
   await page.goto('/Sandbox');

+ 2 - 7
apps/app/playwright/23-editor/saving.spec.ts

@@ -1,12 +1,7 @@
-import { expect, type Page, test } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 import path from 'path';
 
-const appendTextToEditorUntilContains = async (page: Page, text: string) => {
-  await page.locator('.cm-content').fill(text);
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
-    text,
-  );
-};
+import { appendTextToEditorUntilContains } from '../utils/AppendTextToEditorUntilContains';
 
 test('Successfully create page under specific path', async ({ page }) => {
   const newPagePath = '/child';

+ 77 - 0
apps/app/playwright/23-editor/vim-keymap.spec.ts

@@ -0,0 +1,77 @@
+import { expect, type Page, test } from '@playwright/test';
+
+import { appendTextToEditorUntilContains } from '../utils/AppendTextToEditorUntilContains';
+
+/**
+ * Tests for Vim keymap functionality in the editor
+ * @see https://github.com/growilabs/growi/issues/8814
+ * @see https://github.com/growilabs/growi/issues/10701
+ */
+
+const changeKeymap = async (page: Page, keymap: string) => {
+  // Open OptionsSelector
+  await expect(page.getByTestId('options-selector-btn')).toBeVisible();
+  await page.getByTestId('options-selector-btn').click();
+  await expect(page.getByTestId('options-selector-menu')).toBeVisible();
+
+  // Click keymap selection button to navigate to keymap selector
+  await expect(page.getByTestId('keymap_current_selection')).toBeVisible();
+  await page.getByTestId('keymap_current_selection').click();
+
+  // Select Vim keymap
+  await expect(page.getByTestId(`keymap_radio_item_${keymap}`)).toBeVisible();
+  await page.getByTestId(`keymap_radio_item_${keymap}`).click();
+
+  // Close OptionsSelector
+  await page.getByTestId('options-selector-btn').click();
+  await expect(page.getByTestId('options-selector-menu')).not.toBeVisible();
+};
+
+test.describe
+  .serial('Vim keymap mode', () => {
+    test.beforeEach(async ({ page }) => {
+      await page.goto('/Sandbox/vim-keymap-test-page');
+
+      // Open Editor
+      await expect(page.getByTestId('editor-button')).toBeVisible();
+      await page.getByTestId('editor-button').click();
+      await expect(page.locator('.cm-content')).toBeVisible();
+      await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+    });
+
+    test('Insert mode should persist while typing multiple characters', async ({
+      page,
+    }) => {
+      const testText = 'Hello World';
+
+      // Change to Vim keymap
+      await changeKeymap(page, 'vim');
+
+      // Focus the editor
+      await page.locator('.cm-content').click();
+
+      // Enter insert mode
+      await page.keyboard.type('i');
+
+      // Append text
+      await appendTextToEditorUntilContains(page, testText);
+    });
+
+    test('Write command (:w) should save the page successfully', async ({
+      page,
+    }) => {
+      // Enter command mode
+      await page.keyboard.type(':');
+      await expect(page.locator('.cm-vim-panel')).toBeVisible();
+
+      // Type write command and execute
+      await page.keyboard.type('w');
+      await page.keyboard.press('Enter');
+
+      // Expect a success toaster to be displayed
+      await expect(page.locator('.Toastify__toast--success')).toBeVisible();
+
+      // Restore keymap to default
+      await changeKeymap(page, 'default');
+    });
+  });

+ 3 - 8
apps/app/playwright/23-editor/with-navigation.spec.ts

@@ -1,7 +1,9 @@
-import { expect, type Page, test } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 import { readFileSync } from 'fs';
 import path from 'path';
 
+import { appendTextToEditorUntilContains } from '../utils/AppendTextToEditorUntilContains';
+
 /**
  * for the issues:
  * @see https://redmine.weseek.co.jp/issues/122040
@@ -61,13 +63,6 @@ test('should not be cleared and should prevent GrantSelector from modified', asy
   );
 });
 
-const appendTextToEditorUntilContains = async (page: Page, text: string) => {
-  await page.locator('.cm-content').fill(text);
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
-    text,
-  );
-};
-
 /**
  * for the issue:
  * @see https://redmine.weseek.co.jp/issues/115285

+ 6 - 3
apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts

@@ -29,9 +29,12 @@ test.describe('Access to sidebar', () => {
   test('Successfully access to custom sidebar', async ({ page }) => {
     await page.getByTestId('grw-sidebar-nav-primary-custom-sidebar').click();
     await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
-    await expect(
-      page.locator('.grw-sidebar-content-header > h3').locator('a'),
-    ).toBeVisible();
+
+    // Check if edit_note icon is visible within the button
+    const editNoteIcon = page
+      .locator('.grw-custom-sidebar-content button .material-symbols-outlined')
+      .filter({ hasText: 'edit_note' });
+    await expect(editNoteIcon).toBeVisible();
   });
 
   test('Successfully access to GROWI Docs page', async ({ page }) => {

+ 2 - 1
apps/app/playwright/60-home/home.spec.ts

@@ -1,3 +1,5 @@
+/** biome-ignore-all lint/performance/noAwaitInLoops: Allow in tests */
+
 import { expect, test } from '@playwright/test';
 
 test('Visit User home', async ({ page }) => {
@@ -74,7 +76,6 @@ test('Access Password setting', async ({ page }) => {
 
   const toastElementsCount = await toastElements.count();
   for (let i = 0; i < toastElementsCount; i++) {
-    // eslint-disable-next-line no-await-in-loop
     await toastElements.nth(i).click();
   }
 

+ 11 - 0
apps/app/playwright/utils/AppendTextToEditorUntilContains.ts

@@ -0,0 +1,11 @@
+import { expect, type Page } from '@playwright/test';
+
+export const appendTextToEditorUntilContains = async (
+  page: Page,
+  text: string,
+) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+    text,
+  );
+};

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

@@ -56,6 +56,11 @@
       "enable_force_delete_user_homepage_on_user_deletion": "When you delete a user, the user's homepage and all its sub pages will be completely deleted",
       "desc": "You will be able to delete a deleted user's homepage."
     },
+    "disable_user_pages": {
+      "disable_user_pages": "Disable user pages",
+      "disable_user_pages_label": "Disable user pages",
+      "desc": "By disabling user pages, creating, viewing, editing, and duplicating all user pages will be disabled.</br>Additionally, user pages will not appear in page trees, recent changes, or search results."
+    },
     "session": "Session",
     "max_age": "Max age (msec)",
     "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
@@ -313,7 +318,7 @@
       "done": "Copied to clipboard!"
     },
     "bug_report": "Submitting a bug report",
-    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>then submit your issue to GitHub.</a>"
+    "submit_bug_report": "Submit your issue to GitHub."
   },
   "v5_page_migration": {
     "migration_desc": "There are some pages with old v4 compatibility. To take advantage of new features such as page trees and easy renaming, please convert all your pages to v5 compatibility.",

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

@@ -56,6 +56,11 @@
       "enable_force_delete_user_homepage_on_user_deletion": "Supprimer la page d'accueil et ses pages enfants",
       "desc": "Les pages d'accueil utilisateurs pourront être supprimées."
     },
+    "disable_user_pages": {
+      "disable_user_pages": "Désactiver les pages utilisateur",
+      "disable_user_pages_label": "Désactiver les pages utilisateur",
+      "desc": "En désactivant les pages utilisateur, la création, la consultation, la modification et la duplication de toutes les pages utilisateur seront désactivées.</br>De plus, les pages utilisateur n'apparaîtront pas dans l'arborescence des pages, les modifications récentes ou les résultats de recherche."
+    },
     "session": "Session",
     "max_age": "Âge maximal (ms)",
     "max_age_desc": "Spécifie (en milliseconde) l'âge maximal d'une session <br>Par défaut: 2592000000 (30 jours)",
@@ -313,7 +318,7 @@
       "done": "Copié dans le presse-papier!"
     },
     "bug_report": "Informations de diagnostic",
-    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>soummettre ensuite sur GitHub.</a>"
+    "submit_bug_report": "Soummettre ensuite sur GitHub."
   },
   "v5_page_migration": {
     "migration_desc": "Des pages sont encore en V4. Pour profiter des nouvelles fonctionnalitées, convertir toutes les pages vers la V5.",

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

@@ -65,6 +65,11 @@
       "enable_force_delete_user_homepage_on_user_deletion": "ユーザーを削除したとき、ユーザーホームページとその配下のページを完全削除する",
       "desc": "削除済みユーザーのユーザーホームページを削除できるようになります。"
     },
+    "disable_user_pages": {
+      "disable_user_pages": "ユーザーページの無効化",
+      "disable_user_pages_label": "ユーザーページを無効にする",
+      "desc": "ユーザーページを無効にすることで、すべてのユーザーページに対する作成・閲覧・編集・複製ができなくなります。</br>また、ページツリーや最近の変更、検索結果などでもユーザーページが表示されなくなります。"
+    },
     "session": "セッション",
     "max_age": "有効期間 (ミリ秒)",
     "max_age_desc": "ユーザーのセッション情報の有効期間をミリ秒で指定できます。<br>デフォルト値: 2592000000 (30日間)",
@@ -322,7 +327,7 @@
       "done": "クリップボードにコピーしました!"
     },
     "bug_report": "バグを報告する",
-    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
+    "submit_bug_report": "GitHub で Issue を投稿"
   },
   "v5_page_migration": {
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",

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

@@ -56,6 +56,11 @@
       "enable_force_delete_user_homepage_on_user_deletion": "사용자를 삭제할 때, 사용자의 홈페이지와 모든 하위 페이지가 완전히 삭제됩니다.",
       "desc": "삭제된 사용자의 홈페이지를 삭제할 수 있습니다."
     },
+    "disable_user_pages": {
+      "disable_user_pages": "사용자 페이지 비활성화",
+      "disable_user_pages_label": "사용자 페이지 비활성화",
+      "desc": "사용자 페이지를 비활성화하면 모든 사용자 페이지의 생성, 조회, 편집 및 복제가 비활성화됩니다.</br>또한, 사용자 페이지는 페이지 트리, 최근 변경 사항 또는 검색 결과에도 표시되지 않습니다."
+    },
     "session": "세션",
     "max_age": "최대 수명 (밀리초)",
     "max_age_desc": "사용자 세션이 만료되는 시간(밀리초)을 지정합니다.<br>기본값: 2592000000 (30일)",
@@ -298,7 +303,7 @@
   },
   "mailer_setup_required": "<a href='/admin/app'>이메일 설정</a>이 전송에 필요합니다.",
   "admin_top": {
-    "management_wiki": "관리 위키",
+    "management_wiki": "위키 관리",
     "system_information": "시스템 정보",
     "wiki_administrator": "위키 관리자만 이 페이지에 접근할 수 있습니다",
     "assign_administrator": "사용자 관리 페이지에서 '관리자 권한 부여' 버튼을 사용하여 선택한 사용자에게 위키 관리자 권한을 부여할 수 있습니다.",
@@ -313,7 +318,7 @@
       "done": "클립보드에 복사되었습니다!"
     },
     "bug_report": "버그 보고서 제출",
-    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>그런 다음 GitHub에 문제를 제출하십시오.</a>"
+    "submit_bug_report": "GitHub 에 이슈를 제출하세요."
   },
   "v5_page_migration": {
     "migration_desc": "일부 페이지는 이전 v4 호환성을 가지고 있습니다. 페이지 트리 및 쉬운 이름 변경과 같은 새로운 기능을 활용하려면 모든 페이지를 v5 호환성으로 변환하십시오.",

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

@@ -65,6 +65,11 @@
       "enable_force_delete_user_homepage_on_user_deletion": "删除用户时,该用户的主页及其所有子页面将被完全删除",
       "desc": "您可以删除已删除用户的主页。"
     },
+    "disable_user_pages": {
+      "disable_user_pages": "禁用用户页面",
+      "disable_user_pages_label": "禁用用户页面",
+      "desc": "通过禁用用户页面,将无法创建、查看、编辑和复制所有用户页面。</br>此外,用户页面也不会出现在页面树、最近更改或搜索结果中。"
+    },
     "session": "会议",
     "max_age": "有效期间  (msec)",
     "max_age_desc": "指定使用户会话过期的数量(以毫秒为单位)。<br>默认值: 2592000000 (30天)",
@@ -322,7 +327,7 @@
       "done": "复制到剪贴板!"
     },
     "bug_report": "提交一个错误报告",
-    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
+    "submit_bug_report": "将您的问题提交到 GitHub。"
   },
   "v5_page_migration": {
     "migration_desc": "有一些页面具有旧的v4兼容性。为了利用新的功能,如页面树和容易重命名,请将您的所有页面转换为v5兼容性。",

+ 0 - 5
apps/app/src/client/components/.eslintrc.js

@@ -1,5 +0,0 @@
-module.exports = {
-  extends: '../../../.eslintrc.js',
-  rules: {
-  },
-};

+ 69 - 63
apps/app/src/client/components/Admin/AdminHome/AdminHome.jsx → apps/app/src/client/components/Admin/AdminHome/AdminHome.tsx

@@ -1,43 +1,43 @@
-import React, { useCallback, useEffect } from 'react';
+import type { FC } from 'react';
+import { useId, useState } from 'react';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { Tooltip } from 'reactstrap';
 
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import { toastError } from '~/client/util/toastr';
+import { useSWRxAdminHome } from '~/stores/admin/admin-home';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
-import loggerFactory from '~/utils/logger';
+import { generatePrefilledHostInformationMarkdown } from '~/utils/admin-home';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { EnvVarsTable } from './EnvVarsTable';
 import SystemInfomationTable from './SystemInfomationTable';
 
-const logger = loggerFactory('growi:admin');
+const COPY_STATE = {
+  DEFAULT: 'default',
+  DONE: 'done',
+} as const;
 
-const AdminHome = (props) => {
-  const { adminHomeContainer } = props;
+export const AdminHome: FC = () => {
   const { t } = useTranslation();
+  const { data: adminHomeData } = useSWRxAdminHome();
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
+  const [copyState, setCopyState] = useState<string>(COPY_STATE.DEFAULT);
 
-  const fetchAdminHomeData = useCallback(async () => {
-    try {
-      await adminHomeContainer.retrieveAdminHomeData();
-    } catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [adminHomeContainer]);
+  const handleCopyPrefilledHostInformation = () => {
+    setCopyState(COPY_STATE.DONE);
+    setTimeout(() => {
+      setCopyState(COPY_STATE.DEFAULT);
+    }, 500);
+  };
 
-  useEffect(() => {
-    fetchAdminHomeData();
-  }, [fetchAdminHomeData]);
+  // Generate CSS-safe ID by removing colons from useId() result
+  const copyButtonIdRaw = useId();
+  const copyButtonId = `copy-button-${copyButtonIdRaw.replace(/:/g, '')}`;
 
   return (
     <div data-testid="admin-home">
       {
         // Alert message will be displayed in case that the GROWI is under maintenance
-        adminHomeContainer.state.isMaintenanceMode && (
+        adminHomeData?.isMaintenanceMode && (
           <div className="alert alert-danger alert-link" role="alert">
             <h3 className="alert-heading">
               {t('admin:maintenance_mode.maintenance_mode')}
@@ -104,7 +104,7 @@ const AdminHome = (props) => {
               __html: t('admin:admin_top.about_security'),
             }}
           />
-          <EnvVarsTable envVars={adminHomeContainer.state.envVars} />
+          <EnvVarsTable envVars={adminHomeData?.envVars} />
         </div>
       </div>
 
@@ -113,50 +113,56 @@ const AdminHome = (props) => {
           <h2 className="admin-setting-header">
             {t('admin:admin_top.bug_report')}
           </h2>
-          <div className="d-flex align-items-center">
-            <CopyToClipboard
-              text={adminHomeContainer.generatePrefilledHostInformationMarkdown()}
-              onCopy={() => adminHomeContainer.onCopyPrefilledHostInformation()}
-            >
-              <button
-                id="prefilledHostInformationButton"
-                type="button"
-                className="btn btn-primary"
+          <ol className="mb-0">
+            <li className="mb-3">
+              <CopyToClipboard
+                text={generatePrefilledHostInformationMarkdown({
+                  growiVersion: adminHomeData?.growiVersion,
+                  nodeVersion: adminHomeData?.nodeVersion,
+                  npmVersion: adminHomeData?.npmVersion,
+                  pnpmVersion: adminHomeData?.pnpmVersion,
+                })}
+                onCopy={handleCopyPrefilledHostInformation}
               >
-                {t('admin:admin_top:copy_prefilled_host_information:default')}
-              </button>
-            </CopyToClipboard>
-            <Tooltip
-              placement="bottom"
-              isOpen={
-                adminHomeContainer.state.copyState ===
-                adminHomeContainer.copyStateValues.DONE
-              }
-              target="prefilledHostInformationButton"
-              fade={false}
-            >
-              {t('admin:admin_top:copy_prefilled_host_information:done')}
-            </Tooltip>
-            <span
-              className="ms-2"
-              // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
-              dangerouslySetInnerHTML={{
-                __html: t('admin:admin_top:submit_bug_report'),
-              }}
-            />
-          </div>
+                <button
+                  type="button"
+                  className="btn btn-outline-secondary btn-sm"
+                  style={{ verticalAlign: 'baseline' }}
+                  onClick={(e) => e.preventDefault()}
+                >
+                  <span
+                    id={copyButtonId}
+                    className="material-symbols-outlined"
+                    aria-hidden="true"
+                  >
+                    content_copy
+                  </span>
+                  {t('admin:admin_top:copy_prefilled_host_information:default')}
+                </button>
+              </CopyToClipboard>
+              <Tooltip
+                placement="bottom"
+                isOpen={copyState === COPY_STATE.DONE}
+                target={copyButtonId}
+                fade={false}
+              >
+                {t('admin:admin_top:copy_prefilled_host_information:done')}
+              </Tooltip>
+            </li>
+            <li>
+              <a
+                className="link-secondary link-offset-1"
+                style={{ textDecoration: 'underline' }}
+                href="https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A"
+                target="_blank"
+                rel="noreferrer"
+              >
+                {t('admin:admin_top:submit_bug_report')}
+              </a>
+            </li>
+          </ol>
         </div>
       </div>
     </div>
   );
 };
-
-const AdminHomeWrapper = withUnstatedContainers(AdminHome, [
-  AdminHomeContainer,
-]);
-
-AdminHome.propTypes = {
-  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
-};
-
-export default AdminHomeWrapper;

+ 5 - 20
apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -1,19 +1,12 @@
-import React from 'react';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
+import { useSWRxAdminHome } from '~/stores/admin/admin-home';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-type Props = {
-  adminHomeContainer: AdminHomeContainer;
-};
-
-const SystemInformationTable = (props: Props) => {
-  const { adminHomeContainer } = props;
+const SystemInformationTable = () => {
+  const { data: adminHomeData } = useSWRxAdminHome();
 
   const { growiVersion, nodeVersion, npmVersion, pnpmVersion } =
-    adminHomeContainer.state;
+    adminHomeData ?? {};
 
   if (
     growiVersion == null ||
@@ -51,12 +44,4 @@ const SystemInformationTable = (props: Props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const SystemInformationTableWrapper = withUnstatedContainers(
-  SystemInformationTable,
-  [AdminHomeContainer],
-);
-
-export default SystemInformationTableWrapper;
+export default SystemInformationTable;

+ 1 - 0
apps/app/src/client/components/Admin/AdminHome/index.ts

@@ -0,0 +1 @@
+export * from './AdminHome';

+ 0 - 11
apps/app/src/client/components/Admin/App/AzureSetting.tsx

@@ -87,7 +87,6 @@ export const AzureSettingMolecule = (
       {azureUseOnlyEnvVars && (
         <p
           className="alert alert-info"
-          // eslint-disable-next-line react/no-danger
           // biome-ignore lint/security/noDangerouslySetInnerHtml: includes <br> and <code> from i18n strings
           dangerouslySetInnerHTML={{
             __html: t('admin:app_setting.azure_note_for_the_only_env_option', {
@@ -129,9 +128,7 @@ export const AzureSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {
@@ -159,9 +156,7 @@ export const AzureSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {
@@ -189,9 +184,7 @@ export const AzureSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {
@@ -221,9 +214,7 @@ export const AzureSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {
@@ -253,9 +244,7 @@ export const AzureSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {

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

@@ -135,9 +135,7 @@ const FileUploadSetting = (): JSX.Element => {
             <span className="material-symbols-outlined">help</span>
             <b>FIXED</b>
             <br />
-            {/* eslint-disable-next-line react/no-danger */}
             <b
-              // eslint-disable-next-line react/no-danger
               // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
               dangerouslySetInnerHTML={{
                 __html: t('admin:app_setting.fixed_by_env_var', {

+ 0 - 7
apps/app/src/client/components/Admin/App/GcsSetting.tsx

@@ -82,7 +82,6 @@ export const GcsSettingMolecule = (
       {gcsUseOnlyEnvVars && (
         <p
           className="alert alert-info"
-          // eslint-disable-next-line react/no-danger
           // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
           dangerouslySetInnerHTML={{
             __html: t('admin:app_setting.note_for_the_only_env_option', {
@@ -126,9 +125,7 @@ export const GcsSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {
@@ -158,9 +155,7 @@ export const GcsSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {
@@ -190,9 +185,7 @@ export const GcsSettingMolecule = (
                 tabIndex={-1}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                   dangerouslySetInnerHTML={{
                     __html: t('admin:app_setting.use_env_var_if_empty', {

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

@@ -42,7 +42,6 @@ export const MaintenanceMode: FC = () => {
       );
     }
 
-    // eslint-disable-next-line max-len
     toastSuccess(
       isMaintenanceMode
         ? t('admin:maintenance_mode.successfully_ended_maintenance_mode')
@@ -65,7 +64,6 @@ export const MaintenanceMode: FC = () => {
             ? t('admin:maintenance_mode.warning_message_to_end')
             : t('admin:maintenance_mode.warning_message_to_start')
         }
-        // eslint-disable-next-line max-len
         supplymentaryMessage={
           isMaintenanceMode
             ? null

+ 0 - 1
apps/app/src/client/components/Admin/App/MaskedInput.tsx

@@ -10,7 +10,6 @@ type Props = {
   value?: string;
   onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
   tabIndex?: number | undefined;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   register?: UseFormRegister<any>;
   fieldName?: string;
 };

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

@@ -105,9 +105,7 @@ const PageBulkExportSettings = (): JSX.Element => {
               </div>
               {data?.useOnlyEnvVarsForIsBulkExportPagesEnabled && (
                 <p className="form-text text-muted">
-                  {/* eslint-disable-next-line react/no-danger */}
                   <b
-                    // eslint-disable-next-line react/no-danger
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                     dangerouslySetInnerHTML={{
                       __html: t('admin:app_setting.fixed_by_env_var', {

+ 0 - 1
apps/app/src/client/components/Admin/App/SesSetting.tsx

@@ -7,7 +7,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 type Props = {
   adminAppContainer?: AdminAppContainer;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   register: UseFormRegister<any>;
 };
 

+ 0 - 3
apps/app/src/client/components/Admin/App/SiteUrlSetting.tsx

@@ -60,7 +60,6 @@ const SiteUrlSetting = (props: Props) => {
         <div className="row">
           <p
             className="alert alert-info"
-            // eslint-disable-next-line react/no-danger
             // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
             dangerouslySetInnerHTML={{
               __html: t('site_url.note_for_the_only_env_option', {
@@ -96,7 +95,6 @@ const SiteUrlSetting = (props: Props) => {
                   {...register('siteUrl')}
                 />
                 <p className="form-text text-muted">
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                     dangerouslySetInnerHTML={{ __html: t('site_url.help') }}
@@ -111,7 +109,6 @@ const SiteUrlSetting = (props: Props) => {
                   readOnly
                 />
                 <p className="form-text text-muted">
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
                     dangerouslySetInnerHTML={{

+ 0 - 1
apps/app/src/client/components/Admin/App/SmtpSetting.tsx

@@ -8,7 +8,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 type Props = {
   adminAppContainer?: AdminAppContainer;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   register: UseFormRegister<any>;
 };
 

+ 0 - 1
apps/app/src/client/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -19,7 +19,6 @@ export const AuditLogDisableMode: FC = () => {
                 {t('audit_log_management.audit_log')}
               </h1>
               <h3
-                // eslint-disable-next-line react/no-danger
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
                   __html: t('audit_log_management.disable_mode_explanation'),

+ 0 - 1
apps/app/src/client/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -33,7 +33,6 @@ export const AuditLogSettings: FC = () => {
         <b>FIXED</b>
         <br />
         <b
-          // eslint-disable-next-line react/no-danger
           // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
           dangerouslySetInnerHTML={{
             __html: t('admin:audit_log_management.fixed_by_env_var', {

+ 0 - 1
apps/app/src/client/components/Admin/AuditLogManagement.tsx

@@ -158,7 +158,6 @@ export const AuditLogManagement: FC = () => {
       const isNan = Number.isNaN(inputNumber);
 
       if (!isNan) {
-        // eslint-disable-next-line no-nested-ternary
         const jumpPageNumber =
           inputNumber > totalPagingPages
             ? totalPagingPages

+ 0 - 1
apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -15,7 +15,6 @@ const AdminUpdateButtonRow = (props: Props): JSX.Element => {
       <div className="col-md-3"></div>
       <div className="col-md-9">
         <button
-          // eslint-disable-next-line react/button-has-type
           type={props.type ?? 'button'}
           className="btn btn-primary"
           onClick={props.onClick}

+ 50 - 52
apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -56,68 +56,66 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   }
 
   return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">
-            {t('customize_settings.layout')}
-          </h2>
+    <div className="row">
+      <div className="col-12">
+        <h2 className="admin-setting-header">
+          {t('customize_settings.layout')}
+        </h2>
 
-          <div className="d-flex justify-content-around mt-5">
-            <div className="row row-cols-2">
-              <div className="col">
-                <button
-                  type="button"
-                  className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
-                  onClick={() => setIsContainerFluid(false)}
-                  aria-pressed={!isContainerFluid}
-                >
-                  {/* eslint-disable-next-line @next/next/no-img-element */}
-                  <img
-                    className="card-img-top"
-                    src={`/images/customize-settings/default-${resolvedTheme}.svg`}
-                    alt={t('customize_settings.layout_options.default')}
-                  />
-                  <div className="card-body text-center">
-                    {t('customize_settings.layout_options.default')}
-                  </div>
-                </button>
-              </div>
-              <div className="col">
-                <button
-                  type="button"
-                  className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
-                  onClick={() => setIsContainerFluid(true)}
-                  aria-pressed={isContainerFluid}
-                >
-                  {/* eslint-disable-next-line @next/next/no-img-element */}
-                  <img
-                    className="card-img-top"
-                    src={`/images/customize-settings/fluid-${resolvedTheme}.svg`}
-                    alt={t('customize_settings.layout_options.expanded')}
-                  />
-                  <div className="card-body text-center">
-                    {t('customize_settings.layout_options.expanded')}
-                  </div>
-                </button>
-              </div>
+        <div className="d-flex justify-content-around mt-5">
+          <div className="row row-cols-2">
+            <div className="col">
+              <button
+                type="button"
+                className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
+                onClick={() => setIsContainerFluid(false)}
+                aria-pressed={!isContainerFluid}
+              >
+                {/* biome-ignore lint/performance/noImgElement: Ignore for SVG */}
+                <img
+                  className="card-img-top"
+                  src={`/images/customize-settings/default-${resolvedTheme}.svg`}
+                  alt={t('customize_settings.layout_options.default')}
+                />
+                <div className="card-body text-center">
+                  {t('customize_settings.layout_options.default')}
+                </div>
+              </button>
             </div>
-          </div>
-
-          <div className="row my-3">
-            <div className="mx-auto">
+            <div className="col">
               <button
                 type="button"
-                className="btn btn-primary"
-                onClick={onClickSubmit}
+                className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
+                onClick={() => setIsContainerFluid(true)}
+                aria-pressed={isContainerFluid}
               >
-                {t('Update')}
+                {/* biome-ignore lint/performance/noImgElement: Ignore for SVG */}
+                <img
+                  className="card-img-top"
+                  src={`/images/customize-settings/fluid-${resolvedTheme}.svg`}
+                  alt={t('customize_settings.layout_options.expanded')}
+                />
+                <div className="card-body text-center">
+                  {t('customize_settings.layout_options.expanded')}
+                </div>
               </button>
             </div>
           </div>
         </div>
+
+        <div className="row my-3">
+          <div className="mx-auto">
+            <button
+              type="button"
+              className="btn btn-primary"
+              onClick={onClickSubmit}
+            >
+              {t('Update')}
+            </button>
+          </div>
+        </div>
       </div>
-    </React.Fragment>
+    </div>
   );
 };
 

+ 50 - 56
apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -51,67 +51,61 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
   );
 
   return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">
-            {t('admin:customize_settings.custom_noscript')}
-          </h2>
+    <div className="row">
+      <div className="col-12">
+        <h2 className="admin-setting-header">
+          {t('admin:customize_settings.custom_noscript')}
+        </h2>
 
-          <Card className="card custom-card bg-body-tertiary my-3">
-            <CardBody className="px-0 py-2">
-              <span
-                // eslint-disable-next-line react/no-danger
-                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                dangerouslySetInnerHTML={{
-                  __html: t('admin:customize_settings.custom_noscript_detail'),
-                }}
-              />
-            </CardBody>
-          </Card>
+        <Card className="card custom-card bg-body-tertiary my-3">
+          <CardBody className="px-0 py-2">
+            <span
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+              dangerouslySetInnerHTML={{
+                __html: t('admin:customize_settings.custom_noscript_detail'),
+              }}
+            />
+          </CardBody>
+        </Card>
 
-          <form onSubmit={handleSubmit(onSubmit)}>
-            <div>
-              <textarea
-                className="form-control mb-2"
-                rows={8}
-                {...register('customizeNoscript')}
-              />
-            </div>
+        <form onSubmit={handleSubmit(onSubmit)}>
+          <div>
+            <textarea
+              className="form-control mb-2"
+              rows={8}
+              {...register('customizeNoscript')}
+            />
+          </div>
 
-            <button
-              type="button"
-              className="btn btn-link text-muted p-0"
-              data-bs-toggle="collapse"
-              data-bs-target="#collapseExampleHtml"
-              aria-expanded="false"
-              aria-controls="collapseExampleHtml"
-            >
-              <span
-                className="material-symbols-outlined me-1"
-                aria-hidden="true"
-              >
-                navigate_next
-              </span>
-              Example for Google Tag Manager
-            </button>
-            <div className="collapse" id="collapseExampleHtml">
-              <PrismAsyncLight style={oneDark} language="javascript">
-                {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
-  height="0"
-  width="0"
-  style="display:none;visibility:hidden"></iframe>`}
-              </PrismAsyncLight>
-            </div>
+          <button
+            type="button"
+            className="btn btn-link text-muted p-0"
+            data-bs-toggle="collapse"
+            data-bs-target="#collapseExampleHtml"
+            aria-expanded="false"
+            aria-controls="collapseExampleHtml"
+          >
+            <span className="material-symbols-outlined me-1" aria-hidden="true">
+              navigate_next
+            </span>
+            Example for Google Tag Manager
+          </button>
+          <div className="collapse" id="collapseExampleHtml">
+            <PrismAsyncLight style={oneDark} language="javascript">
+              {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
+height="0"
+width="0"
+style="display:none;visibility:hidden"></iframe>`}
+            </PrismAsyncLight>
+          </div>
 
-            <AdminUpdateButtonRow
-              type="submit"
-              disabled={adminCustomizeContainer.state.retrieveError != null}
-            />
-          </form>
-        </div>
+          <AdminUpdateButtonRow
+            type="submit"
+            disabled={adminCustomizeContainer.state.retrieveError != null}
+          />
+        </form>
       </div>
-    </React.Fragment>
+    </div>
   );
 };
 

+ 43 - 45
apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -37,62 +37,60 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const { isSidebarCollapsedMode } = data;
 
   return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">
-            {t('customize_settings.default_sidebar_mode.title')}
-          </h2>
+    <div className="row">
+      <div className="col-12">
+        <h2 className="admin-setting-header">
+          {t('customize_settings.default_sidebar_mode.title')}
+        </h2>
 
-          <Card className="card custom-card bg-body-tertiary my-3">
-            <CardBody className="px-0 py-2">
-              {t('customize_settings.default_sidebar_mode.desc')}
-            </CardBody>
-          </Card>
+        <Card className="card custom-card bg-body-tertiary my-3">
+          <CardBody className="px-0 py-2">
+            {t('customize_settings.default_sidebar_mode.desc')}
+          </CardBody>
+        </Card>
 
-          <div className="d-flex justify-content-around mt-5">
-            <div className="row row-cols-2">
-              <div className="col">
-                <button
-                  type="button"
-                  className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
-                  onClick={() => setIsSidebarCollapsedMode(true)}
-                  aria-pressed={isSidebarCollapsedMode}
-                >
-                  {/* eslint-disable-next-line @next/next/no-img-element */}
-                  <img src={collapsedIconFileName} alt="Collapsed Mode" />
-                  <div className="card-body text-center">Collapsed Mode</div>
-                </button>
-              </div>
-              <div className="col">
-                <button
-                  type="button"
-                  className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
-                  onClick={() => setIsSidebarCollapsedMode(false)}
-                  aria-pressed={!isSidebarCollapsedMode}
-                >
-                  {/* eslint-disable-next-line @next/next/no-img-element */}
-                  <img src={dockIconFileName} alt="Dock Mode" />
-                  <div className="card-body  text-center">Dock Mode</div>
-                </button>
-              </div>
+        <div className="d-flex justify-content-around mt-5">
+          <div className="row row-cols-2">
+            <div className="col">
+              <button
+                type="button"
+                className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
+                onClick={() => setIsSidebarCollapsedMode(true)}
+                aria-pressed={isSidebarCollapsedMode}
+              >
+                {/* biome-ignore lint/performance/noImgElement: Ignore for SVG */}
+                <img src={collapsedIconFileName} alt="Collapsed Mode" />
+                <div className="card-body text-center">Collapsed Mode</div>
+              </button>
             </div>
-          </div>
-
-          <div className="row my-3">
-            <div className="mx-auto">
+            <div className="col">
               <button
                 type="button"
-                onClick={onClickSubmit}
-                className="btn btn-primary"
+                className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
+                onClick={() => setIsSidebarCollapsedMode(false)}
+                aria-pressed={!isSidebarCollapsedMode}
               >
-                {t('Update')}
+                {/* biome-ignore lint/performance/noImgElement: Ignore for SVG */}
+                <img src={dockIconFileName} alt="Dock Mode" />
+                <div className="card-body  text-center">Dock Mode</div>
               </button>
             </div>
           </div>
         </div>
+
+        <div className="row my-3">
+          <div className="mx-auto">
+            <button
+              type="button"
+              onClick={onClickSubmit}
+              className="btn btn-primary"
+            >
+              {t('Update')}
+            </button>
+          </div>
+        </div>
       </div>
-    </React.Fragment>
+    </div>
   );
 };
 

+ 2 - 6
apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -1,4 +1,4 @@
-import React, { type JSX, useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { useTranslation } from 'next-i18next';
 
@@ -8,11 +8,7 @@ import { useSWRxGrowiThemeSetting } from '~/stores/admin/customize';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 
-// eslint-disable-next-line @typescript-eslint/ban-types
-type Props = {};
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const CustomizeThemeSetting = (props: Props): JSX.Element => {
+const CustomizeThemeSetting = (): JSX.Element => {
   const { t } = useTranslation();
 
   const { data, error, update } = useSWRxGrowiThemeSetting();

+ 70 - 74
apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx

@@ -1,5 +1,5 @@
 import type { FC } from 'react';
-import React, { useCallback, useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 import { Card, CardBody } from 'reactstrap';
@@ -44,82 +44,78 @@ export const CustomizeTitle: FC = () => {
   );
 
   return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">
-            {t('admin:customize_settings.custom_title')}
-          </h2>
-        </div>
+    <div className="row">
+      <div className="col-12">
+        <h2 className="admin-setting-header">
+          {t('admin:customize_settings.custom_title')}
+        </h2>
+      </div>
+
+      <div className="col-12">
+        <Card className="card custom-card bg-body-tertiary mb-3">
+          <CardBody className="px-0 py-2">
+            <p
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+              dangerouslySetInnerHTML={{
+                __html: t('admin:customize_settings.custom_title_detail'),
+              }}
+            />
+            <ul>
+              <li>
+                <span
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'admin:customize_settings.custom_title_detail_placeholder1',
+                    ),
+                  }}
+                />
+              </li>
+              <li>
+                <span
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'admin:customize_settings.custom_title_detail_placeholder2',
+                    ),
+                  }}
+                />
+              </li>
+              <li>
+                <span
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'admin:customize_settings.custom_title_detail_placeholder3',
+                    ),
+                  }}
+                />
+              </li>
+            </ul>
+          </CardBody>
+        </Card>
+      </div>
 
+      {/* TODO i18n */}
+      <div className="form-text text-muted col-12 mb-3">
+        Default Value:{' '}
+        <code>
+          &#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;
+        </code>
+        <br />
+        Default Output Example:{' '}
+        <code className="xml">
+          &lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;
+        </code>
+      </div>
+      <form onSubmit={handleSubmit(onSubmit)}>
         <div className="col-12">
-          <Card className="card custom-card bg-body-tertiary mb-3">
-            <CardBody className="px-0 py-2">
-              {/* eslint-disable react/no-danger */}
-              <p
-                // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                dangerouslySetInnerHTML={{
-                  __html: t('admin:customize_settings.custom_title_detail'),
-                }}
-              />
-              <ul>
-                <li>
-                  <span
-                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                    dangerouslySetInnerHTML={{
-                      __html: t(
-                        'admin:customize_settings.custom_title_detail_placeholder1',
-                      ),
-                    }}
-                  />
-                </li>
-                <li>
-                  <span
-                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                    dangerouslySetInnerHTML={{
-                      __html: t(
-                        'admin:customize_settings.custom_title_detail_placeholder2',
-                      ),
-                    }}
-                  />
-                </li>
-                <li>
-                  <span
-                    // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                    dangerouslySetInnerHTML={{
-                      __html: t(
-                        'admin:customize_settings.custom_title_detail_placeholder3',
-                      ),
-                    }}
-                  />
-                </li>
-              </ul>
-              {/* eslint-enable react/no-danger */}
-            </CardBody>
-          </Card>
+          <input className="form-control" {...register('customizeTitle')} />
         </div>
-
-        {/* TODO i18n */}
-        <div className="form-text text-muted col-12 mb-3">
-          Default Value:{' '}
-          <code>
-            &#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;
-          </code>
-          <br />
-          Default Output Example:{' '}
-          <code className="xml">
-            &lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;
-          </code>
+        <div className="col-12">
+          <AdminUpdateButtonRow type="submit" disabled={false} />
         </div>
-        <form onSubmit={handleSubmit(onSubmit)}>
-          <div className="col-12">
-            <input className="form-control" {...register('customizeTitle')} />
-          </div>
-          <div className="col-12">
-            <AdminUpdateButtonRow type="submit" disabled={false} />
-          </div>
-        </form>
-      </div>
-    </React.Fragment>
+      </form>
+    </div>
   );
 };

+ 0 - 1
apps/app/src/client/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -29,7 +29,6 @@ class StatusTable extends React.PureComponent {
       );
     } else {
       connectionStatusLabel = isConnected ? (
-        // eslint-disable-next-line max-len
         <span
           data-testid="connection-status-badge-connected"
           className="badge text-bg-success"

+ 23 - 31
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -1,5 +1,3 @@
-/* eslint-disable react/no-danger */
-
 import React from 'react';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
@@ -54,7 +52,6 @@ class ImportCollectionConfigurationModal extends React.Component {
     const translationBase =
       'admin:importer_management.growi_settings.configuration.pages';
 
-    /* eslint-disable react/no-unescaped-entities */
     return (
       <>
         <div className="form-check form-check-warning">
@@ -187,7 +184,6 @@ class ImportCollectionConfigurationModal extends React.Component {
         </div>
       </>
     );
-    /* eslint-enable react/no-unescaped-entities */
   }
 
   renderRevisionsContents() {
@@ -197,36 +193,32 @@ class ImportCollectionConfigurationModal extends React.Component {
     const translationBase =
       'admin:importer_management.growi_settings.configuration.revisions';
 
-    /* eslint-disable react/no-unescaped-entities */
     return (
-      <>
-        <div className="form-check form-check-warning">
-          <input
-            id="cbOpt1"
-            type="checkbox"
-            className="form-check-input"
-            checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() =>
-              this.changeHandler({
-                isOverwriteAuthorWithCurrentUser:
-                  !option.isOverwriteAuthorWithCurrentUser,
-              })
-            }
+      <div className="form-check form-check-warning">
+        <input
+          id="cbOpt1"
+          type="checkbox"
+          className="form-check-input"
+          checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
+          onChange={() =>
+            this.changeHandler({
+              isOverwriteAuthorWithCurrentUser:
+                !option.isOverwriteAuthorWithCurrentUser,
+            })
+          }
+        />
+        <label htmlFor="cbOpt1" className="form-label form-check-label">
+          {t(`${translationBase}.overwrite_author.label`)}
+          <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 htmlFor="cbOpt1" className="form-label form-check-label">
-            {t(`${translationBase}.overwrite_author.label`)}
-            <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>
-        </div>
-      </>
+        </label>
+      </div>
     );
-    /* eslint-enable react/no-unescaped-entities */
   }
 
   render() {

+ 0 - 1
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -110,7 +110,6 @@ class ImportForm extends React.Component {
     const { socket } = this.props;
 
     // websocket event
-    // eslint-disable-next-line object-curly-newline
     socket.on(
       'admin:onProgressForImport',
       ({ collectionName, collectionProgress, appendedErrors }) => {

+ 0 - 1
apps/app/src/client/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -18,7 +18,6 @@ class UploadForm extends React.Component {
 
   changeFileName(e) {
     // to trigger rerender at onChange event
-    // eslint-disable-next-line react/no-unused-state
     this.setState({ dummy: e.target.files[0].name });
   }
 

+ 0 - 2
apps/app/src/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -38,7 +38,6 @@ const LegacySlackIntegration = (props) => {
       {isDisabled && (
         <div className="alert alert-danger">
           <span className="material-symbols-outlined">remove</span>
-          {/* eslint-disable-next-line react/no-danger */}
           <span
             // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
             dangerouslySetInnerHTML={{
@@ -50,7 +49,6 @@ const LegacySlackIntegration = (props) => {
 
       <div className="alert alert-warning">
         <span className="material-symbols-outlined">info</span>
-        {/* eslint-disable-next-line react/no-danger */}
         <span
           // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
           dangerouslySetInnerHTML={{

+ 0 - 3
apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -157,7 +157,6 @@ const SlackConfiguration = (props) => {
                 RECOMMENDED
               </span>
               <br />
-              {/* eslint-disable-next-line react/no-danger */}
               <span
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
                 dangerouslySetInnerHTML={{
@@ -224,7 +223,6 @@ const SlackConfiguration = (props) => {
           <li className="ms-3">
             {t('notification_settings.how_to.workspace')}
             <ol>
-              {/* eslint-disable-next-line react/no-danger */}
               <li
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
                 dangerouslySetInnerHTML={{
@@ -238,7 +236,6 @@ const SlackConfiguration = (props) => {
           <li className="ms-3">
             {t('notification_settings.how_to.at_growi')}
             <ol>
-              {/* eslint-disable-next-line react/no-danger */}
               <li
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: translation contains HTML markup
                 dangerouslySetInnerHTML={{

+ 0 - 1
apps/app/src/client/components/Admin/MarkdownSetting/IndentForm.tsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import React, { useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import {

+ 0 - 1
apps/app/src/client/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';

+ 0 - 4
apps/app/src/client/components/Admin/Notification/GlobalNotification.jsx

@@ -40,7 +40,6 @@ const GlobalNotification = (props) => {
       </h2>
 
       <p className="card custom-card bg-body-tertiary">
-        {/* eslint-disable-next-line react/no-danger */}
         <span
           // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
           dangerouslySetInnerHTML={{
@@ -67,7 +66,6 @@ const GlobalNotification = (props) => {
               className="form-label form-check-label"
               htmlFor="isNotificationForOwnerPageEnabled"
             >
-              {/* eslint-disable-next-line react/no-danger */}
               <span
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
@@ -98,7 +96,6 @@ const GlobalNotification = (props) => {
               className="form-label form-check-label"
               htmlFor="isNotificationForGroupPageEnabled"
             >
-              {/* eslint-disable-next-line react/no-danger */}
               <span
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
@@ -136,7 +133,6 @@ const GlobalNotification = (props) => {
         <thead>
           <tr>
             <th>ON/OFF</th>
-            {/* eslint-disable-next-line react/no-danger */}
             <th>
               {t('notification_settings.trigger_path')}{' '}
               <span

+ 0 - 3
apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx

@@ -148,7 +148,6 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
           <h3>
             <label htmlFor="triggerPath" className="form-label">
               {t('notification_settings.trigger_path')}
-              {/* eslint-disable-next-line react/no-danger */}
               <small
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
@@ -230,7 +229,6 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
               </div>
 
               <p className="p-2">
-                {/* eslint-disable-next-line react/no-danger */}
                 {!isMailerSetup && (
                   <span
                     className="form-text text-muted"
@@ -270,7 +268,6 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 />
               </div>
               <p className="p-2">
-                {/* eslint-disable-next-line react/no-danger */}
                 <span
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                   dangerouslySetInnerHTML={{

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

@@ -50,7 +50,7 @@ NotificationDeleteModal.propTypes = {
   notificationForConfiguration: PropTypes.object.isRequired,
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
 const NotificationDeleteModalWrapperFC = (props) => {
   const { t } = useTranslation('admin');
 

+ 1 - 8
apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { SlackbotType } from '@growi/slack';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
@@ -16,8 +16,6 @@ import UserTriggerNotification from './UserTriggerNotification';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
-let retrieveErrors = null;
-
 const SettingsIcon = () => (
   <span className="material-symbols-outlined">settings</span>
 );
@@ -33,7 +31,6 @@ const navTabMapping = {
   },
 };
 
-// eslint-disable-next-line react/prop-types
 const Badge = ({ isEnabled }) => {
   const { t } = useTranslation('admin');
 
@@ -57,7 +54,6 @@ const SkeletonListItem = () => (
   </li>
 );
 
-// eslint-disable-next-line react/prop-types
 const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
   const { t } = useTranslation('admin');
 
@@ -78,7 +74,6 @@ const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
       </h4>
       {isCautionVisible && (
         <ul className="mt-2 ps-4">
-          {/* eslint-disable-next-line react/no-danger */}
           <li
             // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
             dangerouslySetInnerHTML={{
@@ -91,7 +86,6 @@ const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
   );
 };
 
-// eslint-disable-next-line react/prop-types
 const LegacySlackIntegrationListItem = ({ isEnabled }) => {
   const { t } = useTranslation('admin');
 
@@ -106,7 +100,6 @@ const LegacySlackIntegrationListItem = ({ isEnabled }) => {
       {isEnabled && (
         <ul className="mt-2 ps-4">
           <li>
-            {/* eslint-disable-next-line react/no-danger */}
             <span
               className="text-danger"
               // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup

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

@@ -34,7 +34,7 @@ TriggerEventCheckBox.propTypes = {
   children: PropTypes.object.isRequired,
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
 const TriggerEventCheckBoxWrapperFC = (props) => {
   const { t } = useTranslation('admin');
 

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

@@ -46,7 +46,7 @@ UserNotificationRow.propTypes = {
   onClickDeleteBtn: PropTypes.func.isRequired,
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
 const UserNotificationRowWrapperWrapperFC = (props) => {
   const { t } = useTranslation();
 

+ 0 - 2
apps/app/src/client/components/Admin/Notification/UserTriggerNotification.jsx

@@ -113,7 +113,6 @@ class UserTriggerNotification extends React.Component {
                   }}
                 />
                 <p className="p-2 mb-0">
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
@@ -142,7 +141,6 @@ class UserTriggerNotification extends React.Component {
                   />
                 </div>
                 <p className="p-2 mb-0">
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{

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

@@ -48,7 +48,7 @@ DeleteAllShareLinksModal.propTypes = {
   onClickDeleteButton: PropTypes.func,
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
 const DeleteAllShareLinksModalWrapperFC = (props) => {
   const { t } = useTranslation('admin');
 

+ 202 - 206
apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.tsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React, { useCallback, useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
@@ -62,243 +61,240 @@ const GitHubSecurityManagementContents = (props: Props) => {
 
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
-      <React.Fragment>
-        <h2 className="alert-anchor border-bottom">
-          {t('security_settings.OAuth.GitHub.name')}
-        </h2>
+      <h2 className="alert-anchor border-bottom">
+        {t('security_settings.OAuth.GitHub.name')}
+      </h2>
 
-        {retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>
-              {t('Error occurred')} : {retrieveError}
-            </p>
+      {retrieveError != null && (
+        <div className="alert alert-danger">
+          <p>
+            {t('Error occurred')} : {retrieveError}
+          </p>
+        </div>
+      )}
+
+      <div className="row my-4">
+        <div className="col-6 offset-3">
+          <div className="form-check form-switch form-check-success">
+            <input
+              id="isGitHubEnabled"
+              className="form-check-input"
+              type="checkbox"
+              checked={
+                adminGeneralSecurityContainer.state.isGitHubEnabled || false
+              }
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled();
+              }}
+            />
+            <label
+              className="form-label form-check-label"
+              htmlFor="isGitHubEnabled"
+            >
+              {t('security_settings.OAuth.GitHub.enable_github')}
+            </label>
           </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 className="row my-4">
-          <div className="col-6 offset-3">
-            <div className="form-check form-switch form-check-success">
-              <input
-                id="isGitHubEnabled"
-                className="form-check-input"
-                type="checkbox"
-                checked={
-                  adminGeneralSecurityContainer.state.isGitHubEnabled || false
-                }
-                onChange={() => {
-                  adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled();
+      <div className="row mb-4">
+        <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">
+          <input
+            id="gitHubCallbackUrl"
+            className="form-control"
+            type="text"
+            value={gitHubCallbackUrl}
+            readOnly
+          />
+          <p className="form-text text-muted small">
+            {t('security_settings.desc_of_callback_URL', {
+              AuthName: 'OAuth',
+            })}
+          </p>
+          {(siteUrl == null || siteUrl === '') && (
+            <div className="alert alert-danger">
+              <span className="material-symbols-outlined">error</span>
+              <span
+                // 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',
+                  }),
                 }}
               />
-              <label
-                className="form-label form-check-label"
-                htmlFor="isGitHubEnabled"
-              >
-                {t('security_settings.OAuth.GitHub.enable_github')}
-              </label>
             </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 className="row mb-4">
-          <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">
-            <input
-              id="gitHubCallbackUrl"
-              className="form-control"
-              type="text"
-              value={gitHubCallbackUrl}
-              readOnly
-            />
-            <p className="form-text text-muted small">
-              {t('security_settings.desc_of_callback_URL', {
-                AuthName: 'OAuth',
-              })}
-            </p>
-            {(siteUrl == null || siteUrl === '') && (
-              <div className="alert alert-danger">
-                <span className="material-symbols-outlined">error</span>
-                <span // eslint-disable-next-line max-len
+      {isGitHubEnabled && (
+        <>
+          <h3 className="border-bottom mb-4">
+            {t('security_settings.configuration')}
+          </h3>
+
+          <div className="row mb-4">
+            <label
+              htmlFor="githubClientId"
+              className="col-3 text-end py-2 form-label"
+            >
+              {t('security_settings.clientID')}
+            </label>
+            <div className="col-6">
+              <input
+                className="form-control"
+                type="text"
+                {...register('githubClientId')}
+              />
+              <p className="form-text text-muted">
+                <small
                   // 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',
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_GITHUB_CLIENT_ID',
                     }),
                   }}
                 />
-              </div>
-            )}
+              </p>
+            </div>
           </div>
-        </div>
 
-        {isGitHubEnabled && (
-          <React.Fragment>
-            <h3 className="border-bottom mb-4">
-              {t('security_settings.configuration')}
-            </h3>
-
-            <div className="row mb-4">
-              <label
-                htmlFor="githubClientId"
-                className="col-3 text-end py-2 form-label"
-              >
-                {t('security_settings.clientID')}
-              </label>
-              <div className="col-6">
-                <input
-                  className="form-control"
-                  type="text"
-                  {...register('githubClientId')}
+          <div className="row mb-3">
+            <label
+              htmlFor="githubClientSecret"
+              className="col-3 text-end py-2 form-label"
+            >
+              {t('security_settings.client_secret')}
+            </label>
+            <div className="col-6">
+              <input
+                className="form-control"
+                type="text"
+                {...register('githubClientSecret')}
+              />
+              <p className="form-text text-muted">
+                <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 className="form-text text-muted">
-                  <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>
-              </div>
+              </p>
             </div>
+          </div>
 
-            <div className="row mb-3">
-              <label
-                htmlFor="githubClientSecret"
-                className="col-3 text-end py-2 form-label"
-              >
-                {t('security_settings.client_secret')}
-              </label>
-              <div className="col-6">
+          <div className="row mb-3">
+            <div className="offset-3 col-6 text-start">
+              <div className="form-check form-check-success">
                 <input
-                  className="form-control"
-                  type="text"
-                  {...register('githubClientSecret')}
+                  id="bindByUserNameGitHub"
+                  className="form-check-input"
+                  type="checkbox"
+                  checked={
+                    adminGitHubSecurityContainer.state
+                      .isSameUsernameTreatedAsIdenticalUser || false
+                  }
+                  onChange={() => {
+                    adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
+                  }}
                 />
-                <p className="form-text text-muted">
-                  <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>
-              </div>
-            </div>
-
-            <div className="row mb-3">
-              <div className="offset-3 col-6 text-start">
-                <div className="form-check form-check-success">
-                  <input
-                    id="bindByUserNameGitHub"
-                    className="form-check-input"
-                    type="checkbox"
-                    checked={
-                      adminGitHubSecurityContainer.state
-                        .isSameUsernameTreatedAsIdenticalUser || false
-                    }
-                    onChange={() => {
-                      adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
-                    }}
-                  />
-                  <label
-                    className="form-check-label"
-                    htmlFor="bindByUserNameGitHub"
-                  >
-                    <span
-                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                      dangerouslySetInnerHTML={{
-                        __html: t(
-                          'security_settings.Treat email matching as identical',
-                        ),
-                      }}
-                    />
-                  </label>
-                </div>
-                <p className="form-text text-muted">
-                  <small
+                <label
+                  className="form-check-label"
+                  htmlFor="bindByUserNameGitHub"
+                >
+                  <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
                       __html: t(
-                        'security_settings.Treat email matching as identical_warn',
+                        'security_settings.Treat email matching as identical',
                       ),
                     }}
                   />
-                </p>
+                </label>
               </div>
+              <p className="form-text text-muted">
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat email matching as identical_warn',
+                    ),
+                  }}
+                />
+              </p>
             </div>
+          </div>
 
-            <div className="row mb-4">
-              <div className="offset-3 col-5">
-                <button
-                  type="submit"
-                  className="btn btn-primary"
-                  disabled={retrieveError != null}
-                >
-                  {t('Update')}
-                </button>
-              </div>
+          <div className="row mb-4">
+            <div className="offset-3 col-5">
+              <button
+                type="submit"
+                className="btn btn-primary"
+                disabled={retrieveError != null}
+              >
+                {t('Update')}
+              </button>
             </div>
-          </React.Fragment>
-        )}
+          </div>
+        </>
+      )}
 
-        <hr />
+      <hr />
 
-        <div style={{ minHeight: '300px' }}>
-          <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>
-          </h4>
-          <div className="card custom-card bg-body-tertiary">
-            <ol id="collapseHelpForGitHubOauth" className="collapse mb-0">
-              {/* eslint-disable-next-line max-len */}
-              <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>
-          </div>
+      <div style={{ minHeight: '300px' }}>
+        <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>
+        </h4>
+        <div className="card custom-card bg-body-tertiary">
+          <ol id="collapseHelpForGitHubOauth" className="collapse mb-0">
+            <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>
         </div>
-      </React.Fragment>
+      </div>
     </form>
   );
 };

+ 213 - 218
apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.tsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import React, { useCallback, useEffect } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
@@ -62,256 +61,252 @@ const GoogleSecurityManagementContents = (props: Props) => {
 
   return (
     <form onSubmit={handleSubmit(onClickSubmit)}>
-      <React.Fragment>
-        <h2 className="alert-anchor border-bottom">
-          {t('security_settings.OAuth.Google.name')}
-        </h2>
+      <h2 className="alert-anchor border-bottom">
+        {t('security_settings.OAuth.Google.name')}
+      </h2>
 
-        {retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>
-              {t('Error occurred')} : {retrieveError}
-            </p>
+      {retrieveError != null && (
+        <div className="alert alert-danger">
+          <p>
+            {t('Error occurred')} : {retrieveError}
+          </p>
+        </div>
+      )}
+
+      <div className="row my-4">
+        <div className="col-6 offset-3">
+          <div className="form-check form-switch form-check-success">
+            <input
+              id="isGoogleEnabled"
+              className="form-check-input"
+              type="checkbox"
+              checked={
+                adminGeneralSecurityContainer.state.isGoogleEnabled || false
+              }
+              onChange={() => {
+                adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled();
+              }}
+            />
+            <label
+              className="form-label form-check-label"
+              htmlFor="isGoogleEnabled"
+            >
+              {t('security_settings.OAuth.Google.enable_google')}
+            </label>
           </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 className="row my-4">
-          <div className="col-6 offset-3">
-            <div className="form-check form-switch form-check-success">
-              <input
-                id="isGoogleEnabled"
-                className="form-check-input"
-                type="checkbox"
-                checked={
-                  adminGeneralSecurityContainer.state.isGoogleEnabled || false
-                }
-                onChange={() => {
-                  adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled();
+      <div className="row mb-5">
+        <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">
+          <input
+            id="googleCallbackUrl"
+            className="form-control"
+            type="text"
+            value={googleCallbackUrl}
+            readOnly
+          />
+          <p className="form-text text-muted small">
+            {t('security_settings.desc_of_callback_URL', {
+              AuthName: 'OAuth',
+            })}
+          </p>
+          {(siteUrl == null || siteUrl === '') && (
+            <div className="alert alert-danger">
+              <span className="material-symbols-outlined">error</span>
+              <span
+                // 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',
+                  }),
                 }}
               />
-              <label
-                className="form-label form-check-label"
-                htmlFor="isGoogleEnabled"
-              >
-                {t('security_settings.OAuth.Google.enable_google')}
-              </label>
             </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 className="row mb-5">
-          <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">
-            <input
-              id="googleCallbackUrl"
-              className="form-control"
-              type="text"
-              value={googleCallbackUrl}
-              readOnly
-            />
-            <p className="form-text text-muted small">
-              {t('security_settings.desc_of_callback_URL', {
-                AuthName: 'OAuth',
-              })}
-            </p>
-            {(siteUrl == null || siteUrl === '') && (
-              <div className="alert alert-danger">
-                <span className="material-symbols-outlined">error</span>
-                <span
-                  // eslint-disable-next-line max-len
+      {isGoogleEnabled && (
+        <React.Fragment>
+          <h3 className="border-bottom mb-4">
+            {t('security_settings.configuration')}
+          </h3>
+
+          <div className="row mb-4">
+            <label
+              htmlFor="googleClientId"
+              className="col-3 text-end py-2 form-label"
+            >
+              {t('security_settings.clientID')}
+            </label>
+            <div className="col-6">
+              <input
+                className="form-control"
+                type="text"
+                {...register('googleClientId')}
+              />
+              <p className="form-text text-muted">
+                <small
                   // 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',
+                    __html: t('security_settings.Use env var if empty', {
+                      env: 'OAUTH_GOOGLE_CLIENT_ID',
                     }),
                   }}
                 />
-              </div>
-            )}
+              </p>
+            </div>
           </div>
-        </div>
 
-        {isGoogleEnabled && (
-          <React.Fragment>
-            <h3 className="border-bottom mb-4">
-              {t('security_settings.configuration')}
-            </h3>
-
-            <div className="row mb-4">
-              <label
-                htmlFor="googleClientId"
-                className="col-3 text-end py-2 form-label"
-              >
-                {t('security_settings.clientID')}
-              </label>
-              <div className="col-6">
-                <input
-                  className="form-control"
-                  type="text"
-                  {...register('googleClientId')}
+          <div className="row mb-4">
+            <label
+              htmlFor="googleClientSecret"
+              className="col-3 text-end py-2 form-label"
+            >
+              {t('security_settings.client_secret')}
+            </label>
+            <div className="col-6">
+              <input
+                className="form-control"
+                type="password"
+                {...register('googleClientSecret')}
+              />
+              <p className="form-text text-muted">
+                <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 className="form-text text-muted">
-                  <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>
-              </div>
+              </p>
             </div>
+          </div>
 
-            <div className="row mb-4">
-              <label
-                htmlFor="googleClientSecret"
-                className="col-3 text-end py-2 form-label"
-              >
-                {t('security_settings.client_secret')}
-              </label>
-              <div className="col-6">
+          <div className="row mb-3">
+            <div className="offset-3 col-6">
+              <div className="form-check form-check-success">
                 <input
-                  className="form-control"
-                  type="password"
-                  {...register('googleClientSecret')}
+                  id="bindByUserNameGoogle"
+                  className="form-check-input"
+                  type="checkbox"
+                  checked={
+                    adminGoogleSecurityContainer.state
+                      .isSameEmailTreatedAsIdenticalUser || false
+                  }
+                  onChange={() => {
+                    adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
+                  }}
                 />
-                <p className="form-text text-muted">
-                  <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>
-              </div>
-            </div>
-
-            <div className="row mb-3">
-              <div className="offset-3 col-6">
-                <div className="form-check form-check-success">
-                  <input
-                    id="bindByUserNameGoogle"
-                    className="form-check-input"
-                    type="checkbox"
-                    checked={
-                      adminGoogleSecurityContainer.state
-                        .isSameEmailTreatedAsIdenticalUser || false
-                    }
-                    onChange={() => {
-                      adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser();
-                    }}
-                  />
-                  <label
-                    className="form-check-label"
-                    htmlFor="bindByUserNameGoogle"
-                  >
-                    <span
-                      // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
-                      dangerouslySetInnerHTML={{
-                        __html: t(
-                          'security_settings.Treat email matching as identical',
-                        ),
-                      }}
-                    />
-                  </label>
-                </div>
-                <p className="form-text text-muted">
-                  <small
+                <label
+                  className="form-check-label"
+                  htmlFor="bindByUserNameGoogle"
+                >
+                  <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
                       __html: t(
-                        'security_settings.Treat email matching as identical_warn',
+                        'security_settings.Treat email matching as identical',
                       ),
                     }}
                   />
-                </p>
+                </label>
               </div>
+              <p className="form-text text-muted">
+                <small
+                  // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
+                  dangerouslySetInnerHTML={{
+                    __html: t(
+                      'security_settings.Treat email matching as identical_warn',
+                    ),
+                  }}
+                />
+              </p>
             </div>
+          </div>
 
-            <div className="row mb-4">
-              <div className="offset-3 col-5">
-                <button
-                  type="submit"
-                  className="btn btn-primary"
-                  disabled={retrieveError != null}
-                >
-                  {t('Update')}
-                </button>
-              </div>
+          <div className="row mb-4">
+            <div className="offset-3 col-5">
+              <button
+                type="submit"
+                className="btn btn-primary"
+                disabled={retrieveError != null}
+              >
+                {t('Update')}
+              </button>
             </div>
-          </React.Fragment>
-        )}
+          </div>
+        </React.Fragment>
+      )}
 
-        <hr />
+      <hr />
 
-        <div style={{ minHeight: '300px' }}>
-          <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>
-          </h4>
-          <div className="card custom-card bg-body-tertiary">
-            <ol id="collapseHelpForGoogleOauth" className="collapse mb-0">
-              {/* eslint-disable-next-line max-len */}
-              <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>
-          </div>
+      <div style={{ minHeight: '300px' }}>
+        <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>
+        </h4>
+        <div className="card custom-card bg-body-tertiary">
+          <ol id="collapseHelpForGoogleOauth" className="collapse mb-0">
+            <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>
         </div>
-      </React.Fragment>
+      </div>
     </form>
   );
 };

+ 0 - 12
apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.tsx

@@ -157,7 +157,6 @@ const LdapSecuritySettingContents = (props: Props) => {
               <small>
                 <p
                   className="form-text text-muted"
-                  // eslint-disable-next-line react/no-danger
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                   dangerouslySetInnerHTML={{
                     __html: t('security_settings.ldap.server_url_detail'),
@@ -238,7 +237,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                   <small>
                     {t('security_settings.ldap.bind_DN_user_detail1')}
                     <br />
-                    {/* eslint-disable-next-line react/no-danger */}
                     <span
                       // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                       dangerouslySetInnerHTML={{
@@ -322,7 +320,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                 <small>
                   {t('security_settings.ldap.search_filter_detail1')}
                   <br />
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
@@ -330,7 +327,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                     }}
                   />
                   <br />
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
@@ -374,7 +370,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                 {...register('ldapAttrMapUsername')}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                   dangerouslySetInnerHTML={{
@@ -405,7 +400,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                   htmlFor="isSameUsernameTreatedAsIdenticalUser"
                 >
                   <span
-                    // eslint-disable-next-line react/no-danger
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
                       __html: t(
@@ -416,7 +410,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                 </label>
               </div>
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                   dangerouslySetInnerHTML={{
@@ -489,7 +482,6 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               <p className="form-text text-muted">
                 <small>
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
@@ -521,7 +513,6 @@ const LdapSecuritySettingContents = (props: Props) => {
               />
               <p className="form-text text-muted">
                 <small>
-                  {/* eslint-disable react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
@@ -548,13 +539,11 @@ const LdapSecuritySettingContents = (props: Props) => {
                       ),
                     }}
                   />
-                  {/* eslint-enable react/no-danger */}
                 </small>
               </p>
               <p className="form-text text-muted">
                 <small>
                   {t('security_settings.example')}:
-                  {/* eslint-disable-next-line react/no-danger */}
                   <span
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
@@ -585,7 +574,6 @@ const LdapSecuritySettingContents = (props: Props) => {
                 {...register('ldapGroupDnProperty')}
               />
               <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
                 <small
                   // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                   dangerouslySetInnerHTML={{

+ 0 - 1
apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.tsx

@@ -75,7 +75,6 @@ const LocalSecuritySettingContents = (props: Props): JSX.Element => {
       {adminLocalSecurityContainer.state.useOnlyEnvVars && (
         <p
           className="alert alert-info"
-          // eslint-disable-next-line max-len
           // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
           dangerouslySetInnerHTML={{
             __html: t('security_settings.Local.note for the only env option', {

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

@@ -180,7 +180,6 @@ const OidcSecurityManagementContents = (props: Props) => {
             <div className="alert alert-danger">
               <span className="material-symbols-outlined">error</span>
               <span
-                // eslint-disable-next-line max-len
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
                   __html: t('alert.siteUrl_is_not_set', {
@@ -628,7 +627,6 @@ const OidcSecurityManagementContents = (props: Props) => {
                 <div className="alert alert-danger">
                   <span className="material-symbols-outlined">error</span>
                   <span
-                    // eslint-disable-next-line max-len
                     // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                     dangerouslySetInnerHTML={{
                       __html: t('alert.siteUrl_is_not_set', {

+ 0 - 4
apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import React, { useCallback, useEffect, useState } from 'react';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
@@ -158,7 +157,6 @@ const SamlSecurityManagementContents = (props: Props) => {
             <div className="alert alert-danger">
               <span className="material-symbols-outlined">error</span>
               <span
-                // eslint-disable-next-line max-len
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{
                   __html: t('alert.siteUrl_is_not_set', {
@@ -466,7 +464,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapFirstName')}
                   />
                   <p className="form-text text-muted">
-                    {/* eslint-disable-next-line max-len */}
                     <small
                       // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                       dangerouslySetInnerHTML={{
@@ -522,7 +519,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     {...register('samlAttrMapLastName')}
                   />
                   <p className="form-text text-muted">
-                    {/* eslint-disable-next-line max-len */}
                     <small
                       // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                       dangerouslySetInnerHTML={{

+ 0 - 2
apps/app/src/client/components/Admin/Security/SecuritySetting/PageAccessRightsSettings.tsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import type React from 'react';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
@@ -68,7 +67,6 @@ export const PageAccessRightsSettings: React.FC<Props> = ({
               <span className="material-symbols-outlined me-1">error</span>
               <b>FIXED</b>
               <br />
-              {/* eslint-disable-next-line react/no-danger */}
               <b
                 // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                 dangerouslySetInnerHTML={{

+ 0 - 1
apps/app/src/client/components/Admin/Security/SecuritySetting/PageDeleteRightsSettings.tsx

@@ -330,7 +330,6 @@ export const PageDeleteRightsSettings: React.FC<Props> = ({
                     <p className="card custom-card bg-warning-sublte">
                       <span className="text-warning">
                         <span className="material-symbols-outlined">info</span>
-                        {/* eslint-disable-next-line react/no-danger */}
                         <span
                           // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
                           dangerouslySetInnerHTML={{

+ 0 - 1
apps/app/src/client/components/Admin/Security/SecuritySetting/SessionMaxAgeSettings.tsx

@@ -25,7 +25,6 @@ export const SessionMaxAgeSettings: React.FC<Props> = ({ register, t }) => {
             {...register('sessionMaxAge')}
             placeholder="2592000000"
           />
-          {/* eslint-disable-next-line react/no-danger */}
           <p
             className="form-text text-muted"
             // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup

+ 0 - 1
apps/app/src/client/components/Admin/Security/SecuritySetting/UserHomepageDeletionSettings.tsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import type React from 'react';
 
 import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';

+ 52 - 0
apps/app/src/client/components/Admin/Security/SecuritySetting/UserPageVisibilitySettings.tsx

@@ -0,0 +1,52 @@
+/* eslint-disable react/no-danger */
+import type React from 'react';
+
+import type AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+
+type Props = {
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
+  t: (key: string) => string;
+};
+
+export const UserPageVisibilitySettings: React.FC<Props> = ({
+  adminGeneralSecurityContainer,
+  t,
+}) => {
+  return (
+    <>
+      <h4 className="mb-3">
+        {t('security_settings.disable_user_pages.disable_user_pages')}
+      </h4>
+      <div className="row mb-4">
+        <div className="col-md-10 offset-md-2">
+          <div className="form-check form-switch form-check-success">
+            <input
+              type="checkbox"
+              className="form-check-input"
+              id="is-user-pages-visible"
+              checked={adminGeneralSecurityContainer.state.disableUserPages}
+              onChange={() => {
+                adminGeneralSecurityContainer.changeUserPageVisibility();
+              }}
+            />
+            <label
+              className="form-label form-check-label"
+              htmlFor="is-user-pages-visible"
+            >
+              {t(
+                'security_settings.disable_user_pages.disable_user_pages_label',
+              )}
+            </label>
+          </div>
+          <p
+            className="form-text text-muted small mt-2"
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: includes <br> and <code> from i18n strings
+            dangerouslySetInnerHTML={{
+              __html: t('security_settings.disable_user_pages.desc'),
+            }}
+          />
+        </div>
+      </div>
+    </>
+  );
+};

+ 7 - 0
apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx

@@ -13,6 +13,7 @@ import { PageDeleteRightsSettings } from './PageDeleteRightsSettings';
 import { PageListDisplaySettings } from './PageListDisplaySettings';
 import { SessionMaxAgeSettings } from './SessionMaxAgeSettings';
 import { UserHomepageDeletionSettings } from './UserHomepageDeletionSettings';
+import { UserPageVisibilitySettings } from './UserPageVisibilitySettings';
 
 type FormData = {
   sessionMaxAge: string;
@@ -63,6 +64,8 @@ const SecuritySettingComponent: React.FC<Props> = ({
           hideRestrictedByOwner:
             adminGeneralSecurityContainer.state
               .currentOwnerRestrictionDisplayMode === 'Hidden',
+          disableUserPages:
+            adminGeneralSecurityContainer.state.disableUserPages,
           isUsersHomepageDeletionEnabled:
             adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
           isForceDeleteUserHomepageOnUserDeletion:
@@ -114,6 +117,10 @@ const SecuritySettingComponent: React.FC<Props> = ({
             adminGeneralSecurityContainer={adminGeneralSecurityContainer}
             t={t}
           />
+          <UserPageVisibilitySettings
+            adminGeneralSecurityContainer={adminGeneralSecurityContainer}
+            t={t}
+          />
           <CommentManageRightsSettings
             adminGeneralSecurityContainer={adminGeneralSecurityContainer}
             t={t}

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

@@ -35,7 +35,6 @@ const BridgeCore = (props: BridgeCoreProps): JSX.Element => {
           <span className={iconClass}>{iconName}</span>
           <small
             className="ms-2 d-none d-lg-inline"
-            // eslint-disable-next-line react/no-danger
             // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
             dangerouslySetInnerHTML={{ __html: description }}
           />
@@ -52,7 +51,6 @@ const BridgeCore = (props: BridgeCoreProps): JSX.Element => {
         className="d-block d-lg-none"
       >
         <small
-          // eslint-disable-next-line react/no-danger
           // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
           dangerouslySetInnerHTML={{ __html: description }}
         />

+ 0 - 2
apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -80,7 +80,6 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
             readOnly
           />
           <p className="form-text text-muted">
-            {/* eslint-disable-next-line max-len, react/no-danger */}
             <small
               // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
               dangerouslySetInnerHTML={{
@@ -114,7 +113,6 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
             readOnly
           />
           <p className="form-text text-muted">
-            {/* eslint-disable-next-line react/no-danger */}
             <small
               // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
               dangerouslySetInnerHTML={{

+ 1 - 5
apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -31,8 +31,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
   const successMessage = 'Successfully sent to Slack workspace.';
 
   const { t } = useTranslation();
-  // eslint-disable-next-line no-unused-vars
-  const [defaultOpenAccordionKeys, setDefaultOpenAccordionKeys] = useState(
+  const [defaultOpenAccordionKeys, _setDefaultOpenAccordionKeys] = useState(
     new Set([activeStep]),
   );
   const [isLatestConnectionSuccess, setIsLatestConnectionSuccess] =
@@ -177,7 +176,6 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         defaultIsActive={defaultOpenAccordionKeys.has(
           botInstallationStep.REGISTER_SLACK_CONFIGURATION,
         )}
-        // eslint-disable-next-line max-len
         title={
           <>
             <span className="me-3">3</span>
@@ -202,7 +200,6 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         defaultIsActive={defaultOpenAccordionKeys.has(
           botInstallationStep.CONNECTION_TEST,
         )}
-        // eslint-disable-next-line max-len
         title={
           <>
             <span className="me-3">4</span>
@@ -219,7 +216,6 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         defaultIsActive={defaultOpenAccordionKeys.has(
           botInstallationStep.CONNECTION_TEST,
         )}
-        // eslint-disable-next-line max-len
         title={
           <>
             <span className="me-3">5</span>

+ 0 - 1
apps/app/src/client/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx

@@ -48,7 +48,6 @@ export const DeleteSlackBotSettingsModal = React.memo(
         : t('admin:slack_integration.slackbot_settings_notice');
       return (
         <span
-          // eslint-disable-next-line react/no-danger
           // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
           dangerouslySetInnerHTML={{ __html: htmlContent }}
         />

+ 1 - 1
apps/app/src/client/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -236,7 +236,7 @@ PermissionSettingsForEachCategoryComponent.propTypes = {
   permissionSettings: PropTypes.object,
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
 const ManageCommandsProcess = ({
   slackAppIntegrationId,
   permissionsForBroadcastUseCommands,

+ 1 - 2
apps/app/src/client/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx

@@ -174,7 +174,7 @@ SinglePermissionSettingComponent.propTypes = {
   onPermissionListChanged: PropTypes.func,
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+// biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
 const ManageCommandsProcessWithoutProxy = ({
   commandPermission,
   eventActionsPermission,
@@ -263,7 +263,6 @@ const ManageCommandsProcessWithoutProxy = ({
           <div className="form-check">
             <div className="row mb-5 d-block">
               {defaultCommandsName.map((commandName) => {
-                // eslint-disable-next-line max-len
                 return (
                   <SinglePermissionSettingComponent
                     key={`${commandName}-component`}

+ 1 - 7
apps/app/src/client/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/prop-types */
-import React, { useState } from 'react';
+import { useState } from 'react';
 import { SlackbotType } from '@growi/slack';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
@@ -138,7 +137,6 @@ const RegisteringProxyUrlProcess = () => {
       <ol>
         <li>
           <p
-            // eslint-disable-next-line react/no-danger
             // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
             dangerouslySetInnerHTML={{
               __html: t('admin:slack_integration.accordion.copy_proxy_url'),
@@ -154,7 +152,6 @@ const RegisteringProxyUrlProcess = () => {
         </li>
         <li>
           <p
-            // eslint-disable-next-line react/no-danger
             // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
             dangerouslySetInnerHTML={{
               __html: t(
@@ -271,7 +268,6 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = (props) => {
           <li>
             <p
               className="ms-2"
-              // eslint-disable-next-line react/no-danger
               // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
               dangerouslySetInnerHTML={{
                 __html: t(
@@ -285,7 +281,6 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = (props) => {
               className="ms-2"
               // TODO: Add dynamic link
               // TODO: Add logo
-              // eslint-disable-next-line react/no-danger
               // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
               dangerouslySetInnerHTML={{
                 __html: t('admin:slack_integration.accordion.paste_growi_url'),
@@ -309,7 +304,6 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = (props) => {
           <li>
             <p
               className="ms-2"
-              // eslint-disable-next-line react/no-danger
               // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
               dangerouslySetInnerHTML={{
                 __html: t(

+ 0 - 5
apps/app/src/client/components/Admin/UserGroupDetail/use-user-group-resource.ts

@@ -13,7 +13,6 @@ import {
   useSWRxUserGroupRelations,
 } from '~/stores/user-group';
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export const useUserGroup = (userGroupId: string, isExternalGroup: boolean) => {
   const userGroupRes = useSWRxUserGroup(isExternalGroup ? null : userGroupId);
   const externalUserGroupRes = useSWRxExternalUserGroup(
@@ -22,7 +21,6 @@ export const useUserGroup = (userGroupId: string, isExternalGroup: boolean) => {
   return isExternalGroup ? externalUserGroupRes : userGroupRes;
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export const useUserGroupRelations = (
   userGroupId: string,
   isExternalGroup: boolean,
@@ -36,7 +34,6 @@ export const useUserGroupRelations = (
   return isExternalGroup ? externalUserGroupRes : userGroupRes;
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export const useChildUserGroupList = (
   userGroupId: string,
   isExternalGroup: boolean,
@@ -52,7 +49,6 @@ export const useChildUserGroupList = (
   return isExternalGroup ? externalUserGroupRes : userGroupRes;
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export const useUserGroupRelationList = (
   userGroupIds: string[],
   isExternalGroup: boolean,
@@ -66,7 +62,6 @@ export const useUserGroupRelationList = (
   return isExternalGroup ? externalUserGroupRes : userGroupRes;
 };
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export const useAncestorUserGroups = (
   userGroupId: string,
   isExternalGroup: boolean,

+ 0 - 1
apps/app/src/client/components/Admin/Users/GrantAdminButton.tsx

@@ -40,7 +40,6 @@ const GrantAdminButton = (props: GrantAdminButtonProps): JSX.Element => {
 /**
  * Wrapper component for using unstated
  */
-// eslint-disable-next-line max-len
 const GrantAdminButtonWrapper: React.ForwardRefExoticComponent<
   Pick<any, string | number | symbol> & React.RefAttributes<any>
 > = withUnstatedContainers(GrantAdminButton, [AdminUsersContainer]);

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