Browse Source

Merge branch 'master' of https://github.com/growilabs/growi into support/156162-170979-growi-plugin-feature-biome

Futa Arai 6 months ago
parent
commit
f87de30d65
100 changed files with 1546 additions and 478 deletions
  1. 1 1
      .changeset/config.json
  2. 2 1
      .devcontainer/app/devcontainer.json
  3. 1 0
      .devcontainer/app/postCreateCommand.sh
  4. 2 3
      .devcontainer/compose.extend.template.yml
  5. 2 6
      .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. 1 1
      .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. 2 2
      .github/workflows/reusable-app-prod.yml
  24. 1 1
      .github/workflows/reusable-app-reg-suit.yml
  25. 1 1
      .mcp.json
  26. 0 14
      .roo/mcp.json
  27. 1 0
      .serena/.gitignore
  28. 186 0
      .serena/memories/apps-app-pagetree-performance-refactor-plan.md
  29. 1 1
      .serena/memories/project_overview.md
  30. 13 7
      .serena/memories/suggested_commands.md
  31. 2 1
      .vscode/settings.json
  32. 193 108
      CHANGELOG.md
  33. 1 1
      LICENSE
  34. 14 14
      README.md
  35. 14 14
      README_JP.md
  36. 1 1
      THIRD-PARTY-NOTICES.md
  37. 1 0
      apps/app/.eslintrc.js
  38. 1 1
      apps/app/bin/openapi/generate-operation-ids/cli.spec.ts
  39. 1 1
      apps/app/bin/openapi/generate-operation-ids/cli.ts
  40. 3 3
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts
  41. 415 0
      apps/app/bin/print-memory-consumption.ts
  42. 0 14
      apps/app/config/cdn.js
  43. 1 1
      apps/app/config/migrate-mongo-config.js
  44. 1 0
      apps/app/config/next-i18next.config.js
  45. 2 2
      apps/app/docker/Dockerfile
  46. 10 10
      apps/app/docker/README.md
  47. 44 44
      apps/app/docker/codebuild/.terraform.lock.hcl
  48. 1 1
      apps/app/docker/codebuild/buildspec.yml
  49. 1 1
      apps/app/docker/codebuild/codebuild.tf
  50. 1 1
      apps/app/docker/codebuild/main.tf
  51. 1 1
      apps/app/docker/codebuild/oidc.tf
  52. 2 2
      apps/app/next.config.js
  53. 4 5
      apps/app/package.json
  54. 2 2
      apps/app/public/static/locales/en_US/admin.json
  55. 2 2
      apps/app/public/static/locales/en_US/translation.json
  56. 2 2
      apps/app/public/static/locales/fr_FR/admin.json
  57. 2 2
      apps/app/public/static/locales/fr_FR/translation.json
  58. 2 2
      apps/app/public/static/locales/ja_JP/admin.json
  59. 4 4
      apps/app/public/static/locales/ja_JP/translation.json
  60. 2 2
      apps/app/public/static/locales/ko_KR/admin.json
  61. 2 2
      apps/app/public/static/locales/ko_KR/translation.json
  62. 2 2
      apps/app/public/static/locales/zh_CN/admin.json
  63. 2 2
      apps/app/public/static/locales/zh_CN/translation.json
  64. 18 8
      apps/app/src/client/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  65. 22 31
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  66. 287 0
      apps/app/src/client/components/LoginForm/LoginForm.spec.tsx
  67. 25 20
      apps/app/src/client/components/LoginForm/LoginForm.tsx
  68. 0 2
      apps/app/src/client/components/NotAvailableForReadOnlyUser.spec.tsx
  69. 2 2
      apps/app/src/client/components/PageComment/CommentEditor.tsx
  70. 2 2
      apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx
  71. 1 1
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  72. 0 2
      apps/app/src/client/components/PageHeader/PageTitleHeader.spec.tsx
  73. 2 2
      apps/app/src/client/components/PageSideContents/PageAccessoriesControl.tsx
  74. 3 3
      apps/app/src/client/components/PageSideContents/PageSideContents.tsx
  75. 9 9
      apps/app/src/client/components/SearchPage/SearchPageBase.tsx
  76. 2 2
      apps/app/src/client/components/TableOfContents.tsx
  77. 5 2
      apps/app/src/client/components/TreeItem/TreeItemLayout.tsx
  78. 1 1
      apps/app/src/client/services/AdminHomeContainer.js
  79. 2 2
      apps/app/src/client/services/page-operation.ts
  80. 1 1
      apps/app/src/client/util/apiv1-client.ts
  81. 1 1
      apps/app/src/client/util/apiv3-client.ts
  82. 3 2
      apps/app/src/components/PageView/PageContentFooter.module.scss
  83. 1 1
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  84. 1 1
      apps/app/src/features/comment/server/events/event-emitter.ts
  85. 1 1
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx
  86. 1 0
      apps/app/src/features/external-user-group/server/models/external-user-group-relation.ts
  87. 1 0
      apps/app/src/features/external-user-group/server/models/external-user-group.ts
  88. 1 1
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  89. 1 1
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx
  90. 1 1
      apps/app/src/features/growi-plugin/server/consts/index.ts
  91. 15 15
      apps/app/src/features/growi-plugin/server/models/growi-plugin.integ.ts
  92. 47 9
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx
  93. 1 1
      apps/app/src/features/opentelemetry/server/anonymization/handlers/page-access-handler.spec.ts
  94. 6 3
      apps/app/src/features/opentelemetry/server/anonymization/handlers/page-access-handler.ts
  95. 1 1
      apps/app/src/features/opentelemetry/server/anonymization/handlers/page-api-handler.spec.ts
  96. 5 2
      apps/app/src/features/opentelemetry/server/anonymization/handlers/page-api-handler.ts
  97. 1 1
      apps/app/src/features/opentelemetry/server/anonymization/handlers/page-listing-api-handler.spec.ts
  98. 5 2
      apps/app/src/features/opentelemetry/server/anonymization/handlers/page-listing-api-handler.ts
  99. 1 1
      apps/app/src/features/opentelemetry/server/anonymization/handlers/search-api-handler.spec.ts
  100. 1 1
      apps/app/src/features/opentelemetry/server/anonymization/handlers/search-api-handler.ts

+ 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": [],

+ 2 - 1
.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"
     }
     }
   },
   },
 
 
@@ -35,6 +35,7 @@
         "vitest.explorer",
         "vitest.explorer",
         "ms-playwright.playwright",
         "ms-playwright.playwright",
         // git/github
         // git/github
+        "codeinklingon.git-worktree-menu",
         "github.vscode-pull-request-github",
         "github.vscode-pull-request-github",
         "mhutchie.git-graph",
         "mhutchie.git-graph",
         "eamodio.gitlens",
         "eamodio.gitlens",

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

@@ -17,6 +17,7 @@ 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

+ 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

+ 2 - 6
.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
@@ -23,7 +21,7 @@ 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/v9
       context: ../../growi-docker-compose/elasticsearch/v9
@@ -47,8 +45,6 @@ services:
 
 
 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()
 
 

+ 1 - 1
.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

+ 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

+ 2 - 2
.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

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

+ 1 - 1
.mcp.json

@@ -14,7 +14,7 @@
         "--context",
         "--context",
         "ide-assistant",
         "ide-assistant",
         "--project",
         "--project",
-        "/workspace/growi"
+        "."
       ],
       ],
       "env": {}
       "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

+ 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をハイブリッド活用

+ 1 - 1
.serena/memories/project_overview.md

@@ -8,7 +8,7 @@ GROWIは、マークダウンを使用したチームコラボレーションソ
 - **バージョン**: 7.3.0-RC.0
 - **バージョン**: 7.3.0-RC.0
 - **ライセンス**: MIT
 - **ライセンス**: MIT
 - **作者**: Yuki Takei <yuki@weseek.co.jp>
 - **作者**: Yuki Takei <yuki@weseek.co.jp>
-- **リポジトリ**: https://github.com/weseek/growi.git
+- **リポジトリ**: https://github.com/growilabs/growi.git
 - **公式サイト**: https://growi.org
 - **公式サイト**: https://growi.org
 
 
 ## 主な特徴
 ## 主な特徴

+ 13 - 7
.serena/memories/suggested_commands.md

@@ -11,7 +11,7 @@ pnpm install
 ## 開発サーバー
 ## 開発サーバー
 ```bash
 ```bash
 # メインアプリケーション開発モード
 # メインアプリケーション開発モード
-cd apps/app && pnpm run dev
+cd /workspace/growi/apps/app && pnpm run dev
 
 
 # ルートから起動(本番用ビルド後)
 # ルートから起動(本番用ビルド後)
 pnpm start
 pnpm start
@@ -31,20 +31,26 @@ turbo run build
 
 
 ## Lint・フォーマット
 ## Lint・フォーマット
 ```bash
 ```bash
+# 全てのLint実行
+pnpm run lint
+```
+
+## apps/app の Lint・フォーマット
+```bash
 # 【推奨】Biome実行(lint + format)
 # 【推奨】Biome実行(lint + format)
-pnpm run lint:biome
+cd /workspace/growi/apps/app pnpm run lint:biome
 
 
 # 【過渡期】ESLint実行(廃止予定)
 # 【過渡期】ESLint実行(廃止予定)
