Browse Source

Merge pull request #10477 from growilabs/pdf-converter-1.1.2

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

+ 1 - 1
.changeset/config.json

@@ -1,6 +1,6 @@
 {
 {
   "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
   "$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,
   "commit": false,
   "fixed": [],
   "fixed": [],
   "linked": [],
   "linked": [],

+ 16 - 8
.devcontainer/app/devcontainer.json

@@ -8,7 +8,7 @@
 
 
   "features": {
   "features": {
     "ghcr.io/devcontainers/features/node:1": {
     "ghcr.io/devcontainers/features/node:1": {
-      "version": "22.17.0"
+      "version": "20.18.3"
     }
     }
   },
   },
 
 
@@ -23,21 +23,29 @@
   "customizations": {
   "customizations": {
     "vscode": {
     "vscode": {
       "extensions": [
       "extensions": [
+        // AI
+        "anthropic.claude-code",
+        // linter
         "dbaeumer.vscode-eslint",
         "dbaeumer.vscode-eslint",
         "biomejs.biome",
         "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",
         "mhutchie.git-graph",
         "eamodio.gitlens",
         "eamodio.gitlens",
-        "github.vscode-pull-request-github",
         "cschleiden.vscode-github-actions",
         "cschleiden.vscode-github-actions",
+        // DB
         "cweijan.vscode-database-client2",
         "cweijan.vscode-database-client2",
         "mongodb.mongodb-vscode",
         "mongodb.mongodb-vscode",
+        // Debug
         "msjsdiag.debugger-for-chrome",
         "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": {
       "settings": {
         "terminal.integrated.defaultProfile.linux": "bash"
         "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
 # Setup pnpm
 SHELL=bash pnpm setup
 SHELL=bash pnpm setup
 eval "$(cat /home/vscode/.bashrc)"
 eval "$(cat /home/vscode/.bashrc)"
+pnpm config set store-dir /workspace/.pnpm-store
 
 
 # Install turbo
 # Install turbo
 pnpm install turbo --global
 pnpm install turbo --global
 
 
+# Install Claude Code
+pnpm install @anthropic-ai/claude-code --global
+
 # Install dependencies
 # Install dependencies
 turbo run bootstrap
 turbo run bootstrap

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

@@ -3,10 +3,9 @@
 services:
 services:
   pdf-converter:
   pdf-converter:
     # enabling devcontainer 'features' was not working for secondary devcontainer (https://github.com/devcontainers/features/issues/1175)
     # enabling devcontainer 'features' was not working for secondary devcontainer (https://github.com/devcontainers/features/issues/1175)
-    image: mcr.microsoft.com/vscode/devcontainers/javascript-node:1-22
+    image: mcr.microsoft.com/vscode/devcontainers/javascript-node:1-20
     volumes:
     volumes:
       - ..:/workspace/growi:delegated
       - ..:/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
       - page_bulk_export_tmp:/tmp/page-bulk-export
     tty: true
     tty: true

+ 6 - 10
.devcontainer/compose.yml

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

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

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

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

@@ -18,7 +18,7 @@ Environment
 |Using Docker|yes/no|
 |Using Docker|yes/no|
 |Using [growi-docker-compose][growi-docker-compose]|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)*
 *(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
 blank_issues_enabled: false
 contact_links:
 contact_links:
   - name: User request or Suggestions
   - 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.
     about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
   - name: Questions
   - name: Questions
     url: https://communityinviter.com/apps/wsgrowi/invite/
     url: https://communityinviter.com/apps/wsgrowi/invite/

+ 5 - 5
.github/mergify.yml

@@ -7,17 +7,17 @@ queue_rules:
       - check-success ~= ci-app-launch-dev
       - check-success ~= ci-app-launch-dev
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-slackbot-
       - -check-failure ~= ci-slackbot-
-      - -check-failure ~= test-prod-node22 /
+      - -check-failure ~= test-prod-node20 /
     merge_conditions:
     merge_conditions:
       - check-success ~= ci-app-lint
       - check-success ~= ci-app-lint
       - check-success ~= ci-app-test
       - check-success ~= ci-app-test
       - check-success ~= ci-app-launch-dev
       - check-success ~= ci-app-launch-dev
-      - check-success = test-prod-node22 / build-prod
-      - check-success = test-prod-node22 / launch-prod
-      - check-success ~= test-prod-node22 / run-playwright
+      - check-success = test-prod-node20 / build-prod
+      - check-success ~= test-prod-node20 / launch-prod
+      - check-success ~= test-prod-node20 / run-playwright
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-slackbot-
       - -check-failure ~= ci-slackbot-
-      - -check-failure ~= test-prod-node22 /
+      - -check-failure ~= test-prod-node20 /
 
 
 pull_request_rules:
 pull_request_rules:
   - name: Automatic queue to merge
   - name: Automatic queue to merge

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

@@ -39,8 +39,8 @@ concurrency:
 
 
 jobs:
 jobs:
 
 
-  test-prod-node20:
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+  test-prod-node18:
+    uses: growilabs/growi/.github/workflows/reusable-app-prod.yml@master
     if: |
     if: |
       ( github.event_name == 'push'
       ( github.event_name == 'push'
         || github.base_ref == 'master'
         || github.base_ref == 'master'
@@ -48,14 +48,14 @@ jobs:
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
-      node-version: 20.x
+      node-version: 18.x
       skip-e2e-test: true
       skip-e2e-test: true
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
 
 
-  test-prod-node22:
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+  test-prod-node20:
+    uses: growilabs/growi/.github/workflows/reusable-app-prod.yml@master
     if: |
     if: |
       ( github.event_name == 'push'
       ( github.event_name == 'push'
         || github.base_ref == 'master'
         || github.base_ref == 'master'
@@ -63,7 +63,7 @@ jobs:
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
-      node-version: 22.x
+      node-version: 20.x
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -71,7 +71,7 @@ jobs:
   # run-reg-suit-node20:
   # run-reg-suit-node20:
   #   needs: [test-prod-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()
   #   if: always()
 
 

+ 6 - 4
.github/workflows/ci-app.yml

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

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

@@ -29,7 +29,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [22.x]
+        node-version: [20.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
@@ -104,7 +104,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [22.x]
+        node-version: [20.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4

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

@@ -30,7 +30,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [22.x]
+        node-version: [20.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
@@ -85,7 +85,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [22.x]
+        node-version: [20.x]
 
 
     services:
     services:
       mysql:
       mysql:
@@ -163,7 +163,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [22.x]
+        node-version: [20.x]
 
 
     services:
     services:
       mysql:
       mysql:

+ 1 - 1
.github/workflows/list-unhealthy-branches.yml

@@ -16,7 +16,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '20'
+        node-version: '18'
 
 
     - name: List branches
     - name: List branches
       id: list-branches
       id: list-branches

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

@@ -72,7 +72,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [22.x]
+        node-version: [20.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4

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

@@ -46,9 +46,9 @@ jobs:
 
 
 
 
   build-image-rc:
   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:
     with:
-      image-name: weseek/growi
+      image-name: growilabs/growi
       tag-temporary: latest-rc
       tag-temporary: latest-rc
     secrets:
     secrets:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
@@ -57,11 +57,12 @@ jobs:
   publish-image-rc:
   publish-image-rc:
     needs: [determine-tags, build-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:
     with:
       tags: ${{ needs.determine-tags.outputs.TAGS }}
       tags: ${{ needs.determine-tags.outputs.TAGS }}
       registry: docker.io
       registry: docker.io
       image-name: weseek/growi
       image-name: weseek/growi
+      docker-registry-username: wsmoogle
       tag-temporary: latest-rc
       tag-temporary: latest-rc
     secrets:
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

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

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

@@ -92,7 +92,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '20'
+        node-version: '18'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies

+ 2 - 2
.github/workflows/release-subpackages.yml

@@ -32,7 +32,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '22'
+        node-version: '20'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
@@ -75,7 +75,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '22'
+        node-version: '20'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies

+ 50 - 17
.github/workflows/release.yml

@@ -1,3 +1,4 @@
+# TODO: https://redmine.weseek.co.jp/issues/171293
 name: Release
 name: Release
 
 
 on:
 on:
@@ -26,7 +27,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '22'
+        node-version: '20'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
@@ -80,7 +81,8 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     outputs:
     outputs:
-      TAGS: ${{ steps.meta.outputs.tags }}
+      TAGS_WESEEK: ${{ steps.meta-weseek.outputs.tags }}
+      TAGS_GROWILABS: ${{ steps.meta-growilabs.outputs.tags }}
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
@@ -89,9 +91,9 @@ jobs:
       uses: myrotvorets/info-from-package-json-action@v2.0.2
       uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
 
 
-    - name: Docker meta for docker.io
+    - name: Docker meta for weseek/growi
       uses: docker/metadata-action@v5
       uses: docker/metadata-action@v5
-      id: meta
+      id: meta-weseek
       with:
       with:
         images: docker.io/weseek/growi
         images: docker.io/weseek/growi
         sep-tags: ','
         sep-tags: ','
@@ -101,36 +103,67 @@ 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}}
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
           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:
   build-app-image:
     needs: create-github-release
     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:
     with:
       source-version: refs/tags/v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
       source-version: refs/tags/v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
-      image-name: weseek/growi
+      image-name: growilabs/growi
       tag-temporary: latest
       tag-temporary: latest
     secrets:
     secrets:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
       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]
     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:
     with:
-      tags: ${{ needs.determine-tags.outputs.TAGS }}
+      tags: ${{ needs.determine-tags.outputs.TAGS_WESEEK }}
       registry: docker.io
       registry: docker.io
-      image-name: weseek/growi
+      image-name: 'growilabs/growi'
+      docker-registry-username: 'wsmoogle'
       tag-temporary: latest
       tag-temporary: latest
     secrets:
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
 
 
-
   post-publish:
   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
     runs-on: ubuntu-latest
 
 
+    strategy:
+      matrix:
+        include:
+          - repository: weseek/growi
+            username: wsmoogle
+          - repository: growilabs/growi
+            username: growimoogle
+
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
       with:
       with:
@@ -139,9 +172,9 @@ jobs:
     - name: Update Docker Hub Description
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v4
       uses: peter-evans/dockerhub-description@v4
       with:
       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
         readme-filepath: ./apps/app/docker/README.md
 
 
     - name: Slack Notification
     - name: Slack Notification
@@ -153,7 +186,7 @@ jobs:
 
 
 
 
   create-pr-for-next-rc:
   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
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
@@ -165,7 +198,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '22'
+        node-version: '20'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies

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

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

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

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

+ 13 - 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:
 on:
   workflow_call:
   workflow_call:
@@ -16,7 +16,7 @@ on:
       node-version:
       node-version:
         required: true
         required: true
         type: string
         type: string
-        default: 20.x
+        default: 22.x
       skip-e2e-test:
       skip-e2e-test:
         type: boolean
         type: boolean
         default: false
         default: false
@@ -107,13 +107,17 @@ jobs:
     needs: [build-prod]
     needs: [build-prod]
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
+    strategy:
+      matrix:
+        mongodb-version: ['6.0', '8.0']
+
     services:
     services:
       mongodb:
       mongodb:
-        image: mongo:6.0
+        image: mongo:${{ matrix.mongodb-version }}
         ports:
         ports:
         - 27017/tcp
         - 27017/tcp
       elasticsearch:
       elasticsearch:
-        image: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
+        image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
         ports:
         ports:
         - 9200/tcp
         - 9200/tcp
         env:
         env:
@@ -182,14 +186,15 @@ jobs:
       matrix:
       matrix:
         browser: [chromium, firefox, webkit]
         browser: [chromium, firefox, webkit]
         shard: [1/2, 2/2]
         shard: [1/2, 2/2]
+        mongodb-version: ['6.0', '8.0']
 
 
     services:
     services:
       mongodb:
       mongodb:
-        image: mongo:6.0
+        image: mongo:${{ matrix.mongodb-version }}
         ports:
         ports:
         - 27017/tcp
         - 27017/tcp
       elasticsearch:
       elasticsearch:
-        image: docker.elastic.co/elasticsearch/elasticsearch:7.17.1
+        image: docker.elastic.co/elasticsearch/elasticsearch:9.0.1
         ports:
         ports:
         - 9200/tcp
         - 9200/tcp
         env:
         env:
@@ -279,7 +284,7 @@ jobs:
       uses: actions/upload-artifact@v4
       uses: actions/upload-artifact@v4
       if: always()
       if: always()
       with:
       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
         path: ./apps/app/blob-report
         retention-days: 30
         retention-days: 30
 
 
@@ -288,7 +293,7 @@ jobs:
       if: failure()
       if: failure()
       with:
       with:
         type: ${{ job.status }}
         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'
         channel: '#ci'
         isCompactMode: true
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}

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

@@ -32,7 +32,7 @@ jobs:
 
 
   run-reg-suit:
   run-reg-suit:
     # use secrets for "VRT" environment
     # 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
     environment: VRT
 
 
     if: ${{ !inputs.skip-reg-suit }}
     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 - 14
.roo/mcp.json

@@ -1,14 +0,0 @@
-{
-  "mcpServers": {
-    "fetch": {
-      "command": "uvx",
-      "args": ["mcp-server-fetch"],
-      "alwaysAllow": ["fetch"]
-    },
-    "context7": {
-      "type": "streamable-http",
-      "url": "https://mcp.context7.com/mcp",
-      "alwaysAllow": ["resolve-library-id", "get-library-docs"]
-    }
-  }
-}

+ 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"
+      ]
+    }
+  }
+}

+ 6 - 1
.vscode/settings.json

@@ -11,6 +11,10 @@
     "editor.defaultFormatter": "biomejs.biome"
     "editor.defaultFormatter": "biomejs.biome"
   },
   },
 
 
+  "[json]": {
+    "editor.defaultFormatter": "biomejs.biome"
+  },
+
   // use vscode-stylelint
   // use vscode-stylelint
   // see https://github.com/stylelint/vscode-stylelint
   // see https://github.com/stylelint/vscode-stylelint
   "stylelint.validate": ["css", "less", "scss"],
   "stylelint.validate": ["css", "less", "scss"],
@@ -96,6 +100,7 @@
     {
     {
       "text": "Always write commit messages in English."
       "text": "Always write commit messages in English."
     }
     }
-  ]
+  ],
+  "git-worktree-menu.worktreeDir": "/workspace"
 
 
 }
 }

File diff suppressed because it is too large
+ 249 - 98
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
 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
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 of this software and associated documentation files (the "Software"), to deal

+ 15 - 15
README.md

@@ -6,7 +6,7 @@
   </a>
   </a>
 </p>
 </p>
 <p align="center">
 <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>
   <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>
 </p>
 
 
@@ -16,10 +16,10 @@
 
 
 # GROWI
 # 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
 ## Demonstration
 <video src="https://private-user-images.githubusercontent.com/34241526/333079483-fee540d7-2fa6-46d7-833e-74014c5340e3.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTY0NDk2OTEsIm5iZiI6MTcxNjQ0OTM5MSwicGF0aCI6Ii8zNDI0MTUyNi8zMzMwNzk0ODMtZmVlNTQwZDctMmZhNi00NmQ3LTgzM2UtNzQwMTRjNTM0MGUzLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MjMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTIzVDA3Mjk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBkYWFkMmYyYmIwMTI2YWE3ZmQzZTFiNWU3ZThkMDc5NDA5N2Q3YWE5ZGM1NDgwNjk0OGNjYjZmOTJkM2IzZGQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.FAvLseWBzE62yFA7wt26uERamvEVQdIGRVdBwk0uLhE"></video>
 <video src="https://private-user-images.githubusercontent.com/34241526/333079483-fee540d7-2fa6-46d7-833e-74014c5340e3.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTY0NDk2OTEsIm5iZiI6MTcxNjQ0OTM5MSwicGF0aCI6Ii8zNDI0MTUyNi8zMzMwNzk0ODMtZmVlNTQwZDctMmZhNi00NmQ3LTgzM2UtNzQwMTRjNTM0MGUzLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MjMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTIzVDA3Mjk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBkYWFkMmYyYmIwMTI2YWE3ZmQzZTFiNWU3ZThkMDc5NDA5N2Q3YWE5ZGM1NDgwNjk0OGNjYjZmOTJkM2IzZGQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.FAvLseWBzE62yFA7wt26uERamvEVQdIGRVdBwk0uLhE"></video>
@@ -81,11 +81,11 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 
 ## Dependencies
 ## Dependencies
 
 
-- Node.js v20.x or v22.x
-- npm 10.x
-- pnpm 10.x
+- Node.js v18.x or v20.x
+- npm 6.x
+- pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 6.0 or above
+- MongoDB v6.x or v8.x
 
 
 ### Optional Dependencies
 ### Optional Dependencies
 
 
@@ -138,11 +138,11 @@ If you have questions or suggestions, you can [join our Slack team](https://comm
 # License
 # License
 
 
 - The MIT License (MIT)
 - 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
 [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

+ 15 - 15
README_JP.md

@@ -6,7 +6,7 @@
   </a>
   </a>
 </p>
 </p>
 <p align="center">
 <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>
   <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>
 </p>
 
 
@@ -16,10 +16,10 @@
 
 
 # GROWI
 # 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>
 <video src="https://private-user-images.githubusercontent.com/34241526/333079216-cec7f7d8-c3cc-4ee7-bc94-167b056d4ce2.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTY0NDk0MDQsIm5iZiI6MTcxNjQ0OTEwNCwicGF0aCI6Ii8zNDI0MTUyNi8zMzMwNzkyMTYtY2VjN2Y3ZDgtYzNjYy00ZWU3LWJjOTQtMTY3YjA1NmQ0Y2UyLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MjMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTIzVDA3MjUwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ2M2IwZjc0ZGNhOWQxNWE4MGIyZTZlMzQ0ZDQ4MGZlNjEzMWE3MTQ1YmMwYzg3MmY1NWMyZWI2NzQ3NGIwMTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.qLfu5120JrkdfpghXlLG8wCn0p4JNZ7W8AD3zUJTIYY"></video>
@@ -81,11 +81,11 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 
 
 ## 依存関係
 ## 依存関係
 
 
-- Node.js v20.x or v22.x
-- npm 10.x
-- pnpm 10.x
+- Node.js v18.x or v20.x
+- npm 6.x
+- pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
 - [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)
 - 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
   [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.
 The attached notices are provided for information only.
 
 
 For any licenses that require disclosure of source, sources are available at  
 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
 1. Apache License, Version 2.0 Derivative Works

+ 3 - 1
apps/app/.env.development

@@ -30,7 +30,9 @@ OGP_URI="http://ogp:8088"
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 
 
-# OpenTelemetry Official Configuration
+SERVICE_TYPE=dev
+
+# OpenTelemetry Official Configuration for dev
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OPENTELEMETRY_ENABLED=false
 OPENTELEMETRY_ENABLED=false
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 1 - 1
apps/app/.env.production

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

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

@@ -2,12 +2,8 @@
  * @type {import('eslint').Linter.Config}
  * @type {import('eslint').Linter.Config}
  */
  */
 module.exports = {
 module.exports = {
-  extends: [
-    'next/core-web-vitals',
-    'weseek/react',
-  ],
-  plugins: [
-  ],
+  extends: ['next/core-web-vitals', 'weseek/react'],
+  plugins: [],
   ignorePatterns: [
   ignorePatterns: [
     'dist/**',
     'dist/**',
     '**/dist/**',
     '**/dist/**',
@@ -16,6 +12,52 @@ module.exports = {
     'src/linter-checker/**',
     'src/linter-checker/**',
     'tmp/**',
     'tmp/**',
     'next-env.d.ts',
     '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',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript
@@ -24,10 +66,6 @@ module.exports = {
     },
     },
   },
   },
   rules: {
   rules: {
-    'no-restricted-imports': ['error', {
-      name: 'axios',
-      message: 'Please use src/utils/axios instead.',
-    }],
     '@typescript-eslint/no-var-requires': 'off',
     '@typescript-eslint/no-var-requires': 'off',
 
 
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei

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

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
       variables: {
         server: {
         server: {
           default: 'https://demo.growi.org',
           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.',
         },
         },
       },
       },
     },
     },

+ 3 - 11
apps/app/bin/openapi/definition-apiv3.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
       variables: {
         server: {
         server: {
           default: 'https://demo.growi.org',
           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.',
         },
         },
       },
       },
     },
     },
@@ -115,16 +116,7 @@ module.exports = {
     },
     },
     {
     {
       name: 'Public API',
       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';
 import { generateOperationIds } from './generate-operation-ids';
 
 
@@ -23,7 +20,7 @@ describe('cli', () => {
     vi.spyOn(console, 'error').mockImplementation(() => {});
     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
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
 
@@ -35,13 +32,15 @@ describe('cli', () => {
     await cliModule.main();
     await cliModule.main();
 
 
     // Verify generateOperationIds was called with correct arguments
     // 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
     // Verify writeFileSync was called with correct arguments
     expect(writeFileSync).toHaveBeenCalledWith('output.json', mockJsonStrings);
     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
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
 
@@ -53,13 +52,15 @@ describe('cli', () => {
     await cliModule.main();
     await cliModule.main();
 
 
     // Verify generateOperationIds was called with correct arguments
     // 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
     // Verify writeFileSync was called with input file as output
     expect(writeFileSync).toHaveBeenCalledWith('input.json', mockJsonStrings);
     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
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
 
@@ -71,10 +72,12 @@ describe('cli', () => {
     await cliModule.main();
     await cliModule.main();
 
 
     // Verify generateOperationIds was called with overwriteExisting option
     // 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
     // Mock generateOperationIds to throw error
     const error = new Error('Test error');
     const error = new Error('Test error');
     vi.mocked(generateOperationIds).mockRejectedValue(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 { Command } from 'commander';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';
 
 
-export const main = async(): Promise<void> => {
+export const main = async (): Promise<void> => {
   // parse command line arguments
   // parse command line arguments
   const program = new Command();
   const program = new Command();
   program
   program
@@ -18,7 +17,9 @@ export const main = async(): Promise<void> => {
   const [inputFile] = program.args;
   const [inputFile] = program.args;
 
 
   // eslint-disable-next-line no-console
   // 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) {
   if (jsonStrings != null) {
     writeFileSync(outputFile ?? inputFile, jsonStrings);
     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 type { OpenAPI3 } from 'openapi-typescript';
 import { describe, expect, it } from 'vitest';
 import { describe, expect, it } from 'vitest';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';
 
 
-
 async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
 async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
   const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
   const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
   const filePath = path.join(tempDir, 'openapi.json');
   const filePath = path.join(tempDir, 'openapi.json');
@@ -19,15 +17,14 @@ async function cleanup(filePath: string): Promise<void> {
   try {
   try {
     await fs.unlink(filePath);
     await fs.unlink(filePath);
     await fs.rmdir(path.dirname(filePath));
     await fs.rmdir(path.dirname(filePath));
-  }
-  catch (err) {
+  } catch (err) {
     // eslint-disable-next-line no-console
     // eslint-disable-next-line no-console
     console.error('Cleanup failed:', err);
     console.error('Cleanup failed:', err);
   }
   }
 }
 }
 
 
 describe('generateOperationIds', () => {
 describe('generateOperationIds', () => {
-  it('should generate correct operationId for simple paths', async() => {
+  it('should generate correct operationId for simple paths', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.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'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
       expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       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 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -72,14 +68,15 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
       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);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate correct operationId for nested resources', async() => {
+  it('should generate correct operationId for nested resources', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -96,13 +93,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
       expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       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 existingOperationId = 'existingOperation';
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
@@ -118,17 +114,18 @@ describe('generateOperationIds', () => {
 
 
     const filePath = await createTempOpenAPIFile(spec);
     const filePath = await createTempOpenAPIFile(spec);
     try {
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: false });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: false,
+      });
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
       expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       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 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -143,17 +140,18 @@ describe('generateOperationIds', () => {
 
 
     const filePath = await createTempOpenAPIFile(spec);
     const filePath = await createTempOpenAPIFile(spec);
     try {
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: true });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: true,
+      });
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate correct operationId for root path', async() => {
+  it('should generate correct operationId for root path', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
       info: { title: 'Test API', version: '1.0.0' },
@@ -170,13 +168,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
       const parsed = JSON.parse(result);
 
 
       expect(parsed.paths['/'].get.operationId).toBe('getRoot');
       expect(parsed.paths['/'].get.operationId).toBe('getRoot');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       await cleanup(filePath);
     }
     }
   });
   });
 
 
-  it('should generate operationId for all HTTP methods', async() => {
+  it('should generate operationId for all HTTP methods', async () => {
     const spec: OpenAPI3 = {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.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'].options.operationId).toBe('optionsFoo');
       expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
       expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
       expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
       expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
       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 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 => {
 const createParamSuffix = (params: string[]): string => {
   return params.length > 0
   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.
  * 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 {
 function createOperationId(method: string, path: string): string {
   const segments = path.split('/').filter(Boolean);
   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);
   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('')}`;
   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]) => {
   Object.entries(api.paths || {}).forEach(([path, pathItem]) => {
     const item = pathItem as PathItemObject;
     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);
   const output = JSON.stringify(api, null, 2);

+ 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 - 0
apps/app/config/logger/config.dev.js

@@ -43,4 +43,5 @@ module.exports = {
   // 'growi:cli:ItemsTree': 'debug',
   // 'growi:cli:ItemsTree': 'debug',
   'growi:searchResultList': 'debug',
   'growi:searchResultList': 'debug',
   'growi:service:openai': '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 isProduction = process.env.NODE_ENV === 'production';
 
 
-const { URL } = require('url');
+const { URL } = require('node:url');
 
 
 const { getMongoUri, mongoOptions } = isProduction
 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');
   : require('../src/server/util/mongoose-utils');
 
 
 // get migrationsDir from env var
 // 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;
 const { reRequire } = mockRequire;
 
 
-
 describe('config/migrate-mongo-config.js', () => {
 describe('config/migrate-mongo-config.js', () => {
-
   test.concurrent('throws an error when MIGRATIONS_DIR is not set', () => {
   test.concurrent('throws an error when MIGRATIONS_DIR is not set', () => {
-
     const getMongoUriMock = vi.fn();
     const getMongoUriMock = vi.fn();
     const mongoOptionsMock = vi.fn();
     const mongoOptionsMock = vi.fn();
 
 
@@ -32,13 +29,11 @@ describe('config/migrate-mongo-config.js', () => {
     ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
     ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
     ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
     ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
   `('returns', ({ MONGO_URI, expectedDbName }) => {
   `('returns', ({ MONGO_URI, expectedDbName }) => {
-
-    beforeEach(async() => {
+    beforeEach(async () => {
       process.env.MIGRATIONS_DIR = 'testdir/migrations';
       process.env.MIGRATIONS_DIR = 'testdir/migrations';
     });
     });
 
 
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
-
       const getMongoUriMock = vi.fn(() => MONGO_URI);
       const getMongoUriMock = vi.fn(() => MONGO_URI);
       const mongoOptionsMock = vi.fn();
       const mongoOptionsMock = vi.fn();
 
 
@@ -49,7 +44,9 @@ describe('config/migrate-mongo-config.js', () => {
       });
       });
 
 
       // use reRequire to avoid using module cache
       // 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');
       mockRequire.stop('../src/server/util/mongoose-utils');
 
 
@@ -61,5 +58,4 @@ describe('config/migrate-mongo-config.js', () => {
       expect(changelogCollectionName).toBe('migrations');
       expect(changelogCollectionName).toBe('migrations');
     });
     });
   });
   });
-
 });
 });

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

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

+ 2 - 2
apps/app/docker/Dockerfile

@@ -6,7 +6,7 @@ ARG PNPM_HOME="/root/.local/share/pnpm"
 ##
 ##
 ## base
 ## base
 ##
 ##
-FROM node:22-slim AS base
+FROM node:20-slim AS base
 
 
 ARG OPT_DIR
 ARG OPT_DIR
 ARG PNPM_HOME
 ARG PNPM_HOME
@@ -72,7 +72,7 @@ RUN tar -zcf /tmp/packages.tar.gz \
 ##
 ##
 ## release
 ## release
 ##
 ##
-FROM node:22-slim
+FROM node:20-slim
 LABEL maintainer="Yuki Takei <yuki@weseek.co.jp>"
 LABEL maintainer="Yuki Takei <yuki@weseek.co.jp>"
 
 
 ARG OPT_DIR
 ARG OPT_DIR

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

@@ -2,7 +2,7 @@
 GROWI Official docker image
 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)
 ![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
 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?
 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
 Requirements
@@ -41,7 +41,7 @@ Usage
 ```bash
 ```bash
 docker run -d \
 docker run -d \
     -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
     -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
-    weseek/growi
+    growilabs/growi
 ```
 ```
 
 
 and go to `http://localhost:3000/` .
 and go to `http://localhost:3000/` .
@@ -52,7 +52,7 @@ If you use ElasticSearch, type this:
 docker run -d \
 docker run -d \
     -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
     -e MONGO_URI=mongodb://MONGODB_HOST:MONGODB_PORT/growi \
     -e ELASTICSEARCH_URI=http://ELASTICSEARCH_HOST:ELASTICSEARCH_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.
 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
 Configuration
@@ -76,5 +76,5 @@ See [GROWI Docs: Admin Guide](https://docs.growi.org/en/admin-guide/) ([en](http
 Issues
 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.
 # Manual edits may be lost in future updates.
 
 
 provider "registry.terraform.io/hashicorp/aws" {
 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 = [
   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: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" {
 provider "registry.terraform.io/hashicorp/random" {
-  version     = "3.4.3"
+  version     = "3.7.2"
   constraints = ">= 2.1.0"
   constraints = ">= 2.1.0"
   hashes = [
   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: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" {
 provider "registry.terraform.io/hashicorp/tls" {
-  version     = "4.0.4"
-  constraints = ">= 3.0.0"
+  version     = "4.1.0"
+  constraints = ">= 4.0.0"
   hashes = [
   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:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
+    "zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54",
   ]
   ]
 }
 }

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

@@ -11,7 +11,7 @@ phases:
   pre_build:
   pre_build:
     commands:
     commands:
       # login to docker.io
       # 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:
   build:
     commands:
     commands:
       - docker build -t ${IMAGE_TAG} -f ./apps/app/docker/Dockerfile .
       - 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"
   artifact_type       = "NO_ARTIFACTS"
 
 
   source_type         = "GITHUB"
   source_type         = "GITHUB"
-  source_location     = "https://github.com/weseek/growi.git"
+  source_location     = "https://github.com/growilabs/growi.git"
   source_version      = "refs/heads/master"
   source_version      = "refs/heads/master"
   git_clone_depth     = 1
   git_clone_depth     = 1
 
 

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

@@ -10,7 +10,7 @@ terraform {
   required_providers {
   required_providers {
     aws = {
     aws = {
       source  = "hashicorp/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 = [
   github_repositories = [
-    "weseek/growi",
+    "growilabs/growi",
   ]
   ]
 }
 }
 
 

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

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

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

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

+ 1 - 4
apps/app/nodemon.json

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

+ 19 - 14
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.3.0-RC.0",
+  "version": "7.3.5-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "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",
     "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:typecheck": "vue-tsc --noEmit",
     "lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
     "lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
+    "lint:biome": "biome check",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
     "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:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
     "lint": "run-p lint:**",
     "lint": "run-p lint:**",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv1": "pnpm run openapi:generate-spec:apiv1",
     "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: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 -- ",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
     "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": "",
     "//// misc": "",
     "console": "npm run repl",
     "console": "npm run repl",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
@@ -69,8 +70,9 @@
     "@azure/storage-blob": "^12.16.0",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@cspell/dynamic-import": "^8.15.4",
     "@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",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "workspace:^",
     "@growi/core": "workspace:^",
@@ -102,7 +104,7 @@
     "archiver": "^5.3.0",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
     "async-canvas-to-blob": "^1.0.3",
-    "axios": "^0.24.0",
+    "axios": "^1.11.0",
     "axios-retry": "^3.2.4",
     "axios-retry": "^3.2.4",
     "babel-plugin-superjson-next": "^0.4.2",
     "babel-plugin-superjson-next": "^0.4.2",
     "body-parser": "^1.20.3",
     "body-parser": "^1.20.3",
@@ -156,7 +158,7 @@
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-wiki-link": "^0.1.2",
     "mdast-util-wiki-link": "^0.1.2",
-    "mermaid": "^11.7.0",
+    "mermaid": "^11.10.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-wiki-link": "^0.0.4",
     "micromark-extension-wiki-link": "^0.0.4",
@@ -164,13 +166,13 @@
     "mkdirp": "^1.0.3",
     "mkdirp": "^1.0.3",
     "mongodb": "^4.17.2",
     "mongodb": "^4.17.2",
     "mongoose": "^6.13.6",
     "mongoose": "^6.13.6",
-    "mongoose-gridfs": "^1.2.42",
+    "mongoose-gridfs": "^1.3.0",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
     "mustache": "^4.2.0",
-    "next": "^14.2.30",
+    "next": "^14.2.32",
     "next-dynamic-loading-props": "^0.1.1",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
     "next-i18next": "^15.3.1",
     "next-superjson": "^0.0.4",
     "next-superjson": "^0.0.4",
@@ -244,7 +246,7 @@
     "url-join": "^4.0.0",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "usehooks-ts": "^2.6.0",
     "uuid": "^11.0.3",
     "uuid": "^11.0.3",
-    "validator": "^13.7.0",
+    "validator": "^13.15.20",
     "ws": "^8.17.1",
     "ws": "^8.17.1",
     "xss": "^1.0.15",
     "xss": "^1.0.15",
     "y-mongodb-provider": "^0.2.0",
     "y-mongodb-provider": "^0.2.0",
@@ -271,9 +273,7 @@
     "@popperjs/core": "^2.11.8",
     "@popperjs/core": "^2.11.8",
     "@swc-node/jest": "^1.8.1",
     "@swc-node/jest": "^1.8.1",
     "@swc/jest": "^0.2.36",
     "@swc/jest": "^0.2.36",
-    "@testing-library/dom": "^10.4.0",
     "@testing-library/jest-dom": "^6.5.0",
     "@testing-library/jest-dom": "^6.5.0",
-    "@testing-library/react": "^16.0.1",
     "@testing-library/user-event": "^14.5.2",
     "@testing-library/user-event": "^14.5.2",
     "@types/archiver": "^6.0.2",
     "@types/archiver": "^6.0.2",
     "@types/bunyan": "^1.8.11",
     "@types/bunyan": "^1.8.11",
@@ -288,12 +288,14 @@
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
     "@types/react-stickynode": "^4.0.3",
+    "@types/supertest": "^6.0.3",
     "@types/testing-library__dom": "^7.5.0",
     "@types/testing-library__dom": "^7.5.0",
     "@types/throttle-debounce": "^5.0.1",
     "@types/throttle-debounce": "^5.0.1",
     "@types/unist": "^3.0.3",
     "@types/unist": "^3.0.3",
     "@types/unzip-stream": "^0.3.4",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
     "@types/url-join": "^4.0.2",
     "@types/uuid": "^10.0.0",
     "@types/uuid": "^10.0.0",
+    "@types/ws": "^8.18.1",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
     "bootstrap": "=5.3.2",
     "bootstrap": "=5.3.2",
     "commander": "^14.0.0",
     "commander": "^14.0.0",
@@ -336,6 +338,9 @@
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.7.5",
     "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",
     "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 fs from 'node:fs';
 import path from 'node:path';
 import path from 'node:path';
-
 import { defineConfig, devices, type Project } from '@playwright/test';
 import { defineConfig, devices, type Project } from '@playwright/test';
 
 
 const authFile = path.resolve(__dirname, './playwright/.auth/admin.json');
 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 supportedBrowsers = ['chromium', 'firefox', 'webkit'] as const;
 
 
-const projects: Array<Project> = supportedBrowsers.map(browser => ({
+const projects: Array<Project> = supportedBrowsers.map((browser) => ({
   name: browser,
   name: browser,
   use: { ...devices[`Desktop ${browser}`], storageState },
   use: { ...devices[`Desktop ${browser}`], storageState },
   testIgnore: /(10-installer|21-basic-features-for-guest)\/.*\.spec\.ts/,
   testIgnore: /(10-installer|21-basic-features-for-guest)\/.*\.spec\.ts/,
   dependencies: ['setup', 'auth'],
   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.
  * Read environment variables from file.
@@ -48,12 +49,7 @@ export default defineConfig({
   /* Opt out of parallel tests on CI. */
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
   /* 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: {
   webServer: {
     command: 'pnpm run server',
     command: 'pnpm run server',
@@ -79,7 +75,11 @@ export default defineConfig({
   /* Configure projects for major browsers */
   /* Configure projects for major browsers */
   projects: [
   projects: [
     // Setup project
     // 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/ },
     { name: 'auth', testMatch: /auth\.setup\.ts/ },
 
 
     {
     {
@@ -113,5 +113,4 @@ export default defineConfig({
     //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
     //   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.goto('/');
   await page.waitForURL('/installer');
   await page.waitForURL('/installer');
 
 
@@ -11,18 +11,26 @@ test('Installer', async({ page }) => {
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguageMenu-ja_JP').click();
   await page.getByTestId('dropdownLanguageMenu-ja_JP').click();
   await expect(page.getByRole('textbox', { name: 'ユーザーID' })).toBeVisible();
   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
   // choose Chinese
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguageMenu-zh_CN').click();
   await page.getByTestId('dropdownLanguageMenu-zh_CN').click();
   await expect(page.getByRole('textbox', { name: '用户ID' })).toBeVisible();
   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
   // // choose English
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguage').click();
   await page.getByTestId('dropdownLanguageMenu-en_US').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' })).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();
   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 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');
   await page.goto('/Sandbox');
 
 
   // Expect a title "to contain" a substring.
   // Expect a title "to contain" a substring.
   await expect(page).toHaveTitle(/Sandbox/);
   await expect(page).toHaveTitle(/Sandbox/);
 });
 });
 
 
-test('get h1', async({ page }) => {
+test('get h1', async ({ page }) => {
   await page.goto('/Sandbox');
   await page.goto('/Sandbox');
 
 
   // Expects page to have a heading with the name of Installation.
   // 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');
   await page.goto('/Sandbox/Math');
 
 
   // Expect the Math-specific elements to be present
   // Expect the Math-specific elements to be present
   await expect(page.locator('.katex').first()).toBeVisible();
   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');
   await page.goto('/Sandbox#edit');
 
 
   // Expect the Editor-specific elements to be present
   // 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();
   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');
   await page.goto('/me');
 
 
   // Expect the UserSettgins-specific elements to be present when accessing /me (UserSettgins)
   // Expect the UserSettgins-specific elements to be present when accessing /me (UserSettgins)
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   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');
   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
   // 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();
   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 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 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.goto('/');
   await page.getByTestId('pageListButton').click();
   await page.getByTestId('pageListButton').click();
   await expect(page.getByTestId('descendants-page-list-modal')).toBeVisible();
   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 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 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();
   await expect(page.locator('.dropdown-menu.show')).toBeVisible();
 });
 });
 
 
-test('Successfully close modal', async({ page }) => {
+test('Successfully close modal', async ({ page }) => {
   await openPageAccessoriesModal(page);
   await openPageAccessoriesModal(page);
   await page.locator('.btn-close').click();
   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 openPageAccessoriesModal(page);
   await page.getByTestId('timeline-tab-button').click();
   await page.getByTestId('timeline-tab-button').click();
   await expect(page.locator('.card-timeline').first()).toBeVisible();
   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.describe('Click page icons', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/Sandbox');
     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');
     const subscribeButton = page.locator('.btn-subscribe');
 
 
     // Subscribe
     // Subscribe
@@ -17,7 +17,7 @@ test.describe('Click page icons', () => {
     await expect(subscribeButton).not.toHaveClass(/active/);
     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();
     const likeButton = page.locator('.btn-like').first();
 
 
     // Like
     // Like
@@ -29,7 +29,7 @@ test.describe('Click page icons', () => {
     await expect(likeButton).not.toHaveClass(/active/);
     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();
     const bookmarkButton = page.locator('.btn-bookmark').first();
 
 
     // Bookmark
     // Bookmark
@@ -41,10 +41,13 @@ test.describe('Click page icons', () => {
     await expect(bookmarkButton).not.toHaveClass(/active/);
     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();
     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);
     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', () => {
 test.describe('Comment', () => {
-
   // make tests run in serial
   // make tests run in serial
   test.describe.configure({ mode: 'serial' });
   test.describe.configure({ mode: 'serial' });
 
 
-  test('Create comment page', async({ page }) => {
+  test('Create comment page', async ({ page }) => {
     await page.goto('/comment');
     await page.goto('/comment');
     await page.getByTestId('editor-button').click();
     await page.getByTestId('editor-button').click();
     await page.getByTestId('save-page-btn').click();
     await page.getByTestId('save-page-btn').click();
     await expect(page.locator('.page-meta')).toBeVisible();
     await expect(page.locator('.page-meta')).toBeVisible();
   });
   });
 
 
-  test('Successfully add comments', async({ page }) => {
+  test('Successfully add comments', async ({ page }) => {
     const commentText = 'add comment';
     const commentText = 'add comment';
     await page.goto('/comment');
     await page.goto('/comment');
 
 
@@ -23,10 +22,12 @@ test.describe('Comment', () => {
     await page.getByTestId('comment-submit-button').first().click();
     await page.getByTestId('comment-submit-button').first().click();
 
 
     await expect(page.locator('.page-comment-body')).toHaveText(commentText);
     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';
     const commentText = 'reply comment';
     await page.goto('/comment');
     await page.goto('/comment');
 
 
@@ -35,8 +36,12 @@ test.describe('Comment', () => {
     await page.locator('.cm-content').fill(commentText);
     await page.locator('.cm-content').fill(commentText);
     await page.getByTestId('comment-submit-button').first().click();
     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 }) => {
   // test('Successfully delete comments', async({ page }) => {
@@ -51,5 +56,4 @@ test.describe('Comment', () => {
   // });
   // });
 
 
   // TODO: https://redmine.weseek.co.jp/issues/139520
   // 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.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.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
     // should be focused
     await expect(page.getByPlaceholder('Input page name')).toBeFocused();
     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.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('/');
     await page.goto('/');
 
 
     // open dropdown menu
     // open dropdown menu
     await page.getByTestId('grw-page-create-button').hover();
     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();
     await page.getByRole('menuitem', { name: 'Create today page' }).click();
 
 
     // should not be visible
     // 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('/');
   await page.goto('/');
 
 
   // show presentation modal
   // 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();
   await page.getByTestId('open-presentation-modal-btn').click();
 
 
   // check the content of the h1
   // 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.describe('Sticky features', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/');
     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
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 250));
     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
     // Scroll back to top
     await page.evaluate(() => window.scrollTo(0, 0));
     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
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 250));
     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
     // Move to /Sandbox page
     await page.goto('/Sandbox');
     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
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 250));
     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
     // Click editor button
     await page.getByTestId('editor-button').click();
     await page.getByTestId('editor-button').click();
     await expect(page.locator('.layout-root')).toHaveClass(/editing/);
     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
     // Scroll down to trigger sticky effect
     await page.evaluate(() => window.scrollTo(0, 500));
     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
     // Set viewport to small size
     await page.setViewportSize({ width: 600, height: 1024 });
     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 nav = page.getByTestId('grw-contextual-sub-nav');
   const button = nav.getByTestId('open-page-item-control-btn');
   const button = nav.getByTestId('open-page-item-control-btn');
 
 
@@ -19,7 +19,7 @@ const openPageItemControl = async(page: Page): Promise<void> => {
   await button.click();
   await button.click();
 };
 };
 
 
-test('PageDeleteModal is shown successfully', async({ page }) => {
+test('PageDeleteModal is shown successfully', async ({ page }) => {
   await page.goto('/Sandbox');
   await page.goto('/Sandbox');
 
 
   await openPageItemControl(page);
   await openPageItemControl(page);
@@ -28,7 +28,7 @@ test('PageDeleteModal is shown successfully', async({ page }) => {
   await expect(page.getByTestId('page-delete-modal')).toBeVisible();
   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 page.goto('/Sandbox');
 
 
   await openPageItemControl(page);
   await openPageItemControl(page);
@@ -37,7 +37,7 @@ test('PageDuplicateModal is shown successfully', async({ page }) => {
   await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
   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 page.goto('/Sandbox');
 
 
   await openPageItemControl(page);
   await openPageItemControl(page);
@@ -57,35 +57,45 @@ test('PageMoveRenameModal is shown successfully', async({ page }) => {
 // });
 // });
 
 
 test.describe('Page Accessories Modal', () => {
 test.describe('Page Accessories Modal', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/');
     await page.goto('/');
     await openPageItemControl(page);
     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();
     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();
     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';
   const tag = 'we';
   await page.goto('/Sandbox/Bootstrap5');
   await page.goto('/Sandbox/Bootstrap5');
 
 
   await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
   await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
   await expect(page.locator('#edit-tag-modal')).toBeVisible();
   await expect(page.locator('#edit-tag-modal')).toBeVisible();
   await page.locator('.rbt-input-main').fill(tag);
   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.locator('#tag-typeahead-asynctypeahead-item-0').click();
   await page.getByTestId('tag-edit-done-btn').click();
   await page.getByTestId('tag-edit-done-btn').click();
   await expect(page.getByTestId('grw-tag-labels')).toContainText(tag);
   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';
 import { collapseSidebar } from '../utils';
 
 
-test('/Sandbox is successfully loaded', async({ page }) => {
-
+test('/Sandbox is successfully loaded', async ({ page }) => {
   await page.goto('/Sandbox');
   await page.goto('/Sandbox');
 
 
   // Expect a title "to contain" a substring.
   // Expect a title "to contain" a substring.
   await expect(page).toHaveTitle(/Sandbox/);
   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');
   await page.goto('/Sandbox/Math');
 
 
   // Check if the math elements are visible
   // Check if the math elements are visible
   await expect(page.locator('.katex').first()).toBeVisible();
   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');
   await page.goto('/me');
 
 
   // Expect to be redirected to /login when accessing /me
   // Expect to be redirected to /login when accessing /me
   await expect(page.getByTestId('login-form')).toBeVisible();
   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');
   await page.goto('/trash');
 
 
   // Expect the trash page specific elements to be present when accessing /trash
   // Expect the trash page specific elements to be present when accessing /trash
   await expect(page.getByTestId('trash-page-list')).toBeVisible();
   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 page.goto('/');
 
 
   await collapseSidebar(page, false);
   await collapseSidebar(page, false);
   await page.getByTestId('grw-sidebar-nav-primary-tags').click();
   await page.getByTestId('grw-sidebar-nav-primary-tags').click();
   await expect(page.getByTestId('grw-sidebar-content-tags')).toBeVisible();
   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()).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 page.getByTestId('check-all-tags-button').click();
   await expect(page.getByTestId('tags-page')).toBeVisible();
   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');
   await page.goto('/Sandbox');
 
 
   // Sticky
   // Sticky
   await page.evaluate(() => window.scrollTo(0, 250));
   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
   // Not sticky
   await page.evaluate(() => window.scrollTo(0, 0));
   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';
 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 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 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 newPagePath = '/child';
   const openPageCreateModalShortcutKey = 'c';
   const openPageCreateModalShortcutKey = 'c';
 
 
@@ -16,7 +16,10 @@ test('Successfully create page under specific path', async({ page }) => {
 
 
   await page.keyboard.press(openPageCreateModalShortcutKey);
   await page.keyboard.press(openPageCreateModalShortcutKey);
   await expect(page.getByTestId('page-create-modal')).toBeVisible();
   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();
   page.getByTestId('btn-create-page-under-below').click();
   await page.getByTestId('view-button').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);
   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 body1 = 'hello';
   const body2 = ' world!';
   const body2 = ' world!';
   const savePageShortcutKey = 'Control+s';
   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 jaText = '今日の目標';
   const enText = "TODAY'S GOALS";
   const enText = "TODAY'S GOALS";
   await page.goto('/Sandbox/TemplateModal');
   await page.goto('/Sandbox/TemplateModal');
@@ -16,10 +16,14 @@ test('Successfully select template and template locale', async({ page }) => {
 
 
   // select template and template locale
   // select template and template locale
   await templateModal.locator('.list-group-item').nth(0).click();
   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-toggle').click();
   await templateModal.getByTestId('select-locale-dropdown-item').nth(1).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
   // insert
   await templateModal.locator('.btn-primary').click();
   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 { readFileSync } from 'fs';
 import path from 'path';
 import path from 'path';
 
 
-import { test, expect, type Page } from '@playwright/test';
-
 /**
 /**
  * for the issues:
  * for the issues:
  * @see https://redmine.weseek.co.jp/issues/122040
  * @see https://redmine.weseek.co.jp/issues/122040
  * @see https://redmine.weseek.co.jp/issues/124281
  * @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');
   await page.goto('/Sandbox/for-122040');
 
 
   // Open Editor
   // 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 filePath = path.resolve(__dirname, '../23-editor/assets/example.txt');
   const buffer = readFileSync(filePath).toString('base64');
   const buffer = readFileSync(filePath).toString('base64');
   const dataTransfer = await page.evaluateHandle(
   const dataTransfer = await page.evaluateHandle(
-    async({ bufferData, localFileName, localFileType }) => {
+    async ({ bufferData, localFileName, localFileType }) => {
       const dt = new DataTransfer();
       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, {
       const file = new File([blobData], localFileName, {
         type: localFileType,
         type: localFileType,
@@ -43,33 +44,41 @@ test('should not be cleared and should prevent GrantSelector from modified', asy
       localFileType: 'application/octet-stream',
       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
   // Save page
   await page.getByTestId('save-page-btn').click();
   await page.getByTestId('save-page-btn').click();
 
 
   // Expect grant not to be reset after uploading an attachment
   // 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 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:
  * for the issue:
  * @see https://redmine.weseek.co.jp/issues/115285
  * @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 page1Path = '/Sandbox/for-115285/page1';
   const page2Path = '/Sandbox/for-115285/page2';
   const page2Path = '/Sandbox/for-115285/page2';
 
 
   const page1Body = 'Hello';
   const page1Body = 'Hello';
   const page2Body = 'World';
   const page2Body = 'World';
 
 
-
   await page.goto(page1Path);
   await page.goto(page1Path);
 
 
   // Open Editor (page1)
   // Open Editor (page1)
@@ -85,7 +94,10 @@ test('Successfully updating the page body', async({ page }) => {
   await expect(page.locator('.main')).toContainText(page1Body);
   await expect(page.locator('.main')).toContainText(page1Body);
 
 
   // Duplicate page1
   // 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 page.getByTestId('open-page-duplicate-modal-btn').click();
   await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
   await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
   await page.locator('.form-control').fill(page2Path);
   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();
   await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
 
 
   // Expect to see the text from which you are duplicating
   // 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
   // Append text
   await appendTextToEditorUntilContains(page, page1Body + page2Body);
   await appendTextToEditorUntilContains(page, page1Body + page2Body);
 
 
-
   await page.goto(page1Path);
   await page.goto(page1Path);
 
 
   // Open Editor (page1)
   // Open Editor (page1)
   await page.getByTestId('editor-button').click();
   await page.getByTestId('editor-button').click();
   await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
   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
   // Navigate to the search page with query parameters
   await page.goto('/_search?q=alerts');
   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();
   await expect(page.locator('.wiki')).toBeVisible();
 });
 });
 
 
-test('checkboxes behaviors', async({ page }) => {
+test('checkboxes behaviors', async ({ page }) => {
   // Navigate to the search page with query parameters
   // Navigate to the search page with query parameters
   await page.goto('/_search?q=alerts');
   await page.goto('/_search?q=alerts');
 
 
@@ -28,7 +28,10 @@ test('checkboxes behaviors', async({ page }) => {
   await page.getByTestId('cb-select').first().click({ force: true });
   await page.getByTestId('cb-select').first().click({ force: true });
 
 
   // Click the select all checkbox
   // 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 });
   await page.getByTestId('cb-select-all').click({ force: true });
 
 
   // Unclick the first checkbox after selecting all
   // 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 });
   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');
   await page.goto('/_private-legacy-pages');
 
 
   // Confirm search result elements are visible
   // 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.goto('/');
   await page.getByTestId('open-search-modal-button').click();
   await page.getByTestId('open-search-modal-button').click();
   await expect(page.getByTestId('search-modal')).toBeVisible();
   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();
   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.describe('Sort with dropdown', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/_search?q=sand');
     await page.goto('/_search?q=sand');
 
 
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     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();
     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');
     const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
 
 
     await expect(dropdownMenu).toBeVisible();
     await expect(dropdownMenu).toBeVisible();
     await dropdownMenu.locator('.dropdown-item').nth(0).click();
     await dropdownMenu.locator('.dropdown-item').nth(0).click();
 
 
-
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).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');
     const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
 
 
     await expect(dropdownMenu).toBeVisible();
     await expect(dropdownMenu).toBeVisible();
     await dropdownMenu.locator('.dropdown-item').nth(1).click();
     await dropdownMenu.locator('.dropdown-item').nth(1).click();
 
 
-
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).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');
     const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
 
 
     await expect(dropdownMenu).toBeVisible();
     await expect(dropdownMenu).toBeVisible();
     await dropdownMenu.locator('.dropdown-item').nth(2).click();
     await dropdownMenu.locator('.dropdown-item').nth(2).click();
 
 
-
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).toBeVisible();
@@ -155,22 +160,26 @@ test.describe('Sort with dropdown', () => {
 });
 });
 
 
 test.describe('Search and use', () => {
 test.describe('Search and use', () => {
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/_search?q=alerts');
     await page.goto('/_search?q=alerts');
 
 
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-base')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-list')).toBeVisible();
     await expect(page.getByTestId('search-result-content')).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();
     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();
     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');
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
 
     await expect(dropdonwMenu).toBeVisible();
     await expect(dropdonwMenu).toBeVisible();
@@ -178,10 +187,15 @@ test.describe('Search and use', () => {
     // Add bookmark
     // Add bookmark
     await dropdonwMenu.getByTestId('add-bookmark-btn').click();
     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');
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
 
     await expect(dropdonwMenu).toBeVisible();
     await expect(dropdonwMenu).toBeVisible();
@@ -191,7 +205,7 @@ test.describe('Search and use', () => {
     await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
     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');
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
 
     await expect(dropdonwMenu).toBeVisible();
     await expect(dropdonwMenu).toBeVisible();
@@ -201,7 +215,7 @@ test.describe('Search and use', () => {
     await expect(page.getByTestId('page-rename-modal')).toBeVisible();
     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');
     const dropdonwMenu = page.locator('.dropdown-menu.show');
 
 
     await expect(dropdonwMenu).toBeVisible();
     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('/');
   await page.goto('/');
   const searchText = 'GROWI';
   const searchText = 'GROWI';
 
 

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

@@ -1,13 +1,15 @@
-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 page.goto('/admin');
 
 
   await expect(page.getByTestId('admin-home')).toBeVisible();
   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 page.goto('/admin/app');
 
 
   await expect(page.getByTestId('admin-app-settings')).toBeVisible();
   await expect(page.getByTestId('admin-app-settings')).toBeVisible();
@@ -15,46 +17,50 @@ test('admin/app is successfully loaded', async({ page }) => {
   await expect(page.locator('#cbFileUpload')).toBeChecked();
   await expect(page.locator('#cbFileUpload')).toBeChecked();
 });
 });
 
 
-test('admin/security is successfully loaded', async({ page }) => {
+test('admin/security is successfully loaded', async ({ page }) => {
   await page.goto('/admin/security');
   await page.goto('/admin/security');
 
 
   await expect(page.getByTestId('admin-security')).toBeVisible();
   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 page.goto('/admin/markdown');
 
 
   await expect(page.getByTestId('admin-markdown')).toBeVisible();
   await expect(page.getByTestId('admin-markdown')).toBeVisible();
   await expect(page.locator('#isEnabledLinebreaksInComments')).toBeChecked();
   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 page.goto('/admin/customize');
 
 
   await expect(page.getByTestId('admin-customize')).toBeVisible();
   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 page.goto('/admin/importer');
 
 
   await expect(page.getByTestId('admin-import-data')).toBeVisible();
   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 page.goto('/admin/export');
 
 
   await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
   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 page.goto('/admin/data-transfer');
 
 
   await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
   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 page.goto('/admin/notification');
 
 
   await expect(page.getByTestId('admin-notification')).toBeVisible();
   await expect(page.getByTestId('admin-notification')).toBeVisible();
@@ -62,7 +68,7 @@ test('admin/notification is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('slack-integration-list-item')).toBeVisible();
   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 page.goto('/admin/slack-integration');
 
 
   await expect(page.getByTestId('admin-slack-integration')).toBeVisible();
   await expect(page.getByTestId('admin-slack-integration')).toBeVisible();
@@ -70,27 +76,31 @@ test('admin/slack-integration is successfully loaded', async({ page }) => {
   await expect(page.locator('img.bot-difficulty-icon').first()).toBeVisible();
   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 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 page.goto('/admin/users');
 
 
   await expect(page.getByTestId('admin-users')).toBeVisible();
   await expect(page.getByTestId('admin-users')).toBeVisible();
   await expect(page.getByTestId('user-table-tr').first()).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 page.goto('/admin/user-groups');
 
 
   await expect(page.getByTestId('admin-user-groups')).toBeVisible();
   await expect(page.getByTestId('admin-user-groups')).toBeVisible();
   await expect(page.getByTestId('grw-user-group-table').first()).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 page.goto('/admin/search');
 
 
   await expect(page.getByTestId('admin-full-text-search')).toBeVisible();
   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';
 import { collapseSidebar } from '../utils';
 
 
-
 test.describe('Access to sidebar', () => {
 test.describe('Access to sidebar', () => {
-
-  test.beforeEach(async({ page }) => {
+  test.beforeEach(async ({ page }) => {
     await page.goto('/');
     await page.goto('/');
     await collapseSidebar(page, false);
     await collapseSidebar(page, false);
   });
   });
 
 
-  test('Successfully show sidebar', async({ page }) => {
+  test('Successfully show sidebar', async ({ page }) => {
     await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
     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 page.getByTestId('grw-sidebar-nav-primary-page-tree').click();
     await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
     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 page.getByTestId('grw-sidebar-nav-primary-recent-changes').click();
     await expect(page.getByTestId('grw-recent-changes')).toBeVisible();
     await expect(page.getByTestId('grw-recent-changes')).toBeVisible();
     await expect(page.locator('.list-group-item').first()).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 page.getByTestId('grw-sidebar-nav-primary-custom-sidebar').click();
     await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
     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');
     const docsUrl = await linkElement.getAttribute('href');
     if (docsUrl == null) {
     if (docsUrl == null) {
       throw new Error('url is null');
       throw new Error('url is null');
@@ -43,12 +47,13 @@ test.describe('Access to sidebar', () => {
     expect(body).toContain('</html>');
     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();
     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
   // 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();
   //     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';
 import { collapseSidebar } from '../utils';
 
 
-
-test('Switch sidebar mode', async({ page }) => {
+test('Switch sidebar mode', async ({ page }) => {
   await page.goto('/');
   await page.goto('/');
   await collapseSidebar(page, false);
   await collapseSidebar(page, false);
   await collapseSidebar(page, true);
   await collapseSidebar(page, true);

+ 47 - 15
apps/app/playwright/60-home/home.spec.ts

@@ -1,31 +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');
   await page.goto('dummy');
 
 
   // Open PersonalDropdown
   // Open PersonalDropdown
   await page.getByTestId('personal-dropdown-button').click();
   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
   // Click UserHomeMenu
   await page.getByTestId('grw-personal-dropdown-menu-user-home').click();
   await page.getByTestId('grw-personal-dropdown-menu-user-home').click();
   await expect(page.getByTestId('grw-users-info')).toBeVisible();
   await expect(page.getByTestId('grw-users-info')).toBeVisible();
 });
 });
 
 
-test('Vist User settings', async({ page }) => {
+test('Vist User settings', async ({ page }) => {
   await page.goto('dummy');
   await page.goto('dummy');
 
 
   // Open PersonalDropdown
   // Open PersonalDropdown
   await page.getByTestId('personal-dropdown-button').click();
   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
   // Click UserSettingsMenu
   page.getByTestId('grw-personal-dropdown-menu-user-settings').click();
   page.getByTestId('grw-personal-dropdown-menu-user-settings').click();
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
 });
 
 
-test('Access User information', async({ page }) => {
+test('Access User information', async ({ page }) => {
   await page.goto('/me');
   await page.goto('/me');
 
 
   // Click BasicInfoSettingUpdateButton
   // Click BasicInfoSettingUpdateButton
@@ -36,7 +39,7 @@ test('Access User information', async({ page }) => {
   await expect(page.locator('.Toastify__toast')).toBeVisible();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });
 });
 
 
-test('Access External account', async({ page }) => {
+test('Access External account', async ({ page }) => {
   await page.goto('/me');
   await page.goto('/me');
 
 
   // Click ExternalAccountsTabButton
   // Click ExternalAccountsTabButton
@@ -52,7 +55,7 @@ test('Access External account', async({ page }) => {
   await expect(page.locator('.Toastify__toast')).not.toBeVisible();
   await expect(page.locator('.Toastify__toast')).not.toBeVisible();
 });
 });
 
 
-test('Access Password setting', async({ page }) => {
+test('Access Password setting', async ({ page }) => {
   await page.goto('/me');
   await page.goto('/me');
 
 
   // Click PasswordSettingTabButton
   // Click PasswordSettingTabButton
@@ -72,8 +75,7 @@ test('Access Password setting', async({ page }) => {
   await expect(page.getByTestId('.Toastify__toast')).not.toBeVisible();
   await expect(page.getByTestId('.Toastify__toast')).not.toBeVisible();
 });
 });
 
 
-
-test('Access API setting', async({ page }) => {
+test('Access API setting', async ({ page }) => {
   await page.goto('/me');
   await page.goto('/me');
 
 
   // Click ApiSettingTabButton
   // Click ApiSettingTabButton
@@ -85,19 +87,49 @@ test('Access API setting', async({ page }) => {
   await expect(page.locator('.Toastify__toast')).toBeVisible();
   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');
   await page.goto('/me');
 
 
   // Click InAppNotificationSettingTabButton
   // Click InAppNotificationSettingTabButton
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   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
   // 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();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });
 });
 
 
-test('Acccess Other setting', async({ page }) => {
+test('Acccess Other setting', async ({ page }) => {
   await page.goto('/me');
   await page.goto('/me');
 
 
   // Click OtherSettingTabButton
   // Click OtherSettingTabButton

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

@@ -4,6 +4,6 @@ import { login } from './utils/Login';
 
 
 // Commonised login process for use elsewhere
 // Commonised login process for use elsewhere
 // see: https://github.com/microsoft/playwright/issues/22114
 // 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);
   await login(page);
 });
 });

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

@@ -1,7 +1,12 @@
 import { expect, type Page } from '@playwright/test';
 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) {
   if (isSidebarContentsHidden === isCollapsed) {
     return;
     return;
   }
   }
@@ -12,8 +17,7 @@ export const collapseSidebar = async(page: Page, isCollapsed: boolean): Promise<
 
 
   if (isCollapsed) {
   if (isCollapsed) {
     await expect(page.locator('.grw-sidebar-dock')).not.toBeVisible();
     await expect(page.locator('.grw-sidebar-dock')).not.toBeVisible();
-  }
-  else {
+  } else {
     await expect(page.locator('.grw-sidebar-dock')).toBeVisible();
     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 path from 'node:path';
-
 import { expect, type Page } from '@playwright/test';
 import { expect, type Page } from '@playwright/test';
 
 
 const authFile = path.resolve(__dirname, '../.auth/admin.json');
 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.
   // Perform authentication steps. Replace these actions with your own.
   await page.goto('/admin');
   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"
+    }
+  ]
+}

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

@@ -313,7 +313,7 @@
       "done": "Copied to clipboard!"
       "done": "Copied to clipboard!"
     },
     },
     "bug_report": "Submitting a bug report",
     "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": {
   "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.",
     "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.",

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

@@ -123,5 +123,97 @@
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",
     "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.",
     "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>"
     "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."
+      }
+    }
   }
   }
 }
 }

+ 48 - 4
apps/app/public/static/locales/en_US/translation.json

@@ -221,6 +221,9 @@
       "profile_image2": "Set up AWS or enable local uploads."
       "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": {
   "page_me_apitoken": {
     "api_token": "API Token",
     "api_token": "API Token",
     "notice": {
     "notice": {
@@ -230,6 +233,35 @@
     },
     },
     "form_help": {}
     "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": "Password",
   "Password Settings": "Password settings",
   "Password Settings": "Password settings",
   "personal_settings": {
   "personal_settings": {
@@ -260,9 +292,6 @@
   },
   },
   "API Settings": "API settings",
   "API Settings": "API settings",
   "Other Settings": "Other 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": "In-App Notification Settings",
     "in_app_notification_settings": "In-App Notification Settings",
     "subscribe_settings": "Settings to automatically subscribe (Receive notifications) to pages",
     "subscribe_settings": "Settings to automatically subscribe (Receive notifications) to pages",
@@ -571,6 +600,17 @@
       "create_failed": "Failed to create assistant",
       "create_failed": "Failed to create assistant",
       "update_failed": "Failed to update 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.",
     "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",
     "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",
     "add_page_button": "Add page",
@@ -635,6 +675,10 @@
       "thread_deleted_failed": "Failed to delete thread",
       "thread_deleted_failed": "Failed to delete thread",
       "ai_assistant_set_default_success": "Default assistant set successfully",
       "ai_assistant_set_default_success": "Default assistant set successfully",
       "ai_assistant_set_default_failed": "Failed to set default assistant"
       "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": {
   "link_edit": {
@@ -853,7 +897,7 @@
     "Password field is required": "Password field is required.",
     "Password field is required": "Password field is required.",
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
     "user_not_found": "User not found.",
     "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": {
   "grid_edit": {
     "create_bootstrap_4_grid": "Create Bootstrap 4 Grid",
     "create_bootstrap_4_grid": "Create Bootstrap 4 Grid",

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

@@ -313,7 +313,7 @@
       "done": "Copié dans le presse-papier!"
       "done": "Copié dans le presse-papier!"
     },
     },
     "bug_report": "Informations de diagnostic",
     "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": {
   "v5_page_migration": {
     "migration_desc": "Des pages sont encore en V4. Pour profiter des nouvelles fonctionnalitées, convertir toutes les pages vers la V5.",
     "migration_desc": "Des pages sont encore en V4. Pour profiter des nouvelles fonctionnalitées, convertir toutes les pages vers la V5.",

Some files were not shown because too many files changed in this diff