فهرست منبع

Merge branch 'feat/158232-prevent-inline-mime-type-sniffing-vulnerabilities' into feat/167084-mime-type-settings-frontend-component

arvid-e 4 ماه پیش
والد
کامیت
883058f68d
100فایلهای تغییر یافته به همراه3601 افزوده شده و 1061 حذف شده
  1. 1 1
      .changeset/config.json
  2. 15 7
      .devcontainer/app/devcontainer.json
  3. 4 0
      .devcontainer/app/postCreateCommand.sh
  4. 1 2
      .devcontainer/compose.extend.template.yml
  5. 6 10
      .devcontainer/compose.yml
  6. 2 0
      .devcontainer/pdf-converter/postCreateCommand.sh
  7. 1 1
      .github/ISSUE_TEMPLATE/bug-report.md
  8. 1 1
      .github/ISSUE_TEMPLATE/config.yml
  9. 1 1
      .github/mergify.yml
  10. 3 3
      .github/workflows/ci-app-prod.yml
  11. 5 3
      .github/workflows/ci-app.yml
  12. 7 5
      .github/workflows/release-pdf-converter.yml
  13. 4 3
      .github/workflows/release-rc-scheduled.yml
  14. 33 10
      .github/workflows/release-rc.yml
  15. 1 1
      .github/workflows/release-slackbot-proxy.yml
  16. 49 16
      .github/workflows/release.yml
  17. 1 1
      .github/workflows/reusable-app-build-image.yml
  18. 5 2
      .github/workflows/reusable-app-create-manifests.yml
  19. 19 8
      .github/workflows/reusable-app-prod.yml
  20. 1 1
      .github/workflows/reusable-app-reg-suit.yml
  21. 22 0
      .mcp.json
  22. 0 9
      .roo/mcp.json
  23. 1 0
      .serena/.gitignore
  24. 398 0
      .serena/memories/apps-app-admin-forms-react-hook-form-migration-guide.md
  25. 186 0
      .serena/memories/apps-app-pagetree-performance-refactor-plan.md
  26. 71 0
      .serena/memories/coding_conventions.md
  27. 45 0
      .serena/memories/development_environment.md
  28. 26 0
      .serena/memories/project_overview.md
  29. 90 0
      .serena/memories/project_structure.md
  30. 100 0
      .serena/memories/suggested_commands.md
  31. 95 0
      .serena/memories/task_completion_checklist.md
  32. 42 0
      .serena/memories/tech_stack.md
  33. 68 0
      .serena/project.yml
  34. 20 0
      .vscode/mcp.json
  35. 63 1
      .vscode/settings.json
  36. 303 92
      CHANGELOG.md
  37. 95 0
      CLAUDE.md
  38. 1 1
      LICENSE
  39. 12 12
      README.md
  40. 12 12
      README_JP.md
  41. 1 1
      THIRD-PARTY-NOTICES.md
  42. 4 2
      apps/app/.env.development
  43. 4 2
      apps/app/.env.production
  44. 51 10
      apps/app/.eslintrc.js
  45. 2 1
      apps/app/bin/openapi/definition-apiv1.js
  46. 6 13
      apps/app/bin/openapi/definition-apiv3.js
  47. 15 12
      apps/app/bin/openapi/generate-operation-ids/cli.spec.ts
  48. 5 4
      apps/app/bin/openapi/generate-operation-ids/cli.ts
  49. 31 33
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts
  50. 42 16
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts
  51. 0 1
      apps/app/bin/openapi/generate-spec-apiv3.sh
  52. 416 0
      apps/app/bin/print-memory-consumption.ts
  53. 0 8
      apps/app/config/cdn.js
  54. 1 1
      apps/app/config/logger/config.dev.js
  55. 3 3
      apps/app/config/migrate-mongo-config.js
  56. 4 8
      apps/app/config/migrate-mongo-config.spec.ts
  57. 7 7
      apps/app/config/next-i18next.config.js
  58. 10 10
      apps/app/docker/README.md
  59. 44 44
      apps/app/docker/codebuild/.terraform.lock.hcl
  60. 1 1
      apps/app/docker/codebuild/buildspec.yml
  61. 1 1
      apps/app/docker/codebuild/codebuild.tf
  62. 1 1
      apps/app/docker/codebuild/main.tf
  63. 1 1
      apps/app/docker/codebuild/oidc.tf
  64. 11 4
      apps/app/jest.config.js
  65. 19 16
      apps/app/next.config.js
  66. 1 4
      apps/app/nodemon.json
  67. 30 23
      apps/app/package.json
  68. 14 15
      apps/app/playwright.config.ts
  69. 13 5
      apps/app/playwright/10-installer/install.spec.ts
  70. 101 73
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  71. 17 9
      apps/app/playwright/20-basic-features/access-to-pagelist.spec.ts
  72. 10 7
      apps/app/playwright/20-basic-features/click-page-icons.spec.ts
  73. 13 9
      apps/app/playwright/20-basic-features/comments.spec.ts
  74. 16 6
      apps/app/playwright/20-basic-features/create-page-button.spec.ts
  75. 9 5
      apps/app/playwright/20-basic-features/presentation.spec.ts
  76. 35 13
      apps/app/playwright/20-basic-features/sticky-features.spec.ts
  77. 25 15
      apps/app/playwright/20-basic-features/use-tools.spec.ts
  78. 9 9
      apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts
  79. 10 5
      apps/app/playwright/21-basic-features-for-guest/sticky-for-guest.spec.ts
  80. 38 33
      apps/app/playwright/22-sharelink/access-to-sharelink.spec.ts
  81. 13 9
      apps/app/playwright/23-editor/saving.spec.ts
  82. 8 4
      apps/app/playwright/23-editor/template-modal.spec.ts
  83. 31 17
      apps/app/playwright/23-editor/with-navigation.spec.ts
  84. 82 68
      apps/app/playwright/30-search/search.spect.ts
  85. 29 21
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  86. 20 16
      apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts
  87. 1 2
      apps/app/playwright/50-sidebar/switching-sidebar-mode.spec.ts
  88. 49 29
      apps/app/playwright/60-home/home.spec.ts
  89. 1 1
      apps/app/playwright/auth.setup.ts
  90. 8 4
      apps/app/playwright/utils/CollapseSidebar.ts
  91. 1 2
      apps/app/playwright/utils/Login.ts
  92. 40 40
      apps/app/public/images/icons/favicon/manifest.json
  93. 33 43
      apps/app/public/static/locales/en_US/admin.json
  94. 92 36
      apps/app/public/static/locales/en_US/commons.json
  95. 86 26
      apps/app/public/static/locales/en_US/translation.json
  96. 2 12
      apps/app/public/static/locales/fr_FR/admin.json
  97. 92 23
      apps/app/public/static/locales/fr_FR/commons.json
  98. 87 27
      apps/app/public/static/locales/fr_FR/translation.json
  99. 2 12
      apps/app/public/static/locales/ja_JP/admin.json
  100. 93 35
      apps/app/public/static/locales/ja_JP/commons.json

+ 1 - 1
.changeset/config.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
-  "changelog": ["@changesets/changelog-github", { "repo": "weseek/growi" }],
+  "changelog": ["@changesets/changelog-github", { "repo": "growilabs/growi" }],
   "commit": false,
   "fixed": [],
   "linked": [],

+ 15 - 7
.devcontainer/app/devcontainer.json

@@ -23,21 +23,29 @@
   "customizations": {
     "vscode": {
       "extensions": [
+        // AI
+        "anthropic.claude-code",
+        // linter
         "dbaeumer.vscode-eslint",
         "biomejs.biome",
+        "editorconfig.editorconfig",
+        "shinnn.stylelint",
+        "stylelint.vscode-stylelint",
+        // Test
+        "vitest.explorer",
+        "ms-playwright.playwright",
+        // git/github
+        "codeinklingon.git-worktree-menu",
+        "github.vscode-pull-request-github",
         "mhutchie.git-graph",
         "eamodio.gitlens",
-        "github.vscode-pull-request-github",
         "cschleiden.vscode-github-actions",
+        // DB
         "cweijan.vscode-database-client2",
         "mongodb.mongodb-vscode",
+        // Debug
         "msjsdiag.debugger-for-chrome",
-        "firefox-devtools.vscode-firefox-debug",
-        "editorconfig.editorconfig",
-        "shinnn.stylelint",
-        "stylelint.vscode-stylelint",
-        "vitest.explorer",
-        "ms-playwright.playwright"
+        "firefox-devtools.vscode-firefox-debug"
       ],
       "settings": {
         "terminal.integrated.defaultProfile.linux": "bash"

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

@@ -17,9 +17,13 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
 # Setup pnpm
 SHELL=bash pnpm setup
 eval "$(cat /home/vscode/.bashrc)"
+pnpm config set store-dir /workspace/.pnpm-store
 
 # Install turbo
 pnpm install turbo --global
 
+# Install Claude Code
+pnpm install @anthropic-ai/claude-code --global
+
 # Install dependencies
 turbo run bootstrap

+ 1 - 2
.devcontainer/compose.extend.template.yml

@@ -6,7 +6,6 @@ services:
     image: mcr.microsoft.com/vscode/devcontainers/javascript-node:1-20
     volumes:
       - ..:/workspace/growi:delegated
-      - pnpm-store:/workspace/growi/.pnpm-store
-      - node_modules:/workspace/growi/node_modules
+      - pnpm-store:/workspace/.pnpm-store
       - page_bulk_export_tmp:/tmp/page-bulk-export
     tty: true

+ 6 - 10
.devcontainer/compose.yml

@@ -3,9 +3,7 @@ services:
     image: mcr.microsoft.com/devcontainers/base:ubuntu
     volumes:
       - ..:/workspace/growi:delegated
-      - pnpm-store:/workspace/growi/.pnpm-store
-      - node_modules:/workspace/growi/node_modules
-      - buildcache_app:/workspace/growi/apps/app/.next
+      - pnpm-store:/workspace/.pnpm-store
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../share:/workspace/share:delegated
       - page_bulk_export_tmp:/tmp/page-bulk-export
@@ -15,7 +13,7 @@ services:
     - opentelemetry-collector-dev-setup_default
 
   mongo:
-    image: mongo:6.0
+    image: mongo:8.0
     restart: unless-stopped
     ports:
       - 27017
@@ -23,13 +21,13 @@ services:
       - /data/db
 
   # This container requires '../../growi-docker-compose' repository
-  #   cloned from https://github.com/weseek/growi-docker-compose.git
+  #   cloned from https://github.com/growilabs/growi-docker-compose.git
   elasticsearch:
     build:
-      context: ../../growi-docker-compose/elasticsearch/v8
+      context: ../../growi-docker-compose/elasticsearch/v9
       dockerfile: ./Dockerfile
       args:
-        - version=8.7.0
+        - version=9.0.3
     restart: unless-stopped
     ports:
       - 9200
@@ -43,12 +41,10 @@ services:
         hard: -1
     volumes:
       - /usr/share/elasticsearch/data
-      - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
+      - ../../growi-docker-compose/elasticsearch/v9/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
 volumes:
   pnpm-store:
-  node_modules:
-  buildcache_app:
   page_bulk_export_tmp:
 
 networks:

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

@@ -12,6 +12,8 @@ sudo chmod 700 /tmp/page-bulk-export
 # Setup pnpm
 SHELL=bash pnpm setup
 eval "$(cat /home/node/.bashrc)"
+pnpm config set store-dir /workspace/.pnpm-store
+
 # Update pnpm
 pnpm i -g pnpm
 

+ 1 - 1
.github/ISSUE_TEMPLATE/bug-report.md

@@ -18,7 +18,7 @@ Environment
 |Using Docker|yes/no|
 |Using [growi-docker-compose][growi-docker-compose]|yes/no|
 
-[growi-docker-compose]: https://github.com/weseek/growi-docker-compose
+[growi-docker-compose]: https://github.com/growilabs/growi-docker-compose
 
 *(Accessing https://{GROWI_HOST}/admin helps you to fill in above versions)*
 

+ 1 - 1
.github/ISSUE_TEMPLATE/config.yml

@@ -1,7 +1,7 @@
 blank_issues_enabled: false
 contact_links:
   - name: User request or Suggestions
-    url: https://github.com/weseek/growi/discussions
+    url: https://github.com/growilabs/growi/discussions
     about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
   - name: Questions
     url: https://communityinviter.com/apps/wsgrowi/invite/

+ 1 - 1
.github/mergify.yml

@@ -13,7 +13,7 @@ queue_rules:
       - check-success ~= ci-app-test
       - check-success ~= ci-app-launch-dev
       - check-success = test-prod-node20 / build-prod
-      - check-success = test-prod-node20 / launch-prod
+      - check-success ~= test-prod-node20 / launch-prod
       - check-success ~= test-prod-node20 / run-playwright
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-slackbot-

+ 3 - 3
.github/workflows/ci-app-prod.yml

@@ -40,7 +40,7 @@ concurrency:
 jobs:
 
   test-prod-node18:
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    uses: growilabs/growi/.github/workflows/reusable-app-prod.yml@master
     if: |
       ( github.event_name == 'push'
         || github.base_ref == 'master'
@@ -55,7 +55,7 @@ jobs:
 
 
   test-prod-node20:
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    uses: growilabs/growi/.github/workflows/reusable-app-prod.yml@master
     if: |
       ( github.event_name == 'push'
         || github.base_ref == 'master'
@@ -71,7 +71,7 @@ jobs:
   # run-reg-suit-node20:
   #   needs: [test-prod-node20]
 
-  #   uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
+  #   uses: growilabs/growi/.github/workflows/reusable-app-reg-suit.yml@master
 
   #   if: always()
 

+ 5 - 3
.github/workflows/ci-app.yml

@@ -93,10 +93,11 @@ jobs:
     strategy:
       matrix:
         node-version: [20.x]
+        mongodb-version: ['6.0', '8.0']
 
     services:
       mongodb:
-        image: mongo:6.0
+        image: mongo:${{ matrix.mongodb-version }}
         ports:
           - 27017/tcp
 
@@ -135,7 +136,7 @@ jobs:
       - name: Upload coverage report as artifact
         uses: actions/upload-artifact@v4
         with:
-          name: Coverage Report
+          name: coverage-mongo${{ matrix.mongodb-version }}
           path: |
             apps/app/coverage
             packages/remark-growi-directive/coverage
@@ -157,10 +158,11 @@ jobs:
     strategy:
       matrix:
         node-version: [20.x]
+        mongodb-version: ['6.0', '8.0']
 
     services:
       mongodb:
-        image: mongo:6.0
+        image: mongo:${{ matrix.mongodb-version }}
         ports:
           - 27017/tcp
 

+ 7 - 5
.github/workflows/release-pdf-converter.yml

@@ -16,19 +16,21 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       with:
         workingDir: apps/pdf-converter
 
     - name: Docker meta
       id: meta
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       with:
         images: growilabs/pdf-converter
         tags: |
           type=raw,value=latest
-          type=raw,value=${{ steps.package-json.outputs.packageVersion }}
+          type=semver,value=${{ steps.package-json.outputs.packageVersion }},pattern={{major}}
+          type=semver,value=${{ steps.package-json.outputs.packageVersion }},pattern={{major}}.{{minor}}
+          type=semver,value=${{ steps.package-json.outputs.packageVersion }},pattern={{major}}.{{minor}}.{{patch}}
 
     - name: Login to docker.io registry
       run: |
@@ -57,7 +59,7 @@ jobs:
         VERBOSE : true
 
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v3
+      uses: peter-evans/dockerhub-description@v4
       with:
         username: growimoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
@@ -96,7 +98,7 @@ jobs:
         turbo run version:prerelease --filter=@growi/pdf-converter
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       with:
         workingDir: apps/pdf-converter

+ 4 - 3
.github/workflows/release-rc-scheduled.yml

@@ -46,9 +46,9 @@ jobs:
 
 
   build-image-rc:
-    uses: weseek/growi/.github/workflows/reusable-app-build-image.yml@master
+    uses: growilabs/growi/.github/workflows/reusable-app-build-image.yml@master
     with:
-      image-name: weseek/growi
+      image-name: growilabs/growi
       tag-temporary: latest-rc
     secrets:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
@@ -57,11 +57,12 @@ jobs:
   publish-image-rc:
     needs: [determine-tags, build-image-rc]
 
-    uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
+    uses: growilabs/growi/.github/workflows/reusable-app-create-manifests.yml@master
     with:
       tags: ${{ needs.determine-tags.outputs.TAGS }}
       registry: docker.io
       image-name: weseek/growi
+      docker-registry-username: wsmoogle
       tag-temporary: latest-rc
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

+ 33 - 10
.github/workflows/release-rc.yml

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

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

@@ -70,7 +70,7 @@ jobs:
         VERBOSE : true
 
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v3
+      uses: peter-evans/dockerhub-description@v4
       with:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

+ 49 - 16
.github/workflows/release.yml

@@ -1,3 +1,4 @@
+# TODO: https://redmine.weseek.co.jp/issues/171293
 name: Release
 
 on:
@@ -80,7 +81,8 @@ jobs:
     runs-on: ubuntu-latest
 
     outputs:
-      TAGS: ${{ steps.meta.outputs.tags }}
+      TAGS_WESEEK: ${{ steps.meta-weseek.outputs.tags }}
+      TAGS_GROWILABS: ${{ steps.meta-growilabs.outputs.tags }}
 
     steps:
     - uses: actions/checkout@v4
@@ -89,9 +91,9 @@ jobs:
       uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
 
-    - name: Docker meta for docker.io
+    - name: Docker meta for weseek/growi
       uses: docker/metadata-action@v5
-      id: meta
+      id: meta-weseek
       with:
         images: docker.io/weseek/growi
         sep-tags: ','
@@ -101,47 +103,78 @@ jobs:
           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
+      with:
+        images: docker.io/growilabs/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}}
 
   build-app-image:
     needs: create-github-release
 
-    uses: weseek/growi/.github/workflows/reusable-app-build-image.yml@master
+    uses: growilabs/growi/.github/workflows/reusable-app-build-image.yml@master
     with:
       source-version: refs/tags/v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
-      image-name: weseek/growi
+      image-name: growilabs/growi
       tag-temporary: latest
     secrets:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
 
+  publish-app-image-for-growilabs:
+    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 }}
+      registry: docker.io
+      image-name: 'growilabs/growi'
+      docker-registry-username: 'growimoogle'
+      tag-temporary: latest
+    secrets:
+      DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
 
-  publish-app-image:
+  publish-app-image-for-weseek:
     needs: [determine-tags, build-app-image]
 
-    uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
+    uses: growilabs/growi/.github/workflows/reusable-app-create-manifests.yml@master
     with:
-      tags: ${{ needs.determine-tags.outputs.TAGS }}
+      tags: ${{ needs.determine-tags.outputs.TAGS_WESEEK }}
       registry: docker.io
-      image-name: weseek/growi
+      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]
+    needs: [create-github-release, publish-app-image-for-growilabs, publish-app-image-for-weseek]
     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@v3
+      uses: peter-evans/dockerhub-description@v4
       with:
-        username: wsmoogle
-        password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
-        repository: weseek/growi
+        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 }}
         readme-filepath: ./apps/app/docker/README.md
 
     - name: Slack Notification
@@ -153,7 +186,7 @@ jobs:
 
 
   create-pr-for-next-rc:
-    needs: [create-github-release, publish-app-image]
+    needs: [create-github-release, publish-app-image-for-growilabs, publish-app-image-for-weseek]
     runs-on: ubuntu-latest
 
     steps:

+ 1 - 1
.github/workflows/reusable-app-build-image.yml

@@ -8,7 +8,7 @@ on:
         default: ${{ github.sha }}
       image-name:
         type: string
-        default: weseek/growi
+        default: growilabs/growi
       tag-temporary:
         type: string
         default: latest

+ 5 - 2
.github/workflows/reusable-app-create-manifests.yml

@@ -11,7 +11,10 @@ on:
         default: 'docker.io'
       image-name:
         type: string
-        default: weseek/growi
+        default: growilabs/growi
+      docker-registry-username:
+        type: string
+        default: growimoogle
       tag-temporary:
         type: string
         default: latest
@@ -41,7 +44,7 @@ jobs:
       uses: docker/login-action@v3
       with:
         registry: ${{ inputs.registry }}