-pnpm run lint:eslint
+cd /workspace/growi/apps/app pnpm run lint:eslint
 
 
 # Stylelint実行
 # Stylelint実行
-pnpm run lint:styles
+cd /workspace/growi/apps/app pnpm run lint:styles
 
 
-# 全てのLint実行(過渡期対応)
-pnpm run lint
+# 全てのLint実行
+cd /workspace/growi/apps/app pnpm run lint
 
 
 # TypeScript型チェック
 # TypeScript型チェック
-pnpm run lint:typecheck
+cd /workspace/growi/apps/app pnpm run lint:typecheck
 ```
 ```
 
 
 ## テスト
 ## テスト

+ 2 - 1
.vscode/settings.json

@@ -96,6 +96,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
+ 193 - 108
CHANGELOG.md


+ 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

+ 14 - 14
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,9 +81,9 @@ 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 v6.x or v8.x
 - MongoDB v6.x or v8.x
 
 
@@ -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

+ 14 - 14
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,9 +81,9 @@ 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 v6.x or v8.x
 - 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

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

@@ -25,6 +25,7 @@ module.exports = {
     'test/integration/middlewares/**',
     'test/integration/middlewares/**',
     'test/integration/migrations/**',
     'test/integration/migrations/**',
     'test/integration/models/**',
     'test/integration/models/**',
+    'test/integration/service/**',
     'test/integration/setup.js',
     'test/integration/setup.js',
     'bin/**',
     'bin/**',
     'config/**',
     'config/**',

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

@@ -1,4 +1,4 @@
-import { writeFileSync } from 'fs';
+import { writeFileSync } from 'node:fs';
 
 
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 

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

@@ -1,5 +1,5 @@
+import { writeFileSync } from 'node:fs';
 import { Command } from 'commander';
 import { Command } from 'commander';
-import { writeFileSync } from 'fs';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';
 
 

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

@@ -1,7 +1,7 @@
-import fs from 'fs/promises';
+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 { tmpdir } from 'os';
-import path from 'path';
 import { describe, expect, it } from 'vitest';
 import { describe, expect, it } from 'vitest';
 
 
 import { generateOperationIds } from './generate-operation-ids';
 import { generateOperationIds } from './generate-operation-ids';

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

@@ -0,0 +1,415 @@
+#!/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 - 14
apps/app/config/cdn.js

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

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

@@ -6,7 +6,7 @@
  */
  */
 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
   ? // eslint-disable-next-line import/extensions, import/no-unresolved

+ 1 - 0
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');

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

+ 2 - 2
apps/app/next.config.js

@@ -5,7 +5,7 @@
  * 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 {
 const {
@@ -93,7 +93,7 @@ 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} */

+ 4 - 5
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.3.0-RC.0",
+  "version": "7.3.3-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "scripts": {
@@ -104,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",
@@ -166,7 +166,7 @@
     "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",
@@ -273,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",
@@ -296,6 +294,7 @@
     "@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",

+ 2 - 2
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.",
@@ -1139,4 +1139,4 @@
     "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, configure the <code>AI_ENABLED</code> environment variable along with the required additional variables.<br><br>For details, please refer to the <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, configure the <code>AI_ENABLED</code> environment variable along with the required additional variables.<br><br>For details, please refer to the <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "ai_search_management": "AI search management"
     "ai_search_management": "AI search management"
   }
   }
-}
+}

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

@@ -897,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",
@@ -1062,4 +1062,4 @@
     "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
     "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
     "error-toaster": "Synchronization of the latest text failed"
     "error-toaster": "Synchronization of the latest text failed"
   }
   }
-}
+}

+ 2 - 2
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.",
@@ -1138,4 +1138,4 @@
     "disable_mode_explanation": "Actuellement, l'intégration AI est désactivée. Pour l'activer, configurez la variable d'environnement <code>AI_ENABLED</code> ainsi que les autres variables nécessaires.<br><br>Pour plus de détails, veuillez consulter la <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "disable_mode_explanation": "Actuellement, l'intégration AI est désactivée. Pour l'activer, configurez la variable d'environnement <code>AI_ENABLED</code> ainsi que les autres variables nécessaires.<br><br>Pour plus de détails, veuillez consulter la <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "ai_search_management": "Gestion de la recherche par l'IA"
     "ai_search_management": "Gestion de la recherche par l'IA"
   }
   }
-}
+}

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

@@ -891,7 +891,7 @@
     "Password field is required": "Mot de passe requis.",
     "Password field is required": "Mot de passe requis.",
     "Username or E-mail has invalid characters": "Le nom d'utilisateur ou l'adresse courriel contient des caractères invalides",
     "Username or E-mail has invalid characters": "Le nom d'utilisateur ou l'adresse courriel contient des caractères invalides",
     "user_not_found": "Utilisateur introuvable.",
     "user_not_found": "Utilisateur introuvable.",
-    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException</strong></p><p class='mb-0'> L'authentification est réussie pour {{ failedProviderForDuplicatedUsernameException }} , mais la création d'un utilisateur a échouée. Voir <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException</strong></p><p class='mb-0'> L'authentification est réussie pour {{ failedProviderForDuplicatedUsernameException }} , mais la création d'un utilisateur a échouée. Voir <a href='https://github.com/growilabs/growi/issues/193'>#193</a>.</p>"
   },
   },
   "grid_edit": {
   "grid_edit": {
     "create_bootstrap_4_grid": "Créer grille Bootstrap 4",
     "create_bootstrap_4_grid": "Créer grille Bootstrap 4",
@@ -1053,4 +1053,4 @@
     "skipped-toaster": "L'éditeur n'est pas actif. Synchronisation annulée.",
     "skipped-toaster": "L'éditeur n'est pas actif. Synchronisation annulée.",
     "error-toaster": "Synchronisation échouée"
     "error-toaster": "Synchronisation échouée"
   }
   }
-}
+}

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

@@ -322,7 +322,7 @@
       "done": "クリップボードにコピーしました!"
       "done": "クリップボードにコピーしました!"
     },
     },
     "bug_report": "バグを報告する",
     "bug_report": "バグを報告する",
-    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
+    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",
@@ -1148,4 +1148,4 @@
     "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> の他、必要な環境変数を設定してください。<br><br>詳細は<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}ja/guide/features/ai-knowledge-assistant.html>ドキュメント</a>を参照してください。",
     "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> の他、必要な環境変数を設定してください。<br><br>詳細は<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}ja/guide/features/ai-knowledge-assistant.html>ドキュメント</a>を参照してください。",
     "ai_search_management": "AI 検索管理"
     "ai_search_management": "AI 検索管理"
   }
   }
-}
+}

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

@@ -244,7 +244,7 @@
     "scope_read": "Read",
     "scope_read": "Read",
     "action": "アクション",
     "action": "アクション",
     "create_token": "トークンを作成",
     "create_token": "トークンを作成",
-    "no_tokens_found":"アクセストークンが見つかりません",
+    "no_tokens_found": "アクセストークンが見つかりません",
     "new_token": {
     "new_token": {
       "title": "新しいアクセストークン",
       "title": "新しいアクセストークン",
       "copy_to_clipboard": "クリップボードにコピーしました",
       "copy_to_clipboard": "クリップボードにコピーしました",
@@ -708,7 +708,7 @@
       "thread_deleted_failed": "スレッドの削除に失敗しました",
       "thread_deleted_failed": "スレッドの削除に失敗しました",
       "ai_assistant_set_default_success": "デフォルトアシスタントを設定しました",
       "ai_assistant_set_default_success": "デフォルトアシスタントを設定しました",
       "ai_assistant_set_default_failed": "デフォルトアシスタントの設定に失敗しました"
       "ai_assistant_set_default_failed": "デフォルトアシスタントの設定に失敗しました"
-      },
+    },
     "delete_modal": {
     "delete_modal": {
       "title": "アシスタントを削除する",
       "title": "アシスタントを削除する",
       "confirm_message": "本当にアシスタントを削除しますか?"
       "confirm_message": "本当にアシスタントを削除しますか?"
@@ -930,7 +930,7 @@
     "Password field is required": "パスワードの欄は必ず入力してください",
     "Password field is required": "パスワードの欄は必ず入力してください",
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "user_not_found": "ユーザーが見つかりません",
     "user_not_found": "ユーザーが見つかりません",
-    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/growilabs/growi/issues/193'>こちら: #193</a>.</p>"
   },
   },
   "grid_edit": {
   "grid_edit": {
     "create_bootstrap_4_grid": "Bootstrap 4 グリッドを作成",
     "create_bootstrap_4_grid": "Bootstrap 4 グリッドを作成",
@@ -1095,4 +1095,4 @@
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "error-toaster": "最新の本文の同期に失敗しました"
     "error-toaster": "最新の本文の同期に失敗しました"
   }
   }
-}
+}

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

@@ -313,7 +313,7 @@
       "done": "클립보드에 복사되었습니다!"
       "done": "클립보드에 복사되었습니다!"
     },
     },
     "bug_report": "버그 보고서 제출",
     "bug_report": "버그 보고서 제출",
-    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>그런 다음 GitHub에 문제를 제출하십시오.</a>"
+    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>그런 다음 GitHub에 문제를 제출하십시오.</a>"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
     "migration_desc": "일부 페이지는 이전 v4 호환성을 가지고 있습니다. 페이지 트리 및 쉬운 이름 변경과 같은 새로운 기능을 활용하려면 모든 페이지를 v5 호환성으로 변환하십시오.",
     "migration_desc": "일부 페이지는 이전 v4 호환성을 가지고 있습니다. 페이지 트리 및 쉬운 이름 변경과 같은 새로운 기능을 활용하려면 모든 페이지를 v5 호환성으로 변환하십시오.",
@@ -1139,4 +1139,4 @@
     "disable_mode_explanation": "현재 AI 통합이 비활성화되어 있습니다. 활성화하려면 <code>AI_ENABLED</code> 환경 변수와 필요한 추가 변수를 구성하십시오.<br><br>자세한 내용은 <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>문서</a>를 참조하십시오.",
     "disable_mode_explanation": "현재 AI 통합이 비활성화되어 있습니다. 활성화하려면 <code>AI_ENABLED</code> 환경 변수와 필요한 추가 변수를 구성하십시오.<br><br>자세한 내용은 <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>문서</a>를 참조하십시오.",
     "ai_search_management": "AI 검색 관리"
     "ai_search_management": "AI 검색 관리"
   }
   }
-}
+}

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

@@ -857,7 +857,7 @@
     "Password field is required": "비밀번호 필드는 필수입니다.",
     "Password field is required": "비밀번호 필드는 필수입니다.",
     "Username or E-mail has invalid characters": "사용자 이름 또는 이메일에 유효하지 않은 문자가 있습니다.",
     "Username or E-mail has invalid characters": "사용자 이름 또는 이메일에 유효하지 않은 문자가 있습니다.",
     "user_not_found": "사용자를 찾을 수 없습니다.",
     "user_not_found": "사용자를 찾을 수 없습니다.",
-    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException 발생</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 인증은 성공했지만 새 사용자를 생성할 수 없습니다. <a href='https://github.com/weseek/growi/issues/193'>#193</a> 문제를 참조하십시오.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException 발생</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 인증은 성공했지만 새 사용자를 생성할 수 없습니다. <a href='https://github.com/growilabs/growi/issues/193'>#193</a> 문제를 참조하십시오.</p>"
   },
   },
   "grid_edit": {
   "grid_edit": {
     "create_bootstrap_4_grid": "Bootstrap 4 그리드 생성",
     "create_bootstrap_4_grid": "Bootstrap 4 그리드 생성",
@@ -1022,4 +1022,4 @@
     "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
     "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
     "error-toaster": "최신 텍스트 동기화 실패"
     "error-toaster": "최신 텍스트 동기화 실패"
   }
   }
-}
+}

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

@@ -322,7 +322,7 @@
       "done": "复制到剪贴板!"
       "done": "复制到剪贴板!"
     },
     },
     "bug_report": "提交一个错误报告",
     "bug_report": "提交一个错误报告",
-    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
+    "submit_bug_report": "<a href='https://github.com/growilabs/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
     "migration_desc": "有一些页面具有旧的v4兼容性。为了利用新的功能,如页面树和容易重命名,请将您的所有页面转换为v5兼容性。",
     "migration_desc": "有一些页面具有旧的v4兼容性。为了利用新的功能,如页面树和容易重命名,请将您的所有页面转换为v5兼容性。",
@@ -1148,4 +1148,4 @@
     "disable_mode_explanation": "目前,AI 集成已被禁用。若要启用,请配置 <code>AI_ENABLED</code> 环境变量以及其他必要的变量。<br><br>详细信息请参考<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>文档</a>。",
     "disable_mode_explanation": "目前,AI 集成已被禁用。若要启用,请配置 <code>AI_ENABLED</code> 环境变量以及其他必要的变量。<br><br>详细信息请参考<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>文档</a>。",
     "ai_search_management": "AI 搜索管理"
     "ai_search_management": "AI 搜索管理"
   }
   }
-}
+}

+ 2 - 2
apps/app/public/static/locales/zh_CN/translation.json

@@ -902,7 +902,7 @@
     "Password field is required": "密码字段是必需的",
     "Password field is required": "密码字段是必需的",
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "user_not_found": "未找到用户",
     "user_not_found": "未找到用户",
-    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<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>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/growilabs/growi/issues/193'>#193</a>.</p>"
   },
   },
   "grid_edit": {
   "grid_edit": {
     "create_bootstrap_4_grid": "创建Bootstrap 4网格",
     "create_bootstrap_4_grid": "创建Bootstrap 4网格",
@@ -1067,4 +1067,4 @@
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "error-toaster": "同步最新文本失败"
     "error-toaster": "同步最新文本失败"
   }
   }
-}
+}

+ 18 - 8
apps/app/src/client/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -14,7 +14,7 @@ import RebuildIndexControls from './RebuildIndexControls';
 import ReconnectControls from './ReconnectControls';
 import ReconnectControls from './ReconnectControls';
 import StatusTable from './StatusTable';
 import StatusTable from './StatusTable';
 
 
-const ElasticsearchManagement = () => {
+const ElasticsearchManagement = (): JSX.Element => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
   const { data: socket } = useAdminSocket();
   const { data: socket } = useAdminSocket();
@@ -43,6 +43,8 @@ const ElasticsearchManagement = () => {
       setIndicesData(info.indices);
       setIndicesData(info.indices);
       setAliasesData(info.aliases);
       setAliasesData(info.aliases);
       setIsNormalized(info.isNormalized);
       setIsNormalized(info.isNormalized);
+
+      return info.isNormalized;
     }
     }
     catch (errors: unknown) {
     catch (errors: unknown) {
       setIsConnected(false);
       setIsConnected(false);
@@ -60,6 +62,7 @@ const ElasticsearchManagement = () => {
         toastError(errors as Error);
         toastError(errors as Error);
       }
       }
 
 
+      return false;
     }
     }
     finally {
     finally {
       setIsInitialized(true);
       setIsInitialized(true);
@@ -67,13 +70,9 @@ const ElasticsearchManagement = () => {
   }, []);
   }, []);
 
 
   useEffect(() => {
   useEffect(() => {
-    const fetchIndicesStatusData = async() => {
-      await retrieveIndicesStatus();
-    };
-    fetchIndicesStatusData();
+    retrieveIndicesStatus();
   }, [retrieveIndicesStatus]);
   }, [retrieveIndicesStatus]);
 
 
-
   useEffect(() => {
   useEffect(() => {
     if (socket == null) {
     if (socket == null) {
       return;
       return;
@@ -83,7 +82,19 @@ const ElasticsearchManagement = () => {
     });
     });
 
 
     socket.on(SocketEventName.FinishAddPage, async(data) => {
     socket.on(SocketEventName.FinishAddPage, async(data) => {
-      await retrieveIndicesStatus();
+      let retryCount = 0;
+      const maxRetries = 5;
+      const retryDelay = 500;
+
+      const retrieveIndicesStatusWithRetry = async() => {
+        const isNormalizedResult = await retrieveIndicesStatus();
+        if (!isNormalizedResult && retryCount < maxRetries) {
+          retryCount++;
+          setTimeout(retrieveIndicesStatusWithRetry, retryDelay);
+        }
+      };
+
+      await retrieveIndicesStatusWithRetry();
       setIsRebuildingProcessing(false);
       setIsRebuildingProcessing(false);
       setIsRebuildingCompleted(true);
       setIsRebuildingCompleted(true);
     });
     });
@@ -99,7 +110,6 @@ const ElasticsearchManagement = () => {
     };
     };
   }, [retrieveIndicesStatus, socket]);
   }, [retrieveIndicesStatus, socket]);
 
 