-        username: wsmoogle
+        username: ${{ inputs.docker-registry-username }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
 
     - name: Create and push manifest images

+ 19 - 8
.github/workflows/reusable-app-prod.yml

@@ -1,4 +1,4 @@
-name: Reusable build app workflow for production
+name: Reusable build and test app for production
 
 on:
   workflow_call:
@@ -16,7 +16,7 @@ on:
       node-version:
         required: true
         type: string
-        default: 20.x
+        default: 22.x
       skip-e2e-test:
         type: boolean
         default: false
@@ -107,13 +107,17 @@ jobs:
     needs: [build-prod]
     runs-on: ubuntu-latest
 
+    strategy:
+      matrix:
+        mongodb-version: ['6.0', '8.0']
+
     services:
       mongodb:
-        image: mongo:6.0
+        image: mongo:${{ matrix.mongodb-version }}
         ports:
         - 27017/tcp
       elasticsearch:
-        image: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
+        image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
         ports:
         - 9200/tcp
         env:
@@ -129,6 +133,12 @@ jobs:
         node-version: ${{ inputs.node-version }}
         cache: 'pnpm'
 
+    # avoid setup-node cache failure; see: https://github.com/actions/setup-node/issues/1137
+    - name: Verify PNPM Cache Directory
+      run: |
+        PNPM_STORE_PATH="$( pnpm store path --silent )"
+        [ -d "$PNPM_STORE_PATH" ] || mkdir -vp "$PNPM_STORE_PATH"
+
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       with:
@@ -176,14 +186,15 @@ jobs:
       matrix:
         browser: [chromium, firefox, webkit]
         shard: [1/2, 2/2]
+        mongodb-version: ['6.0', '8.0']
 
     services:
       mongodb:
-        image: mongo:6.0
+        image: mongo:${{ matrix.mongodb-version }}
         ports:
         - 27017/tcp
       elasticsearch:
-        image: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
+        image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
         ports:
         - 9200/tcp
         env:
@@ -273,7 +284,7 @@ jobs:
       uses: actions/upload-artifact@v4
       if: always()
       with:
-        name: blob-report-${{ matrix.browser }}-${{ steps.shard-id.outputs.shard_id }}
+        name: blob-report-${{ matrix.browser }}-mongo${{ matrix.mongodb-version }}-${{ steps.shard-id.outputs.shard_id }}
         path: ./apps/app/blob-report
         retention-days: 30
 
@@ -282,7 +293,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*Node CI for growi - run-playwright*'
+        job_name: '*Node CI for growi - run-playwright (${{ matrix.browser }}, MongoDB ${{ matrix.mongodb-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 1 - 1
.github/workflows/reusable-app-reg-suit.yml

@@ -32,7 +32,7 @@ jobs:
 
   run-reg-suit:
     # use secrets for "VRT" environment
-    # https://github.com/weseek/growi/settings/environments/376165508/edit
+    # https://github.com/growilabs/growi/settings/environments/376165508/edit
     environment: VRT
 
     if: ${{ !inputs.skip-reg-suit }}

+ 22 - 0
.mcp.json

@@ -0,0 +1,22 @@
+{
+  "mcpServers": {
+    "context7": {
+      "type": "http",
+      "url": "https://mcp.context7.com/mcp"
+    },
+    "serena": {
+      "type": "stdio",
+      "command": "uvx",
+      "args": [
+        "--from",
+        "git+https://github.com/oraios/serena",
+        "serena-mcp-server",
+        "--context",
+        "ide-assistant",
+        "--project",
+        "."
+      ],
+      "env": {}
+    }
+  }
+}

+ 0 - 9
.roo/mcp.json

@@ -1,9 +0,0 @@
-{
-  "mcpServers": {
-    "fetch": {
-      "command": "uvx",
-      "args": ["mcp-server-fetch"],
-      "alwaysAllow": ["fetch"]
-    }
-  }
-}

+ 1 - 0
.serena/.gitignore

@@ -0,0 +1 @@
+/cache

+ 398 - 0
.serena/memories/apps-app-admin-forms-react-hook-form-migration-guide.md

@@ -0,0 +1,398 @@
+# Admin フォーム - React Hook Form 移行ガイドライン
+
+## プロジェクトコンテキスト
+
+### 現状 (2025年10月時点)
+**✅ PR #10051 完了: Admin フォームの IME 問題は100%解決済み**
+
+全27ファイルが React Hook Form に移行完了し、以下の問題を解決:
+1. ✅ **日本語 IME 入力の問題**: 非制御コンポーネント化により完全解決
+2. ✅ **空値更新の問題**: 完全解決
+3. ⏳ **レガシーライブラリ問題**: Unstated は現在も使用中(次のステップで解決予定)
+
+### 最終目標 (理想像)
+- React Hook Form を利用(✅ 完了)
+- Unstated を完全に廃止(⏳ 次のステップ)
+- グローバルステートは Jotai で管理(⏳ 次のステップ)
+
+### 現在の構成 (中間地点)
+**React Hook Form + Unstated Container のハイブリッド構成**
+
+この構成により:
+1. ✅ IME 入力問題を解決(非制御コンポーネント化)
+2. ✅ 空値更新問題を解決
+3. ✅ Container は残しているが、将来的に Jotai への移行パスを確保
+4. ✅ 段階的な移行によりリグレッションを最小化
+
+## 移行パターン(確立済み)
+
+### 基本的なフォームセットアップ
+
+```typescript
+import { useForm } from 'react-hook-form';
+
+type FormData = {
+  fieldName: string;
+  // ... 他のフィールド
+};
+
+const {
+  register,
+  handleSubmit,
+  reset,
+} = useForm<FormData>();
+```
+
+**重要**: `defaultValues` は指定しない。`useEffect` で `reset()` を呼ぶため不要。
+
+### フォーム値の復元
+
+Container の state とフォームを同期するため、`useEffect` で `reset()` を使用:
+
+```typescript
+useEffect(() => {
+  reset({
+    fieldName: container.state.fieldName || '',
+    // ... 他のフィールド
+  });
+}, [container.state.fieldName, reset]);
+```
+
+### Container を使ったフォーム送信
+
+```typescript
+const onSubmit = useCallback(async(data: FormData) => {
+  try {
+    // 重要: API 呼び出し前に setState の完了を待つ
+    await Promise.all([
+      container.changeField1(data.field1),
+      container.changeField2(data.field2),
+    ]);
+    
+    await container.updateHandler();
+    toastSuccess(t('updated_successfully'));
+  }
+  catch (err) {
+    toastError(err);
+  }
+}, [container, t]);
+
+return (
+  <form onSubmit={handleSubmit(onSubmit)}>
+    {/* フォームフィールド */}
+  </form>
+);
+```
+
+## 重要な注意点
+
+### ⚠️ 1. API 呼び出し前に Container の setState を await する(最重要!)
+
+**問題**: Unstated Container の `setState` は非同期処理です。`change*()` メソッドの後に `await` せずに API ハンドラーを即座に呼ぶと、API リクエストは**古い/古びた値**で送信されます。
+
+❌ **間違い:**
+```typescript
+container.changeSiteUrl(data.siteUrl);
+await container.updateHandler(); // 古い値が送信される!
+```
+
+✅ **正しい:**
+```typescript
+await container.changeSiteUrl(data.siteUrl);
+await container.updateHandler(); // 新しい値が送信される
+```
+
+複数フィールドの場合は `Promise.all()` を使用:
+```typescript
+await Promise.all([
+  container.changeTitle(data.title),
+  container.changeConfidential(data.confidential),
+]);
+await container.updateHandler();
+```
+
+### 2. ラジオボタンの値の型の一致
+
+**問題**: ラジオボタンは**文字列**の値を持ちますが、Container の state は boolean かもしれません。型が一致しないと、選択状態の復元ができません。
+
+❌ **間違い:**
+```typescript
+// HTML: <input type="radio" value="true" />
+reset({
+  isEmailPublished: true, // boolean - 文字列 "true" とマッチしない
+});
+```
+
+✅ **正しい:**
+```typescript
+reset({
+  isEmailPublished: String(container.state.isEmailPublished ?? true),
+});
+```
+
+### 3. チェックボックスの値の扱い
+
+チェックボックスは boolean 値を直接使えます(変換不要):
+```typescript
+reset({
+  fileUpload: container.state.fileUpload ?? false,
+});
+```
+
+### 4. リアルタイム Container 更新に watch() を使わない
+
+**削除したパターン**: フォームの変更を `watch()` と `useEffect` でリアルタイムに Container に同期し戻すのは不要で、複雑さを増すだけです。
+
+❌ **これはやらない:**
+```typescript
+const watchedValues = watch();
+useEffect(() => {
+  container.changeField(watchedValues.field);
+}, [watchedValues]);
+```
+
+✅ **submit 時だけ更新:**
+- Container の state は最終的な API リクエストにのみ使用される
+- `onSubmit` で API ハンドラーを呼ぶ前に更新すればよい
+
+### 5. フォームフィールドの disabled vs readOnly
+
+**問題**: `disabled` フィールドはフォーム送信データから除外されます。
+
+フィールドを編集不可にしたいが、フォームデータには含めたい場合:
+- `disabled` の代わりに `readOnly` を使用
+- または属性を削除して Container/API レイヤーで処理
+
+### 6. defaultValues を指定しない
+
+`useForm()` の引数に `defaultValues` を渡さないこと。
+
+理由:
+- `useEffect` で `reset()` を呼んでいるため、初期値はそちらで設定される
+- コードの重複を避ける
+- 他のファイルとパターンを統一
+
+```typescript
+// ❌ 冗長
+const { register, reset } = useForm({
+  defaultValues: { field: container.state.field }
+});
+useEffect(() => {
+  reset({ field: container.state.field });
+}, [container.state.field]);
+
+// ✅ シンプル
+const { register, reset } = useForm();
+useEffect(() => {
+  reset({ field: container.state.field });
+}, [container.state.field]);
+```
+
+## 高度なパターン
+
+### モジュラーコンポーネント設計(SecuritySetting の例)
+
+大規模なフォームは、複数の小さなコンポーネントに分割することを推奨します。
+
+**親コンポーネント(統合):**
+```typescript
+type FormData = {
+  sessionMaxAge: string;
+  // Container で管理される他のフィールドは不要
+};
+
+const Parent: React.FC<Props> = ({ container }) => {
+  const { register, handleSubmit, reset } = useForm<FormData>();
+
+  useEffect(() => {
+    reset({
+      sessionMaxAge: container.state.sessionMaxAge || '',
+    });
+  }, [reset, container.state.sessionMaxAge]);
+
+  const onSubmit = useCallback(async(data: FormData) => {
+    try {
+      // React Hook Form で管理されているフィールドのみ更新
+      await container.setSessionMaxAge(data.sessionMaxAge);
+      // 全ての設定を保存(Container 管理のフィールドも含む)
+      await container.updateGeneralSecuritySetting();
+      toastSuccess(t('updated'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [container, t]);
+
+  return (
+    <form onSubmit={handleSubmit(onSubmit)}>
+      {/* React Hook Form 管理のフィールド */}
+      <SessionMaxAgeSettings register={register} t={t} />
+      
+      {/* Container 直接管理のフィールド */}
+      <PageListDisplaySettings container={container} t={t} />
+      <PageAccessRightsSettings container={container} t={t} />
+      
+      <button type="submit">{t('Update')}</button>
+    </form>
+  );
+};
+```
+
+**子コンポーネント(React Hook Form 管理):**
+```typescript
+type Props = {
+  register: UseFormRegister<{ sessionMaxAge: string }>;
+  t: (key: string) => string;
+};
+
+export const SessionMaxAgeSettings: React.FC<Props> = ({ register, t }) => {
+  return (
+    <input
+      className="form-control"
+      type="text"
+      {...register('sessionMaxAge')}
+      placeholder="2592000000"
+    />
+  );
+};
+```
+
+**子コンポーネント(Container 直接管理):**
+```typescript
+type Props = {
+  container: AdminGeneralSecurityContainer;
+  t: (key: string) => string;
+};
+
+export const PageListDisplaySettings: React.FC<Props> = ({ container, t }) => {
+  return (
+    <select
+      className="form-control"
+      value={container.state.currentOwnerRestrictionDisplayMode}
+      onChange={(e) => container.changeOwnerRestrictionDisplayMode(e.target.value)}
+    >
+      <option value="Displayed">{t('Displayed')}</option>
+      <option value="Hidden">{t('Hidden')}</option>
+    </select>
+  );
+};
+```
+
+### 統一された Submit ボタン
+
+複数のセクションを持つフォームでも、Submit ボタンは1つに統一:
+- React Hook Form のフィールドは `onSubmit` で処理
+- Container 管理のフィールドは既に state に反映されている
+- 1つの `updateHandler()` で全て保存
+
+## テストチェックリスト
+
+フォーム移行後に必ずテストすること:
+
+1. ✅ **日本語 IME 入力と漢字変換** - 最も重要!
+2. ✅ **ページリロード後にフォームの値が正しく復元される**
+3. ✅ **空値を送信できる**(フィールドをクリアできる)
+4. ✅ **フォーム送信で現在の入力値が送信される**(古い/古びた値ではない)
+5. ✅ **ラジオボタンとチェックボックスが正しく復元される**
+6. ✅ **複数セクションがある場合、全ての設定が1つの Submit で保存される**
+
+## PR #10051 の成果
+
+全27ファイルを React Hook Form に移行完了:
+
+### 主要な成果
+1. **企業認証システム**: LDAP (10フィールド)、OIDC (16フィールド)、SAML (9フィールド)
+2. **SecuritySetting のモジュラー化**: 636行のクラスコンポーネント → 8つの Function Component
+3. **セキュリティ設定**: LocalSecurity (1フィールド)、Import (4フィールド)
+4. **カスタマイズ**: CustomizeCss (1フィールド)、Slack (2フィールド)
+5. **その他**: 17ファイル
+
+### アーキテクチャの改善
+- TypeScript 完全対応
+- PropTypes 廃止
+- Function Component への統一
+- モジュラー設計の採用
+- テスト容易性の向上
+
+## 将来の移行パス: Unstated から Jotai へ
+
+### フェーズ 1: React Hook Form 移行(✅ 完了)
+- 全ての Admin フォームを React Hook Form に移行
+- IME 問題と空値問題を解決
+- 非制御コンポーネントパターンを確立
+
+### フェーズ 2: Jotai 導入準備(次のステップ)
+1. **Container の分析**
+   - どの state が本当にグローバルである必要があるか特定
+   - ローカル state で十分なものを useState に移行
+
+2. **API レイヤーの分離**
+   - Container の `update*Handler()` メソッドを独立した API 関数に抽出
+   - `apps/app/src/client/util/apiv3-client.ts` パターンに従う
+
+3. **段階的な Container の削除**
+   - 小さな Container から始める
+   - Jotai atom で置き換え
+   - 各ステップでテストを実行
+
+### フェーズ 3: 完全な Jotai 移行(最終目標)
+```typescript
+// 理想的な最終形態
+import { atom, useAtom } from 'jotai';
+import { useForm } from 'react-hook-form';
+
+// グローバル state
+const sessionMaxAgeAtom = atom<string>('');
+
+const SecuritySetting = () => {
+  const [sessionMaxAge, setSessionMaxAge] = useAtom(sessionMaxAgeAtom);
+  const { register, handleSubmit, reset } = useForm();
+
+  useEffect(() => {
+    reset({ sessionMaxAge });
+  }, [sessionMaxAge, reset]);
+
+  const onSubmit = async(data: FormData) => {
+    // 直接 API 呼び出し
+    await apiv3Put('/admin/security-settings', {
+      sessionMaxAge: data.sessionMaxAge,
+      // ... 他の設定
+    });
+    
+    // Jotai state を更新
+    setSessionMaxAge(data.sessionMaxAge);
+    toastSuccess('Updated');
+  };
+
+  return <form onSubmit={handleSubmit(onSubmit)}>{/* ... */}</form>;
+};
+```
+
+## 適用可能な範囲
+
+このガイドラインは以下の Admin フォームに適用可能:
+
+- Unstated Container でグローバルステートを管理しているフォーム
+- `apps/app/src/client/services/Admin*Container.js` 配下の Container を使用しているフォーム
+- `/admin` ルート配下のコンポーネント
+- 将来的に Jotai に移行予定のフォーム
+
+## 関連ファイル
+
+### 現在使用中
+- Container 群: `apps/app/src/client/services/Admin*Container.js`
+- ボタンコンポーネント: `apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx`
+- React Hook Form: v7.45.4
+
+### 将来導入予定
+- Jotai: グローバル state 管理
+- SWR または React Query: サーバー state 管理(検討中)
+
+## 参考実装
+
+以下のファイルがベストプラクティスの参考になります:
+
+1. **モジュラー構造**: `apps/app/src/client/components/Admin/Security/SecuritySetting/`
+2. **React Hook Form 基本**: `apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.tsx`
+3. **複雑なフォーム**: `apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.tsx`
+4. **既存の良い実装**: `apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx`

+ 186 - 0
.serena/memories/apps-app-pagetree-performance-refactor-plan.md

@@ -0,0 +1,186 @@
+# PageTree パフォーマンス改善リファクタ計画 - 現実的戦略
+
+## 🎯 目標
+現在のパフォーマンス問題を解決:
+- **問題**: 5000件の兄弟ページで初期レンダリングが重い
+- **目標**: 表示速度を10-20倍改善、UX維持
+
+## ✅ 戦略2: API軽量化 - **完了済み**
+
+### 実装済み内容
+- **ファイル**: `apps/app/src/server/service/page-listing/page-listing.ts:77`
+- **変更内容**: `.select('_id path parent descendantCount grant isEmpty createdAt updatedAt wip')` を追加
+- **型定義**: `apps/app/src/interfaces/page.ts` の `IPageForTreeItem` 型も対応済み
+- **追加改善**: 計画にはなかった `wip` フィールドも最適化対象に含める
+
+### 実現できた効果
+- **データサイズ**: 推定 500バイト → 約100バイト(5倍軽量化)
+- **ネットワーク転送**: 5000ページ時 2.5MB → 500KB程度に削減
+- **状況**: **実装完了・効果発現中**
+
+---
+
+## 🚀 戦略1: 既存アーキテクチャ活用 + headless-tree部分導入 - **現実的戦略**
+
+### 前回のreact-window失敗原因
+1. **動的itemCount**: ツリー展開時にアイテム数が変化→react-windowの前提と衝突
+2. **非同期ローディング**: APIレスポンス待ちでフラット化不可
+3. **複雑な状態管理**: SWRとreact-windowの状態同期が困難
+
+### 現実的制約の認識
+**ItemsTree/TreeItemLayoutは廃止困難**:
+- **CustomTreeItemの出し分け**: `PageTreeItem` vs `TreeItemForModal`  
+- **共通副作用処理**: rename/duplicate/delete時のmutation処理
+- **多箇所からの利用**: PageTree, PageSelectModal, AiAssistant等
+
+## 📋 修正された実装戦略: **ハイブリッドアプローチ**
+
+### **核心アプローチ**: ItemsTreeを**dataProvider**として活用
+
+**既存の責務は保持しつつ、内部実装のみheadless-tree化**:
+
+1. **ItemsTree**: UIロジック・副作用処理はそのまま
+2. **TreeItemLayout**: 個別アイテムレンダリングはそのまま  
+3. **データ管理**: 内部でheadless-treeを使用(SWR → headless-tree)
+4. **Virtualization**: ItemsTree内部にreact-virtualを導入
+
+### **実装計画: 段階的移行**
+
+#### **Phase 1: データ層のheadless-tree化**
+
+**ファイル**: `ItemsTree.tsx`
+```typescript
+// Before: 複雑なSWR + 子コンポーネント管理
+const tree = useTree<IPageForTreeItem>({
+  rootItemId: initialItemNode.page._id,
+  dataLoader: {
+    getItem: async (itemId) => {
+      const response = await apiv3Get('/page-listing/item', { id: itemId });
+      return response.data;
+    },
+    getChildren: async (itemId) => {
+      const response = await apiv3Get('/page-listing/children', { id: itemId });
+      return response.data.children.map(child => child._id);
+    },
+  },
+  features: [asyncDataLoaderFeature],
+});
+
+// 既存のCustomTreeItemに渡すためのアダプター
+const adaptedNodes = tree.getItems().map(item => 
+  new ItemNode(item.getItemData())
+);
+
+return (
+  <ul className={`${moduleClass} list-group`}>
+    {adaptedNodes.map(node => (
+      <CustomTreeItem
+        key={node.page._id}
+        itemNode={node}
+        // ... 既存のpropsをそのまま渡す
+        onRenamed={onRenamed}
+        onClickDuplicateMenuItem={onClickDuplicateMenuItem}
+        onClickDeleteMenuItem={onClickDeleteMenuItem}
+      />
+    ))}
+  </ul>
+);
+```
+
+#### **Phase 2: Virtualization導入**
+
+**ファイル**: `ItemsTree.tsx` (Phase1をベースに拡張)
+```typescript
+const virtualizer = useVirtualizer({
+  count: adaptedNodes.length,
+  getScrollElement: () => containerRef.current,
+  estimateSize: () => 40,
+});
+
+return (
+  <div ref={containerRef} className="tree-container">
+    <div style={{ height: virtualizer.getTotalSize() }}>
+      {virtualizer.getVirtualItems().map(virtualItem => {
+        const node = adaptedNodes[virtualItem.index];
+        return (
+          <div
+            key={node.page._id}
+            style={{
+              position: 'absolute',
+              top: virtualItem.start,
+              height: virtualItem.size,
+              width: '100%',
+            }}
+          >
+            <CustomTreeItem
+              itemNode={node}
+              // ... 既存props
+            />
+          </div>
+        );
+      })}
+    </div>
+  </div>
+);
+```
+
+#### **Phase 3 (将来): 完全なheadless-tree移行**
+
+最終的にはdrag&drop、selection等のUI機能もheadless-treeに移行可能ですが、**今回のスコープ外**。
+
+## 📁 現実的なファイル変更まとめ
+
+| アクション | ファイル | 内容 | スコープ |
+|---------|---------|------|------|
+| ✅ **完了** | **apps/app/src/server/service/page-listing/page-listing.ts** | selectクエリ追加 | API軽量化 |
+| ✅ **完了** | **apps/app/src/interfaces/page.ts** | IPageForTreeItem型定義 | API軽量化 |
+| 🔄 **修正** | **src/client/components/ItemsTree/ItemsTree.tsx** | headless-tree + virtualization導入 | **今回の核心** |
+| 🆕 **新規** | **src/client/components/ItemsTree/usePageTreeDataLoader.ts** | データローダー分離 | 保守性向上 |
+| ⚠️ **保持** | **src/client/components/TreeItem/TreeItemLayout.tsx** | 既存のまま(後方互換) | 既存責務保持 |
+| ⚠️ **部分削除** | **src/stores/page-listing.tsx** | useSWRxPageChildren削除 | 重複排除 |
+
+**新規ファイル**: 1個(データローダー分離のみ)  
+**変更ファイル**: 2個(ItemsTree改修 + store整理)  
+**削除ファイル**: 0個(既存アーキテクチャ尊重)
+
+---
+
+## 🎯 実装優先順位
+
+**✅ Phase 1**: API軽量化(低リスク・即効性) - **完了**
+
+**📋 Phase 2-A**: ItemsTree内部のheadless-tree化
+- **工数**: 2-3日
+- **リスク**: 低(外部IF変更なし)
+- **効果**: 非同期ローディング最適化、キャッシュ改善
+
+**📋 Phase 2-B**: Virtualization導入  
+- **工数**: 2-3日
+- **リスク**: 低(内部実装のみ)
+- **効果**: レンダリング性能10-20倍改善
+
+**現在の効果**: API軽量化により 5倍のデータ転送量削減済み  
+**Phase 2完了時の予想効果**: 初期表示速度 20-50倍改善
+
+---
+
+## 🏗️ 実装方針: **既存アーキテクチャ尊重**
+
+**基本方針**:
+- **既存のCustomTreeItem責務**は保持(rename/duplicate/delete等)
+- **データ管理層のみ**をheadless-tree化  
+- **外部インターフェース**は変更せず、内部最適化に集中
+- **段階的移行**で低リスク実装
+
+**今回のスコープ**:
+- ✅ 非同期データローディング最適化
+- ✅ Virtualizationによる大量要素対応  
+- ❌ drag&drop/selection(将来フェーズ)
+- ❌ 既存アーキテクチャの破壊的変更
+
+---
+
+## 技術的参考資料
+- **headless-tree**: https://headless-tree.lukasbach.com/ (データ管理層のみ利用)
+- **react-virtual**: @tanstack/react-virtualを使用  
+- **アプローチ**: 既存ItemsTree内部でheadless-tree + virtualizationをハイブリッド活用

+ 71 - 0
.serena/memories/coding_conventions.md

@@ -0,0 +1,71 @@
+# コーディング規約とスタイルガイド
+
+## Linter・フォーマッター設定
+
+### Biome設定(統一予定)
+- **適用範囲**: 
+  - dist/, node_modules/, coverage/ などは除外
+  - .next/, bin/, config/ などのビルド成果物は除外
+  - package.json, .eslintrc.js などの設定ファイルは除外
+- **推奨**: 新規開発では Biome を使用
+
+### ESLint設定(廃止予定・過渡期)
+- **ベース設定**: weseek ESLint設定を使用
+- **TypeScript**: weseek/typescript 設定を適用
+- **React**: React関連のルールを適用
+- **主要なルール**:
+  - `import/prefer-default-export`: オフ(名前付きエクスポートを推奨)
+  - `import/order`: import文の順序を規定
+    - React を最初に
+    - 内部モジュール(`/**`)をparentグループの前に配置
+
+## TypeScript設定
+- **ターゲット**: ESNext
+- **モジュール**: ESNext  
+- **厳格モード**: 有効(strict: true)
+- **モジュール解決**: Bundler
+- **その他**:
+  - allowJs: true(JSファイルも許可)
+  - skipLibCheck: true(型チェックの最適化)
+  - isolatedModules: true(単独モジュールとしてコンパイル)
+
+## Stylelint設定
+- SCSS/CSSファイルに対して適用
+- recess-order設定を使用(プロパティの順序規定)
+- recommended-scss設定を適用
+
+## ファイル命名規則
+- TypeScript/JavaScriptファイル: キャメルケースまたはケバブケース
+- コンポーネントファイル: PascalCase(Reactコンポーネント)
+- 設定ファイル: ドット記法(.eslintrc.js など)
+
+## テストファイル命名規則(Vitest)
+vitest.workspace.mts の設定に基づく:
+
+### 単体テスト(Unit Test)
+- **ファイル名**: `*.spec.{ts,js}`
+- **環境**: Node.js
+- **例**: `utils.spec.ts`, `helper.spec.js`
+
+### 統合テスト(Integration Test)
+- **ファイル名**: `*.integ.ts`
+- **環境**: Node.js(MongoDB設定あり)
+- **例**: `api.integ.ts`, `service.integ.ts`
+
+### コンポーネントテスト(Component Test)
+- **ファイル名**: `*.spec.{tsx,jsx}`
+- **環境**: happy-dom
+- **例**: `Button.spec.tsx`, `Modal.spec.jsx`
+
+## ディレクトリ構造の規則
+- `src/`: ソースコード
+- `test/`: Jest用の古いテストファイル(廃止予定)
+- `test-with-vite/`: Vitest用の新しいテストファイル
+- `playwright/`: E2Eテストファイル
+- `config/`: 設定ファイル
+- `public/`: 静的ファイル
+- `dist/`: ビルド出力
+
+## 移行ガイドライン
+- 新規開発: Biome + Vitest を使用
+- 既存コード: 段階的に ESLint → Biome、Jest → Vitest に移行

+ 45 - 0
.serena/memories/development_environment.md

@@ -0,0 +1,45 @@
+# 開発環境とツール
+
+## 推奨システム要件
+- **Node.js**: ^20 || ^22
+- **パッケージマネージャー**: pnpm 10.4.1
+- **OS**: Linux(Ubuntuベース)、macOS、Windows
+
+## 利用可能なLinuxコマンド
+基本的なLinuxコマンドが利用可能:
+- `apt`, `dpkg`: パッケージ管理
+- `git`: バージョン管理
+- `curl`, `wget`: HTTP クライアント
+- `ssh`, `scp`, `rsync`: ネットワーク関連
+- `ps`, `lsof`, `netstat`, `top`: プロセス・ネットワーク監視
+- `tree`, `find`, `grep`: ファイル検索・操作
+- `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz`: アーカイブ操作
+
+## 開発用ブラウザ
+```bash
+# ローカルサーバーをブラウザで開く
+"$BROWSER" http://localhost:3000
+```
+
+## 環境変数管理
+- **dotenv-flow**: 環境ごとの設定管理
+- 環境ファイル:
+  - `.env.development`: 開発環境
+  - `.env.production`: 本番環境
+  - `.env.test`: テスト環境
+  - `.env.*.local`: ローカル固有設定
+
+## デバッグ
+```bash
+# デバッグモードでサーバー起動
+cd apps/app && pnpm run dev  # --inspectフラグ付きでnodemon起動
+
+# REPL(Read-Eval-Print Loop)
+cd apps/app && pnpm run repl
+```
+
+## VS Code設定
+`.vscode/` ディレクトリに設定ファイルが含まれており、推奨拡張機能や設定が適用される。
+
+## Docker対応
+各アプリケーションにDockerファイルが含まれており、コンテナベースでの開発も可能。

+ 26 - 0
.serena/memories/project_overview.md

@@ -0,0 +1,26 @@
+# GROWIプロジェクト概要
+
+## 目的
+GROWIは、マークダウンを使用したチームコラボレーションソフトウェアです。Wikiとドキュメント作成ツールの機能を持ち、チーム間の情報共有とコラボレーションを促進します。
+
+## プロジェクトの詳細
+- **プロジェクト名**: GROWI
+- **バージョン**: 7.3.0-RC.0
+- **ライセンス**: MIT
+- **作者**: Yuki Takei <yuki@weseek.co.jp>
+- **リポジトリ**: https://github.com/growilabs/growi.git
+- **公式サイト**: https://growi.org
+
+## 主な特徴
+- Markdownベースのドキュメント作成
+- チームコラボレーション機能
+- Wikiのような情報共有プラットフォーム
+- ドキュメント管理とバージョン管理
+
+## アーキテクチャ
+- **モノレポ構成**: pnpm workspace + Turbo.js を使用
+- **主要アプリケーション**: apps/app (メインアプリケーション)
+- **追加アプリケーション**: 
+  - apps/pdf-converter (PDF変換サービス)
+  - apps/slackbot-proxy (Slackボットプロキシ)
+- **パッケージ**: packages/ 配下に複数の共有ライブラリ

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

@@ -0,0 +1,90 @@
+# プロジェクト構造
+
+## ルートディレクトリ構造
+```
+growi/
+├── apps/                    # アプリケーション群
+│   ├── app/                # メインのGROWIアプリケーション
+│   ├── pdf-converter/      # PDF変換サービス
+│   └── slackbot-proxy/     # Slackボットプロキシ
+├── packages/               # 共有パッケージ群
+│   ├── core/              # コアライブラリ
+│   ├── core-styles/       # 共通スタイル
+│   ├── editor/            # エディターコンポーネント
+│   ├── pluginkit/         # プラグインキット
+│   ├── ui/                # UIコンポーネント
+│   ├── presentation/      # プレゼンテーション層
+│   ├── preset-templates/  # テンプレート
+│   ├── preset-themes/     # テーマ
+│   └── remark-*/          # remarkプラグイン群
+├── bin/                   # ユーティリティスクリプト
+└── 設定ファイル群
+```
+
+## メインアプリケーション (apps/app/)
+```
+apps/app/
+├── src/                   # ソースコード
+├── test/                  # 古いJestテストファイル(廃止予定)
+├── test-with-vite/        # 新しいVitestテストファイル
+├── playwright/            # E2Eテスト(Playwright)
+├── config/                # 設定ファイル
+├── public/                # 静的ファイル
+├── docker/                # Docker関連
+├── bin/                   # スクリプト
+└── 設定ファイル群
+```
+
+## テストディレクトリの詳細
+
+### test/ (廃止予定)
+- Jest用の古いテストファイル
+- 段階的にtest-with-vite/に移行予定
+- 新規テストは作成しない
+
+### test-with-vite/
+- Vitest用の新しいテストファイル
+- 新規テストはここに作成
+- セットアップファイル: `setup/mongoms.ts` (MongoDB用)
+
+### playwright/
+- E2Eテスト用ディレクトリ
+- ブラウザ操作を含むテスト
+
+## テストファイルの配置ルール
+
+### Vitestテストファイル
+以下のパターンでソースコードと同じディレクトリまたはtest-with-vite/配下に配置:
+
+- **単体テスト**: `*.spec.{ts,js}`
+- **統合テスト**: `*.integ.ts` 
+- **コンポーネントテスト**: `*.spec.{tsx,jsx}`
+
+例:
+```
+src/
+├── utils/
+│   ├── helper.ts
+│   └── helper.spec.ts       # 単体テスト
+├── components/
+│   ├── Button.tsx
+│   └── Button.spec.tsx      # コンポーネントテスト
+└── services/
+    ├── api.ts
+    └── api.integ.ts         # 統合テスト
+```
+
+## パッケージ(packages/)
+各パッケージは独立したnpmパッケージとして管理され、以下の構造を持つ:
+- `src/`: ソースコード
+- `dist/`: ビルド出力
+- `package.json`: パッケージ設定
+- `tsconfig.json`: TypeScript設定
+
+## 重要な設定ファイル
+- **pnpm-workspace.yaml**: ワークスペース設定
+- **turbo.json**: Turbo.jsビルド設定
+- **tsconfig.base.json**: TypeScript基本設定
+- **biome.json**: Biome linter/formatter設定
+- **.eslintrc.js**: ESLint設定(廃止予定)
+- **vitest.workspace.mts**: Vitestワークスペース設定

+ 100 - 0
.serena/memories/suggested_commands.md

@@ -0,0 +1,100 @@
+# 推奨開発コマンド集
+
+## セットアップ
+```bash
+# 初期セットアップ
+pnpm run bootstrap
+# または
+pnpm install
+```
+
+## 開発サーバー
+```bash
+# メインアプリケーション開発モード
+cd /workspace/growi/apps/app && pnpm run dev
+
+# ルートから起動(本番用ビルド後)
+pnpm start
+```
+
+## ビルド
+```bash
+# メインアプリケーションのビルド
+pnpm run app:build
+
+# Slackbot Proxyのビルド
+pnpm run slackbot-proxy:build
+
+# 全体ビルド(Turboで並列実行)
+turbo run build
+```
+
+## Lint・フォーマット
+```bash
+# 全てのLint実行
+pnpm run lint
+```
+
+## apps/app の Lint・フォーマット
+```bash
+# 【推奨】Biome実行(lint + format)
+cd /workspace/growi/apps/app pnpm run lint:biome
+
+# 【過渡期】ESLint実行(廃止予定)
+cd /workspace/growi/apps/app pnpm run lint:eslint
+
+# Stylelint実行
+cd /workspace/growi/apps/app pnpm run lint:styles
+
+# 全てのLint実行
+cd /workspace/growi/apps/app pnpm run lint
+
+# TypeScript型チェック
+cd /workspace/growi/apps/app pnpm run lint:typecheck
+```
+
+## テスト
+```bash
+# 【推奨】Vitestテスト実行
+pnpm run test:vitest
+
+# 【過渡期】Jest(統合テスト)(廃止予定)
+pnpm run test:jest
+
+# 全てのテスト実行(過渡期対応)
+pnpm run test
+
+# Vitestで特定のファイルに絞って実行
+pnpm run test:vitest {target-file-name}
+
+# E2Eテスト(Playwright)
+npx playwright test
+```
+
+## データベース関連
+```bash
+# マイグレーション実行
+cd apps/app && pnpm run migrate
+
+# 開発環境でのマイグレーション
+cd apps/app && pnpm run dev:migrate
+
+# マイグレーション状態確認
+cd apps/app && pnpm run dev:migrate:status
+```
+
+## その他の便利コマンド
+```bash
+# REPL起動
+cd apps/app && pnpm run repl
+
+# OpenAPI仕様生成
+cd apps/app && pnpm run openapi:generate-spec:apiv3
+
+# クリーンアップ
+cd apps/app && pnpm run clean
+```
+
+## 注意事項
+- ESLintとJestは廃止予定のため、新規開発ではBiomeとVitestを使用してください
+- 既存のコードは段階的に移行中です

+ 95 - 0
.serena/memories/task_completion_checklist.md

@@ -0,0 +1,95 @@
+# タスク完了時のチェックリスト
+
+## コードを書いた後に必ず実行すべきコマンド
+
+### 1. Lint・フォーマットの実行
+```bash
+# 【推奨】Biome実行(新規開発)
+pnpm run lint:biome
+
+# 【過渡期】全てのLint実行(既存コード)
+pnpm run lint
+
+# 個別実行(必要に応じて)
+pnpm run lint:eslint      # ESLint(廃止予定)
+pnpm run lint:styles      # Stylelint
+pnpm run lint:typecheck   # TypeScript型チェック
+```
+
+### 2. テストの実行
+```bash
+# 【推奨】Vitestテスト実行(新規開発)
+pnpm run test:vitest
+
+# 【過渡期】全てのテスト実行(既存コード)
+pnpm run test
+
+# 個別実行
+pnpm run test:jest        # Jest(廃止予定)
+pnpm run test:vitest {target-file-name}     # Vitest
+```
+
+### 3. E2Eテストの実行(重要な機能変更時)
+```bash
+cd apps/app
+npx playwright test
+```
+
+### 4. ビルドの確認
+```bash
+# メインアプリケーションのビルド
+pnpm run app:build
+
+# 関連パッケージのビルド
+turbo run build
+```
+
+### 5. 動作確認
+```bash
+# 開発サーバーでの動作確認
+cd apps/app && pnpm run dev
+
+# または本番ビルドでの確認
+pnpm start
+```
+
+## 特別な確認事項
+
+### OpenAPI仕様の確認(API変更時)
+```bash
+cd apps/app
+pnpm run openapi:generate-spec:apiv3
+pnpm run lint:openapi:apiv3
+```
+
+### データベーススキーマ変更時
+```bash
+cd apps/app
+pnpm run dev:migrate:status  # 現在の状態確認
+pnpm run dev:migrate         # マイグレーション実行
+```
+
+## テストファイル作成時の注意
+
+### 新規テストファイル
+- **単体テスト**: `*.spec.{ts,js}` (Node.js環境)
+- **統合テスト**: `*.integ.ts` (Node.js + MongoDB環境)  
+- **コンポーネントテスト**: `*.spec.{tsx,jsx}` (happy-dom環境)
+- test-with-vite/ または対象ファイルと同じディレクトリに配置
+
+### 既存テストの修正
+- test/ 配下のJestテストは段階的に移行
+- 可能であればtest-with-vite/にVitestテストとして書き直し
+
+## コミット前の最終チェック
+1. Biome(または過渡期はESLint)エラーが解消されているか
+2. Vitestテスト(または過渡期はJest)がパスしているか
+3. 重要な変更はPlaywright E2Eテストも実行
+4. ビルドが成功するか
+5. 変更による既存機能への影響がないか
+6. 適切なコミットメッセージを作成したか
+
+## 移行期間中の注意事項
+- 新規開発: Biome + Vitest を使用
+- 既存コード修正: 可能な限り Biome + Vitest に移行
+- レガシーツールは段階的に廃止予定

+ 42 - 0
.serena/memories/tech_stack.md

@@ -0,0 +1,42 @@
+# 技術スタック
+
+## プログラミング言語
+- **TypeScript**: メイン言語(~5.0.0)
+- **JavaScript**: 一部のコンポーネント
+
+## フロントエンド
+- **Next.js**: Reactベースのフレームワーク
+- **React**: UIライブラリ
+- **Vite**: ビルドツール、開発サーバー
+- **SCSS**: スタイルシート
+- **SWR**: グローバルステート管理、データフェッチ・キャッシュ管理(^2.3.2)
+
+## バックエンド
+- **Node.js**: ランタイム(^20 || ^22)
+- **Express.js**: Webフレームワーク(推測)
+- **MongoDB**: データベース
+- **Mongoose**: MongoDB用ORM(^6.13.6)
+  - mongoose-gridfs: GridFS対応(^1.2.42)
+  - mongoose-paginate-v2: ページネーション(^1.3.9)
+  - mongoose-unique-validator: バリデーション(^2.0.3)
+
+## 開発ツール
+- **pnpm**: パッケージマネージャー(10.4.1)
+- **Turbo**: モノレポビルドシステム(^2.1.3)
+- **ESLint**: Linter(weseek設定を使用)【廃止予定 - 現在は過渡期】
+- **Biome**: 統一予定のLinter/Formatter
+- **Stylelint**: CSS/SCSSのLinter
+- **Jest**: テスティングフレームワーク【廃止予定 - 現在は過渡期】
+- **Vitest**: 高速テスティングフレームワーク【統一予定】
+- **Playwright**: E2Eテスト【統一予定】
+
+## その他のツール
+- **SWC**: TypeScriptコンパイラー(高速)
+- **ts-node**: TypeScript直接実行
+- **nodemon**: 開発時のホットリロード
+- **dotenv-flow**: 環境変数管理
+- **Swagger/OpenAPI**: API仕様
+
+## 移行計画
+- **Linter**: ESLint → Biome に統一予定
+- **テスト**: Jest → Vitest + Playwright に統一予定

+ 68 - 0
.serena/project.yml

@@ -0,0 +1,68 @@
+# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
+#  * For C, use cpp
+#  * For JavaScript, use typescript
+# Special requirements:
+#  * csharp: Requires the presence of a .sln file in the project folder.
+language: typescript
+
+# whether to use the project's gitignore file to ignore files
+# Added on 2025-04-07
+ignore_all_files_in_gitignore: true
+# list of additional paths to ignore
+# same syntax as gitignore, so you can use * and **
+# Was previously called `ignored_dirs`, please update your config if you are using that.
+# Added (renamed) on 2025-04-07
+ignored_paths: []
+
+# whether the project is in read-only mode
+# If set to true, all editing tools will be disabled and attempts to use them will result in an error
+# Added on 2025-04-18
+read_only: false
+
+
+# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
+# Below is the complete list of tools for convenience.
+# To make sure you have the latest list of tools, and to view their descriptions, 
+# execute `uv run scripts/print_tool_overview.py`.
+#
+#  * `activate_project`: Activates a project by name.
+#  * `check_onboarding_performed`: Checks whether project onboarding was already performed.
+#  * `create_text_file`: Creates/overwrites a file in the project directory.
+#  * `delete_lines`: Deletes a range of lines within a file.
+#  * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
+#  * `execute_shell_command`: Executes a shell command.
+#  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
+#  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
+#  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
+#  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
+#  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
+#  * `initial_instructions`: Gets the initial instructions for the current project.
+#     Should only be used in settings where the system prompt cannot be set,
+#     e.g. in clients you have no control over, like Claude Desktop.
+#  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
+#  * `insert_at_line`: Inserts content at a given line in a file.
+#  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
+#  * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
+#  * `list_memories`: Lists memories in Serena's project-specific memory store.
+#  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
+#  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
+#  * `read_file`: Reads a file within the project directory.
+#  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
+#  * `remove_project`: Removes a project from the Serena configuration.
+#  * `replace_lines`: Replaces a range of lines within a file with new content.
+#  * `replace_symbol_body`: Replaces the full definition of a symbol.
+#  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
+#  * `search_for_pattern`: Performs a search for a pattern in the project.
+#  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
+#  * `switch_modes`: Activates modes by providing a list of their names
+#  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
+#  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
+#  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
+#  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
+excluded_tools: []
+
+# initial prompt for the project. It will always be given to the LLM upon activating the project
+# (contrary to the memories, which are loaded on demand).
+initial_prompt: ""
+
+project_name: "growi"

+ 20 - 0
.vscode/mcp.json

@@ -0,0 +1,20 @@
+{
+  "servers": {
+    "context7": {
+      "type": "http",
+      "url": "https://mcp.context7.com/mcp"
+    },
+    "serena": {
+      "type": "stdio",
+      "command": "uvx",
+      "args": [
+        "--from",
+        "git+https://github.com/oraios/serena",
+        "serena",
+        "start-mcp-server",
+        "--context",
+        "ide-assistant"
+      ]
+    }
+  }
+}

+ 63 - 1
.vscode/settings.json

@@ -11,6 +11,10 @@
     "editor.defaultFormatter": "biomejs.biome"
   },
 