-
   const reconnect = async() => {
   const reconnect = async() => {
     setIsReconnectingProcessing(true);
     setIsReconnectingProcessing(true);
 
 

+ 22 - 31
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -1,7 +1,9 @@
 import React from 'react';
 import React from 'react';
 
 
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import { Progress } from 'reactstrap';
+import {
+  Progress, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
 
 
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 
 
@@ -49,6 +51,8 @@ export default class ImportCollectionItem extends React.Component {
     onOptionChange(collectionName, { mode });
     onOptionChange(collectionName, { mode });
   }
   }
 
 
+  // No toggle state needed when using UncontrolledDropdown
+
   configButtonClickedHandler() {
   configButtonClickedHandler() {
     const { collectionName, onConfigButtonClicked } = this.props;
     const { collectionName, onConfigButtonClicked } = this.props;
 
 
@@ -103,40 +107,28 @@ export default class ImportCollectionItem extends React.Component {
     const {
     const {
       collectionName, option, isImporting,
       collectionName, option, isImporting,
     } = this.props;
     } = this.props;
-
-    const attrMap = MODE_ATTR_MAP[option.mode];
-    const btnColor = `btn-${attrMap.color}`;
-
+    const currentMode = option?.mode || 'insert';
+    const attrMap = MODE_ATTR_MAP[currentMode];
     const modes = MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
     const modes = MODE_RESTRICTED_COLLECTION[collectionName] || Object.keys(MODE_ATTR_MAP);
 
 
     return (
     return (
       <span className="d-inline-flex align-items-center">
       <span className="d-inline-flex align-items-center">
         Mode:&nbsp;
         Mode:&nbsp;
-        <div className="dropdown d-inline-block">
-          <button
-            className={`btn ${btnColor} btn-sm dropdown-toggle`}
-            type="button"
-            id="ddmMode"
-            disabled={isImporting}
-            data-bs-toggle="dropdown"
-            aria-haspopup="true"
-            aria-expanded="true"
-          >
-            {this.renderModeLabel(option.mode)}
-            <span className="caret ms-2"></span>
-          </button>
-          <ul className="dropdown-menu" aria-labelledby="ddmMode">
-            { modes.map((mode) => {
-              return (
-                <li key={`buttonMode_${mode}`}>
-                  <button type="button" className="dropdown-item" role="button" onClick={() => this.modeSelectedHandler(mode)}>
-                    {this.renderModeLabel(mode, true)}
-                  </button>
-                </li>
-              );
-            }) }
-          </ul>
-        </div>
+        <UncontrolledDropdown size="sm" className="d-inline-block">
+          <DropdownToggle color={attrMap.color} caret disabled={isImporting} id={`ddmMode-${collectionName}`}>
+            {this.renderModeLabel(currentMode)}
+          </DropdownToggle>
+          <DropdownMenu>
+            {modes.map(mode => (
+              <DropdownItem
+                key={`buttonMode_${mode}`}
+                onClick={() => this.modeSelectedHandler(mode)}
+              >
+                {this.renderModeLabel(mode, true)}
+              </DropdownItem>
+            ))}
+          </DropdownMenu>
+        </UncontrolledDropdown>
       </span>
       </span>
     );
     );
   }
   }
@@ -190,7 +182,6 @@ export default class ImportCollectionItem extends React.Component {
         }
         }
       </div>
       </div>
     );
     );
-
   }
   }
 
 
   render() {
   render() {

+ 287 - 0
apps/app/src/client/components/LoginForm/LoginForm.spec.tsx

@@ -0,0 +1,287 @@
+import React from 'react';
+
+import {
+  render, screen, fireEvent, waitFor,
+} from '@testing-library/react';
+import {
+  describe, it, expect, vi, beforeEach,
+} from 'vitest';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
+
+import { LoginForm } from './LoginForm';
+
+vi.mock('next-i18next', () => ({
+  useTranslation: () => ({
+    t: (key: string) => key,
+  }),
+}));
+
+vi.mock('next/router', () => ({
+  useRouter: () => ({
+    push: vi.fn(),
+  }),
+}));
+
+vi.mock('~/client/util/t-with-opt', () => ({
+  useTWithOpt: () => (key: string) => key,
+}));
+
+vi.mock('~/client/util/apiv3-client', () => ({
+  apiv3Post: vi.fn(),
+}));
+
+vi.mock('./ExternalAuthButton', () => ({
+  ExternalAuthButton: ({ authType }: { authType: string }) => (
+    <button type="button" data-testid={`external-auth-${authType}`}>
+      External Auth {authType}
+    </button>
+  ),
+}));
+
+vi.mock('../CompleteUserRegistration', () => ({
+  CompleteUserRegistration: () => <div>Complete Registration</div>,
+}));
+
+const defaultProps = {
+  isEmailAuthenticationEnabled: false,
+  registrationMode: 'Open' as const,
+  registrationWhitelist: [],
+  isPasswordResetEnabled: true,
+  isLocalStrategySetup: true,
+  isLdapStrategySetup: false,
+  isLdapSetupFailed: false,
+  minPasswordLength: 8,
+  isMailerSetup: true,
+};
+
+const mockApiv3Post = vi.mocked(apiv3Post);
+
+describe('LoginForm - Error Display', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('when password login is enabled', () => {
+    it('should display login form', () => {
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: true,
+      };
+
+      render(<LoginForm {...props} />);
+
+      expect(screen.getByTestId('login-form')).toBeInTheDocument();
+    });
+
+    it('should display external account login errors', () => {
+      const externalAccountLoginError = {
+        message: 'jwks must be a JSON Web Key Set formatted object',
+        name: 'ExternalAccountLoginError',
+      };
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: true,
+        externalAccountLoginError,
+      };
+
+      render(<LoginForm {...props} />);
+
+      expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument();
+    });
+  });
+
+  describe('when password login is disabled', () => {
+    it('should still display external account login errors', () => {
+      const externalAccountLoginError = {
+        message: 'jwks must be a JSON Web Key Set formatted object',
+        name: 'ExternalAccountLoginError',
+      };
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: false,
+        isLdapStrategySetup: false,
+        enabledExternalAuthType: ['oidc'] satisfies IExternalAuthProviderType[],
+        externalAccountLoginError,
+      };
+
+      render(<LoginForm {...props} />);
+
+      expect(screen.getByText('jwks must be a JSON Web Key Set formatted object')).toBeInTheDocument();
+    });
+
+    it('should not render local/LDAP form but should still show errors', () => {
+      const externalAccountLoginError = {
+        message: 'OIDC authentication failed',
+        name: 'ExternalAccountLoginError',
+      };
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: false,
+        isLdapStrategySetup: false,
+        enabledExternalAuthType: ['oidc'] satisfies IExternalAuthProviderType[],
+        externalAccountLoginError,
+      };
+
+      render(<LoginForm {...props} />);
+
+      expect(screen.queryByTestId('tiUsernameForLogin')).not.toBeInTheDocument();
+      expect(screen.queryByTestId('tiPasswordForLogin')).not.toBeInTheDocument();
+      expect(screen.getByText('OIDC authentication failed')).toBeInTheDocument();
+    });
+  });
+
+  describe('error display priority and login error handling', () => {
+    it('should show external errors when no login errors exist', () => {
+      const externalAccountLoginError = {
+        message: 'External error message',
+        name: 'ExternalAccountLoginError',
+      };
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: true,
+        externalAccountLoginError,
+      };
+
+      render(<LoginForm {...props} />);
+
+      expect(screen.getByText('External error message')).toBeInTheDocument();
+    });
+
+    it('should prioritize login errors over external account login errors after failed login', async() => {
+      const externalAccountLoginError = {
+        message: 'External error message',
+        name: 'ExternalAccountLoginError',
+      };
+
+      // Mock API call to return error
+      mockApiv3Post.mockRejectedValueOnce([
+        {
+          message: 'Invalid username or password',
+          code: 'LOGIN_FAILED',
+          args: {},
+        },
+      ]);
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: true,
+        externalAccountLoginError,
+      };
+
+      render(<LoginForm {...props} />);
+
+      // Initially, external error should be visible
+      expect(screen.getByText('External error message')).toBeInTheDocument();
+
+      // Fill in login form and submit
+      const usernameInput = screen.getByTestId('tiUsernameForLogin');
+      const passwordInput = screen.getByTestId('tiPasswordForLogin');
+      const submitButton = screen.getByTestId('btnSubmitForLogin');
+
+      fireEvent.change(usernameInput, { target: { value: 'testuser' } });
+      fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
+      fireEvent.click(submitButton);
+
+      // Wait for login error to appear and external error to be replaced
+      await waitFor(() => {
+        expect(screen.getByText('Invalid username or password')).toBeInTheDocument();
+      });
+
+      // External error should no longer be visible when login error exists
+      expect(screen.queryByText('External error message')).not.toBeInTheDocument();
+    });
+
+    it('should display dangerouslySetInnerHTML errors for PROVIDER_DUPLICATED_USERNAME_EXCEPTION', async() => {
+      // Mock API call to return PROVIDER_DUPLICATED_USERNAME_EXCEPTION error
+      mockApiv3Post.mockRejectedValueOnce([
+        {
+          message: 'This username is already taken by <a href="/login">another provider</a>',
+          code: 'provider-duplicated-username-exception',
+          args: {},
+        },
+      ]);
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: true,
+      };
+
+      render(<LoginForm {...props} />);
+
+      // Fill in login form and submit
+      const usernameInput = screen.getByTestId('tiUsernameForLogin');
+      const passwordInput = screen.getByTestId('tiPasswordForLogin');
+      const submitButton = screen.getByTestId('btnSubmitForLogin');
+
+      fireEvent.change(usernameInput, { target: { value: 'testuser' } });
+      fireEvent.change(passwordInput, { target: { value: 'password' } });
+      fireEvent.click(submitButton);
+
+      // Wait for the dangerouslySetInnerHTML error to appear
+      await waitFor(() => {
+        // Check that the error with HTML content is rendered
+        expect(screen.getByText(/This username is already taken by/)).toBeInTheDocument();
+      });
+    });
+
+    it('should handle multiple login errors correctly', async() => {
+      // Mock API call to return multiple errors
+      mockApiv3Post.mockRejectedValueOnce([
+        {
+          message: 'Username is required',
+          code: 'VALIDATION_ERROR',
+          args: {},
+        },
+        {
+          message: 'Password is too short',
+          code: 'VALIDATION_ERROR',
+          args: {},
+        },
+      ]);
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: true,
+      };
+
+      render(<LoginForm {...props} />);
+
+      // Submit form without filling inputs
+      const submitButton = screen.getByTestId('btnSubmitForLogin');
+      fireEvent.click(submitButton);
+
+      // Wait for multiple errors to appear
+      await waitFor(() => {
+        expect(screen.getByText('Username is required')).toBeInTheDocument();
+        expect(screen.getByText('Password is too short')).toBeInTheDocument();
+      });
+    });
+  });
+
+  describe('error display when both login methods are disabled', () => {
+    it('should still display external errors when no login methods are available', () => {
+      const externalAccountLoginError = {
+        message: 'Authentication service unavailable',
+        name: 'ExternalAccountLoginError',
+      };
+
+      const props = {
+        ...defaultProps,
+        isLocalStrategySetup: false,
+        isLdapStrategySetup: false,
+        enabledExternalAuthType: undefined,
+        externalAccountLoginError,
+      };
+
+      render(<LoginForm {...props} />);
+
+      expect(screen.getByText('Authentication service unavailable')).toBeInTheDocument();
+    });
+  });
+});