+  "[json]": {
+    "editor.defaultFormatter": "biomejs.biome"
+  },
+
   // use vscode-stylelint
   // see https://github.com/stylelint/vscode-stylelint
   "stylelint.validate": ["css", "less", "scss"],
@@ -39,6 +43,64 @@
   "typescript.validate.enable": true,
   "typescript.surveys.enabled": false,
 
-  "vitest.filesWatcherInclude": "**/*"
+  "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"
 
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 303 - 92
CHANGELOG.md


+ 95 - 0
CLAUDE.md

@@ -0,0 +1,95 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Language
+
+If it is detected at the start or during a session that the user's primary language is not English, always respond in that language from then on. However, technical terms may remain in English as needed.
+
+## 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.

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2018 WESEEK, Inc.
+Copyright (c) 2018 GROWI, Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 12 - 12
README.md

@@ -6,7 +6,7 @@
   </a>
 </p>
 <p align="center">
-  <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg" alt="Latest version"></a>
+  <a href="https://github.com/growilabs/growi/releases/latest"><img src="https://img.shields.io/github/release/growilabs/growi.svg" alt="Latest version"></a>
   <a href="https://communityinviter.com/apps/wsgrowi/invite/"><img src="https://img.shields.io/badge/Slack-Join%20Us-4A154B?style=flat&logo=slack&logoColor=white" alt="Slack - Join US"></a>
 </p>
 
@@ -16,10 +16,10 @@
 
 # GROWI
 