+ 25 - 20
apps/app/src/client/components/LoginForm/LoginForm.tsx

@@ -154,9 +154,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     return (
     return (
       <ul className="alert alert-danger">
       <ul className="alert alert-danger">
         {errors.map((err, index) => (
         {errors.map((err, index) => (
-          <li className={index > 0 ? 'mt-1' : ''}>
+          <small className={index > 0 ? 'mt-1' : ''}>
             {tWithOpt(err.message, err.args)}
             {tWithOpt(err.message, err.args)}
-          </li>
+          </small>
         ))}
         ))}
       </ul>
       </ul>
     );
     );
@@ -165,21 +165,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const renderLocalOrLdapLoginForm = useCallback(() => {
   const renderLocalOrLdapLoginForm = useCallback(() => {
     const { isLdapStrategySetup } = props;
     const { isLdapStrategySetup } = props;
 
 
-    // separate login errors into two arrays based on error code
-    const [loginErrorListForDangerouslySetInnerHTML, loginErrorList] = separateErrorsBasedOnErrorCode(loginErrors);
-    // Generate login error elements using dangerouslySetInnerHTML
-    const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
-    // Generate login error elements using <ul>, <li>
-
-    const loginErrorElement = (loginErrorList ?? []).length > 0
-    // prioritize loginErrorList because the list should contains new error
-      ? generateSafelySetErrors(loginErrorList)
-      : generateSafelySetErrors(props.externalAccountLoginError != null ? [props.externalAccountLoginError] : []);
-
     return (
     return (
       <>
       <>
         {/* !! - DO NOT DELETE HIDDEN ELEMENT - !! -- 7.12 ryoji-s */}
         {/* !! - DO NOT DELETE HIDDEN ELEMENT - !! -- 7.12 ryoji-s */}
-        {/* https://github.com/weseek/growi/pull/7873 */}
+        {/* https://github.com/growilabs/growi/pull/7873 */}
         <div className="visually-hidden">
         <div className="visually-hidden">
           <LoadingSpinner />
           <LoadingSpinner />
         </div>
         </div>
@@ -191,8 +180,6 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             <span dangerouslySetInnerHTML={{ __html: t('login.set_env_var_for_logs') }}></span>
             <span dangerouslySetInnerHTML={{ __html: t('login.set_env_var_for_logs') }}></span>
           </div>
           </div>
         )}
         )}
-        {loginErrorElementWithDangerouslySetInnerHTML}
-        {loginErrorElement}
 
 
         <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
         <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
           <div className="input-group">
           <div className="input-group">
@@ -253,8 +240,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       </>
       </>
     );
     );
   }, [
   }, [
-    props, separateErrorsBasedOnErrorCode, loginErrors, generateDangerouslySetErrors, generateSafelySetErrors,
-    isLdapSetupFailed, t, handleLoginWithLocalSubmit, isLoading,
+    props, isLdapSetupFailed, t, handleLoginWithLocalSubmit, isLoading,
   ]);
   ]);
 
 
 
 
@@ -268,7 +254,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     return (
     return (
       <>
       <>
         <div className="mt-2">
         <div className="mt-2">
-          { enabledExternalAuthType.map(authType => <ExternalAuthButton authType={authType} />) }
+          {enabledExternalAuthType.map(authType => <ExternalAuthButton authType={authType} />)}
         </div>
         </div>
       </>
       </>
     );
     );
@@ -342,7 +328,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             {t('page_register.notice.restricted_defail')}
             {t('page_register.notice.restricted_defail')}
           </p>
           </p>
         )}
         )}