-[![docker pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/)
-[![CodeQL](https://github.com/weseek/growi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/codeql-analysis.yml)
-[![Node CI for app development](https://github.com/weseek/growi/actions/workflows/ci-app.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/ci-app.yml)
-[![Node CI for app production](https://github.com/weseek/growi/actions/workflows/ci-app-prod.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/ci-app-prod.yml)
+[![docker pulls](https://img.shields.io/docker/pulls/growilabs/growi.svg)](https://hub.docker.com/r/growilabs/growi/)
+[![CodeQL](https://github.com/growilabs/growi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/growilabs/growi/actions/workflows/codeql-analysis.yml)
+[![Node CI for app development](https://github.com/growilabs/growi/actions/workflows/ci-app.yml/badge.svg)](https://github.com/growilabs/growi/actions/workflows/ci-app.yml)
+[![Node CI for app production](https://github.com/growilabs/growi/actions/workflows/ci-app-prod.yml/badge.svg)](https://github.com/growilabs/growi/actions/workflows/ci-app-prod.yml)
 
 ## Demonstration
 <video src="https://private-user-images.githubusercontent.com/34241526/333079483-fee540d7-2fa6-46d7-833e-74014c5340e3.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTY0NDk2OTEsIm5iZiI6MTcxNjQ0OTM5MSwicGF0aCI6Ii8zNDI0MTUyNi8zMzMwNzk0ODMtZmVlNTQwZDctMmZhNi00NmQ3LTgzM2UtNzQwMTRjNTM0MGUzLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MjMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTIzVDA3Mjk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBkYWFkMmYyYmIwMTI2YWE3ZmQzZTFiNWU3ZThkMDc5NDA5N2Q3YWE5ZGM1NDgwNjk0OGNjYjZmOTJkM2IzZGQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.FAvLseWBzE62yFA7wt26uERamvEVQdIGRVdBwk0uLhE"></video>
@@ -85,7 +85,7 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 - npm 6.x
 - pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 6.0 or above
+- MongoDB v6.x or v8.x
 
 ### Optional Dependencies
 
@@ -138,11 +138,11 @@ If you have questions or suggestions, you can [join our Slack team](https://comm
 # License
 
 - The MIT License (MIT)
-- See [LICENSE](https://github.com/weseek/growi/blob/master/LICENSE) and [THIRD-PARTY-NOTICES.md](https://github.com/weseek/growi/blob/master/THIRD-PARTY-NOTICES.md).
+- See [LICENSE](https://github.com/growilabs/growi/blob/master/LICENSE) and [THIRD-PARTY-NOTICES.md](https://github.com/growilabs/growi/blob/master/THIRD-PARTY-NOTICES.md).
 
 [crowi]: https://github.com/crowi/crowi
-[growi]: https://github.com/weseek/growi
-[issues]: https://github.com/weseek/growi/issues
-[pulls]: https://github.com/weseek/growi/pulls
-[dockerhub]: https://hub.docker.com/r/weseek/growi
-[docker-compose]: https://github.com/weseek/growi-docker-compose
+[growi]: https://github.com/growilabs/growi
+[issues]: https://github.com/growilabs/growi/issues
+[pulls]: https://github.com/growilabs/growi/pulls
+[dockerhub]: https://hub.docker.com/r/growilabs/growi
+[docker-compose]: https://github.com/growilabs/growi-docker-compose

+ 12 - 12
README_JP.md

@@ -6,7 +6,7 @@
   </a>
 </p>
 <p align="center">
-  <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg" alt="Latest version"></a>
+  <a href="https://github.com/growilabs/growi/releases/latest"><img src="https://img.shields.io/github/release/growilabs/growi.svg" alt="Latest version"></a>
   <a href="https://communityinviter.com/apps/wsgrowi/invite/"><img src="https://img.shields.io/badge/Slack-Join%20Us-4A154B?style=flat&logo=slack&logoColor=white" alt="Slack - Join US"></a>
 </p>
 
@@ -16,10 +16,10 @@
 
 # GROWI
 
-[![docker pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/)
-[![CodeQL](https://github.com/weseek/growi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/codeql-analysis.yml)
-[![Node CI for app development](https://github.com/weseek/growi/actions/workflows/ci-app.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/ci-app.yml)
-[![Node CI for app production](https://github.com/weseek/growi/actions/workflows/ci-app-prod.yml/badge.svg)](https://github.com/weseek/growi/actions/workflows/ci-app-prod.yml)
+[![docker pulls](https://img.shields.io/docker/pulls/growilabs/growi.svg)](https://hub.docker.com/r/growilabs/growi/)
+[![CodeQL](https://github.com/growilabs/growi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/growilabs/growi/actions/workflows/codeql-analysis.yml)
+[![Node CI for app development](https://github.com/growilabs/growi/actions/workflows/ci-app.yml/badge.svg)](https://github.com/growilabs/growi/actions/workflows/ci-app.yml)
+[![Node CI for app production](https://github.com/growilabs/growi/actions/workflows/ci-app-prod.yml/badge.svg)](https://github.com/growilabs/growi/actions/workflows/ci-app-prod.yml)
 
 ## デモ
 <video src="https://private-user-images.githubusercontent.com/34241526/333079216-cec7f7d8-c3cc-4ee7-bc94-167b056d4ce2.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTY0NDk0MDQsIm5iZiI6MTcxNjQ0OTEwNCwicGF0aCI6Ii8zNDI0MTUyNi8zMzMwNzkyMTYtY2VjN2Y3ZDgtYzNjYy00ZWU3LWJjOTQtMTY3YjA1NmQ0Y2UyLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MjMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTIzVDA3MjUwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ2M2IwZjc0ZGNhOWQxNWE4MGIyZTZlMzQ0ZDQ4MGZlNjEzMWE3MTQ1YmMwYzg3MmY1NWMyZWI2NzQ3NGIwMTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.qLfu5120JrkdfpghXlLG8wCn0p4JNZ7W8AD3zUJTIYY"></video>
@@ -85,7 +85,7 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 - npm 6.x
 - pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 6.0 以上
+- MongoDB v6.x or v8.x
 
 ### オプションの依存関係
 
@@ -137,11 +137,11 @@ Issue と Pull requests の作成は英語・日本語どちらでも受け付
 # ライセンス
 
 - The MIT License (MIT)
-- [ライセンス](https://github.com/weseek/growi/blob/master/LICENSE) と [THIRD-PARTY-NOTICES.md](https://github.com/weseek/growi/blob/master/THIRD-PARTY-NOTICES.md) をご覧ください。
+- [ライセンス](https://github.com/growilabs/growi/blob/master/LICENSE) と [THIRD-PARTY-NOTICES.md](https://github.com/growilabs/growi/blob/master/THIRD-PARTY-NOTICES.md) をご覧ください。
 
   [crowi]: https://github.com/crowi/crowi
-  [growi]: https://github.com/weseek/growi
-  [issues]: https://github.com/weseek/growi/issues
-  [pulls]: https://github.com/weseek/growi/pulls
-  [dockerhub]: https://hub.docker.com/r/weseek/growi
-  [docker-compose]: https://github.com/weseek/growi-docker-compose
+  [growi]: https://github.com/growilabs/growi
+  [issues]: https://github.com/growilabs/growi/issues
+  [pulls]: https://github.com/growilabs/growi/pulls
+  [dockerhub]: https://hub.docker.com/r/growilabs/growi
+  [docker-compose]: https://github.com/growilabs/growi-docker-compose

+ 1 - 1
THIRD-PARTY-NOTICES.md

@@ -9,7 +9,7 @@ please bring it to our attention through any of the ways detailed here :
 The attached notices are provided for information only.
 
 For any licenses that require disclosure of source, sources are available at  
-https://github.com/weseek/growi.
+https://github.com/growilabs/growi.
 
 
 1. Apache License, Version 2.0 Derivative Works

+ 4 - 2
apps/app/.env.development

@@ -14,7 +14,6 @@ ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
 OGP_URI="http://ogp:8088"
-QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # S2SMSG_PUBSUB_SERVER_TYPE=nchan
 # PUBLISH_OPEN_API=true
@@ -31,6 +30,9 @@ QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 
-# OpenTelemetry Configuration
+SERVICE_TYPE=dev
+
+# OpenTelemetry Official Configuration for dev
+# Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OPENTELEMETRY_ENABLED=false
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 4 - 2
apps/app/.env.production

@@ -5,6 +5,8 @@
 FORMAT_NODE_LOG=false
 MIGRATIONS_DIR=dist/migrations/
 
-# OpenTelemetry Configuration
-OTEL_TRACES_SAMPLER_ARG=0.1
+# OpenTelemetry Official Configuration
+# Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
+OTEL_TRACES_SAMPLER_ARG=0.01
+OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"

+ 51 - 10
apps/app/.eslintrc.js

@@ -2,12 +2,8 @@
  * @type {import('eslint').Linter.Config}
  */
 module.exports = {
-  extends: [
-    'next/core-web-vitals',
-    'weseek/react',
-  ],
-  plugins: [
-  ],
+  extends: ['next/core-web-vitals', 'weseek/react'],
+  plugins: [],
   ignorePatterns: [
     'dist/**',
     '**/dist/**',
@@ -16,6 +12,55 @@ module.exports = {
     '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/callout/**',
+    'src/features/comment/**',
+    'src/features/templates/**',
+    'src/features/mermaid/**',
+    'src/features/search/**',
+    'src/features/plantuml/**',
+    'src/features/external-user-group/**',
+    'src/features/page-bulk-export/**',
+    'src/features/growi-plugin/**',
+    'src/features/opentelemetry/**',
+    'src/features/openai/**',
+    'src/features/rate-limiter/**',
+    'src/stores-universal/**',
+    'src/interfaces/**',
+    'src/utils/**',
+    'src/components/**',
+    'src/services/**',
+    '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/routes/*.js',
+    'src/server/routes/*.ts',
+    'src/server/routes/attachment/**',
   ],
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
@@ -24,10 +69,6 @@ module.exports = {
     },
   },
   rules: {
-    'no-restricted-imports': ['error', {
-      name: 'axios',
-      message: 'Please use src/utils/axios instead.',
-    }],
     '@typescript-eslint/no-var-requires': 'off',
 
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei

+ 2 - 1
apps/app/bin/openapi/definition-apiv1.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
         server: {
           default: 'https://demo.growi.org',
-          description: 'The base URL for the GROWI API except for the version path (/_api). This can be set to your GROWI instance URL.',
+          description:
+            'The base URL for the GROWI API except for the version path (/_api). This can be set to your GROWI instance URL.',
         },
       },
     },

+ 6 - 13
apps/app/bin/openapi/definition-apiv3.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
         server: {
           default: 'https://demo.growi.org',
-          description: 'The base URL for the GROWI API except for the version path (/_api/v3). This can be set to your GROWI instance URL.',
+          description:
+            'The base URL for the GROWI API except for the version path (/_api/v3). This can be set to your GROWI instance URL.',
         },
       },
     },
@@ -55,7 +56,8 @@ module.exports = {
         name: 'mimeType',
         in: 'path',
         required: true,
-        description: 'Configurable MIME type (e.g., image/png, application/pdf)',
+        description:
+          'Configurable MIME type (e.g., image/png, application/pdf)',
         schema: {
           type: 'string',
           enum: [
@@ -115,11 +117,7 @@ module.exports = {
     },
     {
       name: 'User Personal Settings API',
-      tags: [
-        'GeneralSetting',
-        'EditorSetting',
-        'InAppNotificationSettings',
-      ],
+      tags: ['GeneralSetting', 'EditorSetting', 'InAppNotificationSettings'],
     },
     {
       name: 'System Management API',
@@ -138,8 +136,6 @@ module.exports = {
         'MongoDB',
         'NotificationSetting',
         'Plugins',
-        'Questionnaire',
-        'QuestionnaireSetting',
         'SlackIntegration',
         'SlackIntegrationSettings',
         'SlackIntegrationSettings (with proxy)',
@@ -157,10 +153,7 @@ module.exports = {
     },
     {
       name: 'Public API',
-      tags: [
-        'Healthcheck',
-        'Statistics',
-      ],
+      tags: ['Healthcheck', 'Statistics', '', '', '', '', '', ''],
     },
   ],
 };

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

@@ -1,8 +1,5 @@
-import { writeFileSync } from 'fs';
-
-import {
-  beforeEach, describe, expect, it, vi,
-} from 'vitest';
+import { writeFileSync } from 'node:fs';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { generateOperationIds } from './generate-operation-ids';
 
@@ -23,7 +20,7 @@ describe('cli', () => {
     vi.spyOn(console, 'error').mockImplementation(() => {});
   });
 
-  it('processes input file and writes output to specified file', async() => {
+  it('processes input file and writes output to specified file', async () => {
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
@@ -35,13 +32,15 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify generateOperationIds was called with correct arguments
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: undefined,
+    });
 
     // Verify writeFileSync was called with correct arguments
     expect(writeFileSync).toHaveBeenCalledWith('output.json', mockJsonStrings);
   });
 
-  it('uses input file as output when no output file is specified', async() => {
+  it('uses input file as output when no output file is specified', async () => {
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
@@ -53,13 +52,15 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify generateOperationIds was called with correct arguments
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: undefined,
+    });
 
     // Verify writeFileSync was called with input file as output
     expect(writeFileSync).toHaveBeenCalledWith('input.json', mockJsonStrings);
   });
 
-  it('handles overwrite-existing option correctly', async() => {
+  it('handles overwrite-existing option correctly', async () => {
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
@@ -71,10 +72,12 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify generateOperationIds was called with overwriteExisting option
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: true });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: true,
+    });
   });
 
-  it('handles generateOperationIds error correctly', async() => {
+  it('handles generateOperationIds error correctly', async () => {
     // Mock generateOperationIds to throw error
     const error = new Error('Test error');
     vi.mocked(generateOperationIds).mockRejectedValue(error);

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

@@ -1,10 +1,9 @@
-import { writeFileSync } from 'fs';
-
+import { writeFileSync } from 'node:fs';
 import { Command } from 'commander';
 
 import { generateOperationIds } from './generate-operation-ids';
 
-export const main = async(): Promise<void> => {
+export const main = async (): Promise<void> => {
   // parse command line arguments
   const program = new Command();
   program
@@ -18,7 +17,9 @@ export const main = async(): Promise<void> => {
   const [inputFile] = program.args;
 
   // eslint-disable-next-line no-console
-  const jsonStrings = await generateOperationIds(inputFile, { overwriteExisting }).catch(console.error);
+  const jsonStrings = await generateOperationIds(inputFile, {
+    overwriteExisting,
+  }).catch(console.error);
   if (jsonStrings != null) {
     writeFileSync(outputFile ?? inputFile, jsonStrings);
   }

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

@@ -1,13 +1,11 @@
-import fs from 'fs/promises';
-import { tmpdir } from 'os';
-import path from 'path';
-
+import fs from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
 import type { OpenAPI3 } from 'openapi-typescript';
 import { describe, expect, it } from 'vitest';
 
 import { generateOperationIds } from './generate-operation-ids';
 
-
 async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
   const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
   const filePath = path.join(tempDir, 'openapi.json');
@@ -19,15 +17,14 @@ async function cleanup(filePath: string): Promise<void> {
   try {
     await fs.unlink(filePath);
     await fs.rmdir(path.dirname(filePath));
-  }
-  catch (err) {
+  } catch (err) {
     // eslint-disable-next-line no-console
     console.error('Cleanup failed:', err);
   }
 }
 
 describe('generateOperationIds', () => {
-  it('should generate correct operationId for simple paths', async() => {
+  it('should generate correct operationId for simple paths', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -46,13 +43,12 @@ describe('generateOperationIds', () => {
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate correct operationId for paths with parameters', async() => {
+  it('should generate correct operationId for paths with parameters', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -72,14 +68,15 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
-      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe('getBarByPageByIdForFoo');
-    }
-    finally {
+      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe(
+        'getBarByPageByIdForFoo',
+      );
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate correct operationId for nested resources', async() => {
+  it('should generate correct operationId for nested resources', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -96,13 +93,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should preserve existing operationId when overwriteExisting is false', async() => {
+  it('should preserve existing operationId when overwriteExisting is false', async () => {
     const existingOperationId = 'existingOperation';
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
@@ -118,17 +114,18 @@ describe('generateOperationIds', () => {
 
     const filePath = await createTempOpenAPIFile(spec);
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: false });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: false,
+      });
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should overwrite existing operationId when overwriteExisting is true', async() => {
+  it('should overwrite existing operationId when overwriteExisting is true', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -143,17 +140,18 @@ describe('generateOperationIds', () => {
 
     const filePath = await createTempOpenAPIFile(spec);
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: true });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: true,
+      });
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate correct operationId for root path', async() => {
+  it('should generate correct operationId for root path', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -170,13 +168,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/'].get.operationId).toBe('getRoot');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate operationId for all HTTP methods', async() => {
+  it('should generate operationId for all HTTP methods', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -207,13 +204,14 @@ describe('generateOperationIds', () => {
       expect(parsed.paths['/foo'].options.operationId).toBe('optionsFoo');
       expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
       expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should throw error for non-existent file', async() => {
-    await expect(generateOperationIds('non-existent-file.json')).rejects.toThrow();
+  it('should throw error for non-existent file', async () => {
+    await expect(
+      generateOperationIds('non-existent-file.json'),
+    ).rejects.toThrow();
   });
 });

+ 42 - 16
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts

@@ -1,15 +1,25 @@
 import SwaggerParser from '@apidevtools/swagger-parser';
-import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
+import type {
+  OpenAPI3,
+  OperationObject,
+  PathItemObject,
+} from 'openapi-typescript';
 
-const toPascal = (s: string): string => s.split('-').map(w => w[0]?.toUpperCase() + w.slice(1)).join('');
+const toPascal = (s: string): string =>
+  s
+    .split('-')
+    .map((w) => w[0]?.toUpperCase() + w.slice(1))
+    .join('');
 
 const createParamSuffix = (params: string[]): string => {
   return params.length > 0
-    ? params.reverse().map(param => `By${toPascal(param.slice(1, -1))}`).join('')
+    ? params
+        .reverse()
+        .map((param) => `By${toPascal(param.slice(1, -1))}`)
+        .join('')
     : '';
 };
 
-
 /**
  * Generates a PascalCase operation name based on the HTTP method and path.
  *
@@ -24,8 +34,8 @@ const createParamSuffix = (params: string[]): string => {
  */
 function createOperationId(method: string, path: string): string {
   const segments = path.split('/').filter(Boolean);
-  const params = segments.filter(s => s.startsWith('{'));
-  const paths = segments.filter(s => !s.startsWith('{'));
+  const params = segments.filter((s) => s.startsWith('{'));
+  const paths = segments.filter((s) => !s.startsWith('{'));
 
   const paramSuffix = createParamSuffix(params);
 
@@ -37,19 +47,35 @@ function createOperationId(method: string, path: string): string {
   return `${method.toLowerCase()}${toPascal(resource)}${paramSuffix}For${context.reverse().map(toPascal).join('')}`;
 }
 
-export async function generateOperationIds(inputFile: string, opts?: { overwriteExisting: boolean }): Promise<string> {
-  const api = await SwaggerParser.parse(inputFile) as OpenAPI3;
+export async function generateOperationIds(
+  inputFile: string,
+  opts?: { overwriteExisting: boolean },
+): Promise<string> {
+  const api = (await SwaggerParser.parse(inputFile)) as OpenAPI3;
 
   Object.entries(api.paths || {}).forEach(([path, pathItem]) => {
     const item = pathItem as PathItemObject;
-    (['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'] as const)
-      .forEach((method) => {
-        const operation = item[method] as OperationObject | undefined;
-        if (operation == null || (operation.operationId != null && !opts?.overwriteExisting)) {
-          return;
-        }
-        operation.operationId = createOperationId(method, path);
-      });
+    (
+      [
+        'get',
+        'post',
+        'put',
+        'delete',
+        'patch',
+        'options',
+        'head',
+        'trace',
+      ] as const
+    ).forEach((method) => {
+      const operation = item[method] as OperationObject | undefined;
+      if (
+        operation == null ||
+        (operation.operationId != null && !opts?.overwriteExisting)
+      ) {
+        return;
+      }
+      operation.operationId = createOperationId(method, path);
+    });
   });
 
   const output = JSON.stringify(api, null, 2);

+ 0 - 1
apps/app/bin/openapi/generate-spec-apiv3.sh

@@ -11,7 +11,6 @@ swagger-jsdoc \
   -o "${OUT}" \
   -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
-  "${APP_PATH}/src/features/questionnaire/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \

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

@@ -0,0 +1,416 @@
+#!/usr/bin/env node
+/**
+ * Node.js Memory Consumption checker
+ *
+ * Retrieves heap memory information from a running Node.js server
+ * started with --inspect flag via Chrome DevTools Protocol
+ *
+ * Usage:
+ *   node --experimental-strip-types --experimental-transform-types \
+ *        --experimental-detect-module --no-warnings=ExperimentalWarning \
+ *        print-memory-consumption.ts [--port=9229] [--host=localhost] [--json]
+ */
+
+import { get } from 'node:http';
+import WebSocket from 'ws';
+
+interface MemoryInfo {
+  heapUsed: number;
+  heapTotal: number;
+  rss: number;
+  external: number;
+  arrayBuffers: number;
+  heapLimit?: number;
+  heapLimitSource: 'explicit' | 'estimated';
+  architecture: string;
+  platform: string;
+  nodeVersion: string;
+  pid: number;
+  uptime: number;
+  memoryFlags: string[];
+  timestamp: number;
+}
+
+interface DebugTarget {
+  webSocketDebuggerUrl: string;
+  title: string;
+  id: string;
+}
+
+class NodeMemoryConsumptionChecker {
+  private host: string;
+  private port: number;
+  private outputJson: boolean;
+
+  constructor(host = 'localhost', port = 9229, outputJson = false) {
+    this.host = host;
+    this.port = port;
+    this.outputJson = outputJson;
+  }
+
+  // Helper method to convert bytes to MB
+  private toMB(bytes: number): number {
+    return bytes / 1024 / 1024;
+  }
+
+  // Helper method to get pressure status and icon
+  private getPressureInfo(percentage: number): {
+    status: string;
+    icon: string;
+  } {
+    if (percentage > 90) return { status: 'HIGH PRESSURE', icon: '🔴' };
+    if (percentage > 70) return { status: 'MODERATE PRESSURE', icon: '🟡' };
+    return { status: 'LOW PRESSURE', icon: '🟢' };
+  }
+
+  // Helper method to create standard error
+  private createError(message: string): Error {
+    return new Error(message);
+  }
+
+  // Helper method to handle promise-based HTTP request
+  private httpGet(url: string): Promise<string> {
+    return new Promise((resolve, reject) => {
+      get(url, (res) => {
+        let data = '';
+        res.on('data', (chunk) => {
+          data += chunk;
+        });
+        res.on('end', () => resolve(data));
+      }).on('error', (err) =>
+        reject(this.createError(`Cannot connect to ${url}: ${err.message}`)),
+      );
+    });
+  }
+
+  // Generate JavaScript expression for memory collection
+  private getMemoryCollectionScript(): string {
+    return `JSON.stringify((() => {
+      const mem = process.memoryUsage();
+      const result = { ...mem, architecture: process.arch, platform: process.platform,
+        nodeVersion: process.version, pid: process.pid, uptime: process.uptime(),
+        timestamp: Date.now(), execArgv: process.execArgv };
+
+      const memFlags = process.execArgv.filter(arg =>
+        arg.includes('max-old-space-size') || arg.includes('max-heap-size'));
+      result.memoryFlags = memFlags;
+
+      const maxOldSpaceArg = memFlags.find(flag => flag.includes('max-old-space-size'));
+      if (maxOldSpaceArg) {
+        const match = maxOldSpaceArg.match(/max-old-space-size=(\\\\d+)/);
+        if (match) result.explicitHeapLimit = parseInt(match[1]) * 1024 * 1024;
+      }
+
+      if (!result.explicitHeapLimit) {
+        const is64bit = result.architecture === 'x64' || result.architecture === 'arm64';
+        const nodeVersion = parseInt(result.nodeVersion.split('.')[0].slice(1));
+        result.estimatedHeapLimit = is64bit
+          ? (nodeVersion >= 14 ? 4 * 1024 * 1024 * 1024 : 1.7 * 1024 * 1024 * 1024)
+          : 512 * 1024 * 1024;
+      }
+
+      return result;
+    })())`;
+  }
+
+  async checkMemory(): Promise<MemoryInfo | null> {
+    try {
+      // Get debug targets
+      const targets = await this.getDebugTargets();
+      if (targets.length === 0) {
+        throw new Error(
+          'No debug targets found. Is the Node.js server running with --inspect?',
+        );
+      }
+
+      // Get memory information via WebSocket
+      const memoryInfo = await this.getMemoryInfoViaWebSocket(targets[0]);
+      return memoryInfo;
+    } catch (error: unknown) {
+      const errorMessage =
+        error instanceof Error ? error.message : String(error);
+      if (!this.outputJson) {
+        console.error('❌ Error:', errorMessage);
+      }
+      return null;
+    }
+  }
+
+  private async getDebugTargets(): Promise<DebugTarget[]> {
+    const url = `http://${this.host}:${this.port}/json/list`;
+    try {
+      const data = await this.httpGet(url);
+      return JSON.parse(data);
+    } catch (e) {
+      throw this.createError(`Failed to parse debug targets: ${e}`);
+    }
+  }
+
+  private async getMemoryInfoViaWebSocket(
+    target: DebugTarget,
+  ): Promise<MemoryInfo> {
+    return new Promise((resolve, reject) => {
+      const ws = new WebSocket(target.webSocketDebuggerUrl);
+
+      const timeout = setTimeout(() => {
+        ws.close();
+        reject(new Error('WebSocket connection timeout'));
+      }, 10000);
+
+      ws.on('open', () => {
+        // Send Chrome DevTools Protocol message
+        const message = JSON.stringify({
+          id: 1,
+          method: 'Runtime.evaluate',
+          params: { expression: this.getMemoryCollectionScript() },
+        });
+        ws.send(message);
+      });
+
+      ws.on('message', (data: Buffer | string) => {
+        clearTimeout(timeout);
+
+        try {
+          const response = JSON.parse(data.toString());
+
+          if (response.result?.result?.value) {
+            const rawData = JSON.parse(response.result.result.value);
+
+            const memoryInfo: MemoryInfo = {
+              heapUsed: rawData.heapUsed,
+              heapTotal: rawData.heapTotal,
+              rss: rawData.rss,
+              external: rawData.external,
+              arrayBuffers: rawData.arrayBuffers,
+              heapLimit:
+                rawData.explicitHeapLimit || rawData.estimatedHeapLimit,
+              heapLimitSource: rawData.explicitHeapLimit
+                ? 'explicit'
+                : 'estimated',
+              architecture: rawData.architecture,
+              platform: rawData.platform,
+              nodeVersion: rawData.nodeVersion,
+              pid: rawData.pid,
+              uptime: rawData.uptime,
+              memoryFlags: rawData.memoryFlags || [],
+              timestamp: rawData.timestamp,
+            };
+
+            resolve(memoryInfo);
+          } else {
+            reject(
+              new Error(
+                'Invalid response format from Chrome DevTools Protocol',
+              ),
+            );
+          }
+        } catch (error) {
+          reject(new Error(`Failed to parse WebSocket response: ${error}`));
+        } finally {
+          ws.close();
+        }
+      });
+
+      ws.on('error', (error: Error) => {
+        clearTimeout(timeout);
+        reject(new Error(`WebSocket error: ${error.message}`));
+      });
+    });
+  }
+
+  displayResults(info: MemoryInfo): void {
+    if (this.outputJson) {
+      console.log(JSON.stringify(info, null, 2));
+      return;
+    }
+
+    const [
+      heapUsedMB,
+      heapTotalMB,
+      heapLimitMB,
+      rssMB,
+      externalMB,
+      arrayBuffersMB,
+    ] = [
+      this.toMB(info.heapUsed),
+      this.toMB(info.heapTotal),
+      this.toMB(info.heapLimit || 0),
+      this.toMB(info.rss),
+      this.toMB(info.external),
+      this.toMB(info.arrayBuffers),
+    ];
+
+    console.log('\n📊 Node.js Memory Information');
+    console.log(''.padEnd(50, '='));
+
+    // Current Memory Usage
+    console.log('\n🔸 Current Memory Usage:');
+    console.log(`  Heap Used:      ${heapUsedMB.toFixed(2)} MB`);
+    console.log(`  Heap Total:     ${heapTotalMB.toFixed(2)} MB`);
+    console.log(`  RSS:            ${rssMB.toFixed(2)} MB`);
+    console.log(`  External:       ${externalMB.toFixed(2)} MB`);
+    console.log(`  Array Buffers:  ${arrayBuffersMB.toFixed(2)} MB`);
+
+    // Heap Limits
+    console.log('\n🔸 Heap Limits:');
+    if (info.heapLimit) {
+      const limitType =
+        info.heapLimitSource === 'explicit'
+          ? 'Explicit Limit'
+          : 'Default Limit';
+      const limitSource =
+        info.heapLimitSource === 'explicit'
+          ? '(from --max-old-space-size)'
+          : '(system default)';
+      console.log(
+        `  ${limitType}: ${heapLimitMB.toFixed(2)} MB ${limitSource}`,
+      );
+      console.log(
+        `  Global Usage:   ${((heapUsedMB / heapLimitMB) * 100).toFixed(2)}% of maximum`,
+      );
+    }
+
+    // Heap Pressure Analysis
+    const heapPressure = (info.heapUsed / info.heapTotal) * 100;
+    const { status: pressureStatus, icon: pressureIcon } =
+      this.getPressureInfo(heapPressure);
+    console.log('\n� Memory Pressure Analysis:');
+    console.log(
+      `  Current Pool:   ${pressureIcon} ${pressureStatus} (${heapPressure.toFixed(1)}% of allocated heap)`,
+    );
+
+    if (heapPressure > 90) {
+      console.log(
+        '  📝 Note: High pressure is normal - Node.js will allocate more heap as needed',
+      );
+    }
+
+    // System Information
+    console.log('\n🔸 System Information:');
+    console.log(`  Architecture:   ${info.architecture}`);
+    console.log(`  Platform:       ${info.platform}`);
+    console.log(`  Node.js:        ${info.nodeVersion}`);
+    console.log(`  Process ID:     ${info.pid}`);
+    console.log(`  Uptime:         ${(info.uptime / 60).toFixed(1)} minutes`);
+
+    // Memory Flags
+    if (info.memoryFlags.length > 0) {
+      console.log('\n🔸 Memory Flags:');
+      info.memoryFlags.forEach((flag) => {
+        console.log(`  ${flag}`);
+      });
+    }
+
+    // Summary
+    console.log('\n📋 Summary:');
+    if (info.heapLimit) {
+      const heapUsagePercent = (heapUsedMB / heapLimitMB) * 100;
+      console.log(
+        `Heap Memory: ${heapUsedMB.toFixed(2)} MB / ${heapLimitMB.toFixed(2)} MB (${heapUsagePercent.toFixed(2)}%)`,
+      );
+      console.log(
+        heapUsagePercent > 80
+          ? '⚠️  Consider increasing heap limit with --max-old-space-size if needed'
+          : '✅ Memory usage is within healthy limits',
+      );
+    }
+
+    console.log(''.padEnd(50, '='));
+    console.log(`Retrieved at: ${new Date(info.timestamp).toLocaleString()}`);
+  }
+}
+
+// Command line interface
+function parseArgs(): {
+  host: string;
+  port: number;
+  json: boolean;
+  help: boolean;
+} {
+  const args = process.argv.slice(2);
+  let host = 'localhost';
+  let port = 9229;
+  let json = false;
+  let help = false;
+
+  for (const arg of args) {
+    if (arg.startsWith('--host=')) {
+      host = arg.split('=')[1];
+    } else if (arg.startsWith('--port=')) {
+      port = parseInt(arg.split('=')[1]);
+    } else if (arg === '--json') {
+      json = true;
+    } else if (arg === '--help' || arg === '-h') {
+      help = true;
+    }
+  }
+
+  return {
+    host,
+    port,
+    json,
+    help,
+  };
+}
+
+function showHelp(): void {
+  console.log(`
+Node.js Memory Checker
+
+Retrieves heap memory information from a running Node.js server via Chrome DevTools Protocol.
+
+Usage:
+  node --experimental-strip-types --experimental-transform-types \\
+       --experimental-detect-module --no-warnings=ExperimentalWarning \\
+       print-memory-consumption.ts [OPTIONS]
+
+Options:
+  --host=HOST    Debug host (default: localhost)
+  --port=PORT    Debug port (default: 9229)
+  --json         Output in JSON format
+  --help, -h     Show this help message
+
+Prerequisites:
+  - Target Node.js server must be started with --inspect flag
+  - WebSocket package: npm install ws @types/ws
+
+Example:
+  # Check memory of server running on default debug port
+  node --experimental-strip-types --experimental-transform-types \\
+       --experimental-detect-module --no-warnings=ExperimentalWarning \\
+       print-memory-consumption.ts
+
+  # Check with custom port and JSON output
+  node --experimental-strip-types --experimental-transform-types \\
+       --experimental-detect-module --no-warnings=ExperimentalWarning \\
+       print-memory-consumption.ts --port=9230 --json
+`);
+}
+
+// Main execution
+async function main(): Promise<void> {
+  const { host, port, json, help } = parseArgs();
+
+  if (help) {
+    showHelp();
+    process.exit(0);
+  }
+
+  const checker = new NodeMemoryConsumptionChecker(host, port, json);
+  const memoryInfo = await checker.checkMemory();
+
+  if (memoryInfo) {
+    checker.displayResults(memoryInfo);
+    process.exit(0);
+  } else {
+    process.exit(1);
+  }
+}
+
+// Execute if called directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+  main().catch((error) => {
+    console.error('Fatal error:', error);
+    process.exit(1);
+  });
+}

+ 0 - 8
apps/app/config/cdn.js

@@ -1,8 +0,0 @@
-import path from 'path';
-
-import { projectRoot } from '~/utils/project-dir-utils';
-
-export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
-export const cdnLocalScriptWebRoot = '/static/js/cdn';
-export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
-export const cdnLocalStyleWebRoot = '/static/styles/cdn';

+ 1 - 1
apps/app/config/logger/config.dev.js

@@ -29,7 +29,6 @@ module.exports = {
   'growi-plugin:*': 'debug',
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:g2g-transfer': 'debug',
-  'growi:service:questionnaire': 'debug',
 
   'growi:migration:add-installed-date-to-config': 'debug',
 
@@ -44,4 +43,5 @@ module.exports = {
   // 'growi:cli:ItemsTree': 'debug',
   'growi:searchResultList': 'debug',
   'growi:service:openai': 'debug',
+  'growi:middleware:access-token-parser:access-token': 'debug',
 };

+ 3 - 3
apps/app/config/migrate-mongo-config.js

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

+ 4 - 8
apps/app/config/migrate-mongo-config.spec.ts

@@ -2,11 +2,8 @@ import mockRequire from 'mock-require';
 
 const { reRequire } = mockRequire;
 
-
 describe('config/migrate-mongo-config.js', () => {
-
   test.concurrent('throws an error when MIGRATIONS_DIR is not set', () => {
-
     const getMongoUriMock = vi.fn();
     const mongoOptionsMock = vi.fn();
 
@@ -32,13 +29,11 @@ describe('config/migrate-mongo-config.js', () => {
     ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
     ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
   `('returns', ({ MONGO_URI, expectedDbName }) => {
-
-    beforeEach(async() => {
+    beforeEach(async () => {
       process.env.MIGRATIONS_DIR = 'testdir/migrations';
     });
 
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
-
       const getMongoUriMock = vi.fn(() => MONGO_URI);
       const mongoOptionsMock = vi.fn();
 
@@ -49,7 +44,9 @@ describe('config/migrate-mongo-config.js', () => {
       });
 
       // use reRequire to avoid using module cache
-      const { mongodb, migrationsDir, changelogCollectionName } = reRequire('./migrate-mongo-config');
+      const { mongodb, migrationsDir, changelogCollectionName } = reRequire(
+        './migrate-mongo-config',
+      );
 
       mockRequire.stop('../src/server/util/mongoose-utils');
 
@@ -61,5 +58,4 @@ describe('config/migrate-mongo-config.js', () => {
       expect(changelogCollectionName).toBe('migrations');
     });
   });
-
 });

+ 7 - 7
apps/app/config/next-i18next.config.js

@@ -1,5 +1,6 @@
 const isDev = process.env.NODE_ENV === 'development';
 
+// biome-ignore lint/style/useNodejsImportProtocol: ignore
 const path = require('path');
 
 const { AllLang } = require('@growi/core');
@@ -26,17 +27,17 @@ module.exports = {
     ? isServer()
       ? [new HMRPlugin({ webpack: { server: true } })]
       : [
-        require('i18next-chained-backend').default,
-        new HMRPlugin({ webpack: { client: true } }),
-      ]
+          require('i18next-chained-backend').default,
+          new HMRPlugin({ webpack: { client: true } }),
+        ]
     : [],
   backend: {
     backends: isServer()
       ? []
       : [
-        require('i18next-localstorage-backend').default,
-        require('i18next-http-backend').default,
-      ],
+          require('i18next-localstorage-backend').default,
+          require('i18next-http-backend').default,
+        ],
     backendOptions: [
       // options for i18next-localstorage-backend
       { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
@@ -44,5 +45,4 @@ module.exports = {
       { loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
     ],
   },
-
 };

+ 10 - 10
apps/app/docker/README.md

@@ -2,7 +2,7 @@
 GROWI Official docker image
 ========================
 
-[![Actions Status](https://github.com/weseek/growi/workflows/Release/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
+[![Actions Status](https://github.com/growilabs/growi/workflows/Release/badge.svg)](https://github.com/growilabs/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/growilabs/growi.svg)](https://hub.docker.com/r/growilabs/growi/) 
 
 ![GROWI-x-docker](https://github.com/user-attachments/assets/1a82236d-5a85-4a2e-842a-971b4c1625e6)
 
@@ -10,17 +10,17 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`7.1.0`, `7.1`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.1.0/apps/app/docker/Dockerfile)
-* [`7.0.23`, `7.0` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.23/apps/app/docker/Dockerfile)
-* [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
+* [`7.1.0`, `7.1`, `7`, `latest` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.1.0/apps/app/docker/Dockerfile)
+* [`7.0.23`, `7.0` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.0.23/apps/app/docker/Dockerfile)
+* [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/growilabs/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 
 
 What is GROWI?
 -------------
 
-GROWI is a team collaboration software and it forked from [crowi](https://github.com/weseek/crowi/crowi)
+GROWI is a team collaboration software and it forked from [crowi](https://github.com/crowi/crowi)
 
-see: [weseek/growi](https://github.com/weseek/growi)
+see: [growilabs/growi](https://github.com/growilabs/growi)
 
 
 Requirements
@@ -41,7 +41,7 @@ Usage
 ```bash
 docker run -d \
     -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
-    weseek/growi
+    growilabs/growi
 ```
 
 and go to `http://localhost:3000/` .
@@ -52,7 +52,7 @@ If you use ElasticSearch, type this:
 docker run -d \
     -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
     -e ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_PORT/growi \
-    weseek/growi
+    growilabs/growi
 ```
 
 
@@ -60,7 +60,7 @@ docker run -d \
 
 Using docker-compose is the fastest and the most convenient way to boot GROWI.
 
-see: [weseek/growi-docker-compose](https://github.com/weseek/growi-docker-compose)
+see: [growilabs/growi-docker-compose](https://github.com/growilabs/growi-docker-compose)
 
 
 Configuration
@@ -76,5 +76,5 @@ See [GROWI Docs: Admin Guide](https://docs.growi.org/en/admin-guide/) ([en](http
 Issues
 ------
 
-If you have any issues or questions about this image, please contact us through  [GitHub issue](https://github.com/weseek/growi-docker/issues).
+If you have any issues or questions about this image, please contact us through  [GitHub issue](https://github.com/growilabs/growi-docker/issues).
 

+ 44 - 44
apps/app/docker/codebuild/.terraform.lock.hcl

@@ -2,64 +2,64 @@
 # Manual edits may be lost in future updates.
 
 provider "registry.terraform.io/hashicorp/aws" {
-  version     = "4.49.0"
-  constraints = "~> 4.16"
+  version     = "6.12.0"
+  constraints = ">= 5.0.0, >= 6.0.0, ~> 6.0"
   hashes = [
-    "h1:oOwWQpvQWd1uVP1axBz/TO6xzzLWoL982AY/MQfeF7I=",
-    "zh:09803937f00fdf2873eccf685eec7854408925cbf183c9b683df7c5825be463f",
-    "zh:2af1575e538fb0b669266f8d1385b17bfdaf17c521b6b6329baa1f2971fc4a4d",
-    "zh:3f71882b438cde3108fe68cfe2637839d3eed08157a9721bd97babf3912247a8",
-    "zh:577af1b38f5da8a9f29eebe5eebec9279d26e757cd03b0c8c59311f9ce8a859b",
-    "zh:60160d39094973beefb9b10cfd6aaa5b63a2e68c32445ecffcd1b101356e6f9b",
-    "zh:762656454722548baeccf35cbaa23b887976337e1ed321682df7390419fdf22d",
-    "zh:7f6d7887821659bf3bef815949077dc91ffcdb0d911644a887b6683b264a5ca6",
-    "zh:8f16a352cc903f8951fa4619c36233b3e66e27d724817b131f2035dd8896f524",
-    "zh:8f768f65e370366c8b91c00d01c9a6264fe26ea9ae1819f14bdcd12c066272bc",
-    "zh:95ad78c689a83c08ef7c3e544c3c9aca93ed528054aa77cc968ddd9efa3a1023",
+    "h1:QiSzB4pjONZ4hek1L8Rcd6S9vtP+yMr5iOfczJg5/JI=",
+    "zh:054bcbf13c6ac9ddd2247876f82f9b56493e2f71d8c88baeec142386a395165d",
+    "zh:195489f16ad5621db2cec80be997d33060462a3b8d442c890bef3eceba34fa4d",
+    "zh:3461ef14904ab7de246296e44d24c042f3190e6bead3d7ce1d9fda63dcb0f047",
+    "zh:44517a0035996431e4127f45db5a84f53ce80730eae35629eda3101709df1e5c",
+    "zh:4b0374abaa6b9a9debed563380cc944873e4f30771dd1da7b9e812a49bf485e3",
+    "zh:531468b99465bd98a89a4ce2f1a30168dfadf6edb57f7836df8a977a2c4f9804",
+    "zh:6a95ed7b4852174aa748d3412bff3d45e4d7420d12659f981c3d9f4a1a59a35f",
+    "zh:88c2d21af1e64eed4a13dbb85590c66a519f3ecc54b72875d4bb6326f3ef84e7",
     "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
-    "zh:a47097ab6a4ca8302da82964303ffdd2310ed65e8f8524bfe4058816cf1addb7",
-    "zh:b66d820c70cd5fd628ffe882d2b97e76b969dca4e6827ac2ba0f8d3bc5d6e9c6",
-    "zh:b80f713a4f3e1355c3dd1600e9d08b9f15ed2370054ec792ad2c01f2541abe02",
-    "zh:ce065bc3962cb71fa7652562226b9d486f3d7fcb88285c1020ebe2f550e28641",
+    "zh:a8b648470bb5df098e56b1ec5c6a39e0bbb7b496b23a19ea9f494bf48d4a122a",
+    "zh:b23fb13efdb527677db546bc92aeb2bdf64ff3f480188841f2bfdfa7d3d907c1",
+    "zh:be5858a1951ae5f5a9c388949c3e3c66a3375f684fb79b06b1d1db7a9703b18e",
+    "zh:c368e03a7c922493daf4c7348faafc45f455225815ef218b5491c46cea5f76b7",
+    "zh:e31e75d5d19b8ac08aa01be7e78207966e1faa3b82ed9fe3acfdc2d806be924c",
+    "zh:ea84182343b5fd9252a6fae41e844eed4fdc3311473a753b09f06e49ec0e7853",
   ]
 }
 
 provider "registry.terraform.io/hashicorp/random" {
-  version     = "3.4.3"
+  version     = "3.7.2"
   constraints = ">= 2.1.0"
   hashes = [
-    "h1:xZGZf18JjMS06pFa4NErzANI98qi59SEcBsOcS2P2yQ=",
-    "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752",
-    "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b",
-    "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53",
+    "h1:KG4NuIBl1mRWU0KD/BGfCi1YN/j3F7H4YgeeM7iSdNs=",
+    "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f",
+    "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc",
+    "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab",
+    "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3",
+    "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212",
+    "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f",
     "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
-    "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3",
-    "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5",
-    "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda",
-    "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6",
-    "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1",
-    "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d",
-    "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8",
-    "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93",
+    "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34",
+    "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967",
+    "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d",
+    "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62",
+    "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0",
   ]
 }
 
 provider "registry.terraform.io/hashicorp/tls" {
-  version     = "4.0.4"
-  constraints = ">= 3.0.0"
+  version     = "4.1.0"
+  constraints = ">= 4.0.0"
   hashes = [
-    "h1:pe9vq86dZZKCm+8k1RhzARwENslF3SXb9ErHbQfgjXU=",
-    "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55",
-    "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848",
-    "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be",
-    "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5",
-    "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe",
-    "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e",
-    "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48",
-    "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8",
-    "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60",
-    "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e",
-    "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316",
+    "h1:zEv9tY1KR5vaLSyp2lkrucNJ+Vq3c+sTFK9GyQGLtFs=",
+    "zh:14c35d89307988c835a7f8e26f1b83ce771e5f9b41e407f86a644c0152089ac2",
+    "zh:2fb9fe7a8b5afdbd3e903acb6776ef1be3f2e587fb236a8c60f11a9fa165faa8",
+    "zh:35808142ef850c0c60dd93dc06b95c747720ed2c40c89031781165f0c2baa2fc",
+    "zh:35b5dc95bc75f0b3b9c5ce54d4d7600c1ebc96fbb8dfca174536e8bf103c8cdc",
+    "zh:38aa27c6a6c98f1712aa5cc30011884dc4b128b4073a4a27883374bfa3ec9fac",
+    "zh:51fb247e3a2e88f0047cb97bb9df7c228254a3b3021c5534e4563b4007e6f882",
+    "zh:62b981ce491e38d892ba6364d1d0cdaadcee37cc218590e07b310b1dfa34be2d",
+    "zh:bc8e47efc611924a79f947ce072a9ad698f311d4a60d0b4dfff6758c912b7298",
+    "zh:c149508bd131765d1bc085c75a870abb314ff5a6d7f5ac1035a8892d686b6297",
+    "zh:d38d40783503d278b63858978d40e07ac48123a2925e1a6b47e62179c046f87a",
     "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
+    "zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54",
   ]
 }

+ 1 - 1
apps/app/docker/codebuild/buildspec.yml

@@ -11,7 +11,7 @@ phases:
   pre_build:
     commands:
       # login to docker.io
-      - echo ${DOCKER_REGISTRY_PASSWORD} | docker login --username wsmoogle --password-stdin
+      - echo ${DOCKER_REGISTRY_PASSWORD} | docker login --username growimoogle --password-stdin
   build:
     commands:
       - docker build -t ${IMAGE_TAG} -f ./apps/app/docker/Dockerfile .

+ 1 - 1
apps/app/docker/codebuild/codebuild.tf

@@ -7,7 +7,7 @@ module "codebuild" {
   artifact_type       = "NO_ARTIFACTS"
 
   source_type         = "GITHUB"
-  source_location     = "https://github.com/weseek/growi.git"
+  source_location     = "https://github.com/growilabs/growi.git"
   source_version      = "refs/heads/master"
   git_clone_depth     = 1
 

+ 1 - 1
apps/app/docker/codebuild/main.tf

@@ -10,7 +10,7 @@ terraform {
   required_providers {
     aws = {
       source  = "hashicorp/aws"
-      version = "~> 4.16"
+      version = "~> 6.0"
     }
   }
 

+ 1 - 1
apps/app/docker/codebuild/oidc.tf

@@ -7,7 +7,7 @@ module "oidc_github" {
   }
 
   github_repositories = [
-    "weseek/growi",
+    "growilabs/growi",
   ]
 }
 

+ 11 - 4
apps/app/jest.config.js

@@ -22,9 +22,14 @@ module.exports = {
 
       rootDir: '.',
       roots: ['<rootDir>'],
-      testMatch: ['<rootDir>/test/integration/**/*.test.ts', '<rootDir>/test/integration/**/*.test.js'],
+      testMatch: [
+        '<rootDir>/test/integration/**/*.test.ts',
+        '<rootDir>/test/integration/**/*.test.js',
+      ],
       // https://regex101.com/r/jTaxYS/1
-      modulePathIgnorePatterns: ['<rootDir>/test/integration/*.*/v5(..*)*.[t|j]s'],
+      modulePathIgnorePatterns: [
+        '<rootDir>/test/integration/*.*/v5(..*)*.[t|j]s',
+      ],
       testEnvironment: 'node',
       globalSetup: '<rootDir>/test/integration/global-setup.js',
       globalTeardown: '<rootDir>/test/integration/global-teardown.js',
@@ -43,7 +48,10 @@ module.exports = {
 
       rootDir: '.',
       roots: ['<rootDir>'],
-      testMatch: ['<rootDir>/test/integration/**/v5.*.test.ts', '<rootDir>/test/integration/**/v5.*.test.js'],
+      testMatch: [
+        '<rootDir>/test/integration/**/v5.*.test.ts',
+        '<rootDir>/test/integration/**/v5.*.test.js',
+      ],
 
       testEnvironment: 'node',
       globalSetup: '<rootDir>/test/integration/global-setup.js',
@@ -75,5 +83,4 @@ module.exports = {
     '/resource/',
     '/node_modules/',
   ],
-
 };

+ 19 - 16
apps/app/next.config.js

@@ -5,11 +5,13 @@
  * See: https://github.com/vercel/next.js/discussions/35969#discussioncomment-2522954
  */
 
-const path = require('path');
+const path = require('node:path');
 
 const { withSuperjson } = require('next-superjson');
-const { PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER } = require('next/constants');
-
+const {
+  PHASE_PRODUCTION_BUILD,
+  PHASE_PRODUCTION_SERVER,
+} = require('next/constants');
 
 const getTranspilePackages = () => {
   const { listPrefixedPackages } = require('./src/utils/next.config.utils');
@@ -56,7 +58,14 @@ const getTranspilePackages = () => {
     'github-slugger',
     'html-url-attributes',
     'estree-util-is-identifier-name',
-    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
+    ...listPrefixedPackages([
+      'remark-',
+      'rehype-',
+      'hast-',
+      'mdast-',
+      'micromark-',
+      'unist-',
+    ]),
   ];
 
   // const eazyLogger = require('eazy-logger');
@@ -84,13 +93,11 @@ const optimizePackageImports = [
   '@growi/ui',
 ];
 
-module.exports = async(phase, { defaultConfig }) => {
-
+module.exports = async (phase) => {
   const { i18n, localePath } = require('./config/next-i18next.config');
 
   /** @type {import('next').NextConfig} */
   const nextConfig = {
-
     reactStrictMode: true,
     poweredByHeader: false,
     pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
@@ -103,9 +110,8 @@ module.exports = async(phase, { defaultConfig }) => {
     typescript: {
       tsconfigPath: 'tsconfig.build.client.json',
     },
-    transpilePackages: phase !== PHASE_PRODUCTION_SERVER
-      ? getTranspilePackages()
-      : undefined,
+    transpilePackages:
+      phase !== PHASE_PRODUCTION_SERVER ? getTranspilePackages() : undefined,
     experimental: {
       optimizePackageImports,
     },
@@ -150,7 +156,6 @@ module.exports = async(phase, { defaultConfig }) => {
 
       return config;
     },
-
   };
 
   // production server
@@ -159,11 +164,9 @@ module.exports = async(phase, { defaultConfig }) => {
   }
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
-    enabled: phase === PHASE_PRODUCTION_BUILD
-      && (
-        process.env.ANALYZE === 'true'
-          || process.env.ANALYZE === '1'
-      ),
+    enabled:
+      phase === PHASE_PRODUCTION_BUILD &&
+      (process.env.ANALYZE === 'true' || process.env.ANALYZE === '1'),
   });
 
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 1 - 4
apps/app/nodemon.json

@@ -1,9 +1,6 @@
 {
   "ext": "js,ts,json",
-  "watch": [
-    ".",
-    "../../packages/**/dist"
-  ],
+  "watch": [".", "../../packages/**/dist"],
   "ignore": [
     ".next",
     "public/static",

+ 30 - 23
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.2.7-RC.0",
+  "version": "7.3.6-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -28,18 +28,19 @@
     "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",
     "lint:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
     "lint": "run-p lint:**",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv1": "pnpm run openapi:generate-spec:apiv1",
-    "test": "run-p test:*",
+    "test": "run-p test:jest test:vitest:coverage",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
-    "test:vitest": "vitest run --coverage",
+    "test:vitest": "vitest run",
+    "test:vitest:coverage": "COLUMNS=200 vitest run --coverage",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
-    "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "console": "npm run repl",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
@@ -69,8 +70,9 @@
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@cspell/dynamic-import": "^8.15.4",
-    "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
-    "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
+    "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.4",
+    "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.18.2",
+    "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.3",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "workspace:^",
@@ -86,14 +88,14 @@
     "@growi/slack": "workspace:^",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@opentelemetry/api": "^1.9.0",
-    "@opentelemetry/auto-instrumentations-node": "^0.55.1",
-    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.0",
-    "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
-    "@opentelemetry/resources": "^1.28.0",
-    "@opentelemetry/sdk-metrics": "^1.28.0",
-    "@opentelemetry/sdk-node": "^0.57.0",
-    "@opentelemetry/sdk-trace-node": "^1.28.0",
-    "@opentelemetry/semantic-conventions": "^1.28.0",
+    "@opentelemetry/auto-instrumentations-node": "^0.60.1",
+    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
+    "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0",
+    "@opentelemetry/resources": "^2.0.1",
+    "@opentelemetry/sdk-metrics": "^2.0.1",
+    "@opentelemetry/sdk-node": "^0.202.0",
+    "@opentelemetry/sdk-trace-node": "^2.0.1",
+    "@opentelemetry/semantic-conventions": "^1.34.0",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@types/async": "^3.2.24",
@@ -102,7 +104,7 @@
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
-    "axios": "^0.24.0",
+    "axios": "^1.11.0",
     "axios-retry": "^3.2.4",
     "babel-plugin-superjson-next": "^0.4.2",
     "body-parser": "^1.20.3",
@@ -134,7 +136,7 @@
     "express-session": "^1.16.1",
     "express-validator": "^6.14.0",
     "extensible-custom-error": "^0.0.7",
-    "form-data": "^4.0.0",
+    "form-data": "^4.0.4",
     "graceful-fs": "^4.1.11",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
@@ -156,7 +158,7 @@
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-wiki-link": "^0.1.2",
-    "mermaid": "^11.2.0",
+    "mermaid": "^11.10.0",
     "method-override": "^3.0.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-wiki-link": "^0.0.4",
@@ -164,13 +166,13 @@
     "mkdirp": "^1.0.3",
     "mongodb": "^4.17.2",
     "mongoose": "^6.13.6",
-    "mongoose-gridfs": "^1.2.42",
+    "mongoose-gridfs": "^1.3.0",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
-    "next": "^14.2.26",
+    "next": "^14.2.32",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
     "next-superjson": "^0.0.4",
@@ -244,7 +246,7 @@
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "uuid": "^11.0.3",
-    "validator": "^13.7.0",
+    "validator": "^13.15.20",
     "ws": "^8.17.1",
     "xss": "^1.0.15",
     "y-mongodb-provider": "^0.2.0",
@@ -260,6 +262,7 @@
   },
   "devDependencies": {
     "@apidevtools/swagger-parser": "^10.1.1",
+    "@codemirror/state": "^6.5.2",
     "@emoji-mart/data": "^1.2.1",
     "@growi/core-styles": "workspace:^",
     "@growi/custom-icons": "workspace:^",
@@ -270,9 +273,7 @@
     "@popperjs/core": "^2.11.8",
     "@swc-node/jest": "^1.8.1",
     "@swc/jest": "^0.2.36",
-    "@testing-library/dom": "^10.4.0",
     "@testing-library/jest-dom": "^6.5.0",
-    "@testing-library/react": "^16.0.1",
     "@testing-library/user-event": "^14.5.2",
     "@types/archiver": "^6.0.2",
     "@types/bunyan": "^1.8.11",
@@ -287,12 +288,14 @@
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
+    "@types/supertest": "^6.0.3",
     "@types/testing-library__dom": "^7.5.0",
     "@types/throttle-debounce": "^5.0.1",
     "@types/unist": "^3.0.3",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
     "@types/uuid": "^10.0.0",
+    "@types/ws": "^8.18.1",
     "babel-loader": "^8.2.5",
     "bootstrap": "=5.3.2",
     "commander": "^14.0.0",
@@ -301,6 +304,7 @@
     "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",
     "happy-dom": "^15.7.4",
@@ -334,6 +338,9 @@
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",
-    "swagger2openapi": "^7.0.8"
+    "supertest": "^7.1.4",
+    "swagger2openapi": "^7.0.8",
+    "unist-util-is": "^6.0.0",
+    "unist-util-visit-parents": "^6.0.0"
   }
 }

+ 14 - 15
apps/app/playwright.config.ts

@@ -1,6 +1,5 @@
 import fs from 'node:fs';
 import path from 'node:path';
-
 import { defineConfig, devices, type Project } from '@playwright/test';
 
 const authFile = path.resolve(__dirname, './playwright/.auth/admin.json');
@@ -10,18 +9,20 @@ const storageState = fs.existsSync(authFile) ? authFile : undefined;
 
 const supportedBrowsers = ['chromium', 'firefox', 'webkit'] as const;
 
-const projects: Array<Project> = supportedBrowsers.map(browser => ({
+const projects: Array<Project> = supportedBrowsers.map((browser) => ({
   name: browser,
   use: { ...devices[`Desktop ${browser}`], storageState },
   testIgnore: /(10-installer|21-basic-features-for-guest)\/.*\.spec\.ts/,
   dependencies: ['setup', 'auth'],
 }));
 
-const projectsForGuestMode: Array<Project> = supportedBrowsers.map(browser => ({
-  name: `${browser}/guest-mode`,
-  use: { ...devices[`Desktop ${browser}`] }, // Do not use storageState
-  testMatch: /21-basic-features-for-guest\/.*\.spec\.ts/,
-}));
+const projectsForGuestMode: Array<Project> = supportedBrowsers.map(
+  (browser) => ({
+    name: `${browser}/guest-mode`,
+    use: { ...devices[`Desktop ${browser}`] }, // Do not use storageState
+    testMatch: /21-basic-features-for-guest\/.*\.spec\.ts/,
+  }),
+);
 
 /**
  * Read environment variables from file.
@@ -48,12 +49,7 @@ export default defineConfig({
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: process.env.CI
-    ? [
-      ['github'],
-      ['blob'],
-    ]
-    : 'list',
+  reporter: process.env.CI ? [['github'], ['blob']] : 'list',
 
   webServer: {
     command: 'pnpm run server',
@@ -79,7 +75,11 @@ export default defineConfig({
   /* Configure projects for major browsers */
   projects: [
     // Setup project
-    { name: 'setup', testMatch: /.*\.setup\.ts/, testIgnore: /auth\.setup\.ts/ },
+    {
+      name: 'setup',
+      testMatch: /.*\.setup\.ts/,
+      testIgnore: /auth\.setup\.ts/,
+    },
     { name: 'auth', testMatch: /auth\.setup\.ts/ },
 
     {
@@ -113,5 +113,4 @@ export default defineConfig({
     //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
     // },
   ],
-
 });

+ 13 - 5
apps/app/playwright/10-installer/install.spec.ts

@@ -1,6 +1,6 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-test('Installer', async({ page }) => {
+test('Installer', async ({ page }) => {
   await page.goto('/');
   await page.waitForURL('/installer');
 
@@ -11,18 +11,26 @@ test('Installer', async({ page }) => {
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguageMenu-ja_JP').click();
   await expect(page.getByRole('textbox', { name: 'ユーザーID' })).toBeVisible();
-  await expect(page.getByRole('textbox', { name: 'ユーザーID' })).toHaveAttribute('placeholder', 'ユーザーID');
+  await expect(
+    page.getByRole('textbox', { name: 'ユーザーID' }),
+  ).toHaveAttribute('placeholder', 'ユーザーID');
 
   // choose Chinese
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguageMenu-zh_CN').click();
   await expect(page.getByRole('textbox', { name: '用户ID' })).toBeVisible();
-  await expect(page.getByRole('textbox', { name: '用户ID' })).toHaveAttribute('placeholder', '用户ID');
+  await expect(page.getByRole('textbox', { name: '用户ID' })).toHaveAttribute(
+    'placeholder',
+    '用户ID',
+  );
   // // choose English
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguageMenu-en_US').click();
   await expect(page.getByRole('textbox', { name: 'User ID' })).toBeVisible();
-  await expect(page.getByRole('textbox', { name: 'User ID' })).toHaveAttribute('placeholder', 'User ID');
+  await expect(page.getByRole('textbox', { name: 'User ID' })).toHaveAttribute(
+    'placeholder',
+    'User ID',
+  );
 
   await page.getByRole('textbox', { name: 'User ID' }).focus();
 

+ 101 - 73
apps/app/playwright/20-basic-features/access-to-page.spec.ts

@@ -1,32 +1,36 @@
-import { test, expect, type Page } from '@playwright/test';
+import { expect, type Page, test } from '@playwright/test';
 
-const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+const appendTextToEditorUntilContains = async (page: Page, text: string) => {
   await page.locator('.cm-content').fill(text);
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+    text,
+  );
 };
 
-test('has title', async({ page }) => {
+test('has title', async ({ page }) => {
   await page.goto('/Sandbox');
 
   // Expect a title "to contain" a substring.
   await expect(page).toHaveTitle(/Sandbox/);
 });
 
-test('get h1', async({ page }) => {
+test('get h1', async ({ page }) => {
   await page.goto('/Sandbox');
 
   // Expects page to have a heading with the name of Installation.
-  await expect(page.getByRole('heading').filter({ hasText: /\/Sandbox/ })).toBeVisible();
+  await expect(
+    page.getByRole('heading').filter({ hasText: /\/Sandbox/ }),
+  ).toBeVisible();
 });
 
-test('/Sandbox/Math is successfully loaded', async({ page }) => {
+test('/Sandbox/Math is successfully loaded', async ({ page }) => {
   await page.goto('/Sandbox/Math');
 
   // Expect the Math-specific elements to be present
   await expect(page.locator('.katex').first()).toBeVisible();
 });
 
-test('Sandbox with edit is successfully loaded', async({ page }) => {
+test('Sandbox with edit is successfully loaded', async ({ page }) => {
   await page.goto('/Sandbox#edit');
 
   // Expect the Editor-specific elements to be present
@@ -35,116 +39,140 @@ test('Sandbox with edit is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('grw-grant-selector')).toBeVisible();
 });
 
-test.describe.serial('PageEditor', () => {
-  const body1 = 'hello';
-  const body2 = ' world!';
-  const targetPath = '/Sandbox/testForUseEditingMarkdown';
+test.describe
+  .serial('PageEditor', () => {
+    const body1 = 'hello';
+    const body2 = ' world!';
+    const targetPath = '/Sandbox/testForUseEditingMarkdown';
 
-  test('Edit and save with save-page-btn', async({ page }) => {
-    await page.goto(targetPath);
+    test('Edit and save with save-page-btn', async ({ page }) => {
+      await page.goto(targetPath);
 
-    await page.getByTestId('editor-button').click();
-    await appendTextToEditorUntilContains(page, body1);
-    await page.getByTestId('save-page-btn').click();
+      await page.getByTestId('editor-button').click();
+      await appendTextToEditorUntilContains(page, body1);
+      await page.getByTestId('save-page-btn').click();
 
-    await expect(page.locator('.wiki').first()).toContainText(body1);
-  });
+      await expect(page.locator('.wiki').first()).toContainText(body1);
+    });
 
-  test('Edit and save with shortcut key', async({ page }) => {
-    const savePageShortcutKey = 'Control+s';
+    test('Edit and save with shortcut key', async ({ page }) => {
+      const savePageShortcutKey = 'Control+s';
 
-    await page.goto(targetPath);
+      await page.goto(targetPath);
 
-    await page.getByTestId('editor-button').click();
+      await page.getByTestId('editor-button').click();
 
-    await expect(page.locator('.cm-content')).toContainText(body1);
-    await expect(page.getByTestId('page-editor-preview-body')).toContainText(body1);
+      await expect(page.locator('.cm-content')).toContainText(body1);
+      await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+        body1,
+      );
 
-    await appendTextToEditorUntilContains(page, body1 + body2);
-    await page.keyboard.press(savePageShortcutKey);
-    await page.getByTestId('view-button').click();
+      await appendTextToEditorUntilContains(page, body1 + body2);
+      await page.keyboard.press(savePageShortcutKey);
+      await page.getByTestId('view-button').click();
 
-    await expect(page.locator('.wiki').first()).toContainText(body1 + body2);
+      await expect(page.locator('.wiki').first()).toContainText(body1 + body2);
+    });
   });
-});
 
-test('Access to /me page', async({ page }) => {
+test('Access to /me page', async ({ page }) => {
   await page.goto('/me');
 
   // Expect the UserSettgins-specific elements to be present when accessing /me (UserSettgins)
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
 
-test('All In-App Notification list is successfully loaded', async({ page }) => {
+test('All In-App Notification list is successfully loaded', async ({
+  page,
+}) => {
   await page.goto('/me/all-in-app-notifications');
 
   // Expect the In-App Notification-specific elements to be present when accessing /me/all-in-app-notifications
   await expect(page.getByTestId('grw-in-app-notification-page')).toBeVisible();
 });
 
-test('/trash is successfully loaded', async({ page }) => {
+test('/trash is successfully loaded', async ({ page }) => {
   await page.goto('/trash');
 
-  await expect(page.getByTestId('trash-page-list')).toContainText('There are no pages under this page.');
+  await expect(page.getByTestId('trash-page-list')).toContainText(
+    'There are no pages under this page.',
+  );
 });
 
-test('/tags is successfully loaded', async({ page }) => {
+test('/tags is successfully loaded', async ({ page }) => {
   await page.goto('/tags');
 
-  await expect(page.getByTestId('grw-tags-list')).toContainText('You have no tag, You can set tags on pages');
+  await expect(page.getByTestId('grw-tags-list')).toContainText(
+    'You have no tag, You can set tags on pages',
+  );
 });
 
-test.describe.serial('Access to Template Editing Mode', () => {
-  const templateBody1 = 'Template for children';
-  const templateBody2 = 'Template for descendants';
+test.describe
+  .serial('Access to Template Editing Mode', () => {
+    const templateBody1 = 'Template for children';
+    const templateBody2 = 'Template for descendants';
 
-  test('Successfully created template for children', async({ page }) => {
-    await page.goto('/Sandbox');
+    test('Successfully created template for children', async ({ page }) => {
+      await page.goto('/Sandbox');
 
-    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
-    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
-    await page.getByTestId('open-page-template-modal-btn').click();
-    expect(page.getByTestId('page-template-modal')).toBeVisible();
+      await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+      await page
+        .getByTestId('grw-contextual-sub-nav')
+        .getByTestId('open-page-item-control-btn')
+        .click();
+      await page.getByTestId('open-page-template-modal-btn').click();
+      expect(page.getByTestId('page-template-modal')).toBeVisible();
 
-    await page.getByTestId('template-button-children').click();
+      await page.getByTestId('template-button-children').click();
 
-    await appendTextToEditorUntilContains(page, templateBody1);
-    await page.getByTestId('save-page-btn').click();
+      await appendTextToEditorUntilContains(page, templateBody1);
+      await page.getByTestId('save-page-btn').click();
 
-    await expect(page.locator('.wiki').first()).toContainText(templateBody1);
-  });
+      await expect(page.locator('.wiki').first()).toContainText(templateBody1);
+    });
 
-  test('Template is applied to pages created (template for children)', async({ page }) => {
-    await page.goto('/Sandbox');
+    test('Template is applied to pages created (template for children)', async ({
+      page,
+    }) => {
+      await page.goto('/Sandbox');
 
-    await page.getByTestId('grw-page-create-button').click();
+      await page.getByTestId('grw-page-create-button').click();
 
-    await expect(page.locator('.cm-content')).toContainText(templateBody1);
-    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody1);
-  });
+      await expect(page.locator('.cm-content')).toContainText(templateBody1);
+      await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+        templateBody1,
+      );
+    });
 
-  test('Successfully created template for descendants', async({ page }) => {
-    await page.goto('/Sandbox');
+    test('Successfully created template for descendants', async ({ page }) => {
+      await page.goto('/Sandbox');
 
-    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
-    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
-    await page.getByTestId('open-page-template-modal-btn').click();
-    expect(page.getByTestId('page-template-modal')).toBeVisible();
+      await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+      await page
+        .getByTestId('grw-contextual-sub-nav')
+        .getByTestId('open-page-item-control-btn')
+        .click();
+      await page.getByTestId('open-page-template-modal-btn').click();
+      expect(page.getByTestId('page-template-modal')).toBeVisible();
 
-    await page.getByTestId('template-button-descendants').click();
+      await page.getByTestId('template-button-descendants').click();
 
-    await appendTextToEditorUntilContains(page, templateBody2);
-    await page.getByTestId('save-page-btn').click();
+      await appendTextToEditorUntilContains(page, templateBody2);
+      await page.getByTestId('save-page-btn').click();
 
-    await expect(page.locator('.wiki').first()).toContainText(templateBody2);
-  });
+      await expect(page.locator('.wiki').first()).toContainText(templateBody2);
+    });
 
-  test('Template is applied to pages created (template for descendants)', async({ page }) => {
-    await page.goto('/Sandbox/Bootstrap5');
+    test('Template is applied to pages created (template for descendants)', async ({
+      page,
+    }) => {
+      await page.goto('/Sandbox/Bootstrap5');
 
-    await page.getByTestId('grw-page-create-button').click();
+      await page.getByTestId('grw-page-create-button').click();
 
-    await expect(page.locator('.cm-content')).toContainText(templateBody2);
-    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody2);
+      await expect(page.locator('.cm-content')).toContainText(templateBody2);
+      await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+        templateBody2,
+      );
+    });
   });
-});

+ 17 - 9
apps/app/playwright/20-basic-features/access-to-pagelist.spec.ts

@@ -1,29 +1,37 @@
-import { test, expect, type Page } from '@playwright/test';
+import { expect, type Page, test } from '@playwright/test';
 
-const openPageAccessoriesModal = async(page: Page): Promise<void> => {
+const openPageAccessoriesModal = async (page: Page): Promise<void> => {
   await page.goto('/');
   await page.getByTestId('pageListButton').click();
   await expect(page.getByTestId('descendants-page-list-modal')).toBeVisible();
 };
 
-test('Page list modal is successfully opened', async({ page }) => {
+test('Page list modal is successfully opened', async ({ page }) => {
   await openPageAccessoriesModal(page);
-  await expect(page.getByTestId('page-list-item-L').first()).not.toContainText('You cannot see this page');
+  await expect(page.getByTestId('page-list-item-L').first()).not.toContainText(
+    'You cannot see this page',
+  );
 });
 
-test('Successfully open PageItemControl', async({ page }) => {
+test('Successfully open PageItemControl', async ({ page }) => {
   await openPageAccessoriesModal(page);
-  await page.getByTestId('page-list-item-L').first().getByTestId('open-page-item-control-btn').click();
+  await page
+    .getByTestId('page-list-item-L')
+    .first()
+    .getByTestId('open-page-item-control-btn')
+    .click();
   await expect(page.locator('.dropdown-menu.show')).toBeVisible();
 });
 
-test('Successfully close modal', async({ page }) => {
+test('Successfully close modal', async ({ page }) => {
   await openPageAccessoriesModal(page);
   await page.locator('.btn-close').click();
-  await expect(page.getByTestId('descendants-page-list-modal')).not.toBeVisible();
+  await expect(
+    page.getByTestId('descendants-page-list-modal'),
+  ).not.toBeVisible();
 });
 
-test('Timeline list successfully openend', async({ page }) => {
+test('Timeline list successfully openend', async ({ page }) => {
   await openPageAccessoriesModal(page);
   await page.getByTestId('timeline-tab-button').click();
   await expect(page.locator('.card-timeline').first()).toBeVisible();

+ 10 - 7
apps/app/playwright/20-basic-features/click-page-icons.spec.ts

@@ -1,11 +1,11 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 test.describe('Click page icons', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/Sandbox');
   });
 
-  test('Successfully Subscribe/Unsubscribe a page', async({ page }) => {
+  test('Successfully Subscribe/Unsubscribe a page', async ({ page }) => {
     const subscribeButton = page.locator('.btn-subscribe');
 
     // Subscribe
@@ -17,7 +17,7 @@ test.describe('Click page icons', () => {
     await expect(subscribeButton).not.toHaveClass(/active/);
   });
 
-  test('Successfully Like/Unlike a page', async({ page }) => {
+  test('Successfully Like/Unlike a page', async ({ page }) => {
     const likeButton = page.locator('.btn-like').first();
 
     // Like
@@ -29,7 +29,7 @@ test.describe('Click page icons', () => {
     await expect(likeButton).not.toHaveClass(/active/);
   });
 
-  test('Successfully Bookmark / Unbookmark a page', async({ page }) => {
+  test('Successfully Bookmark / Unbookmark a page', async ({ page }) => {
     const bookmarkButton = page.locator('.btn-bookmark').first();
 
     // Bookmark
@@ -41,10 +41,13 @@ test.describe('Click page icons', () => {
     await expect(bookmarkButton).not.toHaveClass(/active/);
   });
 
-  test('Successfully display list of "seen by user"', async({ page }) => {
+  test('Successfully display list of "seen by user"', async ({ page }) => {
     await page.locator('.btn-seen-user').click();
 
-    const imgCount = await page.locator('.user-list-content').locator('img').count();
+    const imgCount = await page
+      .locator('.user-list-content')
+      .locator('img')
+      .count();
     expect(imgCount).toBe(1);
   });
 });

+ 13 - 9
apps/app/playwright/20-basic-features/comments.spec.ts

@@ -1,18 +1,17 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 test.describe('Comment', () => {
-
   // make tests run in serial
   test.describe.configure({ mode: 'serial' });
 
-  test('Create comment page', async({ page }) => {
+  test('Create comment page', async ({ page }) => {
     await page.goto('/comment');
     await page.getByTestId('editor-button').click();
     await page.getByTestId('save-page-btn').click();
     await expect(page.locator('.page-meta')).toBeVisible();
   });
 
-  test('Successfully add comments', async({ page }) => {
+  test('Successfully add comments', async ({ page }) => {
     const commentText = 'add comment';
     await page.goto('/comment');
 
@@ -23,10 +22,12 @@ test.describe('Comment', () => {
     await page.getByTestId('comment-submit-button').first().click();
 
     await expect(page.locator('.page-comment-body')).toHaveText(commentText);
-    await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('1');
+    await expect(
+      page.getByTestId('page-comment-button').locator('.grw-count-badge'),
+    ).toHaveText('1');
   });
 
-  test('Successfully reply comments', async({ page }) => {
+  test('Successfully reply comments', async ({ page }) => {
     const commentText = 'reply comment';
     await page.goto('/comment');
 
@@ -35,8 +36,12 @@ test.describe('Comment', () => {
     await page.locator('.cm-content').fill(commentText);
     await page.getByTestId('comment-submit-button').first().click();
 
-    await expect(page.locator('.page-comment-body').nth(1)).toHaveText(commentText);
-    await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('2');
+    await expect(page.locator('.page-comment-body').nth(1)).toHaveText(
+      commentText,
+    );
+    await expect(
+      page.getByTestId('page-comment-button').locator('.grw-count-badge'),
+    ).toHaveText('2');
   });
 
   // test('Successfully delete comments', async({ page }) => {
@@ -51,5 +56,4 @@ test.describe('Comment', () => {
   // });
 
   // TODO: https://redmine.weseek.co.jp/issues/139520
-
 });

+ 16 - 6
apps/app/playwright/20-basic-features/create-page-button.spec.ts

@@ -1,10 +1,13 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 test.describe('Create page button', () => {
-  test('click and autofocus to title text input', async({ page }) => {
+  test('click and autofocus to title text input', async ({ page }) => {
     await page.goto('/');
 
-    await page.getByTestId('grw-page-create-button').getByRole('button', { name: 'Create' }).click();
+    await page
+      .getByTestId('grw-page-create-button')
+      .getByRole('button', { name: 'Create' })
+      .click();
 
     // should be focused
     await expect(page.getByPlaceholder('Input page name')).toBeFocused();
@@ -12,13 +15,20 @@ test.describe('Create page button', () => {
 });
 
 test.describe('Create page button dropdown menu', () => {
-  test('open and create today page', async({ page }) => {
+  test('open and create today page', async ({ page }) => {
     await page.goto('/');
 
     // open dropdown menu
     await page.getByTestId('grw-page-create-button').hover();
-    await expect(page.getByTestId('grw-page-create-button').getByLabel('Open create page menu')).toBeVisible();
-    await page.getByTestId('grw-page-create-button').getByLabel('Open create page menu').dispatchEvent('click'); // simulate the click
+    await expect(
+      page
+        .getByTestId('grw-page-create-button')
+        .getByLabel('Open create page menu'),
+    ).toBeVisible();
+    await page
+      .getByTestId('grw-page-create-button')
+      .getByLabel('Open create page menu')
+      .dispatchEvent('click'); // simulate the click
     await page.getByRole('menuitem', { name: 'Create today page' }).click();
 
     // should not be visible

+ 9 - 5
apps/app/playwright/20-basic-features/presentation.spec.ts

@@ -1,13 +1,17 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-test('Presentation', async({ page }) => {
+test('Presentation', async ({ page }) => {
   await page.goto('/');
 
   // show presentation modal
-  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  await page
+    .getByTestId('grw-contextual-sub-nav')
+    .getByTestId('open-page-item-control-btn')
+    .click();
   await page.getByTestId('open-presentation-modal-btn').click();
 
   // check the content of the h1
-  await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
-    .toHaveText(/Welcome to GROWI/);
+  await expect(
+    page.getByRole('application').getByRole('heading', { level: 1 }),
+  ).toHaveText(/Welcome to GROWI/);
 });

+ 35 - 13
apps/app/playwright/20-basic-features/sticky-features.spec.ts

@@ -1,47 +1,69 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 test.describe('Sticky features', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/');
   });
 
-  test('Subnavigation displays changes on scroll down and up', async({ page }) => {
+  test('Subnavigation displays changes on scroll down and up', async ({
+    page,
+  }) => {
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 250));
-    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(
+      /active/,
+    );
 
     // Scroll back to top
     await page.evaluate(() => window.scrollTo(0, 0));
-    await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(/active/);
+    await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(
+      /active/,
+    );
   });
 
-  test('Subnavigation is not displayed when move to other pages', async({ page }) => {
+  test('Subnavigation is not displayed when move to other pages', async ({
+    page,
+  }) => {
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 250));
-    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(
+      /active/,
+    );
 
     // Move to /Sandbox page
     await page.goto('/Sandbox');
-    await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(/active/);
+    await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(
+      /active/,
+    );
   });
 
-  test('Able to click buttons on subnavigation switcher when sticky', async({ page }) => {
+  test('Able to click buttons on subnavigation switcher when sticky', async ({
+    page,
+  }) => {
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 250));
-    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(
+      /active/,
+    );
 
     // Click editor button
     await page.getByTestId('editor-button').click();
     await expect(page.locator('.layout-root')).toHaveClass(/editing/);
   });
 
-  test('Subnavigation is sticky when on small window', async({ page }) => {
+  test('Subnavigation is sticky when on small window', async ({ page }) => {
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 500));
-    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(
+      /active/,
+    );
 
     // Set viewport to small size
     await page.setViewportSize({ width: 600, height: 1024 });
-    await expect(page.getByTestId('grw-contextual-sub-nav').getByTestId('grw-page-editor-mode-manager')).toBeVisible();
+    await expect(
+      page
+        .getByTestId('grw-contextual-sub-nav')
+        .getByTestId('grw-page-editor-mode-manager'),
+    ).toBeVisible();
   });
 });

+ 25 - 15
apps/app/playwright/20-basic-features/use-tools.spec.ts

@@ -1,6 +1,6 @@
-import { test, expect, type Page } from '@playwright/test';
+import { expect, type Page, test } from '@playwright/test';
 
-const openPageItemControl = async(page: Page): Promise<void> => {
+const openPageItemControl = async (page: Page): Promise<void> => {
   const nav = page.getByTestId('grw-contextual-sub-nav');
   const button = nav.getByTestId('open-page-item-control-btn');
 
@@ -19,7 +19,7 @@ const openPageItemControl = async(page: Page): Promise<void> => {
   await button.click();
 };
 
-test('PageDeleteModal is shown successfully', async({ page }) => {
+test('PageDeleteModal is shown successfully', async ({ page }) => {
   await page.goto('/Sandbox');
 
   await openPageItemControl(page);
@@ -28,7 +28,7 @@ test('PageDeleteModal is shown successfully', async({ page }) => {
   await expect(page.getByTestId('page-delete-modal')).toBeVisible();
 });
 
-test('PageDuplicateModal is shown successfully', async({ page }) => {
+test('PageDuplicateModal is shown successfully', async ({ page }) => {
   await page.goto('/Sandbox');
 
   await openPageItemControl(page);
@@ -37,7 +37,7 @@ test('PageDuplicateModal is shown successfully', async({ page }) => {
   await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
 });
 
-test('PageMoveRenameModal is shown successfully', async({ page }) => {
+test('PageMoveRenameModal is shown successfully', async ({ page }) => {
   await page.goto('/Sandbox');
 
   await openPageItemControl(page);
@@ -57,35 +57,45 @@ test('PageMoveRenameModal is shown successfully', async({ page }) => {
 // });
 
 test.describe('Page Accessories Modal', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/');
     await openPageItemControl(page);
   });
 
-  test('Page History is shown successfully', async({ page }) => {
-    await page.getByTestId('open-page-accessories-modal-btn-with-history-tab').click();
-    await expect(page.getByTestId(('page-history'))).toBeVisible();
+  test('Page History is shown successfully', async ({ page }) => {
+    await page
+      .getByTestId('open-page-accessories-modal-btn-with-history-tab')
+      .click();
+    await expect(page.getByTestId('page-history')).toBeVisible();
   });
 
-  test('Page Attachment Data is shown successfully', async({ page }) => {
-    await page.getByTestId('open-page-accessories-modal-btn-with-attachment-data-tab').click();
+  test('Page Attachment Data is shown successfully', async ({ page }) => {
+    await page
+      .getByTestId('open-page-accessories-modal-btn-with-attachment-data-tab')
+      .click();
     await expect(page.getByTestId('page-attachment')).toBeVisible();
   });
 
-  test('Share Link Management is shown successfully', async({ page }) => {
-    await page.getByTestId('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
+  test('Share Link Management is shown successfully', async ({ page }) => {
+    await page
+      .getByTestId(
+        'open-page-accessories-modal-btn-with-share-link-management-data-tab',
+      )
+      .click();
     await expect(page.getByTestId('share-link-management')).toBeVisible();
   });
 });
 
-test('Successfully add new tag', async({ page }) => {
+test('Successfully add new tag', async ({ page }) => {
   const tag = 'we';
   await page.goto('/Sandbox/Bootstrap5');
 
   await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
   await expect(page.locator('#edit-tag-modal')).toBeVisible();
   await page.locator('.rbt-input-main').fill(tag);
-  await expect(page.locator('#tag-typeahead-asynctypeahead-item-0')).toBeVisible();
+  await expect(
+    page.locator('#tag-typeahead-asynctypeahead-item-0'),
+  ).toBeVisible();
   await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
   await page.getByTestId('tag-edit-done-btn').click();
   await expect(page.getByTestId('grw-tag-labels')).toContainText(tag);

+ 9 - 9
apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts

@@ -1,45 +1,45 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 import { collapseSidebar } from '../utils';
 
-test('/Sandbox is successfully loaded', async({ page }) => {
-
+test('/Sandbox is successfully loaded', async ({ page }) => {
   await page.goto('/Sandbox');
 
   // Expect a title "to contain" a substring.
   await expect(page).toHaveTitle(/Sandbox/);
 });
 
-test('/Sandbox/math is successfully loaded', async({ page }) => {
-
+test('/Sandbox/math is successfully loaded', async ({ page }) => {
   await page.goto('/Sandbox/Math');
 
   // Check if the math elements are visible
   await expect(page.locator('.katex').first()).toBeVisible();
 });
 
-test('Access to /me page', async({ page }) => {
+test('Access to /me page', async ({ page }) => {
   await page.goto('/me');
 
   // Expect to be redirected to /login when accessing /me
   await expect(page.getByTestId('login-form')).toBeVisible();
 });
 
-test('Access to /trash page', async({ page }) => {
+test('Access to /trash page', async ({ page }) => {
   await page.goto('/trash');
 
   // Expect the trash page specific elements to be present when accessing /trash
   await expect(page.getByTestId('trash-page-list')).toBeVisible();
 });
 
-test('Access to /tags page', async({ page }) => {
+test('Access to /tags page', async ({ page }) => {
   await page.goto('/');
 
   await collapseSidebar(page, false);
   await page.getByTestId('grw-sidebar-nav-primary-tags').click();
   await expect(page.getByTestId('grw-sidebar-content-tags')).toBeVisible();
   await expect(page.getByTestId('grw-tags-list').first()).toBeVisible();
-  await expect(page.getByTestId('grw-tags-list').first()).toContainText('You have no tag, You can set tags on pages');
+  await expect(page.getByTestId('grw-tags-list').first()).toContainText(
+    'You have no tag, You can set tags on pages',
+  );
 
   await page.getByTestId('check-all-tags-button').click();
   await expect(page.getByTestId('tags-page')).toBeVisible();

+ 10 - 5
apps/app/playwright/21-basic-features-for-guest/sticky-for-guest.spec.ts

@@ -1,14 +1,19 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-
-test('Sub navigation sticky changes when scrolling down and up', async({ page }) => {
+test('Sub navigation sticky changes when scrolling down and up', async ({
+  page,
+}) => {
   await page.goto('/Sandbox');
 
   // Sticky
   await page.evaluate(() => window.scrollTo(0, 250));
-  await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+  await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(
+    /active/,
+  );
 
   // Not sticky
   await page.evaluate(() => window.scrollTo(0, 0));
-  await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(/active/);
+  await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(
+    /active/,
+  );
 });

+ 38 - 33
apps/app/playwright/22-sharelink/access-to-sharelink.spec.ts

@@ -1,37 +1,42 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 import { login } from '../utils/Login';
 
-test.describe.serial('Access to sharelink by guest', () => {
-  let createdSharelink: string | null;
-
-  test('Prepare sharelink', async({ page }) => {
-    await page.goto('/Sandbox/Bootstrap5');
-
-    // Create Sharelink
-    await page.getByTestId('open-page-item-control-btn').click();
-    await page.getByTestId('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
-    await page.getByTestId('btn-sharelink-toggleform').click();
-    await page.getByTestId('btn-sharelink-issue').click();
-
-    // Get ShareLink
-    createdSharelink = await page.getByTestId('share-link').textContent();
-    expect(createdSharelink).toHaveLength(24);
-  });
-
-  test('The sharelink page is successfully loaded', async({ page }) => {
-    await page.goto('/');
-
-    // Logout
-    await page.getByTestId('personal-dropdown-button').click();
-    await expect(page.getByTestId('logout-button')).toBeVisible();
-    await page.getByTestId('logout-button').click();
-    await page.waitForURL('http://localhost:3000/login');
-
-    // Access sharelink
-    await page.goto(`/share/${createdSharelink}`);
-    await expect(page.locator('.page-meta')).toBeVisible();
-
-    await login(page);
+test.describe
+  .serial('Access to sharelink by guest', () => {
+    let createdSharelink: string | null;
+
+    test('Prepare sharelink', async ({ page }) => {
+      await page.goto('/Sandbox/Bootstrap5');
+
+      // Create Sharelink
+      await page.getByTestId('open-page-item-control-btn').click();
+      await page
+        .getByTestId(
+          'open-page-accessories-modal-btn-with-share-link-management-data-tab',
+        )
+        .click();
+      await page.getByTestId('btn-sharelink-toggleform').click();
+      await page.getByTestId('btn-sharelink-issue').click();
+
+      // Get ShareLink
+      createdSharelink = await page.getByTestId('share-link').textContent();
+      expect(createdSharelink).toHaveLength(24);
+    });
+
+    test('The sharelink page is successfully loaded', async ({ page }) => {
+      await page.goto('/');
+
+      // Logout
+      await page.getByTestId('personal-dropdown-button').click();
+      await expect(page.getByTestId('logout-button')).toBeVisible();
+      await page.getByTestId('logout-button').click();
+      await page.waitForURL('http://localhost:3000/login');
+
+      // Access sharelink
+      await page.goto(`/share/${createdSharelink}`);
+      await expect(page.locator('.page-meta')).toBeVisible();
+
+      await login(page);
+    });
   });
-});

+ 13 - 9
apps/app/playwright/23-editor/saving.spec.ts

@@ -1,14 +1,14 @@
+import { expect, type Page, test } from '@playwright/test';
 import path from 'path';
 
-import { test, expect, type Page } from '@playwright/test';
-
-const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+const appendTextToEditorUntilContains = async (page: Page, text: string) => {
   await page.locator('.cm-content').fill(text);
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+    text,
+  );
 };
 
-
-test('Successfully create page under specific path', async({ page }) => {
+test('Successfully create page under specific path', async ({ page }) => {
   const newPagePath = '/child';
   const openPageCreateModalShortcutKey = 'c';
 
@@ -16,7 +16,10 @@ test('Successfully create page under specific path', async({ page }) => {
 
   await page.keyboard.press(openPageCreateModalShortcutKey);
   await expect(page.getByTestId('page-create-modal')).toBeVisible();
-  page.getByTestId('page-create-modal').locator('.rbt-input-main').fill(newPagePath);
+  page
+    .getByTestId('page-create-modal')
+    .locator('.rbt-input-main')
+    .fill(newPagePath);
   page.getByTestId('btn-create-page-under-below').click();
   await page.getByTestId('view-button').click();
 
@@ -24,8 +27,9 @@ test('Successfully create page under specific path', async({ page }) => {
   expect(createdPageId.length).toBe(24);
 });
 
-
-test('Successfully updating a page using a shortcut on a previously created page', async({ page }) => {
+test('Successfully updating a page using a shortcut on a previously created page', async ({
+  page,
+}) => {
   const body1 = 'hello';
   const body2 = ' world!';
   const savePageShortcutKey = 'Control+s';

+ 8 - 4
apps/app/playwright/23-editor/template-modal.spec.ts

@@ -1,6 +1,6 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-test('Successfully select template and template locale', async({ page }) => {
+test('Successfully select template and template locale', async ({ page }) => {
   const jaText = '今日の目標';
   const enText = "TODAY'S GOALS";
   await page.goto('/Sandbox/TemplateModal');
@@ -16,10 +16,14 @@ test('Successfully select template and template locale', async({ page }) => {
 
   // select template and template locale
   await templateModal.locator('.list-group-item').nth(0).click();
-  await expect(templateModal.locator('.card-body').locator('.has-data-line').nth(1)).toHaveText(enText);
+  await expect(
+    templateModal.locator('.card-body').locator('.has-data-line').nth(1),
+  ).toHaveText(enText);
   await templateModal.getByTestId('select-locale-dropdown-toggle').click();
   await templateModal.getByTestId('select-locale-dropdown-item').nth(1).click();
-  await expect(templateModal.locator('.card-body').locator('.has-data-line').nth(1)).toHaveText(jaText);
+  await expect(
+    templateModal.locator('.card-body').locator('.has-data-line').nth(1),
+  ).toHaveText(jaText);
 
   // insert
   await templateModal.locator('.btn-primary').click();

+ 31 - 17
apps/app/playwright/23-editor/with-navigation.spec.ts

@@ -1,14 +1,15 @@
+import { expect, type Page, test } from '@playwright/test';
 import { readFileSync } from 'fs';
 import path from 'path';
 
-import { test, expect, type Page } from '@playwright/test';
-
 /**
  * for the issues:
  * @see https://redmine.weseek.co.jp/issues/122040
  * @see https://redmine.weseek.co.jp/issues/124281
  */
-test('should not be cleared and should prevent GrantSelector from modified', async({ page }) => {
+test('should not be cleared and should prevent GrantSelector from modified', async ({
+  page,
+}) => {
   await page.goto('/Sandbox/for-122040');
 
   // Open Editor
@@ -26,10 +27,10 @@ test('should not be cleared and should prevent GrantSelector from modified', asy
   const filePath = path.resolve(__dirname, '../23-editor/assets/example.txt');
   const buffer = readFileSync(filePath).toString('base64');
   const dataTransfer = await page.evaluateHandle(
-    async({ bufferData, localFileName, localFileType }) => {
+    async ({ bufferData, localFileName, localFileType }) => {
       const dt = new DataTransfer();
 
-      const blobData = await fetch(bufferData).then(res => res.blob());
+      const blobData = await fetch(bufferData).then((res) => res.blob());
 
       const file = new File([blobData], localFileName, {
         type: localFileType,
@@ -43,33 +44,41 @@ test('should not be cleared and should prevent GrantSelector from modified', asy
       localFileType: 'application/octet-stream',
     },
   );
-  await page.locator('.dropzone').first().dispatchEvent('drop', { dataTransfer });
-  await expect(page.getByTestId('page-editor-preview-body').getByTestId('rich-attachment')).toBeVisible();
+  await page
+    .locator('.dropzone')
+    .first()
+    .dispatchEvent('drop', { dataTransfer });
+  await expect(
+    page.getByTestId('page-editor-preview-body').getByTestId('rich-attachment'),
+  ).toBeVisible();
 
   // Save page
   await page.getByTestId('save-page-btn').click();
 
   // Expect grant not to be reset after uploading an attachment
-  await expect(page.getByTestId('page-grant-alert')).toContainText('Browsing of this page is restricted');
+  await expect(page.getByTestId('page-grant-alert')).toContainText(
+    'Browsing of this page is restricted',
+  );
 });
 
-const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+const appendTextToEditorUntilContains = async (page: Page, text: string) => {
   await page.locator('.cm-content').fill(text);
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+    text,
+  );
 };
 
 /**
  * for the issue:
  * @see https://redmine.weseek.co.jp/issues/115285
  */
-test('Successfully updating the page body', async({ page }) => {
+test('Successfully updating the page body', async ({ page }) => {
   const page1Path = '/Sandbox/for-115285/page1';
   const page2Path = '/Sandbox/for-115285/page2';
 
   const page1Body = 'Hello';
   const page2Body = 'World';
 
-
   await page.goto(page1Path);
 
   // Open Editor (page1)
@@ -85,7 +94,10 @@ test('Successfully updating the page body', async({ page }) => {
   await expect(page.locator('.main')).toContainText(page1Body);
 
   // Duplicate page1
-  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  await page
+    .getByTestId('grw-contextual-sub-nav')
+    .getByTestId('open-page-item-control-btn')
+    .click();
   await page.getByTestId('open-page-duplicate-modal-btn').click();
   await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
   await page.locator('.form-control').fill(page2Path);
@@ -96,18 +108,20 @@ test('Successfully updating the page body', async({ page }) => {
   await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
 
   // Expect to see the text from which you are duplicating
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(page1Body);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+    page1Body,
+  );
 
   // Append text
   await appendTextToEditorUntilContains(page, page1Body + page2Body);
 
-
   await page.goto(page1Path);
 
   // Open Editor (page1)
   await page.getByTestId('editor-button').click();
   await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
 
-  await expect(page.getByTestId('page-editor-preview-body')).toContainText(page1Body);
-
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(
+    page1Body,
+  );
 });

+ 82 - 68
apps/app/playwright/30-search/search.spect.ts

@@ -1,6 +1,6 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-test('Search page with "q" param is successfully loaded', async({ page }) => {
+test('Search page with "q" param is successfully loaded', async ({ page }) => {
   // Navigate to the search page with query parameters
   await page.goto('/_search?q=alerts');
 
@@ -11,7 +11,7 @@ test('Search page with "q" param is successfully loaded', async({ page }) => {
   await expect(page.locator('.wiki')).toBeVisible();
 });
 
-test('checkboxes behaviors', async({ page }) => {
+test('checkboxes behaviors', async ({ page }) => {
   // Navigate to the search page with query parameters
   await page.goto('/_search?q=alerts');
 
@@ -28,7 +28,10 @@ test('checkboxes behaviors', async({ page }) => {
   await page.getByTestId('cb-select').first().click({ force: true });
 
   // Click the select all checkbox
-  await page.getByTestId('delete-control-button').first().click({ force: true });
+  await page
+    .getByTestId('delete-control-button')
+    .first()
+    .click({ force: true });
   await page.getByTestId('cb-select-all').click({ force: true });
 
   // Unclick the first checkbox after selecting all
@@ -41,16 +44,19 @@ test('checkboxes behaviors', async({ page }) => {
   await page.getByTestId('cb-select').first().click({ force: true });
 });
 
-
-test('successfully loads /_private-legacy-pages', async({ page }) => {
+test('successfully loads /_private-legacy-pages', async ({ page }) => {
   await page.goto('/_private-legacy-pages');
 
   // Confirm search result elements are visible
-  await expect(page.locator('[data-testid="search-result-base"]')).toBeVisible();
-  await expect(page.locator('[data-testid="search-result-private-legacy-pages"]')).toBeVisible();
+  await expect(
+    page.locator('[data-testid="search-result-base"]'),
+  ).toBeVisible();
+  await expect(
+    page.locator('[data-testid="search-result-private-legacy-pages"]'),
+  ).toBeVisible();
 });
 
-test('Search all pages by word', async({ page }) => {
+test('Search all pages by word', async ({ page }) => {
   await page.goto('/');
   await page.getByTestId('open-search-modal-button').click();
   await expect(page.getByTestId('search-modal')).toBeVisible();
@@ -58,51 +64,51 @@ test('Search all pages by word', async({ page }) => {
   await expect(page.locator('.search-menu-item').first()).toBeVisible();
 });
 
-test.describe.serial('Search all pages', () => {
-  const tag = 'help';
-  const searchText = `tag:${tag}`;
-
-  test('Successfully created tags', async({ page }) => {
-    await page.goto('/');
-
-    // open Edit Tags Modal to add tag
-    await page.locator('.grw-side-contents-sticky-container').isVisible();
-    await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
-    await expect(page.locator('#edit-tag-modal')).toBeVisible();
-    await page.locator('.rbt-input-main').fill(tag);
-    await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
-    await page.getByTestId('tag-edit-done-btn').click();
-
-  });
-
-  test('Search all pages by tag is successfully loaded', async({ page }) => {
-    await page.goto('/');
-
-    // Search
-    await page.getByTestId('open-search-modal-button').click();
-    await expect(page.getByTestId('search-modal')).toBeVisible();
-    await page.locator('.form-control').fill(searchText);
-    await page.getByTestId('search-all-menu-item').click();
-
-    // Confirm search result elements are visible
-    const searchResultList = page.getByTestId('search-result-list');
-    await expect(searchResultList).toBeVisible();
-    await expect(searchResultList.locator('li')).toHaveCount(1);
+test.describe
+  .serial('Search all pages', () => {
+    const tag = 'help';
+    const searchText = `tag:${tag}`;
+
+    test('Successfully created tags', async ({ page }) => {
+      await page.goto('/');
+
+      // open Edit Tags Modal to add tag
+      await page.locator('.grw-side-contents-sticky-container').isVisible();
+      await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
+      await expect(page.locator('#edit-tag-modal')).toBeVisible();
+      await page.locator('.rbt-input-main').fill(tag);
+      await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
+      await page.getByTestId('tag-edit-done-btn').click();
+    });
+
+    test('Search all pages by tag is successfully loaded', async ({ page }) => {
+      await page.goto('/');
+
+      // Search
+      await page.getByTestId('open-search-modal-button').click();
+      await expect(page.getByTestId('search-modal')).toBeVisible();
+      await page.locator('.form-control').fill(searchText);
+      await page.getByTestId('search-all-menu-item').click();
+
+      // Confirm search result elements are visible
+      const searchResultList = page.getByTestId('search-result-list');
+      await expect(searchResultList).toBeVisible();
+      await expect(searchResultList.locator('li')).toHaveCount(1);
+    });
+
+    test('Successfully order page search results by tag', async ({ page }) => {
+      await page.goto('/');
+
+      await page.locator('.grw-tag-simple-bar').locator('a').click();
+
+      expect(page.getByTestId('search-result-base')).toBeVisible();
+      expect(page.getByTestId('search-result-list')).toBeVisible();
+      expect(page.getByTestId('search-result-content')).toBeVisible();
+    });
   });
 
-  test('Successfully order page search results by tag', async({ page }) => {
-    await page.goto('/');
-
-    await page.locator('.grw-tag-simple-bar').locator('a').click();
-
-    expect(page.getByTestId('search-result-base')).toBeVisible();
-    expect(page.getByTestId('search-result-list')).toBeVisible();
-    expect(page.getByTestId('search-result-content')).toBeVisible();
-  });
-});
-
 test.describe('Sort with dropdown', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/_search?q=sand');
 
     await expect(page.getByTestId('search-result-base')).toBeVisible();
@@ -113,41 +119,40 @@ test.describe('Sort with dropdown', () => {
     await page.locator('.search-control').locator('button').first().click();
   });
 
-  test('Open sort dropdown', async({ page }) => {
-    await expect(page.locator('.search-control .dropdown-menu.show')).toBeVisible();
+  test('Open sort dropdown', async ({ page }) => {
+    await expect(
+      page.locator('.search-control .dropdown-menu.show'),
+    ).toBeVisible();
   });
 
-  test('Sort by relevance', async({ page }) => {
+  test('Sort by relevance', async ({ page }) => {
     const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
 
     await expect(dropdownMenu).toBeVisible();
     await dropdownMenu.locator('.dropdown-item').nth(0).click();
 
-
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).toBeVisible();
   });
 
-  test('Sort by creation date', async({ page }) => {
+  test('Sort by creation date', async ({ page }) => {
     const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
 
     await expect(dropdownMenu).toBeVisible();
     await dropdownMenu.locator('.dropdown-item').nth(1).click();
 
-
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).toBeVisible();
   });
 
-  test('Sort by last update date', async({ page }) => {
+  test('Sort by last update date', async ({ page }) => {
     const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
 
     await expect(dropdownMenu).toBeVisible();
     await dropdownMenu.locator('.dropdown-item').nth(2).click();
 
-
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).toBeVisible();
@@ -155,22 +160,26 @@ test.describe('Sort with dropdown', () => {
 });
 
 test.describe('Search and use', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/_search?q=alerts');
 
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).toBeVisible();
 
-    await page.getByTestId('page-list-item-L').first().getByTestId('open-page-item-control-btn').click();
+    await page
+      .getByTestId('page-list-item-L')
+      .first()
+      .getByTestId('open-page-item-control-btn')
+      .click();
     await expect(page.locator('.dropdown-menu.show')).toBeVisible();
   });
 
-  test('Successfully the dropdown is opened', async({ page }) => {
+  test('Successfully the dropdown is opened', async ({ page }) => {
     await expect(page.locator('.dropdown-menu.show')).toBeVisible();
   });
 
-  test('Successfully add bookmark', async({ page }) => {
+  test('Successfully add bookmark', async ({ page }) => {
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
     await expect(dropdonwMenu).toBeVisible();
@@ -178,10 +187,15 @@ test.describe('Search and use', () => {
     // Add bookmark
     await dropdonwMenu.getByTestId('add-bookmark-btn').click();
 
-    await expect(page.getByTestId('search-result-content').locator('.btn-bookmark.active').first()).toBeVisible();
+    await expect(
+      page
+        .getByTestId('search-result-content')
+        .locator('.btn-bookmark.active')
+        .first(),
+    ).toBeVisible();
   });
 
-  test('Successfully open duplicate modal', async({ page }) => {
+  test('Successfully open duplicate modal', async ({ page }) => {
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
     await expect(dropdonwMenu).toBeVisible();
@@ -191,7 +205,7 @@ test.describe('Search and use', () => {
     await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
   });
 
-  test('Successfully open move/rename modal', async({ page }) => {
+  test('Successfully open move/rename modal', async ({ page }) => {
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
     await expect(dropdonwMenu).toBeVisible();
@@ -201,7 +215,7 @@ test.describe('Search and use', () => {
     await expect(page.getByTestId('page-rename-modal')).toBeVisible();
   });
 
-  test('Successfully open delete modal', async({ page }) => {
+  test('Successfully open delete modal', async ({ page }) => {
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
     await expect(dropdonwMenu).toBeVisible();
@@ -212,7 +226,7 @@ test.describe('Search and use', () => {
   });
 });
 
-test('Search current tree by word is successfully loaded', async({ page }) => {
+test('Search current tree by word is successfully loaded', async ({ page }) => {
   await page.goto('/');
   const searchText = 'GROWI';
 

+ 29 - 21
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -1,62 +1,66 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-test('admin is successfully loaded', async({ page }) => {
+test('admin is successfully loaded', async ({ page }) => {
   await page.goto('/admin');
 
   await expect(page.getByTestId('admin-home')).toBeVisible();
-  await expect(page.getByTestId('admin-system-information-table')).toBeVisible();
+  await expect(
+    page.getByTestId('admin-system-information-table'),
+  ).toBeVisible();
 });
 
-test('admin/app is successfully loaded', async({ page }) => {
+test('admin/app is successfully loaded', async ({ page }) => {
   await page.goto('/admin/app');
 
   await expect(page.getByTestId('admin-app-settings')).toBeVisible();
   // await expect(page.getByTestId('v5-page-migration')).toBeVisible();
   await expect(page.locator('#cbFileUpload')).toBeChecked();
-  await expect(page.locator('#isQuestionnaireEnabled')).toBeChecked();
-  await expect(page.locator('#isAppSiteUrlHashed')).not.toBeChecked();
 });
 
-test('admin/security is successfully loaded', async({ page }) => {
+test('admin/security is successfully loaded', async ({ page }) => {
   await page.goto('/admin/security');
 
   await expect(page.getByTestId('admin-security')).toBeVisible();
-  await expect(page.locator('#isShowRestrictedByOwner')).toHaveText('Always displayed');
-  await expect(page.locator('#isShowRestrictedByGroup')).toHaveText('Always displayed');
+  await expect(page.locator('#isShowRestrictedByOwner')).toHaveText(
+    'Always displayed',
+  );
+  await expect(page.locator('#isShowRestrictedByGroup')).toHaveText(
+    'Always displayed',
+  );
 });
 
-test('admin/markdown is successfully loaded', async({ page }) => {
+test('admin/markdown is successfully loaded', async ({ page }) => {
   await page.goto('/admin/markdown');
 
   await expect(page.getByTestId('admin-markdown')).toBeVisible();
   await expect(page.locator('#isEnabledLinebreaksInComments')).toBeChecked();
 });
 
-test('admin/customize is successfully loaded', async({ page }) => {
+test('admin/customize is successfully loaded', async ({ page }) => {
   await page.goto('/admin/customize');
 
   await expect(page.getByTestId('admin-customize')).toBeVisible();
 });
 
-test('admin/importer is successfully loaded', async({ page }) => {
+test('admin/importer is successfully loaded', async ({ page }) => {
   await page.goto('/admin/importer');
 
   await expect(page.getByTestId('admin-import-data')).toBeVisible();
 });
 
-test('admin/export is successfully loaded', async({ page }) => {
+test('admin/export is successfully loaded', async ({ page }) => {
   await page.goto('/admin/export');
 
   await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
 });
 
-test('admin/data-transfer is successfully loaded', async({ page }) => {
+test('admin/data-transfer is successfully loaded', async ({ page }) => {
   await page.goto('/admin/data-transfer');
 
   await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
 });
 
-test('admin/notification is successfully loaded', async({ page }) => {
+test('admin/notification is successfully loaded', async ({ page }) => {
   await page.goto('/admin/notification');
 
   await expect(page.getByTestId('admin-notification')).toBeVisible();
@@ -64,7 +68,7 @@ test('admin/notification is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('slack-integration-list-item')).toBeVisible();
 });
 
-test('admin/slack-integration is successfully loaded', async({ page }) => {
+test('admin/slack-integration is successfully loaded', async ({ page }) => {
   await page.goto('/admin/slack-integration');
 
   await expect(page.getByTestId('admin-slack-integration')).toBeVisible();
@@ -72,27 +76,31 @@ test('admin/slack-integration is successfully loaded', async({ page }) => {
   await expect(page.locator('img.bot-difficulty-icon').first()).toBeVisible();
 });
 
-test('admin/slack-integration-legacy is successfully loaded', async({ page }) => {
+test('admin/slack-integration-legacy is successfully loaded', async ({
+  page,
+}) => {
   await page.goto('/admin/slack-integration-legacy');
 
-  await expect(page.getByTestId('admin-slack-integration-legacy')).toBeVisible();
+  await expect(
+    page.getByTestId('admin-slack-integration-legacy'),
+  ).toBeVisible();
 });
 
-test('admin/users is successfully loaded', async({ page }) => {
+test('admin/users is successfully loaded', async ({ page }) => {
   await page.goto('/admin/users');
 
   await expect(page.getByTestId('admin-users')).toBeVisible();
   await expect(page.getByTestId('user-table-tr').first()).toBeVisible();
 });
 
-test('admin/user-groups is successfully loaded', async({ page }) => {
+test('admin/user-groups is successfully loaded', async ({ page }) => {
   await page.goto('/admin/user-groups');
 
   await expect(page.getByTestId('admin-user-groups')).toBeVisible();
   await expect(page.getByTestId('grw-user-group-table').first()).toBeVisible();
 });
 
-test('admin/search is successfully loaded', async({ page }) => {
+test('admin/search is successfully loaded', async ({ page }) => {
   await page.goto('/admin/search');
 
   await expect(page.getByTestId('admin-full-text-search')).toBeVisible();

+ 20 - 16
apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts

@@ -1,39 +1,43 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 import { collapseSidebar } from '../utils';
 
-
 test.describe('Access to sidebar', () => {
-
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/');
     await collapseSidebar(page, false);
   });
 
-  test('Successfully show sidebar', async({ page }) => {
+  test('Successfully show sidebar', async ({ page }) => {
     await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
   });
 
-  test('Successfully access to page tree', async({ page }) => {
+  test('Successfully access to page tree', async ({ page }) => {
     await page.getByTestId('grw-sidebar-nav-primary-page-tree').click();
     await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
-    await expect(page.getByTestId('grw-pagetree-item-container').first()).toBeVisible();
+    await expect(
+      page.getByTestId('grw-pagetree-item-container').first(),
+    ).toBeVisible();
   });
 
-  test('Successfully access to recent changes', async({ page }) => {
+  test('Successfully access to recent changes', async ({ page }) => {
     await page.getByTestId('grw-sidebar-nav-primary-recent-changes').click();
     await expect(page.getByTestId('grw-recent-changes')).toBeVisible();
     await expect(page.locator('.list-group-item').first()).toBeVisible();
   });
 
-  test('Successfully access to custom sidebar', async({ page }) => {
+  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();
+    await expect(
+      page.locator('.grw-sidebar-content-header > h3').locator('a'),
+    ).toBeVisible();
   });
 
-  test('Successfully access to GROWI Docs page', async({ page }) => {
-    const linkElement = page.locator('.grw-sidebar-nav-secondary-container a[href*="https://docs.growi.org"]');
+  test('Successfully access to GROWI Docs page', async ({ page }) => {
+    const linkElement = page.locator(
+      '.grw-sidebar-nav-secondary-container a[href*="https://docs.growi.org"]',
+    );
     const docsUrl = await linkElement.getAttribute('href');
     if (docsUrl == null) {
       throw new Error('url is null');
@@ -43,12 +47,13 @@ test.describe('Access to sidebar', () => {
     expect(body).toContain('</html>');
   });
 
-  test('Successfully access to trash page', async({ page }) => {
-    await page.locator('.grw-sidebar-nav-secondary-container a[href*="/trash"]').click();
+  test('Successfully access to trash page', async ({ page }) => {
+    await page
+      .locator('.grw-sidebar-nav-secondary-container a[href*="/trash"]')
+      .click();
     await expect(page.getByTestId('trash-page-list')).toBeVisible();
   });
 
-
   //
   // Deactivate: An error occurs that cannot be reproduced in the development environment. -- Yuki Takei 2024.05.10
   //
@@ -166,5 +171,4 @@ test.describe('Access to sidebar', () => {
   //     cy.get('.modal-header > button').click();
   //   });
   // });
-
 });

+ 1 - 2
apps/app/playwright/50-sidebar/switching-sidebar-mode.spec.ts

@@ -2,8 +2,7 @@ import { test } from '@playwright/test';
 
 import { collapseSidebar } from '../utils';
 
-
-test('Switch sidebar mode', async({ page }) => {
+test('Switch sidebar mode', async ({ page }) => {
   await page.goto('/');
   await collapseSidebar(page, false);
   await collapseSidebar(page, true);

+ 49 - 29
apps/app/playwright/60-home/home.spec.ts

@@ -1,43 +1,34 @@
-import { test, expect } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
-
-test('Visit User home', async({ page }) => {
+test('Visit User home', async ({ page }) => {
   await page.goto('dummy');
 
   // Open PersonalDropdown
   await page.getByTestId('personal-dropdown-button').click();
-  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
+  await expect(
+    page.getByTestId('grw-personal-dropdown-menu-user-home'),
+  ).toBeVisible();
 
   // Click UserHomeMenu
   await page.getByTestId('grw-personal-dropdown-menu-user-home').click();
   await expect(page.getByTestId('grw-users-info')).toBeVisible();
 });
 
-test('Vist User settings', async({ page }) => {
+test('Vist User settings', async ({ page }) => {
   await page.goto('dummy');
 
   // Open PersonalDropdown
   await page.getByTestId('personal-dropdown-button').click();
-  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
+  await expect(
+    page.getByTestId('grw-personal-dropdown-menu-user-home'),
+  ).toBeVisible();
 
   // Click UserSettingsMenu
   page.getByTestId('grw-personal-dropdown-menu-user-settings').click();
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
 
-test('Open questionnaire modal', async({ page }) => {
-  await page.goto('/dummy');
-
-  // Open PersonalDropdown
-  await page.getByTestId('personal-dropdown-button').click();
-  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
-
-  // Expect the questionnaire modal to be displayed when the QuestionnaireModalToggleButton is clicked
-  await page.getByTestId('grw-proactive-questionnaire-modal-toggle-btn').click();
-  await expect(page.getByTestId('grw-proactive-questionnaire-modal')).toBeVisible();
-});
-
-test('Access User information', async({ page }) => {
+test('Access User information', async ({ page }) => {
   await page.goto('/me');
 
   // Click BasicInfoSettingUpdateButton
@@ -48,7 +39,7 @@ test('Access User information', async({ page }) => {
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });
 
-test('Access External account', async({ page }) => {
+test('Access External account', async ({ page }) => {
   await page.goto('/me');
 
   // Click ExternalAccountsTabButton
@@ -64,7 +55,7 @@ test('Access External account', async({ page }) => {
   await expect(page.locator('.Toastify__toast')).not.toBeVisible();
 });
 
-test('Access Password setting', async({ page }) => {
+test('Access Password setting', async ({ page }) => {
   await page.goto('/me');
 
   // Click PasswordSettingTabButton
@@ -84,8 +75,7 @@ test('Access Password setting', async({ page }) => {
   await expect(page.getByTestId('.Toastify__toast')).not.toBeVisible();
 });
 
-
-test('Access API setting', async({ page }) => {
+test('Access API setting', async ({ page }) => {
   await page.goto('/me');
 
   // Click ApiSettingTabButton
@@ -97,26 +87,56 @@ test('Access API setting', async({ page }) => {
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });
 
-test('Access In-App Notification setting', async({ page }) => {
+test('Access Access Token setting', async ({ page }) => {
+  await page.goto('/me');
+
+  // Click ApiSettingTabButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+  await page.getByTestId('api-settings-tab-button').first().click();
+
+  // Expect a success toaster to be displayed when new Access Token is generated
+  await page.getByTestId('btn-accesstoken-toggleform').click();
+  await page.getByTestId('grw-accesstoken-textarea-description').fill('dummy');
+  await page.getByTestId('grw-accesstoken-checkbox-read:*').check();
+  await page.getByTestId('grw-accesstoken-create-button').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+  await expect(
+    page.getByTestId('grw-accesstoken-new-token-display'),
+  ).toBeVisible();
+
+  // Expect a success toaster to be displayed when the Access Token is deleted
+  await page.getByTestId('grw-accesstoken-delete-button').click();
+  await page.getByTestId('grw-accesstoken-cancel-button-in-modal').click();
+  await page.getByTestId('grw-accesstoken-delete-button').click();
+  await page.getByTestId('grw-accesstoken-delete-button-in-modal').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+});
+
+test('Access In-App Notification setting', async ({ page }) => {
   await page.goto('/me');
 
   // Click InAppNotificationSettingTabButton
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
-  await page.getByTestId('in-app-notification-settings-tab-button').first().click();
+  await page
+    .getByTestId('in-app-notification-settings-tab-button')
+    .first()
+    .click();
 
   // Expect a success toaster to be displayed when the InAppNotificationSettingsUpdateButton is clicked
-  await page.getByTestId('grw-in-app-notification-settings-update-button').click();
+  await page
+    .getByTestId('grw-in-app-notification-settings-update-button')
+    .click();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });
 
-test('Acccess Other setting', async({ page }) => {
+test('Acccess Other setting', async ({ page }) => {
   await page.goto('/me');
 
   // Click OtherSettingTabButton
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   await page.getByTestId('other-settings-tab-button').first().click();
 
-  // Expect a success toaster to be displayed when the QuestionnaireSettingsUpdateButton is clicked
-  await page.getByTestId('grw-questionnaire-settings-update-btn').click();
+  // Expect a success toaster to be displayed when the updating UI button is clicked
+  await page.getByTestId('grw-ui-settings-update-btn').click();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });

+ 1 - 1
apps/app/playwright/auth.setup.ts

@@ -4,6 +4,6 @@ import { login } from './utils/Login';
 
 // Commonised login process for use elsewhere
 // see: https://github.com/microsoft/playwright/issues/22114
-setup('Authenticate as the "admin" user', async({ page }) => {
+setup('Authenticate as the "admin" user', async ({ page }) => {
   await login(page);
 });

+ 8 - 4
apps/app/playwright/utils/CollapseSidebar.ts

@@ -1,7 +1,12 @@
 import { expect, type Page } from '@playwright/test';
 
-export const collapseSidebar = async(page: Page, isCollapsed: boolean): Promise<void> => {
-  const isSidebarContentsHidden = !(await page.getByTestId('grw-sidebar-contents').isVisible());
+export const collapseSidebar = async (
+  page: Page,
+  isCollapsed: boolean,
+): Promise<void> => {
+  const isSidebarContentsHidden = !(await page
+    .getByTestId('grw-sidebar-contents')
+    .isVisible());
   if (isSidebarContentsHidden === isCollapsed) {
     return;
   }
@@ -12,8 +17,7 @@ export const collapseSidebar = async(page: Page, isCollapsed: boolean): Promise<
 
   if (isCollapsed) {
     await expect(page.locator('.grw-sidebar-dock')).not.toBeVisible();
-  }
-  else {
+  } else {
     await expect(page.locator('.grw-sidebar-dock')).toBeVisible();
   }
 };

+ 1 - 2
apps/app/playwright/utils/Login.ts

@@ -1,10 +1,9 @@
 import path from 'node:path';
-
 import { expect, type Page } from '@playwright/test';
 
 const authFile = path.resolve(__dirname, '../.auth/admin.json');
 
-export const login = async(page: Page): Promise<void> => {
+export const login = async (page: Page): Promise<void> => {
   // Perform authentication steps. Replace these actions with your own.
   await page.goto('/admin');
 

+ 40 - 40
apps/app/public/images/icons/favicon/manifest.json

@@ -1,41 +1,41 @@
 {
- "name": "App",
- "icons": [
-  {
-   "src": "\/android-icon-36x36.png",
-   "sizes": "36x36",
-   "type": "image\/png",
-   "density": "0.75"
-  },
-  {
-   "src": "\/android-icon-48x48.png",
-   "sizes": "48x48",
-   "type": "image\/png",
-   "density": "1.0"
-  },
-  {
-   "src": "\/android-icon-72x72.png",
-   "sizes": "72x72",
-   "type": "image\/png",
-   "density": "1.5"
-  },
-  {
-   "src": "\/android-icon-96x96.png",
-   "sizes": "96x96",
-   "type": "image\/png",
-   "density": "2.0"
-  },
-  {
-   "src": "\/android-icon-144x144.png",
-   "sizes": "144x144",
-   "type": "image\/png",
-   "density": "3.0"
-  },
-  {
-   "src": "\/android-icon-192x192.png",
-   "sizes": "192x192",
-   "type": "image\/png",
-   "density": "4.0"
-  }
- ]
-}
+  "name": "App",
+  "icons": [
+    {
+      "src": "\/android-icon-36x36.png",
+      "sizes": "36x36",
+      "type": "image\/png",
+      "density": "0.75"
+    },
+    {
+      "src": "\/android-icon-48x48.png",
+      "sizes": "48x48",
+      "type": "image\/png",
+      "density": "1.0"
+    },
+    {
+      "src": "\/android-icon-72x72.png",
+      "sizes": "72x72",
+      "type": "image\/png",
+      "density": "1.5"
+    },
+    {
+      "src": "\/android-icon-96x96.png",
+      "sizes": "96x96",
+      "type": "image\/png",
+      "density": "2.0"
+    },
+    {
+      "src": "\/android-icon-144x144.png",
+      "sizes": "144x144",
+      "type": "image\/png",
+      "density": "3.0"
+    },
+    {
+      "src": "\/android-icon-192x192.png",
+      "sizes": "192x192",
+      "type": "image\/png",
+      "density": "4.0"
+    }
+  ]
+}

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

@@ -97,9 +97,9 @@
       "closed": "Closed (Invitation Only)"
     },
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "share_link_notice":"remove all share links",
-    "delete_all_share_links":"Delete all share links",
+    "No_share_links": "No share links",
+    "share_link_notice": "remove all share links",
+    "delete_all_share_links": "Delete all share links",
     "share_link_rights": "Share link rights",
     "enable_link_sharing": "Enable link sharing",
     "all_share_links": "All share links",
@@ -229,7 +229,7 @@
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
     "slack_app_configuration": "Slack app configuration",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-    "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
+    "use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "workspace": "(At Workspace) Add a hook",
@@ -296,7 +296,7 @@
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
   },
-  "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
+  "mailer_setup_required": "<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
     "system_information": "System information",
@@ -305,7 +305,7 @@
     "package_name": "Package name",
     "specified_version": "Specified version",
     "installed_version": "Installed version",
-    "list_of_env_vars":"List of environment variables",
+    "list_of_env_vars": "List of environment variables",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
     "about_security": "Check <a href='/admin/security'>Security Settings</a> for security environment variables.",
     "copy_prefilled_host_information": {
@@ -313,7 +313,7 @@
       "done": "Copied to clipboard!"
     },
     "bug_report": "Submitting a bug report",
-    "submit_bug_report": "<a href='https://github.com/weseek/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": "<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>"
   },
   "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.",
@@ -368,9 +368,9 @@
     "mail_settings": "E-mail Settings",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
     "from_e-mail_address": "From e-mail address",
-    "transmission_method":"Transmission Method",
-    "smtp_label":"SMTP",
-    "ses_label":"SES(AWS)",
+    "transmission_method": "Transmission Method",
+    "smtp_label": "SMTP",
+    "ses_label": "SES(AWS)",
     "send_test_email": "Send a test-email",
     "success_to_send_test_email": "Success to send a test-email",
     "smtp_settings": "SMTP settings",
@@ -380,13 +380,13 @@
     "initialize_mail_settings": "initialize e-mail settings",
     "initialize_mail_modal_header": "Initialize e-mail settings",
     "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
-    "file_upload_settings":"File Upload Settings",
-    "file_upload_method":"File Upload Method",
-    "file_delivery_method":"File Delivery Method",
-    "file_delivery_method_redirect":"Redirect",
-    "file_delivery_method_relay":"Internal System Relay",
-    "file_delivery_method_redirect_info":"Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
-    "file_delivery_method_relay_info":"Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
+    "file_upload_settings": "File Upload Settings",
+    "file_upload_method": "File Upload Method",
+    "file_delivery_method": "File Delivery Method",
+    "file_delivery_method_redirect": "Redirect",
+    "file_delivery_method_relay": "Internal System Relay",
+    "file_delivery_method_redirect_info": "Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
+    "file_delivery_method_relay_info": "Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
     "fixed_by_env_var": "This is fixed by the env var <code>{{envKey}}={{envVar}}</code>.",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
@@ -411,17 +411,7 @@
     "enable": "Enable",
     "disable": "Disable",
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
-    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-    "questionnaire_settings": "Questionnaire settings",
-    "questionnaire_settings_explanation": "This will enable/disable questionnaires on the whole system. When enabled, users can also enable/disable questionnaires individually from \"Other Settings\" in the personal settings page.",
-    "about_data_sent": "About information sent",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "Learn more",
-    "other_info_will_be_sent": "Along with the questionnaire answer, information necessary to improve GROWI will be sent. Personal user info will not be included in the data sent.",
-    "we_will_use_the_data_to_improve_growi": "We will use the data to improve GROWI experience as much as possible.",
-    "anonymize_app_site_url": "Anonymize app site URL in data sent",
-    "url_anonymization_explanation": "The app site URL included in the questionnaire answer will be anonymized. By enabling this, the GROWI application that sends the questionnaire answer will not be identified.",
-    "enable_questionnaire": "Enable questionnaire"
+    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown Settings",
@@ -502,13 +492,13 @@
       "show_page_side_authors": "Always display creators and updaters above the table of contents",
       "show_page_side_authors_desc": "Displays information about the creator and the last updater above the table of contents in the page sidebar."
     },
-      "presentation": "Presentation",
+    "presentation": "Presentation",
     "presentation_options": {
       "enable_marp": "Enable Marp ",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "GROWI Docs - Create slide using Marp",
+      "marp_in_growi": "GROWI Docs - Create slide using Marp",
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     "custom_title": "Custom title",
@@ -525,7 +515,7 @@
     "custom_presentation": "Custom presentation",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
-    "custom_logo" : "Custom Logo",
+    "custom_logo": "Custom Logo",
     "default_logo": "Default Logo",
     "upload_logo": "Upload Logo",
     "current_logo": "Current Logo",
@@ -599,9 +589,9 @@
     },
     "import": "Import",
     "skip_username_and_email_when_overlapped": "Skip username and email using same username and email in new environment",
-    "prepare_new_account_for_migration":"Prepare new account for migration",
-    "archive_data_import_detail":"More Details? Ckick here.",
-    "admin_archive_data_import_guide_url":"https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
+    "prepare_new_account_for_migration": "Prepare new account for migration",
+    "archive_data_import_detail": "More Details? Ckick here.",
+    "admin_archive_data_import_guide_url": "https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
@@ -671,7 +661,7 @@
     "delete": "Delete",
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
-    "integration_failed":"Integration failed",
+    "integration_failed": "Integration failed",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -718,7 +708,7 @@
       "allow_specified_long": "Allow specified (Allowed from only specified channels)",
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
-      "test_connection_only_public_channel":"Please test connection in a public channel",
+      "test_connection_only_public_channel": "Please test connection in a public channel",
       "error_check_logs_below": "An error has occurred. Please check the logs below.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"
@@ -753,10 +743,10 @@
     "status": "Status",
     "invite_modal": {
       "emails": "Emails (Possible to issue multiple people with new lines)",
-      "description1":"Temporarily issue new users by email addresses.",
-      "description2":"A temporary password will be generated for the first login.",
+      "description1": "Temporarily issue new users by email addresses.",
+      "description2": "A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
-      "mail_setting_link":"<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
@@ -791,10 +781,10 @@
       "new_password": "New Password"
     },
     "external_account": "External Account Management",
-    "external_accounts":"External accounts",
-    "create_external_account":"Create external account",
+    "external_accounts": "External accounts",
+    "create_external_account": "Create external account",
     "external_account_list": "External Account List",
-    "external_account_none":"No External Account",
+    "external_account_none": "No External Account",
     "invite": "Invite",
     "invited": "User was invited",
     "back_to_user_management": "Back to User Management",
@@ -979,7 +969,7 @@
     "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
     "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) Settings",
     "ADMIN_MAIL_SES_UPDATE": "Update E-mail(SES) Settings",
-    "ADMIN_MAIL_TEST_SUBMIT" : "Send test mail",
+    "ADMIN_MAIL_TEST_SUBMIT": "Send test mail",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Update File Upload Settings",
     "ADMIN_PLUGIN_UPDATE": "Update Plugin Settings",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",

+ 92 - 36
apps/app/public/static/locales/en_US/commons.json

@@ -7,7 +7,6 @@
   "Sign out": "Logout",
   "New": "New",
   "Delete": "Delete",
-
   "meta": {
     "display_name": "English"
   },
@@ -30,7 +29,6 @@
   "headers": {
     "app_settings": "App Settings"
   },
-
   "header_search_box": {
     "label": {
       "All pages": "All pages",
@@ -41,20 +39,17 @@
       "This tree": "Only children of this tree"
     }
   },
-
   "search_method_menu_item": {
     "search_in_all": "Search in all",
     "only_children_of_this_tree": "Only children of this tree",
     "exact_mutch": "Exact match"
   },
-
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "description": "Description"
   },
-
   "in_app_notification": {
     "notification_list": "In-App Notification List",
     "see_all": "See All",
@@ -65,7 +60,6 @@
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
   },
-
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
@@ -75,7 +69,6 @@
     "use_os_settings": "Use OS settings",
     "feedback": "Feedback"
   },
-
   "create_page_dropdown": {
     "new_page": "Create New Page",
     "open_page_create_modal": "Open new page create modal",
@@ -89,7 +82,6 @@
       "descendants": "Template for descendants"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
@@ -99,14 +91,12 @@
     "Markdown link": "Markdown link",
     "Append params": "Append params"
   },
-
   "crop_image_modal": {
     "image_crop": "Image Crop",
     "crop": "Crop",
     "save": "Save",
     "cancel": "Cancel"
   },
-
   "handsontable_modal": {
     "title": "Edit Table",
     "data_import": "Data Import",
@@ -122,35 +112,9 @@
       "import": "Import"
     }
   },
-
-  "questionnaire_modal": {
-    "required": "Required",
-    "submit": "Submit",
-    "close": "Close",
-    "title": "GROWI questionnaire for service improvement",
-    "more_satisfied_services": "We hope that GROWI customers will be even more satisfied",
-    "strive_to_improve_services": "once we improve our services based on your feedback.",
-    "length_of_experience": {
-      "more_than_two_years": "More than 2 years",
-      "one_to_two_years": "More than 1 year but less than 2 years",
-      "six_months_to_one_year": "More than 6 months but less than 1 year",
-      "three_months_to_six_months": "More than 3 months but less than 6 months",
-      "one_month_to_three_months": "More than 1 month but less than 3 months",
-      "less_than_one_month": "Less than 1 month"
-    },
-    "satisfaction_with_growi": "Satisfaction with GROWI",
-    "history_of_growi_usage": "History of GROWI usage",
-    "occupation": "Occupation",
-    "position": "Position",
-    "comment_on_growi": "Comment on GROWI",
-    "successfully_submitted": "Your survey has been submitted.",
-    "thanks_for_answering": "Thank you very much for answering."
-  },
-
   "not_found_page": {
     "page_not_exist": "This page does not exist."
   },
-
   "g2g_data_transfer": {
     "tab": "Data transfer",
     "data_transfer": "Data Transfer",
@@ -159,5 +123,97 @@
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",
     "once_transfer_key_used": "Once the transfer key is used for transfer, it cannot be used for any other transfer.",
     "transfer_to_growi_cloud": "For more details, please click <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>here.</a>"
+  },
+  "accesstoken_scopes_desc": {
+    "read": {
+      "all": "Grants permission to view all content.",
+      "admin": {
+        "all": "Grants permission to view admin features.",
+        "top": "Grants permission to view Wiki management top.",
+        "app": "Grants permission to view app settings.",
+        "security": "Grants permission to view security settings.",
+        "markdown": "Grants permission to view markdown settings.",
+        "customize": "Grants permission to view customization settings.",
+        "import_data": "Grants permission to view data import settings.",
+        "export_data": "Grants permission to view data archive settings.",
+        "data_transfer": "Grants permission to view data transfer settings.",
+        "external_notification": "Grants permission to view external notification settings.",
+        "slack_integration": "Grants permission to view Slack integration settings.",
+        "legacy_slack_integration": "Grants permission to view legacy Slack integration settings.",
+        "user_management": "Grants permission to view user management.",
+        "user_group_management": "Grants permission to view user group management.",
+        "audit_log": "Grants permission to view audit logs.",
+        "plugin": "Grants permission to view plugin settings.",
+        "ai_integration": "Grants permission to view AI integration settings.",
+        "full_text_search": "Grants permission to view full text search management."
+      },
+      "user_settings": {
+        "all": "Grants permission to view user settings.",
+        "info": "Grants permission to view user information.",
+        "external_account": "Grants permission to view external accounts.",
+        "password": "Grants permission to view password settings.",
+        "api": {
+          "all": "Grants permission to view API settings.",
+          "api_token": "Grants permission to view API token settings.",
+          "access_token": "Grants permission to view access token settings."
+        },
+        "in_app_notification": "Grants permission to view in-app notification settings.",
+        "other": "Grants permission to view other settings."
+      },
+      "features": {
+        "all": "Grants permission to view features.",
+        "ai_assistant": "Grants permission to view AI assistant features.",
+        "page": "Grants permission to view page features.",
+        "share_link": "Grants permission to view share link features.",
+        "bookmark": "Grants permission to view bookmark features.",
+        "attachment": "Grants permission to view attachment features.",
+        "page_bulk_export": "Grants permission to view page bulk export features."
+      }
+    },
+    "write": {
+      "all": "Grants permission to edit all content.",
+      "admin": {
+        "all": "Grants permission to edit admin features.",
+        "top": "Grants permission to edit Wiki management top.",
+        "app": "Grants permission to edit app settings.",
+        "security": "Grants permission to edit security settings.",
+        "markdown": "Grants permission to edit markdown settings.",
+        "customize": "Grants permission to edit customization settings.",
+        "import_data": "Grants permission to edit data import settings.",
+        "export_data": "Grants permission to edit data archive settings.",
+        "data_transfer": "Grants permission to edit data transfer settings.",
+        "external_notification": "Grants permission to edit external notification settings.",
+        "slack_integration": "Grants permission to edit Slack integration settings.",
+        "legacy_slack_integration": "Grants permission to edit legacy Slack integration settings.",
+        "user_management": "Grants permission to edit user management.",
+        "user_group_management": "Grants permission to edit user group management.",
+        "audit_log": "Grants permission to edit audit logs.",
+        "plugin": "Grants permission to edit plugin settings.",
+        "ai_integration": "Grants permission to edit AI integration settings.",
+        "full_text_search": "Grants permission to edit full text search management."
+      },
+      "user_settings": {
+        "all": "Grants permission to edit user settings.",
+        "info": "Grants permission to edit user information.",
+        "external_account": "Grants permission to edit external accounts.",
+        "password": "Grants permission to edit password settings.",
+        "api": {
+          "all": "Grants permission to edit API settings.",
+          "api_token": "Grants permission to edit API token settings.",
+          "access_token": "Grants permission to edit access token settings."
+        },
+        "in_app_notification": "Grants permission to edit in-app notification settings.",
+        "other": "Grants permission to edit other settings."
+      },
+      "features": {
+        "all": "Grants permission to edit features.",
+        "ai_assistant": "Grants permission to edit AI assistant features.",
+        "page": "Grants permission to edit page features.",
+        "share_link": "Grants permission to edit share link features.",
+        "bookmark": "Grants permission to edit bookmark features.",
+        "attachment": "Grants permission to edit attachment features.",
+        "page_bulk_export": "Grants permission to edit page bulk export features."
+      }
+    }
   }
 }

+ 86 - 26
apps/app/public/static/locales/en_US/translation.json

@@ -142,7 +142,6 @@
   "edited this page": "edited this page.",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
@@ -222,6 +221,9 @@
       "profile_image2": "Set up AWS or enable local uploads."
     }
   },
+  "API Token Settings": "API token settings",
+  "Current API Token": "Current API token",
+  "Update API Token": "Update API token",
   "page_me_apitoken": {
     "api_token": "API Token",
     "notice": {
@@ -231,6 +233,35 @@
     },
     "form_help": {}
   },
+  "Access Token Settings": "Access token settings",
+  "page_me_access_token": {
+    "access_token": "Access token",
+    "expiredAt": "Expiration date",
+    "description": "Description",
+    "scope": "Scope",
+    "scope_read": "Read",
+    "action": "Action",
+    "create_token": "Create Token",
+    "no_tokens_found": "No access tokens found",
+    "new_token": {
+      "title": "New access token",
+      "copy_to_clipboard": "Copy to clipboard",
+      "message": "This token will only be displayed once. Please save it securely."
+    },
+    "modal": {
+      "message": "Are you sure you want to delete this access token?",
+      "alert": "This action cannot be undone.",
+      "delete_token": "Delete Token"
+    },
+    "form": {
+      "title": "Create New Access Token",
+      "expiredAt_desc": "Select when this access token should expire.",
+      "description_desc": "Provide a description to help you identify this token later",
+      "description_max_length": "Description must be less than {{length}} characters",
+      "scope_desc": "Select the scope of the access token."
+    },
+    "copy_to_clipboard": "Copy to clipboard"
+  },
   "Password": "Password",
   "Password Settings": "Password settings",
   "personal_settings": {
@@ -261,9 +292,6 @@
   },
   "API Settings": "API settings",
   "Other Settings": "Other Settings",
-  "API Token Settings": "API token settings",
-  "Current API Token": "Current API token",
-  "Update API Token": "Update API token",
   "in_app_notification_settings": {
     "in_app_notification_settings": "In-App Notification Settings",
     "subscribe_settings": "Settings to automatically subscribe (Receive notifications) to pages",
@@ -479,9 +507,27 @@
       "Search in Editor": "Search in Editor",
       "Move Line": "Move Line",
       "Copy Line": "Copy Line",
-      "Toggle Line": "Toggle Line Comment",
       "Insert Line": "Insert Line",
-      "Post Comment": "(Post Comment)"
+      "Post Comment": "(Post Comment)",
+      "Multiple Cursors": "Multiple Cursors",
+      "Or Alt Click": "or Alt + Click"
+    },
+    "format": {
+      "title": "Format Settings (Editor)",
+      "Bold": "Bold",
+      "Italic": "Italic",
+      "Strikethrough": "Strikethrough",
+      "Code Text": "Code Text",
+      "Hyperlink": "Hyperlink"
+    },
+    "line_settings": {
+      "title": "Line Settings (Editor)",
+      "Bullet List": "Bullet List",
+      "Numbered List": "Numbered List",
+      "Quote": "Quote",
+      "Code Block": "Code Block",
+      "Comment Out": "Comment Out",
+      "Comment Out Desc": "(Hide)"
     }
   },
   "modal_resolve_conflict": {
@@ -497,6 +543,8 @@
   },
   "sidebar_ai_assistant": {
     "reference_pages_label": "Reference pages",
+    "recent_chat": "Recent chat",
+    "no_recent_chat": "No recent chat",
     "placeholder": "Ask me anything.",
     "knowledge_assistant_placeholder": "Ask me anything.",
     "editor_assistant_placeholder": "Can I help you with anything?",
@@ -511,6 +559,8 @@
     "budget_exceeded_for_growi_cloud": "You have reached your OpenAI API usage limit. To use the Knowledge Assistant again, please add credits from the GROWI.cloud admin page for Hosted users or from the OpenAI billing page for Owned users.",
     "error_message": "An error has occurred",
     "show_error_detail": "Show error details",
+    "editor_assistant_long_context_warn_with_unit_line": "The text is too long, so the Editor Assistant will reference approximately lines {{startPosition}} to {{endPosition}} for its response.",
+    "editor_assistant_long_context_warn_with_unit_char": "The text is too long, so the Editor Assistant will reference characters {{startPosition}} to {{endPosition}} for its response.",
     "discard": "Discard",
     "accept": "Accept",
     "use_assistant": "Use Assistant",
@@ -550,6 +600,17 @@
       "create_failed": "Failed to create assistant",
       "update_failed": "Failed to update assistant"
     },
+    "select_source_pages": "Select pages for the assistant to reference",
+    "search_reference_pages_by_keyword": "Search for pages the assistant will reference by keyword",
+    "search_by_keyword": "Search by keyword",
+    "enter_keywords": "Enter keywords",
+    "max_items_space_separated_hint": "Enter up to 5 items separated by spaces",
+    "select_assistant_reference_pages": "Select pages for the assistant to reference",
+    "reference_pages": "Reference pages",
+    "no_pages_selected": "No pages selected",
+    "can_add_later": "You can add more later",
+    "next": "Next",
+    "select_from_page_tree": "Select from page tree",
     "edit_page_description": "Edit pages that the assistant can reference.<br> The assistant can reference up to {{limitLearnablePageCountPerAssistant}} pages including child pages.",
     "default_instruction": "You are the knowledge assistant for this Wiki.\n\n## Multilingual Support:\nRespond in the same language the user uses in their input.\n",
     "add_page_button": "Add page",
@@ -601,11 +662,12 @@
   "default_ai_assistant": {
     "not_set": "Default assistant is not set"
   },
-  "ai_assistant_tree": {
+  "ai_assistant_substance": {
     "add_assistant": "Add Assistant",
     "my_assistants": "My Assistants",
     "team_assistants": "Team Assistants",
     "thread_does_not_exist": "No threads exist",
+    "recent_threads": "Recent Items",
     "toaster": {
       "ai_assistant_deleted_success": "Assistant deleted",
       "ai_assistant_deleted_failed": "Failed to delete assistant",
@@ -613,6 +675,10 @@
       "thread_deleted_failed": "Failed to delete thread",
       "ai_assistant_set_default_success": "Default assistant set successfully",
       "ai_assistant_set_default_failed": "Failed to set default assistant"
+    },
+    "delete_modal": {
+      "title": "Delete Assistant",
+      "confirm_message": "Are you sure you want to delete this assistant?"
     }
   },
   "link_edit": {
@@ -831,7 +897,7 @@
     "Password field is required": "Password field is required.",
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
     "user_not_found": "User not found.",
-    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/growilabs/growi/issues/193'>#193</a>.</p>"
   },
   "grid_edit": {
     "create_bootstrap_4_grid": "Create Bootstrap 4 Grid",
@@ -933,7 +999,18 @@
   },
   "user_home_page": {
     "bookmarks": "Bookmarks",
-    "recently_created": "Recently Created"
+    "recently_created": "Recently Created",
+    "recent_activity": "Recent Activity",
+    "unknown_action": "made an unspecified change",
+    "page_create": "created a page",
+    "page_update": "updated a page",
+    "page_delete": "deleted a page",
+    "page_delete_completely": "deleted a page",
+    "page_rename": "renamed a page",
+    "page_revert": "reverted a page",
+    "page_like": "liked a page",
+    "page_duplicate": "duplicated a page",
+    "comment_create": "posted a comment"
   },
   "bookmark_folder": {
     "bookmark_folder": "bookmark folder",
@@ -956,23 +1033,6 @@
     "page_tree_not_avaliable": "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
   },
-  "questionnaire": {
-    "give_us_feedback": "Give us feedback for improvements",
-    "thank_you_for_answering": "Thank you for answering",
-    "additional_feedback": "Send us additional feedback from the user icon dropdown.",
-    "dont_show_again": "Don`t show again",
-    "deny": "Don't answer",
-    "agree": "Agree",
-    "disagree": "Disagree",
-    "answer": "Answer",
-    "no_answer": "No answer",
-    "settings": "Questionnaire settings",
-    "failed_to_send": "Failed to send feedback",
-    "denied": "The questionnaire won't be shown again",
-    "personal_settings_explanation": "Questionnaires for improving GROWI will be shown. If you have other feedbacks, you can send them from the user icon dropdown.",
-    "enable_questionnaire": "Enable questionnaire",
-    "disabled_by_admin": "Questionnaire is disabled by admin"
-  },
   "tag_edit_modal": {
     "edit_tags": "Edit Tags",
     "done": "Done",

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

@@ -313,7 +313,7 @@
       "done": "Copié dans le presse-papier!"
     },
     "bug_report": "Informations de diagnostic",
-    "submit_bug_report": "<a href='https://github.com/weseek/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": "<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>"
   },
   "v5_page_migration": {
     "migration_desc": "Des pages sont encore en V4. Pour profiter des nouvelles fonctionnalitées, convertir toutes les pages vers la V5.",
@@ -411,17 +411,7 @@
     "enable": "Activer",
     "disable": "Désactiver",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
-    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
-    "questionnaire_settings": "Sondages anonymes",
-    "questionnaire_settings_explanation": "Paramètres d'activation des données analytiques. L'utilisateur peut choisir ce paramètre individuellement dans \"Autres paramètres\".",
-    "about_data_sent": "À propos",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "En savoir plus",
-    "other_info_will_be_sent": "En plus des données analytiques, des données diagnostiques pour améliorer GROWI sont envoyées. Les données personnelles ne sont pas incluses.",
-    "we_will_use_the_data_to_improve_growi": "Les données seront utilisées pour améliorer au mieux GROWI",
-    "anonymize_app_site_url": "Ne pas inclure l'URL du site",
-    "url_anonymization_explanation": "L'URL du site configurée ne sera pas inclue dans les données envoyées.",
-    "enable_questionnaire": "Activer les données analytiques"
+    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> ."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown",

+ 92 - 23
apps/app/public/static/locales/fr_FR/commons.json

@@ -111,29 +111,6 @@
       "import": "Importer"
     }
   },
-  "questionnaire_modal": {
-    "required": "Requis",
-    "submit": "Soumettre",
-    "close": "Fermer",
-    "title": "Sondages aléatoires GROWI pour données anonymisées.",
-    "more_satisfied_services": "Nous espérons satisfaire au mieux les utilisateurs de GROWI",
-    "strive_to_improve_services": "et utilisons les retours d'utilisateurs afin d'améliorer l'expérience d'usage GROWI",
-    "length_of_experience": {
-      "more_than_two_years": "Plus de 2 ans",
-      "one_to_two_years": "Plus d'un an, mais moins de 2 ans",
-      "six_months_to_one_year": "Plus de 6 mois, mais moins d'un an",
-      "three_months_to_six_months": "Plus de 3 mois, mais moins de 6 mois",
-      "one_month_to_three_months": "Plus d'un moins, mais moins de 3 mois",
-      "less_than_one_month": "Moins d'un mois"
-    },
-    "satisfaction_with_growi": "Satisfaction avec GROWI",
-    "history_of_growi_usage": "Historique d'usage de GROWI",
-    "occupation": "Occupation",
-    "position": "Position",
-    "comment_on_growi": "Commentaires sur GROWI",
-    "successfully_submitted": "Questionnaire soumis.",
-    "thanks_for_answering": "Merci pour votre avis."
-  },
   "not_found_page": {
     "page_not_exist": "Cette page est introuvable."
   },
@@ -145,5 +122,97 @@
     "transfer_key_limit": "Les clés de transfert sont valides durant une heure.",
     "once_transfer_key_used": "Les clés de transfert sont à usage unique.",
     "transfer_to_growi_cloud": "Pour plus de détails, veuillez cliquer <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>ici.</a>"
+  },
+  "accesstoken_scopes_desc": {
+    "read": {
+      "all": "Accorde la permission de voir tout le contenu.",
+      "admin": {
+        "all": "Accorde la permission de voir les fonctionnalités d'administration.",
+        "top": "Accorde la permission de voir la page principale de gestion du Wiki.",
+        "app": "Accorde la permission de voir les paramètres de l'application.",
+        "security": "Accorde la permission de voir les paramètres de sécurité.",
+        "markdown": "Accorde la permission de voir les paramètres markdown.",
+        "customize": "Accorde la permission de voir les paramètres de personnalisation.",
+        "import_data": "Accorde la permission de voir les paramètres d'importation de données.",
+        "export_data": "Accorde la permission de voir les paramètres d'archivage de données.",
+        "data_transfer": "Accorde la permission de voir les paramètres de transfert de données.",
+        "external_notification": "Accorde la permission de voir les paramètres de notification externe.",
+        "slack_integration": "Accorde la permission de voir les paramètres d'intégration Slack.",
+        "legacy_slack_integration": "Accorde la permission de voir les paramètres d'intégration Slack (ancien).",
+        "user_management": "Accorde la permission de voir la gestion des utilisateurs.",
+        "user_group_management": "Accorde la permission de voir la gestion des groupes d'utilisateurs.",
+        "audit_log": "Accorde la permission de voir les journaux d'audit.",
+        "plugin": "Accorde la permission de voir les paramètres des plugins.",
+        "ai_integration": "Accorde la permission de voir les paramètres d'intégration IA.",
+        "full_text_search": "Accorde la permission de voir la gestion de la recherche en texte intégral."
+      },
+      "user_settings": {
+        "all": "Accorde la permission de voir les paramètres utilisateur.",
+        "info": "Accorde la permission de voir les informations utilisateur.",
+        "external_account": "Accorde la permission de voir les comptes externes.",
+        "password": "Accorde la permission de voir les paramètres de mot de passe.",
+        "api": {
+          "all": "Accorde la permission de voir les paramètres API.",
+          "api_token": "Accorde la permission de voir les paramètres de jeton API.",
+          "access_token": "Accorde la permission de voir les paramètres de jeton d'accès."
+        },
+        "in_app_notification": "Accorde la permission de voir les paramètres de notification dans l'application.",
+        "other": "Accorde la permission de voir les autres paramètres."
+      },
+      "features": {
+        "all": "Accorde la permission de voir les fonctionnalités.",
+        "ai_assistant": "Accorde la permission de voir les fonctionnalités d'assistant IA.",
+        "page": "Accorde la permission de voir les fonctionnalités de page.",
+        "share_link": "Accorde la permission de voir les fonctionnalités de lien de partage.",
+        "bookmark": "Accorde la permission de voir les fonctionnalités de signet.",
+        "attachment": "Accorde la permission de voir les fonctionnalités de pièce jointe.",
+        "page_bulk_export": "Accorde la permission de voir les fonctionnalités d'exportation en masse de pages."
+      }
+    },
+    "write": {
+      "all": "Accorde la permission de modifier tout le contenu.",
+      "admin": {
+        "all": "Accorde la permission de modifier les fonctionnalités d'administration.",
+        "top": "Accorde la permission de modifier la page principale de gestion du Wiki.",
+        "app": "Accorde la permission de modifier les paramètres de l'application.",
+        "security": "Accorde la permission de modifier les paramètres de sécurité.",
+        "markdown": "Accorde la permission de modifier les paramètres markdown.",
+        "customize": "Accorde la permission de modifier les paramètres de personnalisation.",
+        "import_data": "Accorde la permission de modifier les paramètres d'importation de données.",
+        "export_data": "Accorde la permission de modifier les paramètres d'archivage de données.",
+        "data_transfer": "Accorde la permission de modifier les paramètres de transfert de données.",
+        "external_notification": "Accorde la permission de modifier les paramètres de notification externe.",
+        "slack_integration": "Accorde la permission de modifier les paramètres d'intégration Slack.",
+        "legacy_slack_integration": "Accorde la permission de modifier les paramètres d'intégration Slack (ancien).",
+        "user_management": "Accorde la permission de modifier la gestion des utilisateurs.",
+        "user_group_management": "Accorde la permission de modifier la gestion des groupes d'utilisateurs.",
+        "audit_log": "Accorde la permission de modifier les journaux d'audit.",
+        "plugin": "Accorde la permission de modifier les paramètres des plugins.",
+        "ai_integration": "Accorde la permission de modifier les paramètres d'intégration IA.",
+        "full_text_search": "Accorde la permission de modifier la gestion de la recherche en texte intégral."
+      },
+      "user_settings": {
+        "all": "Accorde la permission de modifier les paramètres utilisateur.",
+        "info": "Accorde la permission de modifier les informations utilisateur.",
+        "external_account": "Accorde la permission de modifier les comptes externes.",
+        "password": "Accorde la permission de modifier les paramètres de mot de passe.",
+        "api": {
+          "all": "Accorde la permission de modifier les paramètres API.",
+          "api_token": "Accorde la permission de modifier les paramètres de jeton API.",
+          "access_token": "Accorde la permission de modifier les paramètres de jeton d'accès."
+        },
+        "in_app_notification": "Accorde la permission de modifier les paramètres de notification dans l'application.",
+        "other": "Accorde la permission de modifier les autres paramètres."
+      },
+      "features": {
+        "all": "Accorde la permission de modifier les fonctionnalités.",
+        "ai_assistant": "Accorde la permission de modifier les fonctionnalités d'assistant IA.",
+        "page": "Accorde la permission de modifier les fonctionnalités de page.",
+        "share_link": "Accorde la permission de modifier les fonctionnalités de lien de partage.",
+        "bookmark": "Accorde la permission de modifier les fonctionnalités de signet.",
+        "attachment": "Accorde la permission de modifier les fonctionnalités de pièce jointe.",
+        "page_bulk_export": "Accorde la permission de modifier les fonctionnalités d'exportation en masse de pages."
+      }
+    }
   }
 }

+ 87 - 27
apps/app/public/static/locales/fr_FR/translation.json

@@ -143,7 +143,6 @@
   "edited this page": "à modifié cette page.",
   "List Drafts": "Brouillons",
   "Deleted Pages": "Pages supprimées",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Dissocier",
   "No bookmarks yet": "Aucuns favoris",
   "add_bookmark": "Ajouter aux favoris",
@@ -223,6 +222,9 @@
       "profile_image2": "Configurer AWS ou activer le stockage local."
     }
   },
+  "API Token Settings": "Jetons d'API",
+  "Current API Token": "Mon jeton d'API",
+  "Update API Token": "Regénérer",
   "page_me_apitoken": {
     "api_token": "Jeton API",
     "notice": {
@@ -232,6 +234,35 @@
     },
     "form_help": {}
   },
+  "Access Token Settings": "Jeton d'accès",
+  "page_me_access_token": {
+    "access_token": "Jeton d'accès",
+    "expiredAt": "Date d'expiration",
+    "description": "Description",
+    "scope": "Portée",
+    "scope_read": "Lecture",
+    "action": "Action",
+    "create_token": "Créer un jeton",
+    "no_tokens_found": "Aucun jeton d'accès trouvé",
+    "new_token": {
+      "title": "Nouveau jeton d'accès",
+      "copy_to_clipboard": "Copier dans le presse-papiers",
+      "message": "Ce jeton ne sera affiché qu'une seule fois. Veuillez le sauvegarder en lieu sûr."
+    },
+    "modal": {
+      "message": "Êtes-vous sûr de vouloir supprimer ce jeton d'accès ?",
+      "alert": "Cette action ne peut pas être annulée.",
+      "delete_token": "Supprimer le jeton"
+    },
+    "form": {
+      "title": "Créer un nouveau jeton d'accès",
+      "expiredAt_desc": "Sélectionnez la date d'expiration de ce jeton d'accès.",
+      "description_desc": "Fournissez une description pour vous aider à identifier ce jeton ultérieurement.",
+      "description_max_length": "Veuillez saisir jusqu'à {{length}} caractères.",
+      "scope_desc": "Sélectionnez la portée du jeton d'accès."
+    },
+    "copy_to_clipboard": "Copier dans le presse-papiers"
+  },
   "Password": "Mot de passe",
   "Password Settings": "Sécurité",
   "personal_settings": {
@@ -262,9 +293,6 @@
   },
   "API Settings": "API GROWI",
   "Other Settings": "Autres paramètres",
-  "API Token Settings": "Jetons d'API",
-  "Current API Token": "Mon jeton d'API",
-  "Update API Token": "Regénérer",
   "in_app_notification_settings": {
     "in_app_notification_settings": "Notifications",
     "subscribe_settings": "Paramètres d'abonnement automatique aux notifications de pages",
@@ -460,7 +488,7 @@
       "Create Page": "Créer page",
       "Search": "Rechercher",
       "Show Contributors": "Voir contributeurs",
-      "MirrorMode": "Mode mirroir",
+      "MirrorMode": "Mode miroir",
       "Konami Code": "Code Konami",
       "konami_code_url": "https://fr.wikipedia.org/wiki/Code_Konami"
     },
@@ -474,9 +502,27 @@
       "Search in Editor": "Rechercher dans l'éditeur",
       "Move Line": "Déplacer la ligne",
       "Copy Line": "Copier la ligne",
-      "Toggle Line": "Commenter/Décommenter la ligne",
       "Insert Line": "Insérer une ligne",
-      "Post Comment": "(Publier le commentaire)"
+      "Post Comment": "(Publier le commentaire)",
+      "Multiple Cursors": "Curseurs multiples",
+      "Or Alt Click": "ou Alt + Clic"
+    },
+    "format": {
+      "title": "Paramètres de format (Éditeur)",
+      "Bold": "Gras",
+      "Italic": "Italique",
+      "Strikethrough": "Barré",
+      "Code Text": "Texte de code",
+      "Hyperlink": "Lien hypertexte"
+    },
+    "line_settings": {
+      "title": "Paramètres de ligne (Éditeur)",
+      "Bullet List": "Liste à puces",
+      "Numbered List": "Liste numérotée",
+      "Quote": "Citation",
+      "Code Block": "Bloc de code",
+      "Comment Out": "Masquer",
+      "Comment Out Desc": "(Commenter)"
     }
   },
   "modal_resolve_conflict": {
@@ -492,6 +538,8 @@
   },
   "sidebar_ai_assistant": {
     "reference_pages_label": "Pages de référence",
+    "recent_chat": "Chat récent",
+    "no_recent_chat": "Pas de chat récent",
     "knowledge_assistant_placeholder": "Demandez-moi n'importe quoi.",
     "editor_assistant_placeholder": "Puis-je vous aider ?",
     "summary_mode_label": "Mode résumé",
@@ -505,6 +553,8 @@
     "budget_exceeded_for_growi_cloud": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page d'administration de GROWI.cloud pour les utilisateurs hébergés ou à partir de la page de facturation de l'OpenAI pour les utilisateurs propriétaires.",
     "error_message": "Erreur",
     "show_error_detail": "Détails de l'exposition",
+    "editor_assistant_long_context_warn_with_unit_line": "Le texte est trop long, l'Assistant de rédaction se référera approximativement aux lignes {{startPosition}} à {{endPosition}} pour sa réponse.",
+    "editor_assistant_long_context_warn_with_unit_char": "Le texte est trop long, l'Assistant de rédaction se référera aux caractères {{startPosition}} à {{endPosition}} pour sa réponse.",
     "discard": "Annuler",
     "accept": "Accepter",
     "use_assistant": "Utiliser l'assistant",
@@ -544,6 +594,17 @@
       "create_failed": "Échec de la création de l'assistant",
       "update_failed": "Échec de la mise à jour de l'assistant"
     },
+    "select_source_pages": "Sélectionnez les pages que l'assistant doit référencer",
+    "search_reference_pages_by_keyword": "Rechercher les pages de référence de l'assistant par mot-clé",
+    "search_by_keyword": "Rechercher par mot-clé",
+    "max_items_space_separated_hint": "Saisissez jusqu'à 5 éléments séparés par des espaces",
+    "select_assistant_reference_pages": "Sélectionnez les pages de référence pour l'assistant",
+    "enter_keywords": "Entrer des mots-clés",
+    "reference_pages": "Pages de référence",
+    "no_pages_selected": "Aucune page sélectionnée",
+    "can_add_later": "Vous pouvez en ajouter plus tard",
+    "next": "Suivant",
+    "select_from_page_tree": "Sélectionner depuis l'arborescence des pages",
     "edit_page_description": "Modifier les pages que l'assistant peut référencer.<br> L'assistant peut référencer jusqu'à {{limitLearnablePageCountPerAssistant}} pages, y compris les pages enfants.",
     "default_instruction": "Vous êtes l'assistant de connaissances pour ce Wiki.\n\n## Support multilingue :\nRépondez dans la même langue que celle utilisée par l'utilisateur dans sa requête.\n",
     "add_page_button": "Ajouter une page",
@@ -595,11 +656,12 @@
   "default_ai_assistant": {
     "not_set": "L'assistant par défaut n'est pas configuré"
   },
- "ai_assistant_tree": {
+  "ai_assistant_substance": {
     "add_assistant": "Ajouter un assistant",
     "my_assistants": "Mes assistants",
     "team_assistants": "Assistants d'équipe",
     "thread_does_not_exist": "Aucune discussion",
+    "recent_threads": "Éléments récents",
     "toaster": {
       "ai_assistant_deleted_success": "Assistant supprimé",
       "ai_assistant_deleted_failed": "Échec de la suppression de l'assistant",
@@ -607,6 +669,10 @@
       "thread_deleted_failed": "Échec de la suppression de la discussion",
       "ai_assistant_set_default_success": "Assistant par défaut défini avec succès",
       "ai_assistant_set_default_failed": "Échec de la définition de l'assistant par défaut"
+    },
+    "delete_modal": {
+      "title": "Supprimer l'assistant",
+      "confirm_message": "Êtes-vous sûr de vouloir supprimer cet assistant ?"
     }
   },
   "link_edit": {
@@ -825,7 +891,7 @@
     "Password field is required": "Mot de passe requis.",
     "Username or E-mail has invalid characters": "Le nom d'utilisateur ou l'adresse courriel contient des caractères invalides",
     "user_not_found": "Utilisateur introuvable.",
-    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException</strong></p><p class='mb-0'> L'authentification est réussie pour {{ failedProviderForDuplicatedUsernameException }} , mais la création d'un utilisateur a échouée. Voir <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException</strong></p><p class='mb-0'> L'authentification est réussie pour {{ failedProviderForDuplicatedUsernameException }} , mais la création d'un utilisateur a échouée. Voir <a href='https://github.com/growilabs/growi/issues/193'>#193</a>.</p>"
   },
   "grid_edit": {
     "create_bootstrap_4_grid": "Créer grille Bootstrap 4",
@@ -927,7 +993,18 @@
   },
   "user_home_page": {
     "bookmarks": "Favoris",
-    "recently_created": "Page récentes"
+    "recently_created": "Page récentes",
+    "recent_activity": "Activité récente",
+    "unknown_action": "a effectué une modification non spécifiée",
+    "page_create": "a créé une page",
+    "page_update": "a mis à jour une page",
+    "page_delete": "a supprimé une page",
+    "page_delete_completely": "a supprimé complètement une page",
+    "page_rename": "a renommé une page",
+    "page_revert": "a restauré une page",
+    "page_duplicate": "a dupliqué une page",
+    "page_like": "a aimé une page",
+    "comment_create": "a publié un commentaire"
   },
   "bookmark_folder": {
     "bookmark_folder": "dossier de favoris",
@@ -950,23 +1027,6 @@
     "page_tree_not_avaliable": "Cette fonctionnalité n'est pas encore disponible.",
     "go_to_settings": "Activer cette fonctionnalité dans les paramètres"
   },
-  "questionnaire": {
-    "give_us_feedback": "Faites-nous part de votre avis",
-    "thank_you_for_answering": "Merci pour votre réponse",
-    "additional_feedback": "Envoyez-nous votre avis depuis le menu déroulant sur le menu utilisateur.",
-    "dont_show_again": "Ne plus afficher",
-    "deny": "Ne pas répondre",
-    "agree": "En accord",
-    "disagree": "En désaccord",
-    "answer": "Répondre",
-    "no_answer": "Aucune réponse",
-    "settings": "Sondages anonymes",
-    "failed_to_send": "Échec de l'envoi du sondage",
-    "denied": "Les sondages ne seront plus affichés.",
-    "personal_settings_explanation": "Sondages de satisfaction anonymes.",
-    "enable_questionnaire": "Sondages anonymes",
-    "disabled_by_admin": "Sondages anonymes désactivés par l'administrateur"
-  },
   "tag_edit_modal": {
     "edit_tags": "Étiquettes",
     "done": "Mettre à jour",

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

@@ -322,7 +322,7 @@
       "done": "クリップボードにコピーしました!"
     },
     "bug_report": "バグを報告する",
-    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
+    "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>"
   },
   "v5_page_migration": {
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",
@@ -420,17 +420,7 @@
     "enable": "有効",
     "disable": "無効",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
-    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
-    "questionnaire_settings": "アンケート設定",
-    "questionnaire_settings_explanation": "システム全体でアンケート機能を有効/無効にします。有効の場合、各ユーザーはユーザー設定ページの「その他の設定」から個別にアンケート機能を有効/無効にできます。",
-    "about_data_sent": "送信される情報について",
-    "docs_link": "https://docs.growi.org/ja/admin-guide/management-cookbook/app-settings.html#%E3%82%A2%E3%83%B3%E3%82%B1%E3%83%BC%E3%83%88%E8%A8%AD%E5%AE%9A",
-    "learn_more": "詳細",
-    "other_info_will_be_sent": "アンケートの回答と合わせて、GROWI の改善に必要な情報を送信します。送信されるデータにユーザーの個人情報は含まれません。",
-    "we_will_use_the_data_to_improve_growi": "私たちはそれらを活用し、最大限ユーザーの体験を向上させるよう努めます。",
-    "anonymize_app_site_url": "サイト URL を匿名化して送信する",
-    "url_anonymization_explanation": "アンケート回答データに含まれるサイト URL が匿名化されます。この設定を有効にすることで、アンケート回答データの送信元である GROWI アプリケーションが特定されなくなります。",
-    "enable_questionnaire": "アンケートを有効にする"
+    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
   },
   "markdown_settings": {
     "markdown_settings": "マークダウン設定",

+ 93 - 35
apps/app/public/static/locales/ja_JP/commons.json

@@ -32,7 +32,6 @@
   "headers": {
     "app_settings": "アプリ設定"
   },
-
   "header_search_box": {
     "label": {
       "All pages": "全てのページ",
@@ -43,20 +42,17 @@
       "This tree": "この階層下の子ページのみ"
     }
   },
-
   "search_method_menu_item": {
     "search_in_all": "全てのページ",
     "only_children_of_this_tree": "この階層下の子ページのみ",
     "exact_mutch": "キーワードに完全一致した文字を含むページのみ"
   },
-
   "share_links": {
     "Share Link": "共有用リンク",
     "Page Path": "ページパス",
     "expire": "有効期限",
     "description": "概要"
   },
-
   "in_app_notification": {
     "notification_list": "アプリ内通知一覧",
     "see_all": "通知一覧を見る",
@@ -67,7 +63,6 @@
     "no_unread_messages": "未読はありません",
     "only_unread": "未読のみ"
   },
-
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -77,7 +72,6 @@
     "use_os_settings": "OS設定を利用する",
     "feedback": "ご意見・ご要望"
   },
-
   "create_page_dropdown": {
     "new_page": "新規ページ作成",
     "open_page_create_modal": "新規ページ作成モーダルを表示",
@@ -91,7 +85,6 @@
       "descendants": "下位層テンプレート"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
@@ -101,14 +94,12 @@
     "Markdown link": "マークダウン形式のリンク",
     "Append params": "パラメータの追加"
   },
-
   "crop_image_modal": {
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
     "save": "保存",
     "cancel": "キャンセル"
   },
-
   "handsontable_modal": {
     "title": "テーブル編集",
     "data_import": "データインポート",
@@ -124,35 +115,9 @@
       "import": "インポート"
     }
   },
-
-  "questionnaire_modal": {
-    "required": "必須",
-    "submit": "送信",
-    "close": "閉じる",
-    "title": "GROWI サービス改善のためのアンケート",
-    "more_satisfied_services": "GROWI をご利用の皆さまに更にご満足いただけるよう",
-    "strive_to_improve_services": "皆さまからのご意見を参考にサービス改善に努めてまいります。",
-    "length_of_experience": {
-      "more_than_two_years": "2年以上",
-      "one_to_two_years": "1年以上2年未満",
-      "six_months_to_one_year": "6ヶ月以上1年未満",
-      "three_months_to_six_months": "3ヶ月以上6ヶ月未満",
-      "one_month_to_three_months": "1ヶ月以上3ヶ月未満",
-      "less_than_one_month": "1ヶ月未満"
-    },
-    "satisfaction_with_growi": "GROWI の満足度",
-    "history_of_growi_usage": "GROWI の利用歴",
-    "occupation": "職種",
-    "position": "役職",
-    "comment_on_growi": "GROWI へのコメント",
-    "successfully_submitted": "アンケートの送信が完了しました。",
-    "thanks_for_answering": "アンケートのご回答誠にありがとうございました。"
-  },
-
   "not_found_page": {
     "page_not_exist": "このページは存在しません。"
   },
-
   "g2g_data_transfer": {
     "tab": "データ移行",
     "data_transfer": "データ移行",
@@ -161,5 +126,98 @@
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",
     "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ以降はご利用いただけなくなります。",
     "transfer_to_growi_cloud": "※ 詳しくは <a href='{{documentationUrl}}ja/admin-guide/management-cookbook/g2g-transfer.html'> GROWI お引越し機能</a>をご確認ください。"
+  },
+
+  "accesstoken_scopes_desc": {
+    "read": {
+      "all": "全ての閲覧権限を付与できます。",
+      "admin": {
+        "all": "管理者機能の閲覧権限を付与できます。",
+        "top": "Wiki管理トップの閲覧権限を付与できます。",
+        "app": "アプリ設定の閲覧権限を付与できます。",
+        "security": "セキュリティ設定の閲覧権限を付与できます。",
+        "markdown": "マークダウン設定の閲覧権限を付与できます。",
+        "customize": "カスタマイズの閲覧権限を付与できます。",
+        "import_data": "データインポートの閲覧権限を付与できます。",
+        "export_data": "データアーカイブの閲覧権限を付与できます。",
+        "data_transfer": "データ移行の閲覧権限を付与できます。",
+        "external_notification": "外部ツールへの通知の閲覧権限を付与できます。",
+        "slack_integration": "Slack連携の閲覧権限を付与できます。",
+        "legacy_slack_integration": "Slack連携(レガシー)の閲覧権限を付与できます。",
+        "user_management": "ユーザー管理の閲覧権限を付与できます。",
+        "user_group_management": "ユーザーグループ管理の閲覧権限を付与できます。",
+        "audit_log": "監査ログの閲覧権限を付与できます。",
+        "plugin": "プラグインの閲覧権限を付与できます。",
+        "ai_integration": "AI連携設定の閲覧権限を付与できます。",
+        "full_text_search": "全文検索管理の閲覧権限を付与できます。"
+      },
+      "user_settings": {
+        "all": "ユーザー設定の閲覧権限を付与できます。",
+        "info": "ユーザー情報の閲覧権限を付与できます。",
+        "external_account": "外部アカウントの閲覧権限を付与できます。",
+        "password": "パスワード設定の閲覧権限を付与できます。",
+        "api": {
+          "all": "API 設定の閲覧権限を付与できます。",
+          "api_token": "API トークン設定の閲覧権限を付与できます。",
+          "access_token": "アクセストークン設定の閲覧権限を付与できます。"
+        },
+        "in_app_notification": "アプリ内通知設定の閲覧権限を付与できます。",
+        "other": "その他設定の閲覧権限を付与できます。"
+      },
+      "features": {
+        "all": "機能の閲覧権限を付与できます。",
+        "ai_assistant": "AIアシスタント機能の閲覧権限を付与できます。",
+        "page": "ページ機能の閲覧権限を付与できます。",
+        "share_link": "共有リンク機能の閲覧権限を付与できます。",
+        "bookmark": "ブックマーク機能の閲覧権限を付与できます。",
+        "attachment": "添付ファイル機能の閲覧権限を付与できます。",
+        "page_bulk_export": "ページの一括エクスポート機能の閲覧権限を付与できます。"
+      }
+    },
+    "write": {
+      "all": "全ての編集権限を付与できます。",
+      "admin": {
+        "all": "管理者機能の編集権限を付与できます。",
+        "top": "Wiki管理トップの編集権限を付与できます。",
+        "app": "アプリ設定の編集権限を付与できます。",
+        "security": "セキュリティ設定の編集権限を付与できます。",
+        "markdown": "マークダウン設定の編集権限を付与できます。",
+        "customize": "カスタマイズの編集権限を付与できます。",
+        "import_data": "データインポートの編集権限を付与できます。",
+        "export_data": "データアーカイブの編集権限を付与できます。",
+        "data_transfer": "データ移行の編集権限を付与できます。",
+        "external_notification": "外部ツールへの通知の編集権限を付与できます。",
+        "slack_integration": "Slack連携の編集権限を付与できます。",
+        "legacy_slack_integration": "Slack連携(レガシー)の編集権限を付与できます。",
+        "user_management": "ユーザー管理の編集権限を付与できます。",
+        "user_group_management": "ユーザーグループ管理の編集権限を付与できます。",
+        "audit_log": "監査ログの編集権限を付与できます。",
+        "plugin": "プラグインの編集権限を付与できます。",
+        "ai_integration": "AI連携設定の編集権限を付与できます。",
+        "full_text_search": "全文検索管理の編集権限を付与できます。"
+      },
+      "user_settings": {
+        "all": "ユーザー設定の編集権限を付与できます。",
+        "info": "ユーザー情報の編集権限を付与できます。",
+        "external_account": "外部アカウントの編集権限を付与できます。",
+        "password": "パスワード設定の編集権限を付与できます。",
+        "api": {
+          "all": "API 設定の編集権限を付与できます。",
+          "api_token": "API トークン設定の編集権限を付与できます。",
+          "access_token": "アクセストークン設定の編集権限を付与できます。"
+        },
+        "in_app_notification": "アプリ内通知設定の編集権限を付与できます。",
+        "other": "その他設定の編集権限を付与できます。"
+      },
+      "features": {
+        "all": "機能の編集権限を付与できます。",
+        "ai_assistant": "AIアシスタント機能の編集権限を付与できます。",
+        "page": "ページ機能の編集権限を付与できます。",
+        "share_link": "共有リンク機能の編集権限を付与できます。",
+        "bookmark": "ブックマーク機能の編集権限を付与できます。",
+        "attachment": "添付ファイル機能の編集権限を付与できます。",
+        "page_bulk_export": "ページの一括エクスポート機能の編集権限を付与できます。"
+      }
+    }
   }
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است