-        { (!isMailerSetup && isEmailAuthenticationEnabled) && (
+        {(!isMailerSetup && isEmailAuthenticationEnabled) && (
           <p className="alert alert-danger">
           <p className="alert alert-danger">
             <span>{t('commons:alert.please_enable_mailer')}</span>
             <span>{t('commons:alert.please_enable_mailer')}</span>
           </p>
           </p>
@@ -510,6 +496,25 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           <div className="col-12 px-md-4 pb-5">
           <div className="col-12 px-md-4 pb-5">
             <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
             <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
               <div className="front">
               <div className="front">
+                {/* Error display section - always shown regardless of login method configuration */}
+                {(() => {
+                  // separate login errors into two arrays based on error code
+                  const [loginErrorListForDangerouslySetInnerHTML, loginErrorList] = separateErrorsBasedOnErrorCode(loginErrors);
+                  // Generate login error elements using dangerouslySetInnerHTML
+                  const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
+                  // Generate login error elements - prioritize loginErrorList, fallback to externalAccountLoginError
+                  const loginErrorElement = (loginErrorList ?? []).length > 0
+                    ? generateSafelySetErrors(loginErrorList)
+                    : generateSafelySetErrors(props.externalAccountLoginError != null ? [props.externalAccountLoginError] : []);
+
+                  return (
+                    <>
+                      {loginErrorElementWithDangerouslySetInnerHTML}
+                      {loginErrorElement}
+                    </>
+                  );
+                })()}
+
                 {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()}
                 {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()}
                 {isLocalOrLdapStrategiesEnabled && isSomeExternalAuthEnabled && (
                 {isLocalOrLdapStrategiesEnabled && isSomeExternalAuthEnabled && (
                   <div className="text-center text-line d-flex align-items-center mb-3">
                   <div className="text-center text-line d-flex align-items-center mb-3">

+ 0 - 2
apps/app/src/client/components/NotAvailableForReadOnlyUser.spec.tsx

@@ -1,5 +1,3 @@
-import '@testing-library/jest-dom/vitest';
-
 import { render, screen } from '@testing-library/react';
 import { render, screen } from '@testing-library/react';
 import {
 import {
   describe, it, expect, vi,
   describe, it, expect, vi,

+ 2 - 2
apps/app/src/client/components/PageComment/CommentEditor.tsx

@@ -116,7 +116,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setShowPreview(showPreview);
     setShowPreview(showPreview);
   }, []);
   }, []);
 
 
-  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
+  // DO NOT dependent on slackChannelsData directly: https://github.com/growilabs/growi/pull/7332
   const slackChannelsDataString = slackChannelsData?.toString();
   const slackChannelsDataString = slackChannelsData?.toString();
   const initializeSlackEnabled = useCallback(() => {
   const initializeSlackEnabled = useCallback(() => {
     setSlackChannels(slackChannelsDataString ?? '');
     setSlackChannels(slackChannelsDataString ?? '');
@@ -186,7 +186,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
       const errorMessage = err.message || 'An unknown error occured when posting comment';
       const errorMessage = err.message || 'An unknown error occured when posting comment';
       setError(errorMessage);
       setError(errorMessage);
     }
     }
-  // eslint-disable-next-line max-len
+    // eslint-disable-next-line max-len
   }, [currentCommentId, initializeEditor, onCommented, codeMirrorEditor, updateComment, revisionId, replyTo, isSlackEnabled, slackChannels, postComment]);
   }, [currentCommentId, initializeEditor, onCommented, codeMirrorEditor, updateComment, revisionId, replyTo, isSlackEnabled, slackChannels, postComment]);
 
 
   // the upload event handler
   // the upload event handler

+ 2 - 2
apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx

@@ -38,7 +38,7 @@ declare global {
 const logger = loggerFactory('growi:SavePageControls');
 const logger = loggerFactory('growi:SavePageControls');
 
 
 
 
-const SavePageButton = (props: {slackChannels: string, isSlackEnabled?: boolean, isDeviceLargerThanMd?: boolean}) => {
+const SavePageButton = (props: { slackChannels: string, isSlackEnabled?: boolean, isDeviceLargerThanMd?: boolean }) => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
@@ -159,7 +159,7 @@ export const SavePageControls = (): JSX.Element | null => {
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [slackChannels, setSlackChannels] = useState<string>('');
   const [isSavePageControlsModalShown, setIsSavePageControlsModalShown] = useState<boolean>(false);
   const [isSavePageControlsModalShown, setIsSavePageControlsModalShown] = useState<boolean>(false);
 
 
-  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
+  // DO NOT dependent on slackChannelsData directly: https://github.com/growilabs/growi/pull/7332
   const slackChannelsDataString = slackChannelsData?.toString();
   const slackChannelsDataString = slackChannelsData?.toString();
   useEffect(() => {
   useEffect(() => {
     if (editorMode === 'editor') {
     if (editorMode === 'editor') {

+ 1 - 1
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -189,7 +189,7 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
         ...(opts ?? {}),
         ...(opts ?? {}),
       });
       });
 
 
-      // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+      // to sync revision id with page tree: https://github.com/growilabs/growi/pull/7227
       mutatePageTree();
       mutatePageTree();
 
 
       mutateRecentlyUpdated();
       mutateRecentlyUpdated();

+ 0 - 2
apps/app/src/client/components/PageHeader/PageTitleHeader.spec.tsx

@@ -1,5 +1,3 @@
-import '@testing-library/jest-dom/vitest';
-
 import { faker } from '@faker-js/faker';
 import { faker } from '@faker-js/faker';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import {
 import {

+ 2 - 2
apps/app/src/client/components/PageSideContents/PageAccessoriesControl.tsx

@@ -33,8 +33,8 @@ export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
       <span className="grw-icon d-flex me-lg-2">{icon}</span>
       <span className="grw-icon d-flex me-lg-2">{icon}</span>
       <span className="grw-labels d-none d-lg-flex">
       <span className="grw-labels d-none d-lg-flex">
         {label}
         {label}
-        {/* Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600 */}
-        { count != null
+        {/* Do not display CountBadge if '/trash/*': https://github.com/growilabs/growi/pull/7600 */}
+        {count != null
           ? <CountBadge count={count} offset={offset} />
           ? <CountBadge count={count} offset={offset} />
           : <div className="px-2"></div>}
           : <div className="px-2"></div>}
       </span>
       </span>

+ 3 - 3
apps/app/src/client/components/PageSideContents/PageSideContents.tsx

@@ -109,13 +109,13 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       )}
       )}
 
 
       {/* Tags */}
       {/* Tags */}
-      { page.revision != null && (
+      {page.revision != null && (
         <div ref={tagsRef}>
         <div ref={tagsRef}>
           <Suspense fallback={<PageTagsSkeleton />}>
           <Suspense fallback={<PageTagsSkeleton />}>
             <Tags pageId={page._id} revisionId={page.revision._id} />
             <Tags pageId={page._id} revisionId={page.revision._id} />
           </Suspense>
           </Suspense>
         </div>
         </div>
-      ) }
+      )}
 
 
       <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
       <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
         {/* Page list */}
         {/* Page list */}
@@ -124,7 +124,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             <PageAccessoriesControl
             <PageAccessoriesControl
               icon={<span className="material-symbols-outlined">subject</span>}
               icon={<span className="material-symbols-outlined">subject</span>}
               label={t('page_list')}
               label={t('page_list')}
-              // Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600
+              // Do not display CountBadge if '/trash/*': https://github.com/growilabs/growi/pull/7600
               count={!isTrash && pageInfo != null ? (pageInfo as IPageInfoForOperation).descendantCount : undefined}
               count={!isTrash && pageInfo != null ? (pageInfo as IPageInfoForOperation).descendantCount : undefined}
               offset={1}
               offset={1}
               onClick={() => openDescendantPageListModal(pagePath)}
               onClick={() => openDescendantPageListModal(pagePath)}

+ 9 - 9
apps/app/src/client/components/SearchPage/SearchPageBase.tsx

@@ -20,7 +20,7 @@ import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
 
 // Do not import with next/dynamic
 // Do not import with next/dynamic
-// see: https://github.com/weseek/growi/pull/7923
+// see: https://github.com/growilabs/growi/pull/7923
 import { SearchResultList } from './SearchResultList';
 import { SearchResultList } from './SearchResultList';
 
 
 import styles from './SearchPageBase.module.scss';
 import styles from './SearchPageBase.module.scss';
@@ -52,7 +52,7 @@ const SearchResultContent = dynamic(() => import('./SearchResultContent').then(m
   ssr: false,
   ssr: false,
   loading: () => <></>,
   loading: () => <></>,
 });
 });
-const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
+const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props: Props, ref) => {
 
 
   const {
   const {
     className,
     className,
@@ -63,7 +63,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
     searchControl, searchResultListHead, searchPager,
     searchControl, searchResultListHead, searchPager,
   } = props;
   } = props;
 
 
-  const searchResultListRef = useRef<ISelectableAll|null>(null);
+  const searchResultListRef = useRef<ISelectableAll | null>(null);
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
@@ -182,20 +182,20 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
         <div className="overflow-y-scroll">
         <div className="overflow-y-scroll">
 
 
           {/* Loading */}
           {/* Loading */}
-          { pages == null && (
+          {pages == null && (
             <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
             <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
               <LoadingSpinner className="me-1 fs-3" />
               <LoadingSpinner className="me-1 fs-3" />
             </div>
             </div>
-          ) }
+          )}
 
 
           {/* Loaded */}
           {/* Loaded */}
-          { pages != null && (
+          {pages != null && (
             <>
             <>
               <div className="my-3 px-md-4 px-3">
               <div className="my-3 px-md-4 px-3">
                 {searchResultListHead}
                 {searchResultListHead}
               </div>
               </div>
 
 
-              { pages.length > 0 && (
+              {pages.length > 0 && (
                 <div className={`page-list ${styles['page-list']} px-md-4`}>
                 <div className={`page-list ${styles['page-list']} px-md-4`}>
                   <SearchResultList
                   <SearchResultList
                     ref={searchResultListRef}
                     ref={searchResultListRef}
@@ -206,12 +206,12 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
                     onCheckboxChanged={checkboxChangedHandler}
                     onCheckboxChanged={checkboxChangedHandler}
                   />
                   />
                 </div>
                 </div>
-              ) }
+              )}
               <div className="my-4 d-flex justify-content-center">
               <div className="my-4 d-flex justify-content-center">
                 {searchPager}
                 {searchPager}
               </div>
               </div>
             </>
             </>
-          ) }
+          )}
 
 
         </div>
         </div>
 
 

+ 2 - 2
apps/app/src/client/components/TableOfContents.tsx

@@ -33,7 +33,7 @@ const TableOfContents = ({ tagsElementHeight }: Props): JSX.Element => {
     const containerElem = document.querySelector('#revision-toc');
     const containerElem = document.querySelector('#revision-toc');
 
 
     // rendererOptions for redo calcViewHeight()
     // rendererOptions for redo calcViewHeight()
-    // see: https://github.com/weseek/growi/pull/6791
+    // see: https://github.com/growilabs/growi/pull/6791
     if (parentElem == null || containerElem == null || rendererOptions == null || tagsElementHeight == null) {
     if (parentElem == null || containerElem == null || rendererOptions == null || tagsElementHeight == null) {
       return 0;
       return 0;
     }
     }
@@ -64,7 +64,7 @@ const TableOfContents = ({ tagsElementHeight }: Props): JSX.Element => {
           data-testid="revision-toc-content"
           data-testid="revision-toc-content"
           className="revision-toc-content mb-3"
           className="revision-toc-content mb-3"
         >
         >
-          {/* parse blank to show toc (https://github.com/weseek/growi/pull/6277) */}
+          {/* parse blank to show toc (https://github.com/growilabs/growi/pull/6277) */}
           <ReactMarkdown {...rendererOptions}>{' '}</ReactMarkdown>
           <ReactMarkdown {...rendererOptions}>{' '}</ReactMarkdown>
         </div>
         </div>
       </StickyStretchableScroller>
       </StickyStretchableScroller>

+ 5 - 2
apps/app/src/client/components/TreeItem/TreeItemLayout.tsx

@@ -9,6 +9,8 @@ import React, {
   type JSX,
   type JSX,
 } from 'react';
 } from 'react';
 
 
+import { addTrailingSlash } from '@growi/core/dist/utils/path-utils';
+
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 
 
@@ -88,9 +90,10 @@ export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
     setIsOpen(!isOpen);
     setIsOpen(!isOpen);
   }, [isOpen]);
   }, [isOpen]);
 
 
-  // didMount
   useEffect(() => {
   useEffect(() => {
-    const isPathToTarget = page.path != null && targetPath.startsWith(page.path) && targetPath !== page.path; // Target Page does not need to be opened
+    const isPathToTarget = page.path != null
+      && targetPath.startsWith(addTrailingSlash(page.path))
+      && targetPath !== page.path; // Target Page does not need to be opened
     if (isPathToTarget) setIsOpen(true);
     if (isPathToTarget) setIsOpen(true);
   }, [targetPath, page.path]);
   }, [targetPath, page.path]);
 
 

+ 1 - 1
apps/app/src/client/services/AdminHomeContainer.js

@@ -107,7 +107,7 @@ export default class AdminHomeContainer extends Container {
 |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)*`;
   }
   }

+ 2 - 2
apps/app/src/client/services/page-operation.ts

@@ -112,7 +112,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
   return useCallback(async() => {
   return useCallback(async() => {
     if (pageId == null) { return }
     if (pageId == null) { return }
 
 
-    // update tag before page: https://github.com/weseek/growi/pull/7158
+    // update tag before page: https://github.com/growilabs/growi/pull/7158
     // !! DO NOT CHANGE THE ORDERS OF THE MUTATIONS !! -- 12.26 yuken-t
     // !! DO NOT CHANGE THE ORDERS OF THE MUTATIONS !! -- 12.26 yuken-t
     await mutateTagsInfo(); // get from DB
     await mutateTagsInfo(); // get from DB
     syncTagsInfoForEditor(); // sync global state for client
     syncTagsInfoForEditor(); // sync global state for client
@@ -123,7 +123,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
     if (updatedPage == null || updatedPage.revision == null) { return }
     if (updatedPage == null || updatedPage.revision == null) { return }
 
 
     // supress to mutate only when updated from built-in editor
     // supress to mutate only when updated from built-in editor
-    // and see: https://github.com/weseek/growi/pull/7118
+    // and see: https://github.com/growilabs/growi/pull/7118
     const supressEditingMarkdownMutation = opts?.supressEditingMarkdownMutation ?? false;
     const supressEditingMarkdownMutation = opts?.supressEditingMarkdownMutation ?? false;
     if (!supressEditingMarkdownMutation) {
     if (!supressEditingMarkdownMutation) {
       mutateEditingMarkdown(updatedPage.revision.body);
       mutateEditingMarkdown(updatedPage.revision.body);

+ 1 - 1
apps/app/src/client/util/apiv1-client.ts

@@ -46,7 +46,7 @@ export async function apiPost<T>(path: string, params: unknown = {}): Promise<T>
 }
 }
 
 
 export async function apiPostForm<T>(path: string, formData: FormData): Promise<T> {
 export async function apiPostForm<T>(path: string, formData: FormData): Promise<T> {
-  return apiPost<T>(path, formData);
+  return apiRequest<T>('postForm', path, formData);
 }
 }
 
 
 export async function apiDelete<T>(path: string, params: unknown = {}): Promise<T> {
 export async function apiDelete<T>(path: string, params: unknown = {}): Promise<T> {

+ 1 - 1
apps/app/src/client/util/apiv3-client.ts

@@ -50,7 +50,7 @@ export async function apiv3Post<T = any>(path: string, params: unknown = {}): Pr
 }
 }
 
 
 export async function apiv3PostForm<T = any>(path: string, formData: FormData): Promise<AxiosResponse<T>> {
 export async function apiv3PostForm<T = any>(path: string, formData: FormData): Promise<AxiosResponse<T>> {
-  return apiv3Post<T>(path, formData);
+  return apiv3Request('postForm', path, formData);
 }
 }
 
 
 export async function apiv3Put<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
 export async function apiv3Put<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {

+ 3 - 2
apps/app/src/components/PageView/PageContentFooter.module.scss

@@ -2,13 +2,14 @@
 
 
 .page-content-footer :global {
 .page-content-footer :global {
   border-top: solid 1px var(--bs-border-color);
   border-top: solid 1px var(--bs-border-color);
+
   .page-meta {
   .page-meta {
     font-size: 0.95em;
     font-size: 0.95em;
   }
   }
 }
 }
 
 
-// TODO: Should Soft Coding see: https://github.com/weseek/growi/pull/6404
+// TODO: Should Soft Coding see: https://github.com/growilabs/growi/pull/6404
 .page-content-footer-skeleton :global {
 .page-content-footer-skeleton :global {
   width: 300px;
   width: 300px;
   height: 20px;
   height: 20px;
-}
+}

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -52,7 +52,7 @@ function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
 function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactNode }): JSX.Element {
 function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactNode }): JSX.Element {
   // return alternative element
   // return alternative element
   //   in order to fix "CodeBlock string is be [object Object] if searched"
   //   in order to fix "CodeBlock string is be [object Object] if searched"
-  // see: https://github.com/weseek/growi/pull/7484
+  // see: https://github.com/growilabs/growi/pull/7484
   //
   //
   // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
   // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
 
 

+ 1 - 1
apps/app/src/features/comment/server/events/event-emitter.ts

@@ -1,3 +1,3 @@
-import { EventEmitter } from 'events';
+import { EventEmitter } from 'node:events';
 
 
 export const commentEvent = new EventEmitter();
 export const commentEvent = new EventEmitter();

+ 1 - 1
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx

@@ -153,7 +153,7 @@ export const ExternalGroupManagement: FC = () => {
         hideDeleteModal();
         hideDeleteModal();
 
 
         toastSuccess(`Deleted ${selectedExternalUserGroup?.name} group.`);
         toastSuccess(`Deleted ${selectedExternalUserGroup?.name} group.`);
-      } catch (err) {
+      } catch {
         toastError(new Error('Unable to delete the groups'));
         toastError(new Error('Unable to delete the groups'));
       }
       }
     },
     },

+ 1 - 0
apps/app/src/features/external-user-group/server/models/external-user-group-relation.ts

@@ -15,6 +15,7 @@ export interface ExternalUserGroupRelationDocument
 
 
 export interface ExternalUserGroupRelationModel
 export interface ExternalUserGroupRelationModel
   extends Model<ExternalUserGroupRelationDocument> {
   extends Model<ExternalUserGroupRelationDocument> {
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   [x: string]: any; // for old methods
   [x: string]: any; // for old methods
 
 
   PAGE_ITEMS: 50;
   PAGE_ITEMS: 50;

+ 1 - 0
apps/app/src/features/external-user-group/server/models/external-user-group.ts

@@ -12,6 +12,7 @@ export interface ExternalUserGroupDocument
 
 
 export interface ExternalUserGroupModel
 export interface ExternalUserGroupModel
   extends Model<ExternalUserGroupDocument> {
   extends Model<ExternalUserGroupDocument> {
+  // biome-ignore lint/suspicious/noExplicitAny: ignore
   [x: string]: any; // for old methods
   [x: string]: any; // for old methods
 
 
   PAGE_ITEMS: 10;
   PAGE_ITEMS: 10;

+ 1 - 1
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -964,7 +964,7 @@ module.exports = (crowi: Crowi): Router => {
           req.user.name,
           req.user.name,
           req.body.password,
           req.body.password,
         );
         );
-      } catch (e) {
+      } catch (_e) {
         return res.apiv3Err(
         return res.apiv3Err(
           new ErrorV3(
           new ErrorV3(
             'LDAP group sync failed',
             'LDAP group sync failed',

+ 1 - 1
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginInstallerForm.tsx

@@ -56,7 +56,7 @@ export const PluginInstallerForm = (): JSX.Element => {
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             name="pluginInstallerForm[url]"
             name="pluginInstallerForm[url]"
-            placeholder="https://github.com/weseek/growi-plugins-example"
+            placeholder="https://github.com/growilabs/growi-plugins-example"
             required
             required
             id="repoUrl"
             id="repoUrl"
           />
           />

+ 1 - 1
apps/app/src/features/growi-plugin/server/consts/index.ts

@@ -1,4 +1,4 @@
-import { resolveFromRoot } from '~/utils/project-dir-utils';
+import { resolveFromRoot } from '~/server/util/project-dir-utils';
 
 
 export const PLUGIN_STORING_PATH = resolveFromRoot('tmp/plugins');
 export const PLUGIN_STORING_PATH = resolveFromRoot('tmp/plugins');
 
 

+ 15 - 15
apps/app/src/features/growi-plugin/server/models/growi-plugin.integ.ts

@@ -7,10 +7,10 @@ describe('GrowiPlugin find methods', () => {
     await GrowiPlugin.insertMany([
     await GrowiPlugin.insertMany([
       {
       {
         isEnabled: false,
         isEnabled: false,
-        installedPath: 'weseek/growi-plugin-unenabled1',
-        organizationName: 'weseek',
+        installedPath: 'growilabs/growi-plugin-unenabled1',
+        organizationName: 'growilabs',
         origin: {
         origin: {
-          url: 'https://github.com/weseek/growi-plugin-unenabled1',
+          url: 'https://github.com/growilabs/growi-plugin-unenabled1',
         },
         },
         meta: {
         meta: {
           name: '@growi/growi-plugin-unenabled1',
           name: '@growi/growi-plugin-unenabled1',
@@ -19,10 +19,10 @@ describe('GrowiPlugin find methods', () => {
       },
       },
       {
       {
         isEnabled: false,
         isEnabled: false,
-        installedPath: 'weseek/growi-plugin-unenabled2',
-        organizationName: 'weseek',
+        installedPath: 'growilabs/growi-plugin-unenabled2',
+        organizationName: 'growilabs',
         origin: {
         origin: {
-          url: 'https://github.com/weseek/growi-plugin-unenabled2',
+          url: 'https://github.com/growilabs/growi-plugin-unenabled2',
         },
         },
         meta: {
         meta: {
           name: '@growi/growi-plugin-unenabled2',
           name: '@growi/growi-plugin-unenabled2',
@@ -31,10 +31,10 @@ describe('GrowiPlugin find methods', () => {
       },
       },
       {
       {
         isEnabled: true,
         isEnabled: true,
-        installedPath: 'weseek/growi-plugin-example1',
-        organizationName: 'weseek',
+        installedPath: 'growilabs/growi-plugin-example1',
+        organizationName: 'growilabs',
         origin: {
         origin: {
-          url: 'https://github.com/weseek/growi-plugin-example1',
+          url: 'https://github.com/growilabs/growi-plugin-example1',
         },
         },
         meta: {
         meta: {
           name: '@growi/growi-plugin-example1',
           name: '@growi/growi-plugin-example1',
@@ -43,10 +43,10 @@ describe('GrowiPlugin find methods', () => {
       },
       },
       {
       {
         isEnabled: true,
         isEnabled: true,
-        installedPath: 'weseek/growi-plugin-example2',
-        organizationName: 'weseek',
+        installedPath: 'growilabs/growi-plugin-example2',
+        organizationName: 'growilabs',
         origin: {
         origin: {
-          url: 'https://github.com/weseek/growi-plugin-example2',
+          url: 'https://github.com/growilabs/growi-plugin-example2',
         },
         },
         meta: {
         meta: {
           name: '@growi/growi-plugin-example2',
           name: '@growi/growi-plugin-example2',
@@ -95,10 +95,10 @@ describe('GrowiPlugin activate/deactivate', () => {
     await GrowiPlugin.insertMany([
     await GrowiPlugin.insertMany([
       {
       {
         isEnabled: false,
         isEnabled: false,
-        installedPath: 'weseek/growi-plugin-example1',
-        organizationName: 'weseek',
+        installedPath: 'growilabs/growi-plugin-example1',
+        organizationName: 'growilabs',
         origin: {
         origin: {
-          url: 'https://github.com/weseek/growi-plugin-example1',
+          url: 'https://github.com/growilabs/growi-plugin-example1',
         },
         },
         meta: {
         meta: {
           name: '@growi/growi-plugin-example1',
           name: '@growi/growi-plugin-example1',

+ 47 - 9
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx

@@ -1,7 +1,16 @@
 import type { JSX } from 'react';
 import type { JSX } from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { ModalBody, Input } from 'reactstrap';
+import {
+  ModalBody,
+  Input,
+  UncontrolledDropdown,
+  DropdownToggle,
+  DropdownMenu,
+  DropdownItem,
+} from 'reactstrap';
+
+import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
 
 
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
 
 
@@ -15,28 +24,57 @@ type Props = {
 export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element => {
 export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element => {
   const { instruction, onChange, onReset } = props;
   const { instruction, onChange, onReset } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { changePageMode } = useAiAssistantManagementModal();
+
+  const handleComplete = () => {
+    changePageMode(AiAssistantManagementModalPageMode.HOME);
+  };
 
 
   return (
   return (
     <>
     <>
       <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.instruction" />
       <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.instruction" />
 
 
-      <ModalBody className="px-4">
-        <p className="text-secondary py-1">
-          {t('modal_ai_assistant.instructions.description')}
-        </p>
+      <ModalBody className="p-4">
+        <p
+          className="text-secondary py-1"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: t('modal_ai_assistant.instructions.description') }}
+        />
 
 
         <Input
         <Input
           autoFocus
           autoFocus
           type="textarea"
           type="textarea"
-          className="mb-3"
+          className="mb-4"
           rows="8"
           rows="8"
           value={instruction}
           value={instruction}
           onChange={e => onChange(e.target.value)}
           onChange={e => onChange(e.target.value)}
         />
         />
 
 
-        <button type="button" onClick={onReset} className="btn btn-outline-secondary btn-sm">
-          {t('modal_ai_assistant.instructions.reset_to_default')}
-        </button>
+        <div className="d-flex justify-content-end align-items-center">
+          <UncontrolledDropdown>
+            <DropdownToggle
+              className="btn btn-outline-neutral-secondary text-secondary p-2"
+              tag="button"
+              type="button"
+            >
+              <span className="material-symbols-outlined">more_horiz</span>
+            </DropdownToggle>
+            <DropdownMenu end>
+              <DropdownItem onClick={onReset}>
+                <span className="material-symbols-outlined me-2 align-middle">undo</span>
+                {t('modal_ai_assistant.instructions.reset_to_default')}
+              </DropdownItem>
+            </DropdownMenu>
+          </UncontrolledDropdown>
+
+          <button
+            type="button"
+            onClick={handleComplete}
+            className="btn btn-primary ms-3"
+          >
+            {t('Done')}
+          </button>
+        </div>
       </ModalBody>
       </ModalBody>
     </>
     </>
   );
   );

+ 1 - 1
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-access-handler.spec.ts

@@ -1,4 +1,4 @@
-import type { IncomingMessage } from 'http';
+import type { IncomingMessage } from 'node:http';
 
 
 import { describe, expect, it } from 'vitest';
 import { describe, expect, it } from 'vitest';
 
 

+ 6 - 3
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-access-handler.ts

@@ -1,3 +1,5 @@
+import { createHash } from 'node:crypto';
+import type { IncomingMessage } from 'node:http';
 import {
 import {
   getUsernameByPath,
   getUsernameByPath,
   isCreatablePage,
   isCreatablePage,
@@ -7,8 +9,6 @@ import {
   isUsersTopPage,
   isUsersTopPage,
 } from '@growi/core/dist/utils/page-path-utils';
 } from '@growi/core/dist/utils/page-path-utils';
 import { diag } from '@opentelemetry/api';
 import { diag } from '@opentelemetry/api';
-import { createHash } from 'crypto';
-import type { IncomingMessage } from 'http';
 
 
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
@@ -132,7 +132,10 @@ export const pageAccessModule: AnonymizationModule = {
   /**
   /**
    * Handle anonymization for page access requests
    * Handle anonymization for page access requests
    */
    */
-  handle(request: IncomingMessage, url: string): Record<string, string> | null {
+  handle(
+    _request: IncomingMessage,
+    url: string,
+  ): Record<string, string> | null {
     try {
     try {
       const parsedUrl = new URL(url, 'http://localhost');
       const parsedUrl = new URL(url, 'http://localhost');
       const originalPath = parsedUrl.pathname;
       const originalPath = parsedUrl.pathname;

+ 1 - 1
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-api-handler.spec.ts

@@ -1,4 +1,4 @@
-import type { IncomingMessage } from 'http';
+import type { IncomingMessage } from 'node:http';
 
 
 import { beforeEach, describe, expect, it } from 'vitest';
 import { beforeEach, describe, expect, it } from 'vitest';
 
 

+ 5 - 2
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-api-handler.ts

@@ -1,5 +1,5 @@
+import type { IncomingMessage } from 'node:http';
 import { diag } from '@opentelemetry/api';
 import { diag } from '@opentelemetry/api';
-import type { IncomingMessage } from 'http';
 
 
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
@@ -28,7 +28,10 @@ export const pageApiModule: AnonymizationModule = {
   /**
   /**
    * Handle anonymization for page API endpoints
    * Handle anonymization for page API endpoints
    */
    */
-  handle(request: IncomingMessage, url: string): Record<string, string> | null {
+  handle(
+    _request: IncomingMessage,
+    url: string,
+  ): Record<string, string> | null {
     const attributes: Record<string, string> = {};
     const attributes: Record<string, string> = {};
     let hasAnonymization = false;
     let hasAnonymization = false;
 
 

+ 1 - 1
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-listing-api-handler.spec.ts

@@ -1,4 +1,4 @@
-import type { IncomingMessage } from 'http';
+import type { IncomingMessage } from 'node:http';
 
 
 import { beforeEach, describe, expect, it } from 'vitest';
 import { beforeEach, describe, expect, it } from 'vitest';
 
 

+ 5 - 2
apps/app/src/features/opentelemetry/server/anonymization/handlers/page-listing-api-handler.ts

@@ -1,5 +1,5 @@
+import type { IncomingMessage } from 'node:http';
 import { diag } from '@opentelemetry/api';
 import { diag } from '@opentelemetry/api';
-import type { IncomingMessage } from 'http';
 
 
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
@@ -28,7 +28,10 @@ export const pageListingApiModule: AnonymizationModule = {
   /**
   /**
    * Handle anonymization for page-listing API endpoints
    * Handle anonymization for page-listing API endpoints
    */
    */
-  handle(request: IncomingMessage, url: string): Record<string, string> | null {
+  handle(
+    _request: IncomingMessage,
+    url: string,
+  ): Record<string, string> | null {
     const attributes: Record<string, string> = {};
     const attributes: Record<string, string> = {};
     let hasAnonymization = false;
     let hasAnonymization = false;
 
 

+ 1 - 1
apps/app/src/features/opentelemetry/server/anonymization/handlers/search-api-handler.spec.ts

@@ -1,4 +1,4 @@
-import type { IncomingMessage } from 'http';
+import type { IncomingMessage } from 'node:http';
 
 
 import { beforeEach, describe, expect, it } from 'vitest';
 import { beforeEach, describe, expect, it } from 'vitest';
 
 

+ 1 - 1
apps/app/src/features/opentelemetry/server/anonymization/handlers/search-api-handler.ts

@@ -1,5 +1,5 @@
+import type { IncomingMessage } from 'node:http';
 import { diag } from '@opentelemetry/api';
 import { diag } from '@opentelemetry/api';
-import type { IncomingMessage } from 'http';
 
 
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import { ATTR_HTTP_TARGET } from '../../semconv';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';
 import type { AnonymizationModule } from '../interfaces/anonymization-module';

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