Browse Source

Merge remote-tracking branch 'origin/master' into feat/97800-159429-add-shortcuts-when-editing

WNomunomu 1 year ago
parent
commit
d471c6f026
100 changed files with 1616 additions and 895 deletions
  1. 1 0
      .devcontainer/.gitignore
  2. 9 0
      .devcontainer/compose.yml
  3. 2 0
      .github/workflows/ci-app.yml
  4. 3 3
      .github/workflows/codeql-analysis.yml
  5. 1 1
      .github/workflows/draft-release.yml
  6. 3 3
      .github/workflows/release-rc-scheduled.yml
  7. 2 2
      .github/workflows/release-rc.yml
  8. 4 4
      .github/workflows/release-slackbot-proxy.yml
  9. 4 4
      .github/workflows/release.yml
  10. 2 2
      .github/workflows/reusable-app-create-manifests.yml
  11. 4 14
      .github/workflows/reusable-app-prod.yml
  12. 62 1
      CHANGELOG.md
  13. 4 0
      apps/app/.env.development
  14. 4 0
      apps/app/.env.production
  15. 0 10
      apps/app/.eslintrc.js
  16. 1 0
      apps/app/bin/swagger-jsdoc/definition-apiv3.js
  17. 15 7
      apps/app/package.json
  18. 43 37
      apps/app/playwright/20-basic-features/comments.spec.ts
  19. 2 5
      apps/app/public/static/locales/en_US/admin.json
  20. 2 1
      apps/app/public/static/locales/en_US/translation.json
  21. 102 105
      apps/app/public/static/locales/fr_FR/admin.json
  22. 16 30
      apps/app/public/static/locales/fr_FR/commons.json
  23. 67 65
      apps/app/public/static/locales/fr_FR/translation.json
  24. 1 4
      apps/app/public/static/locales/ja_JP/admin.json
  25. 2 1
      apps/app/public/static/locales/ja_JP/translation.json
  26. 2 5
      apps/app/public/static/locales/zh_CN/admin.json
  27. 2 1
      apps/app/public/static/locales/zh_CN/translation.json
  28. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx
  29. 0 36
      apps/app/src/client/components/Admin/Security/FacebookSecuritySetting.jsx
  30. 1 1
      apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.jsx
  31. 0 8
      apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx
  32. 2 1
      apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx
  33. 10 90
      apps/app/src/client/components/ItemsTree/ItemsTree.tsx
  34. 2 3
      apps/app/src/client/components/LoginForm/ExternalAuthButton.tsx
  35. 0 6
      apps/app/src/client/components/LoginForm/LoginForm.module.scss
  36. 1 1
      apps/app/src/client/components/LoginForm/LoginForm.tsx
  37. 0 6
      apps/app/src/client/components/Me/AssociateModal.tsx
  38. 3 2
      apps/app/src/client/components/Me/DisassociateModal.tsx
  39. 6 7
      apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx
  40. 9 5
      apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  41. 38 32
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  42. 8 0
      apps/app/src/client/components/PagePathNavSticky/PagePathNavSticky.module.scss
  43. 1 3
      apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx
  44. 4 7
      apps/app/src/client/components/Sidebar/PageTree/PageTreeSubstance.tsx
  45. 2 1
      apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.tsx
  46. 8 6
      apps/app/src/client/components/TreeItem/TreeItemLayout.tsx
  47. 2 2
      apps/app/src/client/components/TreeItem/interfaces/index.ts
  48. 1 1
      apps/app/src/client/util/bookmark-utils.ts
  49. 1 1
      apps/app/src/components/Layout/BasicLayout.tsx
  50. 1 1
      apps/app/src/components/PageView/PageContentFooter.module.scss
  51. 4 6
      apps/app/src/components/PageView/PageContentFooter.tsx
  52. 1 1
      apps/app/src/components/PageView/PageViewLayout.tsx
  53. 1 1
      apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx
  54. 28 28
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  55. 7 6
      apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts
  56. 2 1
      apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.integ.ts
  57. 14 13
      apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.ts
  58. 12 10
      apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts
  59. 2 2
      apps/app/src/features/openai/server/routes/middlewares/certify-ai-service.ts
  60. 16 5
      apps/app/src/features/openai/server/services/assistant/assistant.ts
  61. 1 1
      apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts
  62. 1 1
      apps/app/src/features/openai/server/services/client.ts
  63. 4 4
      apps/app/src/features/openai/server/services/cron/thread-deletion-cron.ts
  64. 4 4
      apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts
  65. 1 1
      apps/app/src/features/openai/server/services/is-ai-enabled.ts
  66. 11 10
      apps/app/src/features/openai/server/services/openai.ts
  67. 89 0
      apps/app/src/features/openai/server/utils/convert-markdown-to-html.ts
  68. 0 65
      apps/app/src/features/openai/server/utils/sanitize-markdown.ts
  69. 1 0
      apps/app/src/features/opentelemetry/server/index.ts
  70. 76 0
      apps/app/src/features/opentelemetry/server/logger.ts
  71. 67 0
      apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts
  72. 103 0
      apps/app/src/features/opentelemetry/server/node-sdk.ts
  73. 2 2
      apps/app/src/features/questionnaire/interfaces/condition.ts
  74. 18 0
      apps/app/src/features/questionnaire/interfaces/growi-app-info.ts
  75. 0 58
      apps/app/src/features/questionnaire/interfaces/growi-info.ts
  76. 16 3
      apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts
  77. 14 4
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts
  78. 3 2
      apps/app/src/features/questionnaire/server/models/questionnaire-order.ts
  79. 2 2
      apps/app/src/features/questionnaire/server/models/schema/condition.ts
  80. 26 10
      apps/app/src/features/questionnaire/server/models/schema/growi-info.ts
  81. 20 11
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  82. 22 11
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts
  83. 301 0
      apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts
  84. 8 70
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  85. 9 6
      apps/app/src/features/questionnaire/server/util/condition.ts
  86. 128 0
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts
  87. 40 0
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts
  88. 2 1
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  89. 1 0
      apps/app/src/interfaces/activity.ts
  90. 13 1
      apps/app/src/interfaces/attachment.ts
  91. 3 2
      apps/app/src/interfaces/crowi-request.ts
  92. 9 0
      apps/app/src/interfaces/external-auth-provider.ts
  93. 0 7
      apps/app/src/interfaces/page-listing-results.ts
  94. 38 0
      apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js
  95. 24 0
      apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js
  96. 2 6
      apps/app/src/migrations/20180927102719-init-serverurl.js
  97. 6 6
      apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  98. 3 6
      apps/app/src/migrations/20190618104011-add-config-app-installed.js
  99. 0 1
      apps/app/src/migrations/20200512005851-remove-behavior-type.js
  100. 0 1
      apps/app/src/migrations/20200827045151-remove-layout-setting.js

+ 1 - 0
.devcontainer/.gitignore

@@ -0,0 +1 @@
+.env

+ 9 - 0
.devcontainer/compose.yml

@@ -7,7 +7,11 @@ services:
       - node_modules:/workspace/growi/node_modules
       - node_modules:/workspace/growi/node_modules
       - buildcache_app:/workspace/growi/apps/app/.next
       - buildcache_app:/workspace/growi/apps/app/.next
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
+      - ../../share:/workspace/share:delegated
     tty: true
     tty: true
+    networks:
+    - default
+    - opentelemetry-collector-dev-setup_default
 
 
   mongo:
   mongo:
     image: mongo:6.0
     image: mongo:6.0
@@ -44,3 +48,8 @@ volumes:
   pnpm-store:
   pnpm-store:
   node_modules:
   node_modules:
   buildcache_app:
   buildcache_app:
+
+networks:
+  default:
+  opentelemetry-collector-dev-setup_default:
+    external: ${OPENTELEMETRY_COLLECTOR_DEV_ENABLED:-false}

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

@@ -6,6 +6,8 @@ on:
       - release/**
       - release/**
       - rc/**
       - rc/**
       - changeset-release/**
       - changeset-release/**
+      - mergify/merge-queue/**
+      - tmp-mergify/merge-queue/**
     paths:
     paths:
       - .github/mergify.yml
       - .github/mergify.yml
       - .github/workflows/ci-app.yml
       - .github/workflows/ci-app.yml

+ 3 - 3
.github/workflows/codeql-analysis.yml

@@ -47,7 +47,7 @@ jobs:
 
 
     # Initializes the CodeQL tools for scanning.
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@v3
 
 
     # ℹ️ Command-line programs to run using the OS shell.
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
     # 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
     #   make release
     #   make release
 
 
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3

+ 1 - 1
.github/workflows/draft-release.yml

@@ -26,7 +26,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
 
 
       - name: Retrieve information from package.json
       - name: Retrieve information from package.json
-        uses: myrotvorets/info-from-package-json-action@2.0.1
+        uses: myrotvorets/info-from-package-json-action@v2.0.2
         id: package-json
         id: package-json
 
 
       - uses: release-drafter/release-drafter@v5
       - uses: release-drafter/release-drafter@v5

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

@@ -23,11 +23,11 @@ jobs:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
 
 
     - name: Docker meta for docker.io
     - name: Docker meta for docker.io
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       id: meta
       id: meta
       with:
       with:
         images: docker.io/weseek/growi
         images: docker.io/weseek/growi
@@ -36,7 +36,7 @@ jobs:
           type=raw,value=${{ steps.package-json.outputs.packageVersion }}.{{sha}}
           type=raw,value=${{ steps.package-json.outputs.packageVersion }}.{{sha}}
 
 
     - name: Docker meta for ghcr.io
     - name: Docker meta for ghcr.io
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       id: meta-ghcr
       id: meta-ghcr
       with:
       with:
         images: ghcr.io/weseek/growi
         images: ghcr.io/weseek/growi

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

@@ -23,11 +23,11 @@ jobs:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
 
 
     - name: Docker meta for docker.io
     - name: Docker meta for docker.io
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       id: meta
       id: meta
       with:
       with:
         images: docker.io/weseek/growi
         images: docker.io/weseek/growi

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

@@ -17,14 +17,14 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
       with:
       with:
         workingDir: apps/slackbot-proxy
         workingDir: apps/slackbot-proxy
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       with:
       with:
         images: weseek/growi-slackbot-proxy,asia.gcr.io/${{ secrets.GCP_PRJ_ID_SLACKBOT_PROXY }}/growi-slackbot-proxy
         images: weseek/growi-slackbot-proxy,asia.gcr.io/${{ secrets.GCP_PRJ_ID_SLACKBOT_PROXY }}/growi-slackbot-proxy
         tags: |
         tags: |
@@ -36,7 +36,7 @@ jobs:
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
 
     - name: Authenticate to Google Cloud for GROWI.cloud
     - name: Authenticate to Google Cloud for GROWI.cloud
-      uses: google-github-actions/auth@v1
+      uses: google-github-actions/auth@v2
       with:
       with:
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
 
 
@@ -110,7 +110,7 @@ jobs:
         turbo run version:prerelease --filter=@growi/slackbot-proxy
         turbo run version:prerelease --filter=@growi/slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
       with:
       with:
         workingDir: apps/slackbot-proxy
         workingDir: apps/slackbot-proxy

+ 4 - 4
.github/workflows/release.yml

@@ -40,7 +40,7 @@ jobs:
         sh ./apps/app/bin/github-actions/update-readme.sh
         sh ./apps/app/bin/github-actions/update-readme.sh
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
 
 
     - name: Update Changelog
     - name: Update Changelog
@@ -86,11 +86,11 @@ jobs:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
 
 
     - name: Docker meta for docker.io
     - name: Docker meta for docker.io
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       id: meta
       id: meta
       with:
       with:
         images: docker.io/weseek/growi
         images: docker.io/weseek/growi
@@ -179,7 +179,7 @@ jobs:
         turbo run version:prepatch --filter=@growi/slackbot-proxy
         turbo run version:prepatch --filter=@growi/slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
 
 
     - name: Commit
     - name: Commit

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

@@ -29,7 +29,7 @@ jobs:
     steps:
     steps:
     - name: Docker meta for extra-images
     - name: Docker meta for extra-images
       id: meta-extra-images
       id: meta-extra-images
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       with:
       with:
         images: ${{ inputs.registry }}/${{ inputs.image-name }}
         images: ${{ inputs.registry }}/${{ inputs.image-name }}
         sep-tags: ','
         sep-tags: ','
@@ -38,7 +38,7 @@ jobs:
           type=raw,value=${{ inputs.tag-temporary }}-arm64
           type=raw,value=${{ inputs.tag-temporary }}-arm64
 
 
     - name: Login to Container Registry
     - name: Login to Container Registry
-      uses: docker/login-action@v2
+      uses: docker/login-action@v3
       with:
       with:
         registry: ${{ inputs.registry }}
         registry: ${{ inputs.registry }}
         username: wsmoogle
         username: wsmoogle

+ 4 - 14
.github/workflows/reusable-app-prod.yml

@@ -50,18 +50,6 @@ jobs:
       run: |
       run: |
         pnpm install --frozen-lockfile
         pnpm install --frozen-lockfile
 
 
-    - name: Cache/Restore dist
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/.turbo
-          **/dist
-          **/node_modules/.cache/turbo
-          ${{ github.workspace }}/apps/app/.next
-        key: dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ github.sha }}
-        restore-keys: |
-          dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
-
     - name: Build
     - name: Build
       working-directory: ./apps/app
       working-directory: ./apps/app
       run: |
       run: |
@@ -181,7 +169,7 @@ jobs:
     container:
     container:
       # Match the Playwright version
       # Match the Playwright version
       # https://github.com/microsoft/playwright/issues/20010
       # https://github.com/microsoft/playwright/issues/20010
-      image: mcr.microsoft.com/playwright:v1.46.0-jammy
+      image: mcr.microsoft.com/playwright:v1.49.1-jammy
 
 
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
@@ -315,7 +303,9 @@ jobs:
         pnpm install --frozen-lockfile
         pnpm install --frozen-lockfile
 
 
     - name: Merge into HTML Report
     - name: Merge into HTML Report
-      run: pnpm playwright merge-reports --reporter html ./all-blob-reports
+      run: |
+        mkdir -p all-blob-reports
+        pnpm playwright merge-reports --reporter html ./all-blob-reports
 
 
     - name: Upload HTML report
     - name: Upload HTML report
       uses: actions/upload-artifact@v4
       uses: actions/upload-artifact@v4

+ 62 - 1
CHANGELOG.md

@@ -1,9 +1,70 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.1.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.1.9...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.1.9](https://github.com/weseek/growi/compare/v7.1.8...v7.1.9) - 2025-02-03
+
+### 💎 Features
+
+* feat: Add error handling for data migration (#9582) @miya
+
+### 🚀 Improvement
+
+* imprv: Data migration script performance (#9599) @miya
+* imprv: Initialization for Passport strategies (#9353) @yuki-takei
+* imprv: Make data migration type safe (#9590) @miya
+* imprv: Printing styles (#9576) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Serializing page data for share link (#9602) @yuki-takei
+
+### 🧰 Maintenance
+
+* ci(deps-dev): bump vite from 5.4.6 to 5.4.12 (#9574) @dependabot
+* ci(deps): bump mongoose from 6.13.0 to 6.13.6 (#9570) @dependabot
+* ci(deps): bump katex from 0.16.11 to 0.16.21 (#9564) @dependabot
+
+## [v7.1.8](https://github.com/weseek/growi/compare/v7.1.7...v7.1.8) - 2025-01-21
+
+### 🐛 Bug Fixes
+
+* fix: Escape page path when generating RegExp to find ancestors children (#9550) @yuki-takei
+
+## [v7.1.7](https://github.com/weseek/growi/compare/v7.1.6...v7.1.7) - 2025-01-16
+
+### 🐛 Bug Fixes
+
+* fix: Unable to select group viewing permissions (#9541) @miya
+* fix: Fix i18n of oidc settings (#9536) @ryu-sato
+
+### 🧰 Maintenance
+
+* support: Update Swagger documentation for the PUT endpoint to update a page (#9529) @tkfm1991
+* ci(deps): bump docker/metadata-action from 4 to 5 (#9181) @dependabot
+* ci(deps): bump github/codeql-action from 2 to 3 (#9180) @dependabot
+* ci(deps): bump next from 14.2.15 to 14.2.21 (#9538) @dependabot
+* ci(deps-dev): bump @marp-team/marp-core from 3.9.0 to 3.9.1 (#9530) @dependabot
+
+## [v7.1.6](https://github.com/weseek/growi/compare/v7.1.5...v7.1.6) - 2024-12-26
+
+### 💎 Features
+
+* feat(ai): Save file to VectorStore in HTML format   (#9462) @miya
+
+### 🐛 Bug Fixes
+
+* fix: remark-lsx pagination (#9513) @miya
+* fix: Spelling miss of external_link in i18n (#9456) @reiji-h
+* fix: Wider copy to clipboard area (#9450) @Ryosei-Fukushima
+* fix: Error when creating pages with deep hierarchy (#9487) @reiji-h
+
+### 🧰 Maintenance
+
+* ci(deps): bump next from 14.2.13 to 14.2.15 (#9501) @dependabot
+
 ## [v7.1.5](https://github.com/weseek/growi/compare/v7.1.4...v7.1.5) - 2024-12-13
 ## [v7.1.5](https://github.com/weseek/growi/compare/v7.1.4...v7.1.5) - 2024-12-13
 
 
 ### 🚀 Improvement
 ### 🚀 Improvement

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

@@ -30,3 +30,7 @@ QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # AUDIT_LOG_ACTION_GROUP_SIZE=SMALL
 # AUDIT_LOG_ACTION_GROUP_SIZE=SMALL
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
+
+# OpenTelemetry Configuration
+OPENTELEMETRY_ENABLED=false
+OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

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

@@ -4,3 +4,7 @@
 ##
 ##
 FORMAT_NODE_LOG=false
 FORMAT_NODE_LOG=false
 MIGRATIONS_DIR=dist/migrations/
 MIGRATIONS_DIR=dist/migrations/
+
+# OpenTelemetry Configuration
+OTEL_TRACES_SAMPLER_ARG=0.1
+

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

@@ -4,7 +4,6 @@ module.exports = {
     'weseek/react',
     'weseek/react',
   ],
   ],
   plugins: [
   plugins: [
-    'regex',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript
@@ -17,15 +16,6 @@ module.exports = {
       name: 'axios',
       name: 'axios',
       message: 'Please use src/utils/axios instead.',
       message: 'Please use src/utils/axios instead.',
     }],
     }],
-    'regex/invalid': ['error', [
-      {
-        regex: '\\?\\<\\!',
-        message: 'Do not use any negative lookbehind',
-      }, {
-        regex: '\\?\\<\\=',
-        message: 'Do not use any Positive lookbehind',
-      },
-    ]],
     '@typescript-eslint/no-var-requires': 'off',
     '@typescript-eslint/no-var-requires': 'off',
 
 
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei

+ 1 - 0
apps/app/bin/swagger-jsdoc/definition-apiv3.js

@@ -81,6 +81,7 @@ module.exports = {
         'UserGroups',
         'UserGroups',
         'Users Management',
         'Users Management',
         'FullTextSearch Management',
         'FullTextSearch Management',
+        'Install',
       ],
       ],
     },
     },
     {
     {

+ 15 - 7
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.1.6-RC.0",
+  "version": "7.2.0-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "scripts": {
@@ -21,7 +21,6 @@
     "dev:pre:styles": "pnpm run pre:styles --mode dev",
     "dev:pre:styles": "pnpm run pre:styles --mode dev",
     "dev:migrate-mongo": "cross-env NODE_ENV=development pnpm run ts-node node_modules/migrate-mongo/bin/migrate-mongo",
     "dev:migrate-mongo": "cross-env NODE_ENV=development pnpm run ts-node node_modules/migrate-mongo/bin/migrate-mongo",
     "dev:migrate": "pnpm run dev:migrate:status > tmp/cache/migration-status.out && pnpm run dev:migrate:up",
     "dev:migrate": "pnpm run dev:migrate:status > tmp/cache/migration-status.out && pnpm run dev:migrate:up",
-    "dev:migrate:create": "pnpm run dev:migrate-mongo create -f config/migrate-mongo-config.js",
     "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:up": "pnpm run dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:up": "pnpm run dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "pnpm run dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "dev:migrate:down": "pnpm run dev:migrate-mongo down -f config/migrate-mongo-config.js",
@@ -84,6 +83,15 @@
     "@growi/remark-lsx": "workspace:^",
     "@growi/remark-lsx": "workspace:^",
     "@growi/slack": "workspace:^",
     "@growi/slack": "workspace:^",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@keycloak/keycloak-admin-client": "^18.0.0",
+    "@opentelemetry/api": "^1.9.0",
+    "@opentelemetry/auto-instrumentations-node": "^0.55.1",
+    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.0",
+    "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
+    "@opentelemetry/resources": "^1.28.0",
+    "@opentelemetry/semantic-conventions": "^1.28.0",
+    "@opentelemetry/sdk-metrics": "^1.28.0",
+    "@opentelemetry/sdk-node": "^0.57.0",
+    "@opentelemetry/sdk-trace-node": "^1.28.0",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
     "JSONStream": "^1.3.5",
     "JSONStream": "^1.3.5",
@@ -114,7 +122,6 @@
     "ejs": "^3.1.10",
     "ejs": "^3.1.10",
     "esa-node": "^0.2.2",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "^4.0.0",
     "escape-string-regexp": "^4.0.0",
-    "eslint-plugin-regex": "^1.8.0",
     "expose-gc": "^1.0.0",
     "expose-gc": "^1.0.0",
     "express": "^4.20.0",
     "express": "^4.20.0",
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
@@ -135,7 +142,7 @@
     "is-iso-date": "^0.0.1",
     "is-iso-date": "^0.0.1",
     "js-tiktoken": "^1.0.15",
     "js-tiktoken": "^1.0.15",
     "js-yaml": "^4.1.0",
     "js-yaml": "^4.1.0",
-    "katex": "^0.16.11",
+    "katex": "^0.16.21",
     "ldapjs": "^3.0.2",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^3.0.3",
     "markdown-table": "^3.0.3",
@@ -150,14 +157,14 @@
     "migrate-mongo": "^11.0.0",
     "migrate-mongo": "^11.0.0",
     "mkdirp": "^1.0.3",
     "mkdirp": "^1.0.3",
     "mongodb": "^4.17.2",
     "mongodb": "^4.17.2",
-    "mongoose": "^6.11.3",
+    "mongoose": "^6.13.6",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
     "mustache": "^4.2.0",
-    "next": "^14.2.15",
+    "next": "^14.2.21",
     "next-dynamic-loading-props": "^0.1.1",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
     "next-i18next": "^15.3.1",
     "next-superjson": "^0.0.4",
     "next-superjson": "^0.0.4",
@@ -197,9 +204,11 @@
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rehype-katex": "^7.0.1",
     "rehype-katex": "^7.0.1",
+    "rehype-meta": "^4.0.1",
     "rehype-raw": "^7.0.0",
     "rehype-raw": "^7.0.0",
     "rehype-sanitize": "^6.0.0",
     "rehype-sanitize": "^6.0.0",
     "rehype-slug": "^6.0.0",
     "rehype-slug": "^6.0.0",
+    "rehype-stringify": "^10.0.1",
     "rehype-toc": "^3.0.2",
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^4.0.0",
     "remark-breaks": "^4.0.0",
     "remark-directive": "^3.0.0",
     "remark-directive": "^3.0.0",
@@ -281,7 +290,6 @@
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-jest": "^26.5.3",
-    "eslint-plugin-regex": "^1.8.0",
     "fslightbox-react": "^1.7.6",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
     "happy-dom": "^15.7.4",
     "happy-dom": "^15.7.4",

+ 43 - 37
apps/app/playwright/20-basic-features/comments.spec.ts

@@ -1,49 +1,55 @@
 import { test, expect } from '@playwright/test';
 import { test, expect } from '@playwright/test';
 
 
-test('Create comment page', async({ page }) => {
-  await page.goto('/comment');
-  await page.getByTestId('editor-button').click();
-  await page.getByTestId('save-page-btn').click();
-  await expect(page.locator('.page-meta')).toBeVisible();
-});
+test.describe('Comment', () => {
 
 
-test('Successfully add comments', async({ page }) => {
-  const commentText = 'add comment';
-  await page.goto('/comment');
+  // make tests run in serial
+  test.describe.configure({ mode: 'serial' });
 
 
-  // Add comment
-  await page.getByTestId('page-comment-button').click();
-  await page.getByTestId('open-comment-editor-button').click();
-  await page.locator('.cm-content').fill(commentText);
-  await page.getByTestId('comment-submit-button').first().click();
+  test('Create comment page', async({ page }) => {
+    await page.goto('/comment');
+    await page.getByTestId('editor-button').click();
+    await page.getByTestId('save-page-btn').click();
+    await expect(page.locator('.page-meta')).toBeVisible();
+  });
 
 
-  await expect(page.locator('.page-comment-body')).toHaveText(commentText);
-  await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('1');
-});
+  test('Successfully add comments', async({ page }) => {
+    const commentText = 'add comment';
+    await page.goto('/comment');
 
 
-test('Successfully reply comments', async({ page }) => {
-  const commentText = 'reply comment';
-  await page.goto('/comment');
+    // Add comment
+    await page.getByTestId('page-comment-button').click();
+    await page.getByTestId('open-comment-editor-button').click();
+    await page.locator('.cm-content').fill(commentText);
+    await page.getByTestId('comment-submit-button').first().click();
 
 
-  // Reply comment
-  await page.getByTestId('page-comment-button').click();
-  await page.getByTestId('comment-reply-button').click();
-  await page.locator('.cm-content').fill(commentText);
-  await page.getByTestId('comment-submit-button').first().click();
+    await expect(page.locator('.page-comment-body')).toHaveText(commentText);
+    await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('1');
+  });
 
 
-  await expect(page.locator('.page-comment-body').nth(1)).toHaveText(commentText);
-  await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('2');
-});
+  test('Successfully reply comments', async({ page }) => {
+    const commentText = 'reply comment';
+    await page.goto('/comment');
+
+    // Reply comment
+    await page.getByTestId('comment-reply-button').click();
+    await page.locator('.cm-content').fill(commentText);
+    await page.getByTestId('comment-submit-button').first().click();
 
 
-// test('Successfully delete comments', async({ page }) => {
-//   await page.goto('/comment');
+    await expect(page.locator('.page-comment-body').nth(1)).toHaveText(commentText);
+    await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('2');
+  });
 
 
-//   await page.getByTestId('page-comment-button').click();
-//   await page.getByTestId('comment-delete-button').first().click({ force: true });
-//   await expect(page.getByTestId('page-comment-delete-modal')).toBeVisible();
-//   await page.getByTestId('delete-comment-button').click();
+  // test('Successfully delete comments', async({ page }) => {
+  //   await page.goto('/comment');
 
 
-//   await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('0');
-// });
+  //   await page.getByTestId('page-comment-button').click();
+  //   await page.getByTestId('comment-delete-button').first().click({ force: true });
+  //   await expect(page.getByTestId('page-comment-delete-modal')).toBeVisible();
+  //   await page.getByTestId('delete-comment-button').click();
 
 
-// TODO: https://redmine.weseek.co.jp/issues/139520
+  //   await expect(page.getByTestId('page-comment-button').locator('.grw-count-badge')).toHaveText('0');
+  // });
+
+  // TODO: https://redmine.weseek.co.jp/issues/139520
+
+});

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

@@ -184,9 +184,6 @@
         "register_5": "Copy and paste your ClientID and Client Secret above",
         "register_5": "Copy and paste your ClientID and Client Secret above",
         "updated_google": "Succeeded to update Google OAuth setting"
         "updated_google": "Succeeded to update Google OAuth setting"
       },
       },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "Enable GitHub OAuth",
         "enable_github": "Enable GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
@@ -200,9 +197,9 @@
         "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
         "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
         "username_detail": "Specification of mappings for <code>username</code> when creating new users",
         "username_detail": "Specification of mappings for <code>username</code> when creating new users",
         "name_detail": "Specification of mappings for <code>name</code> when creating new users",
         "name_detail": "Specification of mappings for <code>name</code> when creating new users",
-        "mapping_detail": "Specification of mappings for %s when creating new users",
+        "mapping_detail": "Specification of mappings for {{target}} when creating new users",
         "register_1": "Contact to OIDC IdP Administrator",
         "register_1": "Contact to OIDC IdP Administrator",
-        "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
+        "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>{{url}}</code>",
         "register_3": "Copy and paste your ClientID and Client Secret above",
         "register_3": "Copy and paste your ClientID and Client Secret above",
         "updated_oidc": "Succeeded to update OpenID Connect",
         "updated_oidc": "Succeeded to update OpenID Connect",
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"

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

@@ -743,7 +743,8 @@
   "pagetree": {
   "pagetree": {
     "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'",
     "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'",
     "you_cannot_move_this_page_now": "You cannot move this page now",
     "you_cannot_move_this_page_now": "You cannot move this page now",
-    "something_went_wrong_with_moving_page": "Something went wrong with moving page"
+    "something_went_wrong_with_moving_page": "Something went wrong with moving page",
+    "error_retrieving_the_pagetree": "Error occurred while retrieving the PageTree"
   },
   },
   "duplicated_page_alert": {
   "duplicated_page_alert": {
     "same_page_name_exists": "Same page name exits as「{{pageName}}」",
     "same_page_name_exists": "Same page name exits as「{{pageName}}」",

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

@@ -3,7 +3,7 @@
     "display_name": "Français"
     "display_name": "Français"
   },
   },
   "last_login": "Dernière connexion",
   "last_login": "Dernière connexion",
-  "wiki_management_homepage": "Gestion du wiki",
+  "wiki_management_homepage": "Système",
   "public": "Public",
   "public": "Public",
   "anyone_with_the_link": "Tous les utilisateurs disposant du lien",
   "anyone_with_the_link": "Tous les utilisateurs disposant du lien",
   "specified_users": "Utilisateurs spécifiés",
   "specified_users": "Utilisateurs spécifiés",
@@ -11,7 +11,7 @@
   "only_inside_the_group": "Utilisateurs du groupe",
   "only_inside_the_group": "Utilisateurs du groupe",
   "optional": "Optionnel",
   "optional": "Optionnel",
   "security_settings": {
   "security_settings": {
-    "security_settings": "Paramètres de sécurité",
+    "security_settings": "Sécurité",
     "scope_of_page_disclosure": "Confidentialité de la page",
     "scope_of_page_disclosure": "Confidentialité de la page",
     "set_point": "Valeur",
     "set_point": "Valeur",
     "Guest Users Access": "Accès invité",
     "Guest Users Access": "Accès invité",
@@ -29,13 +29,13 @@
     "for_example": " Par exemple, pour restreindre l'inscription aux utilisateurs dans le domaine growi.org, ajouter ",
     "for_example": " Par exemple, pour restreindre l'inscription aux utilisateurs dans le domaine growi.org, ajouter ",
     "in_this_case": "; dans ce cas particulier, seul les utilisateurs du domaine growi.org peuvent s'inscrire.",
     "in_this_case": "; dans ce cas particulier, seul les utilisateurs du domaine growi.org peuvent s'inscrire.",
     "insert_single": "Insérer une adresse courriel par ligne",
     "insert_single": "Insérer une adresse courriel par ligne",
-    "page_list_and_search_results": "Liste et recherche de pages",
+    "page_list_and_search_results": "Affichage des pages",
     "page_listing_1": "Liste et recherche de pages<br>restreint à 'Seulement moi'",
     "page_listing_1": "Liste et recherche de pages<br>restreint à 'Seulement moi'",
     "page_listing_1_desc": "Voir les pages restreintes à 'Seulement moi' lors de la recherche",
     "page_listing_1_desc": "Voir les pages restreintes à 'Seulement moi' lors de la recherche",
     "page_listing_2": "Liste et recherche de pages<br>restreint au groupe utilisateur",
     "page_listing_2": "Liste et recherche de pages<br>restreint au groupe utilisateur",
     "page_listing_2_desc": "Voir les pages restreintes au groupe utilisateur lors de la recherche",
     "page_listing_2_desc": "Voir les pages restreintes au groupe utilisateur lors de la recherche",
-    "page_access_rights": "Droits de lecture",
-    "page_delete_rights": "Droits de suppression",
+    "page_access_rights": "Lecture",
+    "page_delete_rights": "Suppression",
     "page_delete": "Suppression de page",
     "page_delete": "Suppression de page",
     "page_delete_completely": "Suppression complète de page",
     "page_delete_completely": "Suppression complète de page",
     "comment_manage_rights": "Droits de gestion des commentaires",
     "comment_manage_rights": "Droits de gestion des commentaires",
@@ -52,8 +52,8 @@
     "anyone": "Tout le monde",
     "anyone": "Tout le monde",
     "user_homepage_deletion": {
     "user_homepage_deletion": {
       "user_homepage_deletion": "Suppression de page d'accueil utilisateur",
       "user_homepage_deletion": "Suppression de page d'accueil utilisateur",
-      "enable_user_homepage_deletion": "Activer la suppression de page d'accueil utilisateur",
-      "enable_force_delete_user_homepage_on_user_deletion": "Lorsqu'un utilisateur est supprimé, sa page d'accueil et ses sous-pages sont supprimées.",
+      "enable_user_homepage_deletion": "Suppression de page d'accueil utilisateur",
+      "enable_force_delete_user_homepage_on_user_deletion": "Supprimer la page d'accueil et ses pages enfants",
       "desc": "Les pages d'accueil utilisateurs pourront être supprimées."
       "desc": "Les pages d'accueil utilisateurs pourront être supprimées."
     },
     },
     "session": "Session",
     "session": "Session",
@@ -88,8 +88,8 @@
       "readonly": "Autoriser (Lecture seule)"
       "readonly": "Autoriser (Lecture seule)"
     },
     },
     "read_only_users_comment": {
     "read_only_users_comment": {
-      "deny": "Refuser (Interdire la gestion des commentaires aux utilisateurs lecture seule)",
-      "accept": "Autoriser (Les utilisateurs lecture seule peuvent gérer les commentaires)"
+      "deny": "Ne peut pas commenter",
+      "accept": "Peut commenter"
     },
     },
     "registration_mode": {
     "registration_mode": {
       "open": "Ouvert (Tout le monde peut s'inscrire)",
       "open": "Ouvert (Tout le monde peut s'inscrire)",
@@ -97,9 +97,9 @@
       "closed": "Fermé (Invitation seulement)"
       "closed": "Fermé (Invitation seulement)"
     },
     },
     "share_link_management": "Gestion des liens de partage",
     "share_link_management": "Gestion des liens de partage",
-    "No_share_links":"Aucun liens de partage",
-    "share_link_notice":"Retirer les liens de partage",
-    "delete_all_share_links":"Supprimer tout les liens de partage",
+    "No_share_links": "Aucun liens de partage",
+    "share_link_notice": "Retirer les liens de partage",
+    "delete_all_share_links": "Supprimer tout les liens de partage",
     "share_link_rights": "Permissions de liens de partage",
     "share_link_rights": "Permissions de liens de partage",
     "enable_link_sharing": "Activer les liens de partage",
     "enable_link_sharing": "Activer les liens de partage",
     "all_share_links": "Liens de partage",
     "all_share_links": "Liens de partage",
@@ -184,9 +184,6 @@
         "register_5": "Copier l'ID client et Secret client ci-dessus",
         "register_5": "Copier l'ID client et Secret client ci-dessus",
         "updated_google": "Paramètres mis à jour"
         "updated_google": "Paramètres mis à jour"
       },
       },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "Activer GitHub OAuth",
         "enable_github": "Activer GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
@@ -200,9 +197,9 @@
         "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
         "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
         "username_detail": "Spécifications des liaisons <code>username</code> lors de la création de nouveaux utilisateurs",
         "username_detail": "Spécifications des liaisons <code>username</code> lors de la création de nouveaux utilisateurs",
         "name_detail": "Spécifications des liaisons <code>name</code> lors de la création de nouveaux utilisateurs",
         "name_detail": "Spécifications des liaisons <code>name</code> lors de la création de nouveaux utilisateurs",
-        "mapping_detail": "Spécifications des liaisons pour %s lors de la création de nouveaux utilisateurs",
+        "mapping_detail": "Spécifications des liaisons pour {{target}} lors de la création de nouveaux utilisateurs",
         "register_1": "Contacter votre administrateur OIDC",
         "register_1": "Contacter votre administrateur OIDC",
-        "register_2": "Configurer l'application OAuth avec l'un des URL de redirection autorisés avec <code>%s</code>",
+        "register_2": "Configurer l'application OAuth avec l'un des URL de redirection autorisés avec <code>{{url}}</code>",
         "register_3": "Copier l'ID client et Secret client ci-dessus",
         "register_3": "Copier l'ID client et Secret client ci-dessus",
         "updated_oidc": "Paramètres mis à jour",
         "updated_oidc": "Paramètres mis à jour",
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
@@ -232,7 +229,7 @@
     "prioritize_webhook_desc": "Activer cette option utilisera les webhook entrants plutôt que Slack.",
     "prioritize_webhook_desc": "Activer cette option utilisera les webhook entrants plutôt que Slack.",
     "slack_app_configuration": "Configuration de l'application Slack",
     "slack_app_configuration": "Configuration de l'application Slack",
     "slack_app_configuration_desc": "Cette méthode n'est pas recommandée, car trop complexe.",
     "slack_app_configuration_desc": "Cette méthode n'est pas recommandée, car trop complexe.",
-    "use_instead":"Utiliser plutôt les webhook entrants Slack",
+    "use_instead": "Utiliser plutôt les webhook entrants Slack",
     "how_to": {
     "how_to": {
       "header": "Comment configurer un webhook entrant?",
       "header": "Comment configurer un webhook entrant?",
       "workspace": "(Dans le Workspace) Ajouter un webhook",
       "workspace": "(Dans le Workspace) Ajouter un webhook",
@@ -277,7 +274,7 @@
     "not_found_global_notification_triggerid": "ID global de notification introuvable"
     "not_found_global_notification_triggerid": "ID global de notification introuvable"
   },
   },
   "full_text_search_management": {
   "full_text_search_management": {
-    "full_text_search_management": "Configuration de la recherche",
+    "full_text_search_management": "Moteur de recherche",
     "elasticsearch_management": "Configuration Elasticsearch",
     "elasticsearch_management": "Configuration Elasticsearch",
     "connection_status": "Statut",
     "connection_status": "Statut",
     "connection_status_label_unconfigured": "UNCONFIGURED",
     "connection_status_label_unconfigured": "UNCONFIGURED",
@@ -299,23 +296,23 @@
     "rebuild_description_1": "Reconstruire l'index est les données de pages",
     "rebuild_description_1": "Reconstruire l'index est les données de pages",
     "rebuild_description_2": "Cela peut prendre un certain temps."
     "rebuild_description_2": "Cela peut prendre un certain temps."
   },
   },
-  "mailer_setup_required":"<a href='/admin/app'>Configuration Email</a> sont requis pour envoyer.",
+  "mailer_setup_required": "La <a href='/admin/app'>configuration du SMTP</a> est requise.",
   "admin_top": {
   "admin_top": {
     "management_wiki": "Configuration du wiki",
     "management_wiki": "Configuration du wiki",
     "system_information": "Information système",
     "system_information": "Information système",
-    "wiki_administrator": "Seuls les administrateurs peuvent accéder à cette page",
+    "wiki_administrator": "Seuls les administrateurs peuvent accéder à cette page.",
     "assign_administrator": "Il est possible d'assigner l'accès administrateur en utilisant le bouton 'Ajouter accès administrateur'",
     "assign_administrator": "Il est possible d'assigner l'accès administrateur en utilisant le bouton 'Ajouter accès administrateur'",
     "package_name": "Nom du paquet",
     "package_name": "Nom du paquet",
     "specified_version": "Version spécifiée",
     "specified_version": "Version spécifiée",
     "installed_version": "Version installée",
     "installed_version": "Version installée",
-    "list_of_env_vars":"Variables d'environnement",
-    "env_var_priority": "Les valeurs de la base de données sont priorisées.",
-    "about_security": "Voir les <a href='/admin/security'>paramètres de sécurité</a> pour les variables d'environnement de sécurité.",
+    "list_of_env_vars": "Variables d'environnement",
+    "env_var_priority": "Les valeurs enregistrées dans la base de données sont priorisées.",
+    "about_security": "Pour les variables des paramètres de sécurité, consulter les <a href='/admin/security'>paramètres de sécurité.</a>",
     "copy_prefilled_host_information": {
     "copy_prefilled_host_information": {
       "default": "Copier les informations",
       "default": "Copier les informations",
       "done": "Copié dans le presse-papier!"
       "done": "Copié dans le presse-papier!"
     },
     },
-    "bug_report": "Soumettre un rapport de bogue",
+    "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/weseek/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": {
@@ -345,31 +342,31 @@
     "description": "Le mode maintenance restreint l'utilisation de GROWI. Toujours démarrer le mode maintenance avant l'\"import de données\" et la \"conversion vers la V5\"."
     "description": "Le mode maintenance restreint l'utilisation de GROWI. Toujours démarrer le mode maintenance avant l'\"import de données\" et la \"conversion vers la V5\"."
   },
   },
   "app_setting": {
   "app_setting": {
-    "site_name": "Nom du site",
+    "site_name": "Nom",
     "sitename_change": "Le nom du site utilisé dans l'en-tête et le titre HTML.",
     "sitename_change": "Le nom du site utilisé dans l'en-tête et le titre HTML.",
     "header_content": "Le contenu entré ici sera affiché dans l'en-tête, etc. ",
     "header_content": "Le contenu entré ici sera affiché dans l'en-tête, etc. ",
     "site_url": {
     "site_url": {
-      "title": "Configuration de l'URL du site",
-      "desc": "Configuration de l'URL du site",
-      "warn": "Certaines fonctionnalitées peuvent ne pas fonctionner tant que l'URL du site n'est pas définie.",
-      "help": "URL complet du site démarrant par <code>http://</code> ou <code>https://</code>.",
+      "title": "Adresse publique",
+      "desc": "Adresse URL publique de l'application.",
+      "warn": "Certaines fonctionnalitées sont restreintes tant que l'URL du site n'est pas définie.",
+      "help": "URL complète démarrant par <code>http://</code> ou <code>https://</code>.",
       "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> ."
       "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> ."
     },
     },
-    "confidential_name": "Nom confidentiel",
+    "confidential_name": "Nom interne",
     "confidential_example": "ex): usage interne seulement",
     "confidential_example": "ex): usage interne seulement",
     "default_language": "Langue par défaut",
     "default_language": "Langue par défaut",
-    "default_mail_visibility": "Afficher l'adresse courriel pour les nouveaux utilisateurs",
+    "default_mail_visibility": "Mode d'affichage de l'adresse courriel",
     "file_uploading": "Téléversement de fichiers",
     "file_uploading": "Téléversement de fichiers",
-    "enable_files_except_image": "Autorise le téléversement de fichiers de n'importe quel type. Lorsque désactivé, seul les fichiers de type image sont autorisés.",
-    "attach_enable": "Autorise le téléversement de fichiers de n'importe quel type",
+    "enable_files_except_image": "Autoriser tout les types de fichiers",
+    "attach_enable": "Autorise le téléversement de tout les types de fichiers.",
     "update": "Sauvegarder",
     "update": "Sauvegarder",
-    "mail_settings": "Configuration e-mail",
-    "mailer_is_not_set_up": "Paramètres e-mail non configurés.",
+    "mail_settings": "SMTP",
+    "mailer_is_not_set_up": "Paramètres d'envoi de courriels non configurés.",
     "from_e-mail_address": "Adresse courriel <code>from</code>",
     "from_e-mail_address": "Adresse courriel <code>from</code>",
-    "transmission_method":"Méthode de transmission",
-    "smtp_label":"SMTP",
-    "ses_label":"SES(AWS)",
-    "send_test_email": "Envoi d'un courriel d'essai",
+    "transmission_method": "Mode",
+    "smtp_label": "SMTP",
+    "ses_label": "SES(AWS)",
+    "send_test_email": "Courriel d'essai",
     "success_to_send_test_email": "Courriel d'essai envoyé",
     "success_to_send_test_email": "Courriel d'essai envoyé",
     "smtp_settings": "Configuration SMTP",
     "smtp_settings": "Configuration SMTP",
     "host": "Hôte",
     "host": "Hôte",
@@ -378,13 +375,13 @@
     "initialize_mail_settings": "réinitialiser les paramètres e-mail",
     "initialize_mail_settings": "réinitialiser les paramètres e-mail",
     "initialize_mail_modal_header": "Réinitialiser les paramètres e-mail",
     "initialize_mail_modal_header": "Réinitialiser les paramètres e-mail",
     "confirm_to_initialize_mail_settings": "Les valeurs existantes seront écrasées. Réinitialiser les paramètres e-mail?",
     "confirm_to_initialize_mail_settings": "Les valeurs existantes seront écrasées. Réinitialiser les paramètres e-mail?",
-    "file_upload_settings":"Configuration du téléversement",
-    "file_upload_method":"Méthode de téléversement",
-    "file_delivery_method":"Méthode de récupération",
-    "file_delivery_method_redirect":"Rediriger",
-    "file_delivery_method_relay":"Relai interne du système",
-    "file_delivery_method_redirect_info":"Rediriger: Redirige vers une URL signé, performance excellente.",
-    "file_delivery_method_relay_info":"Relai interne du système: Le serveur GROWI sert les fichiers directement au client, sécurité complète.",
+    "file_upload_settings": "Téléversement de fichiers",
+    "file_upload_method": "Mode",
+    "file_delivery_method": "Méthode de récupération",
+    "file_delivery_method_redirect": "Rediriger",
+    "file_delivery_method_relay": "Relai interne du système",
+    "file_delivery_method_redirect_info": "Rediriger: Redirige vers une URL signé, performance excellente.",
+    "file_delivery_method_relay_info": "Relai interne du système: Le serveur GROWI sert les fichiers directement au client, sécurité complète.",
     "fixed_by_env_var": "Défini par une variable d'environnement <code>FILE_UPLOAD={{fileUploadType}}</code>.",
     "fixed_by_env_var": "Défini par une variable d'environnement <code>FILE_UPLOAD={{fileUploadType}}</code>.",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
@@ -410,7 +407,7 @@
     "disable": "Désactiver",
     "disable": "Désactiver",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
     "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
     "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
-    "questionnaire_settings": "Données analytiques",
+    "questionnaire_settings": "Sondages anonymes",
     "questionnaire_settings_explanation": "Paramètres d'activation des données analytiques. L'utilisateur peut choisir ce paramètre individuellement dans \"Autres paramètres\".",
     "questionnaire_settings_explanation": "Paramètres d'activation des données analytiques. L'utilisateur peut choisir ce paramètre individuellement dans \"Autres paramètres\".",
     "about_data_sent": "À propos",
     "about_data_sent": "À propos",
     "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
     "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
@@ -422,46 +419,46 @@
     "enable_questionnaire": "Activer les données analytiques"
     "enable_questionnaire": "Activer les données analytiques"
   },
   },
   "markdown_settings": {
   "markdown_settings": {
-    "markdown_settings": "Configuration Markdown",
-    "lineBreak_header": "Configuration du saut de ligne",
-    "lineBreak_desc": "Configuration du saut de ligne.",
+    "markdown_settings": "Markdown",
+    "lineBreak_header": "Saut de ligne",
+    "lineBreak_desc": "Conversion du saut de ligne automatique.",
     "lineBreak_options": {
     "lineBreak_options": {
-      "enable_lineBreak": "Activer le saut de ligne",
+      "enable_lineBreak": "Saut de ligne",
       "enable_lineBreak_desc": "Convertir le saut de ligne<code>&lt;br&gt;</code>en HTML",
       "enable_lineBreak_desc": "Convertir le saut de ligne<code>&lt;br&gt;</code>en HTML",
-      "enable_lineBreak_for_comment": "Activer le saut de ligne dans les commentaires",
+      "enable_lineBreak_for_comment": "Saut de ligne dans les commentaires",
       "enable_lineBreak_for_comment_desc": "Convertir le saut de ligne dans les commentaires<code>&lt;br&gt;</code>en HTML"
       "enable_lineBreak_for_comment_desc": "Convertir le saut de ligne dans les commentaires<code>&lt;br&gt;</code>en HTML"
     },
     },
-    "indent_header": "Configuration de l'indentation",
-    "indent_desc": "Configuration de l'indentation",
+    "indent_header": "Indentation",
+    "indent_desc": "Taille d'indentation dans une page.",
     "indent_options": {
     "indent_options": {
-      "indentSize": "Taille par défaut",
+      "indentSize": "Valeur par défaut",
       "indentSize_desc": "Taille par défaut de l'indentation dans l'éditeur Markdown",
       "indentSize_desc": "Taille par défaut de l'indentation dans l'éditeur Markdown",
-      "disallow_indent_change": "Empêcher le changement de taille d'indentation",
-      "disallow_indent_change_desc": "Forcer l'usage de la taille par défaut"
+      "disallow_indent_change": "Empêcher la modification",
+      "disallow_indent_change_desc": "Impose l'usage de la valeur par défaut définie dans les paramètres"
     },
     },
-    "xss_header": "Configuration prévention XSS",
+    "xss_header": "Prévention des attaques XSS",
     "xss_desc": "Configuration de la prévention des attaques XSS (cross-site scripting).",
     "xss_desc": "Configuration de la prévention des attaques XSS (cross-site scripting).",
     "xss_options": {
     "xss_options": {
-      "enable_xss_prevention": "Activer prévention XSS",
+      "enable_xss_prevention": "Prévention XSS",
       "remove_all_tags": "Retirer tout les tags",
       "remove_all_tags": "Retirer tout les tags",
       "remove_all_tags_desc": "Retire tout les tags HTML et CSS",
       "remove_all_tags_desc": "Retire tout les tags HTML et CSS",
       "recommended_setting": "Paramètres recommandés",
       "recommended_setting": "Paramètres recommandés",
       "custom_whitelist": "Liste autorisée",
       "custom_whitelist": "Liste autorisée",
       "tag_names": "Nom de tags",
       "tag_names": "Nom de tags",
       "tag_attributes": "Attributs de tags",
       "tag_attributes": "Attributs de tags",
-      "import_recommended": "Importer les recommendations {{target}}"
+      "import_recommended": "Importer {{target}}"
     }
     }
   },
   },
   "customize_settings": {
   "customize_settings": {
-    "customize_settings": "Personnalisation",
+    "customize_settings": "Interface",
     "default_sidebar_mode": {
     "default_sidebar_mode": {
-      "title": "Mode par défaut de la barre latérale",
-      "desc": "Le mode d'affichage par défaut de la barre latérale pour les utilisateurs.",
+      "title": "Barre latérale",
+      "desc": "Mode d'affichage et comportement par défaut de la barre latérale.",
       "dock_mode_default_desc": "État initial de la barre latérale lorsque le mode Dock est sélectionné.",
       "dock_mode_default_desc": "État initial de la barre latérale lorsque le mode Dock est sélectionné.",
       "dock_mode_default_open": "Afficher la page comme si elle était ouverte",
       "dock_mode_default_open": "Afficher la page comme si elle était ouverte",
       "dock_mode_default_close": "Afficher la page comme si elle était fermée"
       "dock_mode_default_close": "Afficher la page comme si elle était fermée"
     },
     },
-    "layout": "Agencement",
+    "layout": "Largeur du contenu",
     "layout_options": {
     "layout_options": {
       "default": "Largeur par défaut",
       "default": "Largeur par défaut",
       "expanded": "100%"
       "expanded": "100%"
@@ -481,8 +478,8 @@
       "tab_switch": "Sauvegarder le changement d'onglets",
       "tab_switch": "Sauvegarder le changement d'onglets",
       "tab_switch_desc1": "Sauvegarde l'état de navigation dans le navigateur de l'utilisateur.",
       "tab_switch_desc1": "Sauvegarde l'état de navigation dans le navigateur de l'utilisateur.",
       "tab_switch_desc2": "Lorsque désactivé, la navigation est forcé par l'interface.",
       "tab_switch_desc2": "Lorsque désactivé, la navigation est forcé par l'interface.",
-      "attach_title_header": "Ajouter automatiquement une section h1",
-      "attach_title_header_desc": "Ajoute le chemin de la page en tant que h1 lors de création d'une page.",
+      "attach_title_header": "Ajout automatique de titre",
+      "attach_title_header_desc": "Ajoute le chemin de la page en tant que titre de niveau 1 lors de création d'une page.",
       "list_num_s": "Nombre de pages modales",
       "list_num_s": "Nombre de pages modales",
       "list_num_desc_s": "Nombre de pages affichées sur les modales",
       "list_num_desc_s": "Nombre de pages affichées sur les modales",
       "list_num_m": "Nombre de pages articles",
       "list_num_m": "Nombre de pages articles",
@@ -491,20 +488,20 @@
       "list_num_desc_l": "Nombre de pages affichées lors de la recherche",
       "list_num_desc_l": "Nombre de pages affichées lors de la recherche",
       "list_num_xl": "Nombre de pages articles",
       "list_num_xl": "Nombre de pages articles",
       "list_num_desc_xl": "Nombre de pages affichées dans la 'corbeille' ou '404'.",
       "list_num_desc_xl": "Nombre de pages affichées dans la 'corbeille' ou '404'.",
-      "stale_notification": "Afficher les anciennes notifications",
+      "stale_notification": "Anciennes notifications",
       "stale_notification_desc": "Affiche les notifications sur les pages mises à jour il y a plus d'un an",
       "stale_notification_desc": "Affiche les notifications sur les pages mises à jour il y a plus d'un an",
       "show_all_reply_comments": "Afficher tout les commentaires",
       "show_all_reply_comments": "Afficher tout les commentaires",
       "show_all_reply_comments_desc": "Lorsque désactivé, seul les deux commentaires les plus récents sont affichés",
       "show_all_reply_comments_desc": "Lorsque désactivé, seul les deux commentaires les plus récents sont affichés",
       "select_search_scope_children_as_default": "'Seulement enfant de ce chemin' lors de la recherche",
       "select_search_scope_children_as_default": "'Seulement enfant de ce chemin' lors de la recherche",
       "select_search_scope_children_as_default_desc": "Lorsque désactivé, utilise 'Toutes les pages' en portée de recherche."
       "select_search_scope_children_as_default_desc": "Lorsque désactivé, utilise 'Toutes les pages' en portée de recherche."
     },
     },
-      "presentation": "Présentation",
+    "presentation": "Présentation",
     "presentation_options": {
     "presentation_options": {
-      "enable_marp": "Activer Marp",
-      "enable_marp_desc": "Marp est utilisable dans la visualisation de présentation. Potentiellement vulnérable aux attaques XSS.",
+      "enable_marp": "Marp",
+      "enable_marp_desc": "Marp est un syntaxe utilisable dans la visualisation de présentation. Potentiellement vulnérable aux attaques XSS.",
       "marp_official_site": "Site officiel Marp",
       "marp_official_site": "Site officiel Marp",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "GROWI Docs - Créer des présentations avec Marp",
+      "marp_in_growi": "GROWI Docs - Créer des présentations avec Marp",
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     },
     "custom_title": "Titre personnalisé",
     "custom_title": "Titre personnalisé",
@@ -518,10 +515,10 @@
     "write_css": "CSS personnalisé.",
     "write_css": "CSS personnalisé.",
     "ctrl_space": "Ctrl+Space pour l'autocomplétion",
     "ctrl_space": "Ctrl+Space pour l'autocomplétion",
     "custom_script": "Script personnalisé",
     "custom_script": "Script personnalisé",
-    "custom_presentation": "Presentation personnalisé",
+    "custom_presentation": "Mode présentation",
     "write_java": "Code javascript qui sera appliqué au système entier.",
     "write_java": "Code javascript qui sera appliqué au système entier.",
     "reflect_change": "Un rechargement de la page est nécessaire pour afficher les changements.",
     "reflect_change": "Un rechargement de la page est nécessaire pour afficher les changements.",
-    "custom_logo" : "Logo personnalisé",
+    "custom_logo": "Logo personnalisé",
     "default_logo": "Logo par défaut",
     "default_logo": "Logo par défaut",
     "upload_logo": "Téléverser un logo",
     "upload_logo": "Téléverser un logo",
     "current_logo": "Logo actuel",
     "current_logo": "Logo actuel",
@@ -595,9 +592,9 @@
     },
     },
     "import": "Importer",
     "import": "Importer",
     "skip_username_and_email_when_overlapped": "Passe le nom et adresse courriel exactes dans le nouvel environnement",
     "skip_username_and_email_when_overlapped": "Passe le nom et adresse courriel exactes dans le nouvel environnement",
-    "prepare_new_account_for_migration":"Préparer le compte pour la migration",
-    "archive_data_import_detail":"En savoir plus",
-    "admin_archive_data_import_guide_url":"https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
+    "prepare_new_account_for_migration": "Préparer le compte pour la migration",
+    "archive_data_import_detail": "En savoir plus",
+    "admin_archive_data_import_guide_url": "https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
     "page_skip": "Les pages ayant le nom d'une page déjà existante ne seront pas importées.",
     "page_skip": "Les pages ayant le nom d'une page déjà existante ne seront pas importées.",
     "Directory_hierarchy_tag": "Tag de hiérarchie"
     "Directory_hierarchy_tag": "Tag de hiérarchie"
   },
   },
@@ -667,7 +664,7 @@
     "delete": "Supprimer",
     "delete": "Supprimer",
     "integration_procedure": "Procédure d'intégration",
     "integration_procedure": "Procédure d'intégration",
     "custom_bot_without_proxy_settings": "Bot Personnalisé sans proxy",
     "custom_bot_without_proxy_settings": "Bot Personnalisé sans proxy",
-    "integration_failed":"Échec de l'intégration",
+    "integration_failed": "Échec de l'intégration",
     "reset": "Réinitialiser",
     "reset": "Réinitialiser",
     "reset_all_settings": "Réinitialiser tout les paramètres",
     "reset_all_settings": "Réinitialiser tout les paramètres",
     "delete_slackbot_settings": "Supprimer les paramètres du bot Slack",
     "delete_slackbot_settings": "Supprimer les paramètres du bot Slack",
@@ -714,7 +711,7 @@
       "allow_specified_long": "Autoriser sélectionnés (Depuis les canaux sélectionnés)",
       "allow_specified_long": "Autoriser sélectionnés (Depuis les canaux sélectionnés)",
       "test_connection": "Tester la connexion",
       "test_connection": "Tester la connexion",
       "test_connection_by_pressing_button": "Cliquer sur le bouton pour tester la connexion",
       "test_connection_by_pressing_button": "Cliquer sur le bouton pour tester la connexion",
-      "test_connection_only_public_channel":"Testez la connexion dans un canal publique.",
+      "test_connection_only_public_channel": "Testez la connexion dans un canal publique.",
       "error_check_logs_below": "Une erreur est survenue.",
       "error_check_logs_below": "Une erreur est survenue.",
       "send_message_to_slack_work_space": "Envoyer un message vers l'espace de travail Slack.",
       "send_message_to_slack_work_space": "Envoyer un message vers l'espace de travail Slack.",
       "add_slack_workspace": "Ajouter un espace de travail Slack"
       "add_slack_workspace": "Ajouter un espace de travail Slack"
@@ -743,16 +740,16 @@
     "alert_deplicated": "'Ancienne intégration Slack' sera discontinué dans le futur. Utiliser plutôt <a href='/admin/slack-integration'>les nouveaux paramètres</a> "
     "alert_deplicated": "'Ancienne intégration Slack' sera discontinué dans le futur. Utiliser plutôt <a href='/admin/slack-integration'>les nouveaux paramètres</a> "
   },
   },
   "user_management": {
   "user_management": {
-    "user_management": "Configuration des utilisateurs",
-    "invite_users": "Créer un nouvel utilisateur temporaire",
+    "user_management": "Utilisateurs",
+    "invite_users": "Nouvel utilisateur temporaire",
     "click_twice_same_checkbox": "Il est nécessaire de sélectionner une option.",
     "click_twice_same_checkbox": "Il est nécessaire de sélectionner une option.",
     "status": "Statut",
     "status": "Statut",
     "invite_modal": {
     "invite_modal": {
-      "emails": "Adresse Courriel (Supporte l'usage de plusieurs lignes)",
-      "description1":"Créer des utilisateurs temporaires avec une adresse courriel.",
-      "description2":"Un mot de passe temporaire sera généré..",
-      "invite_thru_email": "Envoyer courriel d'invitation",
-      "mail_setting_link":"<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Paramètres courriel</a>",
+      "emails": "Adresse(s) courriel(s) (Supporte l'usage de plusieurs lignes)",
+      "description1": "Créer des utilisateurs temporaires avec une adresse courriel.",
+      "description2": "Un mot de passe temporaire est généré automatiquement.",
+      "invite_thru_email": "Courriel d'invitation",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Paramètres courriel</a>",
       "valid_email": "Adresse courriel valide requise",
       "valid_email": "Adresse courriel valide requise",
       "temporary_password": "Cette utilisateur a un mot de passe temporaire",
       "temporary_password": "Cette utilisateur a un mot de passe temporaire",
       "send_new_password": "Envoyez le nouveau mot de passe à l'utilisateur.",
       "send_new_password": "Envoyez le nouveau mot de passe à l'utilisateur.",
@@ -774,7 +771,7 @@
       "cannot_revoke": "Vous ne pouvez pas révoquer votre propre permission d'administration",
       "cannot_revoke": "Vous ne pouvez pas révoquer votre propre permission d'administration",
       "grant_admin_access": "Ajouter permission administrateur",
       "grant_admin_access": "Ajouter permission administrateur",
       "revoke_read_only_access": "Révoquer permission de lecture",
       "revoke_read_only_access": "Révoquer permission de lecture",
-      "grant_read_only_access": "Ajouter permission de lecture",
+      "grant_read_only_access": "Permission de lecture-seule",
       "send_invitation_email": "Envoyer courriel d'invitation",
       "send_invitation_email": "Envoyer courriel d'invitation",
       "resend_invitation_email": "Renvoyer courriel d'invitation"
       "resend_invitation_email": "Renvoyer courriel d'invitation"
     },
     },
@@ -787,10 +784,10 @@
       "new_password": "Nouveau mot de passe"
       "new_password": "Nouveau mot de passe"
     },
     },
     "external_account": "Configuration des comptes externes",
     "external_account": "Configuration des comptes externes",
-    "external_accounts":"Comptes externes",
-    "create_external_account":"Créer compte externe",
+    "external_accounts": "Comptes externes",
+    "create_external_account": "Créer compte externe",
     "external_account_list": "Liste des comptes externes",
     "external_account_list": "Liste des comptes externes",
-    "external_account_none":"Pas de compte externe",
+    "external_account_none": "Pas de compte externe",
     "invite": "Inviter",
     "invite": "Inviter",
     "invited": "Utilisateur invité",
     "invited": "Utilisateur invité",
     "back_to_user_management": "Gestion des utilisateurs",
     "back_to_user_management": "Gestion des utilisateurs",
@@ -805,17 +802,17 @@
     "current_users": "Utilisateurs:"
     "current_users": "Utilisateurs:"
   },
   },
   "user_group_management": {
   "user_group_management": {
-    "user_group_management": "Configuration des groupes",
-    "create_group": "Créer nouveau groupe",
+    "user_group_management": "Gestion des groupes",
+    "create_group": "Nouveau groupe",
     "add_child_group": "Ajouter groupe enfant",
     "add_child_group": "Ajouter groupe enfant",
     "remove_child_group": "Retirer",
     "remove_child_group": "Retirer",
     "deny_create_group": "Les paramètres actuels ne permettent pas la création du groupe",
     "deny_create_group": "Les paramètres actuels ne permettent pas la création du groupe",
-    "group_name": "Nom du groupe",
+    "group_name": "Nom",
     "group_example": "e.g. : Group1",
     "group_example": "e.g. : Group1",
     "child_user_group": "Groupe utilisateur enfant",
     "child_user_group": "Groupe utilisateur enfant",
-    "parent_group": "Groupe parent",
+    "parent_group": "Parent",
     "select_parent_group": "Sélectionner groupe parent",
     "select_parent_group": "Sélectionner groupe parent",
-    "release_parent_group": "Libérer groupe parent",
+    "release_parent_group": "Retirer groupe parent",
     "add_modal": {
     "add_modal": {
       "description": "L'utilisateur sera ajouté au groupe parent.",
       "description": "L'utilisateur sera ajouté au groupe parent.",
       "add_user": "Ajouter utilisateur au groupe",
       "add_user": "Ajouter utilisateur au groupe",
@@ -825,14 +822,14 @@
       "partial_match": "Correspondance partielle",
       "partial_match": "Correspondance partielle",
       "backward_match": "Correspondance inversée"
       "backward_match": "Correspondance inversée"
     },
     },
-    "group_list": "Liste des groupes",
-    "child_group_list": "Liste des groupes enfants",
+    "group_list": "Groupes",
+    "child_group_list": "Groupes enfants",
     "back_to_list": "Retour à la liste",
     "back_to_list": "Retour à la liste",
-    "basic_info": "Information de base",
-    "user_list": "Liste des utilisateurs",
+    "basic_info": "Création du groupe",
+    "user_list": "Utilisateurs assignés",
     "created_group": "Groupe crée",
     "created_group": "Groupe crée",
     "is_loading_data": "Chargement...",
     "is_loading_data": "Chargement...",
-    "no_pages": "Le groupe n'a pas la permission de voir la page.",
+    "no_pages": "Le groupe n'a pas de pages assignées.",
     "remove_from_group": "Retirer l'utilisateur",
     "remove_from_group": "Retirer l'utilisateur",
     "delete_modal": {
     "delete_modal": {
       "header": "Supprimer groupe",
       "header": "Supprimer groupe",
@@ -853,7 +850,7 @@
     }
     }
   },
   },
   "audit_log_management": {
   "audit_log_management": {
-    "audit_log": "Journal d'audit",
+    "audit_log": "Audit",
     "audit_log_settings": "Configuration des journaux d'audit",
     "audit_log_settings": "Configuration des journaux d'audit",
     "user": "Utilisateur",
     "user": "Utilisateur",
     "username": "Nom d'utilisateur",
     "username": "Nom d'utilisateur",
@@ -883,12 +880,12 @@
   },
   },
   "plugins": {
   "plugins": {
     "plugins": "Plugins",
     "plugins": "Plugins",
-    "plugin_installer": "Configuration de plugins",
+    "plugin_installer": "Installer un plugin",
     "form": {
     "form": {
       "label_url": "URL du plugin",
       "label_url": "URL du plugin",
-      "desc_url": "Les plugins sont installables par URL",
+      "desc_url": "URL vers le code source du plugin. L'URL doit être accessible publiquement.",
       "label_branch": "Branche",
       "label_branch": "Branche",
-      "desc_branch": "Spécification du nom de la branche. Par défaut: `main`"
+      "desc_branch": "Nom de la branche du dépôt"
     },
     },
     "plugin_card": "Plugins",
     "plugin_card": "Plugins",
     "plugin_is_not_installed": "Aucun plugins installés",
     "plugin_is_not_installed": "Aucun plugins installés",
@@ -974,7 +971,7 @@
     "ADMIN_SITE_URL_UPDATE": "Modifier les paramètres d'URL",
     "ADMIN_SITE_URL_UPDATE": "Modifier les paramètres d'URL",
     "ADMIN_MAIL_SMTP_UPDATE": "Modifier les paramètres d'e-mail",
     "ADMIN_MAIL_SMTP_UPDATE": "Modifier les paramètres d'e-mail",
     "ADMIN_MAIL_SES_UPDATE": "Modifier les paramètres d'e-mail (SES)",
     "ADMIN_MAIL_SES_UPDATE": "Modifier les paramètres d'e-mail (SES)",
-    "ADMIN_MAIL_TEST_SUBMIT" : "Envoyer courriel de test",
+    "ADMIN_MAIL_TEST_SUBMIT": "Envoyer courriel de test",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Modifier paramètres de téléversemetnt de fichiers",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Modifier paramètres de téléversemetnt de fichiers",
     "ADMIN_PLUGIN_UPDATE": "Mettre à jour les paramètres de plugins",
     "ADMIN_PLUGIN_UPDATE": "Mettre à jour les paramètres de plugins",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Activer mode maintenance",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Activer mode maintenance",

+ 16 - 30
apps/app/public/static/locales/fr_FR/commons.json

@@ -7,7 +7,6 @@
   "Sign out": "Se déconnecter",
   "Sign out": "Se déconnecter",
   "New": "Nouveau",
   "New": "Nouveau",
   "Delete": "Supprimer",
   "Delete": "Supprimer",
-
   "meta": {
   "meta": {
     "display_name": "Français"
     "display_name": "Français"
   },
   },
@@ -28,9 +27,8 @@
     "email_is_already_in_use": "La configuration SMTP est déjà faite."
     "email_is_already_in_use": "La configuration SMTP est déjà faite."
   },
   },
   "headers": {
   "headers": {
-    "app_settings": "Paramètres de l'application"
+    "app_settings": "Application"
   },
   },
-
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "All pages": "Toutes les pages",
       "All pages": "Toutes les pages",
@@ -41,30 +39,26 @@
       "This tree": "Enfants de cette arbre"
       "This tree": "Enfants de cette arbre"
     }
     }
   },
   },
-
   "search_method_menu_item": {
   "search_method_menu_item": {
     "search_in_all": "Rechercher dans tout",
     "search_in_all": "Rechercher dans tout",
     "only_children_of_this_tree": "Enfants de cet arbre",
     "only_children_of_this_tree": "Enfants de cet arbre",
     "exact_mutch": "Correspondance exacte"
     "exact_mutch": "Correspondance exacte"
   },
   },
-
   "share_links": {
   "share_links": {
     "Share Link": "Liens de partage",
     "Share Link": "Liens de partage",
     "Page Path": "Chemin de la page",
     "Page Path": "Chemin de la page",
     "expire": "Expiration",
     "expire": "Expiration",
     "description": "Description"
     "description": "Description"
   },
   },
-
   "in_app_notification": {
   "in_app_notification": {
     "notification_list": "Notifications d'application",
     "notification_list": "Notifications d'application",
     "see_all": "Voir tout",
     "see_all": "Voir tout",
-    "no_notification": "Vous n'avez pas de notifications.",
+    "no_notification": "Aucune notification.",
     "all": "Toutes",
     "all": "Toutes",
     "unopend": "Non-lues",
     "unopend": "Non-lues",
     "mark_all_as_read": "Tout marquer comme lu",
     "mark_all_as_read": "Tout marquer comme lu",
     "no_unread_messages": "aucun message non lu"
     "no_unread_messages": "aucun message non lu"
   },
   },
-
   "personal_dropdown": {
   "personal_dropdown": {
     "home": "Accueil",
     "home": "Accueil",
     "settings": "Paramètres",
     "settings": "Paramètres",
@@ -72,24 +66,21 @@
     "sidebar_mode": "Navigation latérale",
     "sidebar_mode": "Navigation latérale",
     "sidebar_mode_editor": "Navigation latérale dans l'éditeur",
     "sidebar_mode_editor": "Navigation latérale dans l'éditeur",
     "use_os_settings": "Utiliser les paramètres système",
     "use_os_settings": "Utiliser les paramètres système",
-    "feedback": "Avis"
+    "feedback": "Sondage"
   },
   },
-
-
   "create_page_dropdown": {
   "create_page_dropdown": {
-    "new_page": "Créer nouvelle page",
-    "open_page_create_modal": "Ouvrir une nouvelle page créer une fenêtre modale",
+    "new_page": "Nouvelle page",
+    "open_page_create_modal": "Modale de création de page",
     "todays": {
     "todays": {
-      "desc": "Créer le mémo du jour",
+      "desc": "Mémo du jour",
       "memo": "mémo"
       "memo": "mémo"
     },
     },
     "template": {
     "template": {
-      "desc": "Créer/modifier page modèle",
-      "children": "Modèle page enfant",
-      "descendants": "Modèle pour descendants"
+      "desc": "Modèles",
+      "children": "Modèle pour page enfant",
+      "descendants": "Modèle pour page adjacentes"
     }
     }
   },
   },
-
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "Copier dans le presse-papier",
     "Copy to clipboard": "Copier dans le presse-papier",
     "Page path": "Chemin de la page",
     "Page path": "Chemin de la page",
@@ -99,30 +90,27 @@
     "Markdown link": "Lien Markdown",
     "Markdown link": "Lien Markdown",
     "Append params": "Affixer les paramètres"
     "Append params": "Affixer les paramètres"
   },
   },
-
   "crop_image_modal": {
   "crop_image_modal": {
     "image_crop": "Recadrage d'image",
     "image_crop": "Recadrage d'image",
     "crop": "Recadrer",
     "crop": "Recadrer",
     "save": "Sauvegarder",
     "save": "Sauvegarder",
     "cancel": "Annuler"
     "cancel": "Annuler"
   },
   },
-
   "handsontable_modal": {
   "handsontable_modal": {
-    "title": "Modifier table",
-    "data_import": "Import de données",
+    "title": "Tableau",
+    "data_import": "Importer des données",
     "save": "Sauvegarder",
     "save": "Sauvegarder",
     "cancel": "Annuler",
     "cancel": "Annuler",
-    "done": "Terminer",
+    "done": "Mettre à jour",
     "data_import_form": {
     "data_import_form": {
-      "select_data_format": "Sélectionner format de données",
-      "import_data": "Importer données",
-      "paste_table_data": "Coller les données de la table",
-      "parse_error": "Erreur d'analyse",
+      "select_data_format": "Format",
+      "import_data": "Données du fichier",
+      "paste_table_data": "Coller les données de la fichier",
+      "parse_error": "Erreur lors de l'importation des données",
       "cancel": "Annuler",
       "cancel": "Annuler",
       "import": "Importer"
       "import": "Importer"
     }
     }
   },
   },
-
   "questionnaire_modal": {
   "questionnaire_modal": {
     "required": "Requis",
     "required": "Requis",
     "submit": "Soumettre",
     "submit": "Soumettre",
@@ -146,11 +134,9 @@
     "successfully_submitted": "Questionnaire soumis.",
     "successfully_submitted": "Questionnaire soumis.",
     "thanks_for_answering": "Merci pour votre avis."
     "thanks_for_answering": "Merci pour votre avis."
   },
   },
-
   "not_found_page": {
   "not_found_page": {
     "page_not_exist": "Cette page est introuvable."
     "page_not_exist": "Cette page est introuvable."
   },
   },
-
   "g2g_data_transfer": {
   "g2g_data_transfer": {
     "tab": "Transfert de données",
     "tab": "Transfert de données",
     "data_transfer": "Transfert de données",
     "data_transfer": "Transfert de données",

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

@@ -3,7 +3,7 @@
     "display_name": "Français"
     "display_name": "Français"
   },
   },
   "Help": "Aide",
   "Help": "Aide",
-  "view": "Voir",
+  "View": "Voir",
   "Edit": "Modifier",
   "Edit": "Modifier",
   "Delete": "Supprimer",
   "Delete": "Supprimer",
   "delete_all": "Tout supprimer",
   "delete_all": "Tout supprimer",
@@ -60,7 +60,7 @@
   "Timeline View": "Chronologie",
   "Timeline View": "Chronologie",
   "History": "Historique",
   "History": "Historique",
   "attachment_data": "Pièces jointes",
   "attachment_data": "Pièces jointes",
-  "No_attachments_yet": "Aucunes pièces jointes.",
+  "No_attachments_yet": "Aucune pièce jointe.",
   "Presentation Mode": "Mode présentation",
   "Presentation Mode": "Mode présentation",
   "Not available for guest": "Indisponible pour les invités",
   "Not available for guest": "Indisponible pour les invités",
   "Not available in this version": "Indisponible dans cette version",
   "Not available in this version": "Indisponible dans cette version",
@@ -90,24 +90,25 @@
   "No diff": "Aucune différences",
   "No diff": "Aucune différences",
   "Latest": "Dernière version",
   "Latest": "Dernière version",
   "User ID": "Identifiant utilisateur",
   "User ID": "Identifiant utilisateur",
-  "User Information": "Informations utilisateur",
+  "User Settings": "Paramètres utilisateur",
+  "User Information": "Mon compte",
   "User Activation": "Activation utilisateur",
   "User Activation": "Activation utilisateur",
-  "Basic Info": "Informations de base",
+  "Basic Info": "Informations du compte",
   "Name": "Nom",
   "Name": "Nom",
   "Email": "Adresse courriel",
   "Email": "Adresse courriel",
   "Language": "Langue",
   "Language": "Langue",
   "English": "Anglais",
   "English": "Anglais",
   "Japanese": "Japonais",
   "Japanese": "Japonais",
-  "Set Profile Image": "Sélectionner image de profil",
-  "Upload Image": "Téléverser image",
-  "Current Image": "Image actuelle",
-  "Delete Image": "Supprimer image",
-  "Delete this image?": "Supprimer cette image?",
+  "Set Profile Image": "Photo de profil",
+  "Upload Image": "Photo personalisée",
+  "Current Image": "Photo actuelle",
+  "Delete Image": "Supprimer photo",
+  "Delete this image?": "Supprimer cette photo?",
   "Updated": "Modifié",
   "Updated": "Modifié",
-  "Upload new image": "Téléverser nouvelle image",
+  "Upload new image": "Téléverser une photo",
   "Connected": "Connecté",
   "Connected": "Connecté",
   "Loading": "Chargement...",
   "Loading": "Chargement...",
-  "Disclose E-mail": "Afficher adresse courriel",
+  "Disclose E-mail": "Divulguer adresse courriel",
   "page exists": "cette page est déjà existante",
   "page exists": "cette page est déjà existante",
   "Error occurred": "Une erreur est survenue",
   "Error occurred": "Une erreur est survenue",
   "Input page name": "Nom de la page",
   "Input page name": "Nom de la page",
@@ -122,21 +123,21 @@
   "UserGroup": "Groupe utilisateur",
   "UserGroup": "Groupe utilisateur",
   "Basic Settings": "Paramètres de base",
   "Basic Settings": "Paramètres de base",
   "The contents entered here will be shown in the header etc": "Le contenu entré ici sera visible dans l'en-tête",
   "The contents entered here will be shown in the header etc": "Le contenu entré ici sera visible dans l'en-tête",
-  "Public": "Public",
+  "Public": "Tout le monde",
   "Anyone with the link": "Tous les utilisateurs disposant du lien",
   "Anyone with the link": "Tous les utilisateurs disposant du lien",
   "Specified users only": "Utilisateurs spécifiés",
   "Specified users only": "Utilisateurs spécifiés",
   "Only me": "Seulement moi",
   "Only me": "Seulement moi",
   "Only inside the group": "Utilisateurs du groupe",
   "Only inside the group": "Utilisateurs du groupe",
-  "page_list": "Liste de pages",
+  "page_list": "Pages enfants",
   "comments": "Commentaires",
   "comments": "Commentaires",
   "Reselect the group": "Resélectionner ce groupe",
   "Reselect the group": "Resélectionner ce groupe",
   "Shareable link": "Lien partageable",
   "Shareable link": "Lien partageable",
   "The whitelist of registration permission E-mail address": "Les adresses courriel permises lors de l'inscription",
   "The whitelist of registration permission E-mail address": "Les adresses courriel permises lors de l'inscription",
   "Add tags for this page": "Ajouter des étiquettes",
   "Add tags for this page": "Ajouter des étiquettes",
   "tag_list": "Étiquettes",
   "tag_list": "Étiquettes",
-  "popular_tags": "Étiquettes populaires",
-  "Check All tags": "voir toutes les étiquettes",
-  "You have no tag, You can set tags on pages": "Vous n'avez aucunes étiquettes, vous pouvez assigner des étiquettes aux pages",
+  "popular_tags": "Étiquettes fréquentes",
+  "Check All tags": "Toutes les étiquettes",
+  "You have no tag, You can set tags on pages": "Aucune étiquette existante.",
   "Show latest": "Voir le plus récent",
   "Show latest": "Voir le plus récent",
   "Load latest": "Charger le plus récent",
   "Load latest": "Charger le plus récent",
   "edited this page": "à modifié cette page.",
   "edited this page": "à modifié cette page.",
@@ -147,9 +148,9 @@
   "No bookmarks yet": "Aucuns favoris",
   "No bookmarks yet": "Aucuns favoris",
   "add_bookmark": "Ajouter aux favoris",
   "add_bookmark": "Ajouter aux favoris",
   "remove_bookmark": "Retirer des favoris",
   "remove_bookmark": "Retirer des favoris",
-  "wide_view": "Vue élargie",
+  "wide_view": "Affichage large",
   "Recent Changes": "Modifications récentes",
   "Recent Changes": "Modifications récentes",
-  "Page Tree": "Arbre",
+  "Page Tree": "Arborescence",
   "Bookmarks": "Favoris",
   "Bookmarks": "Favoris",
   "In-App Notification": "Notifications",
   "In-App Notification": "Notifications",
   "original_path": "Chemin originel",
   "original_path": "Chemin originel",
@@ -227,7 +228,7 @@
     "form_help": {}
     "form_help": {}
   },
   },
   "Password": "Mot de passe",
   "Password": "Mot de passe",
-  "Password Settings": "Paramètres de mot passe",
+  "Password Settings": "Sécurité",
   "personal_settings": {
   "personal_settings": {
     "disassociate_external_account": "Dissocier compte externe",
     "disassociate_external_account": "Dissocier compte externe",
     "disassociate_external_account_desc": "Dissocier le compte externe <strong>{{providerType}}</strong> <strong>{{accountId}}</strong>?",
     "disassociate_external_account_desc": "Dissocier le compte externe <strong>{{providerType}}</strong> <strong>{{accountId}}</strong>?",
@@ -241,7 +242,7 @@
   "share_links": {
   "share_links": {
     "Shere this page link to public": "Partager cette page publiquement",
     "Shere this page link to public": "Partager cette page publiquement",
     "share_link_list": "Liens de partage",
     "share_link_list": "Liens de partage",
-    "share_link_management": "Gestion des liens de partage",
+    "share_link_management": "Liens de partage",
     "delete_all_share_links": "Supprimer tout les liens de partage",
     "delete_all_share_links": "Supprimer tout les liens de partage",
     "expire": "Expiration",
     "expire": "Expiration",
     "Days": "Jour",
     "Days": "Jour",
@@ -254,23 +255,23 @@
     "Invalid_Number_of_Date": "Valeurs invalides",
     "Invalid_Number_of_Date": "Valeurs invalides",
     "link_sharing_is_disabled": "Le partage est désactivé"
     "link_sharing_is_disabled": "Le partage est désactivé"
   },
   },
-  "API Settings": "Configuration API",
+  "API Settings": "API GROWI",
   "Other Settings": "Autres paramètres",
   "Other Settings": "Autres paramètres",
-  "API Token Settings": "Paramètres de jetons",
-  "Current API Token": "Jeton d'API actuel",
-  "Update API Token": "Modifier jeton",
+  "API Token Settings": "Jetons d'API",
+  "Current API Token": "Mon jeton d'API",
+  "Update API Token": "Regénérer",
   "in_app_notification_settings": {
   "in_app_notification_settings": {
-    "in_app_notification_settings": "Paramètres de notifications",
+    "in_app_notification_settings": "Notifications",
     "subscribe_settings": "Paramètres d'abonnement automatique aux notifications de pages",
     "subscribe_settings": "Paramètres d'abonnement automatique aux notifications de pages",
     "default_subscribe_rules": {
     "default_subscribe_rules": {
-      "page_create": "S'abonner à la page lors de sa création."
+      "page_create": "S'abonner aux modifications d'une page lors de sa création."
     }
     }
   },
   },
   "ui_settings": {
   "ui_settings": {
-    "ui_settings": "Paramètres UI",
+    "ui_settings": "Interface",
     "side_bar_mode": {
     "side_bar_mode": {
       "settings": "Paramètres navigation latérale",
       "settings": "Paramètres navigation latérale",
-      "side_bar_mode_setting": "Activer la navigation latérale",
+      "side_bar_mode_setting": "Épingler la navigation latérale",
       "description": "Activer pour toujours afficher la barre de navigation latérale lorsque l'écran est large. Si la largeur d'écran est faible, le cas inverse est applicable."
       "description": "Activer pour toujours afficher la barre de navigation latérale lorsque l'écran est large. Si la largeur d'écran est faible, le cas inverse est applicable."
     }
     }
   },
   },
@@ -278,7 +279,7 @@
     "light": "Clair",
     "light": "Clair",
     "dark": "Sombre",
     "dark": "Sombre",
     "system": "Système",
     "system": "Système",
-    "settings": "Paramètres de thème",
+    "settings": "Thème",
     "description": "Affichage en mode clair, sombre ou selon les paramètres système.<br>Seuls les thèmes supportés seront modifiés."
     "description": "Affichage en mode clair, sombre ou selon les paramètres système.<br>Seuls les thèmes supportés seront modifiés."
   },
   },
   "editor_settings": {
   "editor_settings": {
@@ -331,17 +332,17 @@
   "page_edit": {
   "page_edit": {
     "input_channels": "Canal Slack...",
     "input_channels": "Canal Slack...",
     "theme": "Thème",
     "theme": "Thème",
-    "keymap": "Touches",
+    "keymap": "Raccourcis",
     "indent": "Indentation",
     "indent": "Indentation",
     "paste": {
     "paste": {
       "title": "Comportement du collage",
       "title": "Comportement du collage",
-      "both": "Les deux",
+      "both": "Texte et fichier",
       "text": "Texte seulement",
       "text": "Texte seulement",
       "file": "Fichier seulement"
       "file": "Fichier seulement"
     },
     },
-    "editor_config": "Configuration de l'éditeur",
-    "Show active line": "Montrer la ligne active",
-    "auto_format_table": "Formattage les tables",
+    "editor_config": "Préférences de l'éditeur",
+    "Show active line": "Surligner la ligne active",
+    "auto_format_table": "Formatter les tableaux",
     "overwrite_scopes": "{{operation}} et écraser les scopes des pages enfants",
     "overwrite_scopes": "{{operation}} et écraser les scopes des pages enfants",
     "notice": {
     "notice": {
       "conflict": "Sauvegarde impossible, la page est en cours de modification par un autre utilisateur. Recharger la page."
       "conflict": "Sauvegarde impossible, la page est en cours de modification par un autre utilisateur. Recharger la page."
@@ -370,28 +371,28 @@
   },
   },
   "page_history": {
   "page_history": {
     "revision_list": "Historique des modifications",
     "revision_list": "Historique des modifications",
-    "revision": "version",
+    "revision": "Révision",
     "comparing_source": "Source",
     "comparing_source": "Source",
-    "comparing_target": "Destination",
+    "comparing_target": "Cible",
     "comparing_revisions": "Comparer les modifications",
     "comparing_revisions": "Comparer les modifications",
     "compare_latest": "Comparer avec la version la plus récente",
     "compare_latest": "Comparer avec la version la plus récente",
-    "compare_previous": "Compare avec une version précédente"
+    "compare_previous": "Comparer avec la version précédente"
   },
   },
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "Déplacer/renommer page",
       "Move/Rename page": "Déplacer/renommer page",
-      "New page name": "Nom de la page",
+      "New page name": "Nouveau chemin",
       "Failed to get subordinated pages": "échec de récupération des pages subordinnées",
       "Failed to get subordinated pages": "échec de récupération des pages subordinnées",
       "Failed to get exist path": "échec de la récupération du chemin",
       "Failed to get exist path": "échec de la récupération du chemin",
-      "Current page name": "Nom de la page courante",
+      "Current page name": "Chemin actuel",
       "Rename this page only": "Renommer cette page",
       "Rename this page only": "Renommer cette page",
       "Force rename all child pages": "Forcer le renommage des pages",
       "Force rename all child pages": "Forcer le renommage des pages",
       "Other options": "Autres options",
       "Other options": "Autres options",
       "Do not update metadata": "Ne pas modifier les métadonnées",
       "Do not update metadata": "Ne pas modifier les métadonnées",
-      "Redirect": "Rediriger"
+      "Redirect": "Redirection automatique"
     },
     },
     "help": {
     "help": {
-      "redirect": "Rediriger vers la nouvelle page",
+      "redirect": "Redirige automatiquement vers le nouveau chemin de la page.",
       "metadata": "Conserve les métadonnées d'édition de la page",
       "metadata": "Conserve les métadonnées d'édition de la page",
       "recursive": "Déplacer/renommer les pages enfants récursivement"
       "recursive": "Déplacer/renommer les pages enfants récursivement"
     }
     }
@@ -515,7 +516,7 @@
     "initialize_successed": "Initialisation de {{target}} réussie",
     "initialize_successed": "Initialisation de {{target}} réussie",
     "remove_share_link_success": "Suppression de {{shareLinkId}} réussie",
     "remove_share_link_success": "Suppression de {{shareLinkId}} réussie",
     "issue_share_link": "Lien de partage ajouté",
     "issue_share_link": "Lien de partage ajouté",
-    "remove_share_link": "{{count}} liens de partage supprimés",
+    "remove_share_link": "{{count}} liens supprimés",
     "switch_disable_link_sharing_success": "Paramètres des liens de partage modifiés",
     "switch_disable_link_sharing_success": "Paramètres des liens de partage modifiés",
     "failed_to_reset_password": "Échec de la réinitialisation du mot de passe",
     "failed_to_reset_password": "Échec de la réinitialisation du mot de passe",
     "save_succeeded": "Sauvegarde réussie"
     "save_succeeded": "Sauvegarde réussie"
@@ -736,7 +737,8 @@
   "pagetree": {
   "pagetree": {
     "cannot_rename_a_title_that_contains_slash": "Renommage impossible lorsque le titre contient '/'",
     "cannot_rename_a_title_that_contains_slash": "Renommage impossible lorsque le titre contient '/'",
     "you_cannot_move_this_page_now": "Déplacement de la page impossible",
     "you_cannot_move_this_page_now": "Déplacement de la page impossible",
-    "something_went_wrong_with_moving_page": "Échec de déplacement de la page"
+    "something_went_wrong_with_moving_page": "Échec de déplacement de la page",
+    "error_retrieving_the_pagetree": "Une erreur s'est produite lors de la récupération de l'arbre des pages"
   },
   },
   "duplicated_page_alert": {
   "duplicated_page_alert": {
     "same_page_name_exists": "Une page avec ce nom 「{{pageName}}」 existe déjà",
     "same_page_name_exists": "Une page avec ce nom 「{{pageName}}」 existe déjà",
@@ -744,9 +746,9 @@
     "select_page_to_see": "Sélectionner une page"
     "select_page_to_see": "Sélectionner une page"
   },
   },
   "user_group": {
   "user_group": {
-    "select_group": "Sélectionner groupe",
-    "belonging_to_no_group": "Appartenance au groupe introuvable.",
-    "manage_user_groups": "Gestion des groupes utilisateurs"
+    "select_group": "Groupes autorisés",
+    "belonging_to_no_group": "Vous n'appartenez à aucun groupe.",
+    "manage_user_groups": "Gestion des groupes"
   },
   },
   "fix_page_grant": {
   "fix_page_grant": {
     "modal": {
     "modal": {
@@ -777,11 +779,11 @@
   "tooltip": {
   "tooltip": {
     "like": "Like!",
     "like": "Like!",
     "cancel_like": "Annuler",
     "cancel_like": "Annuler",
-    "bookmark": "Favori",
-    "cancel_bookmark": "Annuler favori",
-    "receive_notifications": "Recevoir les notifications",
-    "stop_notification": "Stopper les notifications",
-    "footprints": "Visiteurs",
+    "bookmark": "Ajouter aux favoris",
+    "cancel_bookmark": "Retirer des favoris",
+    "receive_notifications": "S'abonner",
+    "stop_notification": "Se désabonner",
+    "footprints": "Lecteurs",
     "login_required": "Connexion requise",
     "login_required": "Connexion requise",
     "operation": {
     "operation": {
       "attention": {
       "attention": {
@@ -795,7 +797,7 @@
   },
   },
   "user_home_page": {
   "user_home_page": {
     "bookmarks": "Favoris",
     "bookmarks": "Favoris",
-    "recently_created": "Crée récemment"
+    "recently_created": "Page récentes"
   },
   },
   "bookmark_folder": {
   "bookmark_folder": {
     "bookmark_folder": "dossier de favoris",
     "bookmark_folder": "dossier de favoris",
@@ -828,18 +830,18 @@
     "disagree": "En désaccord",
     "disagree": "En désaccord",
     "answer": "Répondre",
     "answer": "Répondre",
     "no_answer": "Aucune réponse",
     "no_answer": "Aucune réponse",
-    "settings": "Configuration du questionnaire",
-    "failed_to_send": "Échec de l'envoi du questionnaire",
-    "denied": "Le questionnaire ne sera plus montré",
-    "personal_settings_explanation": "Les questionnaires de satisfaction seront actifs.",
-    "enable_questionnaire": "Activer questionnaire",
-    "disabled_by_admin": "Questionnaire désactivé par l'administrateur"
+    "settings": "Sondages anonymes",
+    "failed_to_send": "Échec de l'envoi du sondage",
+    "denied": "Les sondages ne seront plus affichés.",
+    "personal_settings_explanation": "Sondages de satisfaction anonymes.",
+    "enable_questionnaire": "Sondages anonymes",
+    "disabled_by_admin": "Sondages anonymes désactivés par l'administrateur"
   },
   },
   "tag_edit_modal": {
   "tag_edit_modal": {
-    "edit_tags": "Modifier étiquettes",
-    "done": "Terminer",
+    "edit_tags": "Étiquettes",
+    "done": "Mettre à jour",
     "tags_input": {
     "tags_input": {
-      "tag_name": "nom de l'étiquette"
+      "tag_name": "Choisir ou créer une étiquette"
     }
     }
   },
   },
   "delete_attachment_modal": {
   "delete_attachment_modal": {
@@ -866,11 +868,11 @@
     "size_l": "Taille: G"
     "size_l": "Taille: G"
   },
   },
   "sync-latest-revision-body": {
   "sync-latest-revision-body": {
-    "menuitem": "Synchroniser le texte de l'éditeur avec le corps de la dernière révision",
-    "confirm": "Delete the draft data being entered into the editor and synchronize the latest text. Are you sure you want to run it?",
+    "menuitem": "Synchroniser avec la dernière révision",
+    "confirm": "Supprime les données en brouillon et synchronise avec la dernière révision. Synchroniser?",
     "alert": "Il se peut que le texte le plus récent n'ait pas été synchronisé. Veuillez recharger et vérifier à nouveau.",
     "alert": "Il se peut que le texte le plus récent n'ait pas été synchronisé. Veuillez recharger et vérifier à nouveau.",
     "success-toaster": "Dernier texte synchronisé",
     "success-toaster": "Dernier texte synchronisé",
-    "skipped-toaster": "Synchronisation ignorée car l'éditeur n'est pas activé. Ouvrir l'éditeur et réessayer.",
-    "error-toaster": "La synchronisation du dernier texte a échoué"
+    "skipped-toaster": "L'éditeur n'est pas actif. Synchronisation annulée.",
+    "error-toaster": "Synchronisation échouée"
   }
   }
 }
 }

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

@@ -193,9 +193,6 @@
         "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
         "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
         "updated_google": "Google OAuth を更新しました"
         "updated_google": "Google OAuth を更新しました"
       },
       },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "GitHub OAuth を有効にする",
         "enable_github": "GitHub OAuth を有効にする",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
@@ -211,7 +208,7 @@
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
         "register_1": "OIDC IdP Administrator へ接続します。",
         "register_1": "OIDC IdP Administrator へ接続します。",
-        "register_2": "OIDCアプリの認証コールバックURLを<code>%s</code>として登録します。",
+        "register_2": "OIDCアプリの認証コールバックURLを<code>{{url}}</code>として登録します。",
         "register_3": "上記のClientIDとClient Secretをコピー&ペーストしてください。",
         "register_3": "上記のClientIDとClient Secretをコピー&ペーストしてください。",
         "updated_oidc": "OpenID Connect を更新しました",
         "updated_oidc": "OpenID Connect を更新しました",
         "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"
         "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"

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

@@ -775,7 +775,8 @@
   "pagetree": {
   "pagetree": {
     "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません",
     "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません",
     "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
     "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
-    "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました"
+    "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました",
+    "error_retrieving_the_pagetree": "ページツリーの取得中にエラーが発生しました"
   },
   },
   "duplicated_page_alert": {
   "duplicated_page_alert": {
     "same_page_name_exists": "ページ名 「{{pageName}}」が重複しています",
     "same_page_name_exists": "ページ名 「{{pageName}}」が重複しています",

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

@@ -193,9 +193,6 @@
 				"register_5": "Copy and paste your ClientID and Client Secret above",
 				"register_5": "Copy and paste your ClientID and Client Secret above",
 				"updated_google": "Succeeded to update Google OAuth setting"
 				"updated_google": "Succeeded to update Google OAuth setting"
 			},
 			},
-			"Facebook": {
-				"name": "Facebook OAuth"
-			},
 			"GitHub": {
 			"GitHub": {
 				"enable_github": "Enable GitHub OAuth",
 				"enable_github": "Enable GitHub OAuth",
 				"name": "GitHub OAuth",
 				"name": "GitHub OAuth",
@@ -209,9 +206,9 @@
 				"id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
 				"id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
 				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
 				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
 				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
 				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
-				"mapping_detail": "Specification of mappings for %s when creating new users",
+				"mapping_detail": "Specification of mappings for {{target}} when creating new users",
 				"register_1": "Contact to OIDC IdP Administrator",
 				"register_1": "Contact to OIDC IdP Administrator",
-				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
+				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>{{url}}</code>",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
 				"updated_oidc": "Succeeded to update OpenID Connect",
 				"updated_oidc": "Succeeded to update OpenID Connect",
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"

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

@@ -745,7 +745,8 @@
   "pagetree": {
   "pagetree": {
     "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题",
     "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题",
     "you_cannot_move_this_page_now": "你现在不能移动这个页面",
     "you_cannot_move_this_page_now": "你现在不能移动这个页面",
-    "something_went_wrong_with_moving_page": "移动页面时出了问题"
+    "something_went_wrong_with_moving_page": "移动页面时出了问题",
+    "error_retrieving_the_pagetree": "检索页面树时发生错误"
   },
   },
   "duplicated_page_alert": {
   "duplicated_page_alert": {
     "same_page_name_exists": "页面名称「{{pageName}}」是重复的",
     "same_page_name_exists": "页面名称「{{pageName}}」是重复的",

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

@@ -16,7 +16,7 @@ export const CustomizeTitle: FC = () => {
 
 
   const { data: customizeTitle } = useCustomizeTitle();
   const { data: customizeTitle } = useCustomizeTitle();
 
 
-  const [currentCustomizeTitle, setCrrentCustomizeTitle] = useState(customizeTitle);
+  const [currentCustomizeTitle, setCrrentCustomizeTitle] = useState(customizeTitle ?? '');
 
 
   const onClickSubmit = async() => {
   const onClickSubmit = async() => {
     try {
     try {

+ 0 - 36
apps/app/src/client/components/Admin/Security/FacebookSecuritySetting.jsx

@@ -1,36 +0,0 @@
-import React from 'react';
-
-import { withTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
-class FacebookSecurityManagement extends React.Component {
-
-  render() {
-    const { t } = this.props;
-    return (
-      <>
-        <h2 className="alert-anchor border-bottom">
-          Facebook OAuth { t('admin:security_settings.configuration') }
-        </h2>
-
-        <p className="card custom-card">(TBD)</p>
-      </>
-    );
-  }
-
-}
-
-
-FacebookSecurityManagement.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-};
-
-const FacebookSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
-
-export default withTranslation()(FacebookSecurityManagementWrapper);

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

@@ -455,7 +455,7 @@ class OidcSecurityManagementContents extends React.Component {
           <div className=" card custom-card bg-body-tertiary">
           <div className=" card custom-card bg-body-tertiary">
             <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
             <ol id="collapseHelpForOidcOauth" className="collapse mb-0">
               <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
               <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
-              <li>{t('security_settings.OAuth.OIDC.register_2')}</li>
+              <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.register_2', { url: oidcCallbackUrl }) }} />
               <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
               <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
             </ol>
             </ol>
           </div>
           </div>

+ 0 - 8
apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx

@@ -6,7 +6,6 @@ import { TabContent, TabPane } from 'reactstrap';
 
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import CustomNav from '../../CustomNavigation/CustomNav';
 
 
-// import FacebookSecuritySetting from './FacebookSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
@@ -53,10 +52,6 @@ const SecurityManagementContents = () => {
         Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
         Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
         i18n: 'GitHub',
         i18n: 'GitHub',
       },
       },
-      // passport_facebook: {
-      //   Icon: () => <span className="growi-custom-icons align-bottom">facebook</span>,
-      //   i18n: '(TBD) Facebook',
-      // },
     };
     };
   }, []);
   }, []);
 
 
@@ -114,9 +109,6 @@ const SecurityManagementContents = () => {
           <TabPane tabId="passport_github">
           <TabPane tabId="passport_github">
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
           </TabPane>
           </TabPane>
-          {/* <TabPane tabId="passport_facebook">
-            {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
-          </TabPane> */}
         </TabContent>
         </TabContent>
       </div>
       </div>
     </div>
     </div>

+ 2 - 1
apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx

@@ -6,6 +6,7 @@ import { useTranslation } from 'next-i18next';
 
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
@@ -76,7 +77,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
-          { adminExternalAccountsContainer.state.externalAccounts.map((ea: IAdminExternalAccount) => {
+          { adminExternalAccountsContainer.state.externalAccounts.map((ea: IAdminExternalAccount<IExternalAuthProviderType>) => {
             return (
             return (
               <tr key={ea._id}>
               <tr key={ea._id}>
                 <td><span>{ea.providerType}</span></td>
                 <td><span>{ea.providerType}</span></td>

+ 10 - 90
apps/app/src/client/components/ItemsTree/ItemsTree.tsx

@@ -1,17 +1,16 @@
 import React, {
 import React, {
-  useEffect, useMemo, useCallback,
+  useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
 import path from 'path';
 import path from 'path';
 
 
-import type { Nullable, IPageHasId, IPageToDeleteWithMeta } from '@growi/core';
+import type { IPageToDeleteWithMeta } from '@growi/core';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
-import type { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
 import type { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import type { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import type { UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
 import type { UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
@@ -19,7 +18,7 @@ import type { IPageForPageDuplicateModal } from '~/stores/modal';
 import { usePageDuplicateModal, usePageDeleteModal } from '~/stores/modal';
 import { usePageDuplicateModal, usePageDeleteModal } from '~/stores/modal';
 import { mutateAllPageInfo, useCurrentPagePath, useSWRMUTxCurrentPage } from '~/stores/page';
 import { mutateAllPageInfo, useCurrentPagePath, useSWRMUTxCurrentPage } from '~/stores/page';
 import {
 import {
-  useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
+  useSWRxRootPage, mutatePageTree, mutatePageList,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
@@ -35,66 +34,12 @@ const moduleClass = styles['items-tree'] ?? '';
 
 
 const logger = loggerFactory('growi:cli:ItemsTree');
 const logger = loggerFactory('growi:cli:ItemsTree');
 
 
-/*
- * Utility to generate initial node
- */
-const generateInitialNodeBeforeResponse = (targetAndAncestors: Partial<IPageHasId>[]): ItemNode => {
-  const nodes = targetAndAncestors.map((page): ItemNode => {
-    return new ItemNode(page, []);
-  });
-
-  // update children for each node
-  const rootNode = nodes.reduce((child, parent) => {
-    parent.children = [child];
-    return parent;
-  });
-
-  return rootNode;
-};
-
-const generateInitialNodeAfterResponse = (ancestorsChildren: Record<string, Partial<IPageHasId>[]>, rootNode: ItemNode): ItemNode => {
-  const paths = Object.keys(ancestorsChildren);
-
-  let currentNode = rootNode;
-  paths.every((path) => {
-    // stop rendering when non-migrated pages found
-    if (currentNode == null) {
-      return false;
-    }
-
-    const childPages = ancestorsChildren[path];
-    currentNode.children = ItemNode.generateNodesFromPages(childPages);
-    const nextNode = currentNode.children.filter((node) => {
-      return paths.includes(node.page.path as string);
-    })[0];
-    currentNode = nextNode;
-    return true;
-  });
-
-  return rootNode;
-};
-
-// user defined typeguard to assert the arg is not null
-type RenderingCondition = {
-  ancestorsChildrenResult: AncestorsChildrenResult | undefined,
-  rootPageResult: RootPageResult | undefined,
-}
-type SecondStageRenderingCondition = {
-  ancestorsChildrenResult: AncestorsChildrenResult,
-  rootPageResult: RootPageResult,
-}
-const isSecondStageRenderingCondition = (condition: RenderingCondition|SecondStageRenderingCondition): condition is SecondStageRenderingCondition => {
-  return condition.ancestorsChildrenResult != null && condition.rootPageResult != null;
-};
-
-
 type ItemsTreeProps = {
 type ItemsTreeProps = {
   isEnableActions: boolean
   isEnableActions: boolean
   isReadOnlyUser: boolean
   isReadOnlyUser: boolean
   isWipPageShown?: boolean
   isWipPageShown?: boolean
   targetPath: string
   targetPath: string
-  targetPathOrId?: Nullable<string>
-  targetAndAncestorsData?: TargetAndAncestors
+  targetPathOrId?: string,
   CustomTreeItem: React.FunctionComponent<TreeItemProps>
   CustomTreeItem: React.FunctionComponent<TreeItemProps>
   onClickTreeItem?: (page: IPageForItem) => void;
   onClickTreeItem?: (page: IPageForItem) => void;
 }
 }
@@ -104,14 +49,13 @@ type ItemsTreeProps = {
  */
  */
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, isWipPageShown, CustomTreeItem, onClickTreeItem,
+    targetPath, targetPathOrId, isEnableActions, isReadOnlyUser, isWipPageShown, CustomTreeItem, onClickTreeItem,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
 
 
-  const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath, { suspense: true });
-  const { data: rootPageResult, error: error2 } = useSWRxRootPage({ suspense: true });
+  const { data: rootPageResult, error } = useSWRxRootPage({ suspense: true });
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
@@ -122,14 +66,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   // for mutation
   // for mutation
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
 
-
-  const renderingCondition = useMemo(() => {
-    return {
-      ancestorsChildrenResult,
-      rootPageResult,
-    };
-  }, [ancestorsChildrenResult, rootPageResult]);
-
   useEffect(() => {
   useEffect(() => {
     if (socket == null) {
     if (socket == null) {
       return;
       return;
@@ -197,34 +133,18 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
   }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
 
 
 
 
-  if (error1 != null || error2 != null) {
-    // TODO: improve message
-    toastError('Error occurred while fetching pages to render PageTree');
+  if (error != null) {
+    toastError(t('pagetree.error_retrieving_the_pagetree'));
     return <></>;
     return <></>;
   }
   }
 
 
-  let initialItemNode;
-  /*
-   * Render second stage
-   */
-  if (isSecondStageRenderingCondition(renderingCondition)) {
-    initialItemNode = generateInitialNodeAfterResponse(
-      renderingCondition.ancestorsChildrenResult.ancestorsChildren,
-      new ItemNode(renderingCondition.rootPageResult.rootPage),
-    );
-  }
-  /*
-   * Before swr response comes back
-   */
-  else if (targetAndAncestorsData != null) {
-    initialItemNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
-  }
-
+  const initialItemNode = rootPageResult ? new ItemNode(rootPageResult.rootPage) : null;
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
       <ul className={`${moduleClass} list-group`}>
       <ul className={`${moduleClass} list-group`}>
         <CustomTreeItem
         <CustomTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
+          targetPath={targetPath}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           itemNode={initialItemNode}
           isOpen
           isOpen

+ 2 - 3
apps/app/src/client/components/LoginForm/ExternalAuthButton.tsx

@@ -1,12 +1,12 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
-import { IExternalAuthProviderType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
+
 const authIcon = {
 const authIcon = {
   [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
   [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
   [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
   [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
-  [IExternalAuthProviderType.facebook]: <span className="growi-custom-icons align-bottom">facebook</span>,
   [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
   [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
   [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
   [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
 };
 };
@@ -14,7 +14,6 @@ const authIcon = {
 const authLabel = {
 const authLabel = {
   [IExternalAuthProviderType.google]: 'Google',
   [IExternalAuthProviderType.google]: 'Google',
   [IExternalAuthProviderType.github]: 'GitHub',
   [IExternalAuthProviderType.github]: 'GitHub',
-  [IExternalAuthProviderType.facebook]: 'Facebook',
   [IExternalAuthProviderType.oidc]: 'OIDC',
   [IExternalAuthProviderType.oidc]: 'OIDC',
   [IExternalAuthProviderType.saml]: 'SAML',
   [IExternalAuthProviderType.saml]: 'SAML',
 };
 };

+ 0 - 6
apps/app/src/client/components/LoginForm/LoginForm.module.scss

@@ -82,12 +82,6 @@
       --bs-btn-active-bg: #{rgba(#403D3E, 0.7)};
       --bs-btn-active-bg: #{rgba(#403D3E, 0.7)};
     }
     }
 
 
-    .btn-auth-facebook {
-      --bs-btn-bg: #{rgba(#29487d, 0.4)};
-      --bs-btn-hover-bg: #{rgba(#29487d, 0.9)};
-      --bs-btn-active-bg: #{rgba(#29487d, 0.9)};
-    }
-
     .btn-auth-oidc {
     .btn-auth-oidc {
       --bs-btn-bg: #{rgba(#835B1A, 0.4)};
       --bs-btn-bg: #{rgba(#835B1A, 0.4)};
       --bs-btn-hover-bg: #{rgba(#835B1A, 0.8)};
       --bs-btn-hover-bg: #{rgba(#835B1A, 0.8)};

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

@@ -2,7 +2,6 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
-import type { IExternalAuthProviderType } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -13,6 +12,7 @@ import { useTWithOpt } from '~/client/util/t-with-opt';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 

+ 0 - 6
apps/app/src/client/components/Me/AssociateModal.tsx

@@ -80,12 +80,6 @@ const AssociateModal = (props: Props): JSX.Element => {
             >
             >
               <span className="growi-custom-icons">google</span> (TBD) Google OAuth
               <span className="growi-custom-icons">google</span> (TBD) Google OAuth
             </NavLink>
             </NavLink>
-            {/* <NavLink
-              className={`${activeTab === 4 ? 'active' : ''} d-flex gap-1 align-items-center`}
-              onClick={() => setActiveTab(4)}
-            >
-              <span className="growi-custom-icons">facebook</span> (TBD) Facebook
-            </NavLink> */}
           </Nav>
           </Nav>
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
             <TabPane tabId={1}>
             <TabPane tabId={1}>

+ 3 - 2
apps/app/src/client/components/Me/DisassociateModal.tsx

@@ -1,6 +1,6 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
-import type { IExternalAccountHasId } from '@growi/core';
+import type { HasObjectId, IExternalAccount } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Modal,
   Modal,
@@ -10,12 +10,13 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 
 type Props = {
 type Props = {
   isOpen: boolean,
   isOpen: boolean,
   onClose: () => void,
   onClose: () => void,
-  accountForDisassociate: IExternalAccountHasId,
+  accountForDisassociate: IExternalAccount<IExternalAuthProviderType> & HasObjectId,
 }
 }
 
 
 
 

+ 6 - 7
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -9,11 +9,10 @@ import type {
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
-import { auto } from '@popperjs/core';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
 import Sticky from 'react-stickynode';
 import Sticky from 'react-stickynode';
 import { DropdownItem, UncontrolledTooltip } from 'reactstrap';
 import { DropdownItem, UncontrolledTooltip } from 'reactstrap';
 
 
@@ -22,11 +21,6 @@ import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
-import {
-  useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsLocalAccountRegistrationEnabled, useIsSharedUser, useShareLinkId,
-} from '~/stores-universal/context';
-import { useEditorMode } from '~/stores-universal/ui';
 import {
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents, type IPageForPageDuplicateModal,
   usePageAccessoriesModal, PageAccessoriesModalContents, type IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
@@ -40,6 +34,11 @@ import {
   useIsAbleToChangeEditorMode,
   useIsAbleToChangeEditorMode,
   useIsDeviceLargerThanMd,
   useIsDeviceLargerThanMd,
 } from '~/stores/ui';
 } from '~/stores/ui';
+import {
+  useCurrentPathname,
+  useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsLocalAccountRegistrationEnabled, useIsSharedUser, useShareLinkId,
+} from '~/stores-universal/context';
+import { useEditorMode } from '~/stores-universal/ui';
 
 
 import { NotAvailable } from '../NotAvailable';
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 import { Skeleton } from '../Skeleton';

+ 9 - 5
apps/app/src/client/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -8,16 +8,20 @@ import styles from './EditorNavbar.module.scss';
 
 
 const moduleClass = styles['editor-navbar'] ?? '';
 const moduleClass = styles['editor-navbar'] ?? '';
 
 
-export const EditorNavbar = (): JSX.Element => {
+const EditingUsers = (): JSX.Element => {
   const { data: editingUsers } = useEditingUsers();
   const { data: editingUsers } = useEditingUsers();
+  return (
+    <EditingUserList
+      userList={editingUsers?.userList ?? []}
+    />
+  );
+};
 
 
+export const EditorNavbar = (): JSX.Element => {
   return (
   return (
     <div className={`${moduleClass} d-flex flex-column flex-sm-row justify-content-between ps-3 ps-md-5 ps-xl-4 pe-4 py-1 align-items-sm-end`}>
     <div className={`${moduleClass} d-flex flex-column flex-sm-row justify-content-between ps-3 ps-md-5 ps-xl-4 pe-4 py-1 align-items-sm-end`}>
       <div className="order-2 order-sm-1"><PageHeader /></div>
       <div className="order-2 order-sm-1"><PageHeader /></div>
-      <div className="order-1 order-sm-2"><EditingUserList
-        userList={editingUsers?.userList ?? []}
-      />
-      </div>
+      <div className="order-1 order-sm-2"><EditingUsers /></div>
     </div>
     </div>
   );
   );
 };
 };

+ 38 - 32
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -81,7 +81,7 @@ type Props = {
   visibility?: boolean,
   visibility?: boolean,
 }
 }
 
 
-export const PageEditor = React.memo((props: Props): JSX.Element => {
+export const PageEditorSubstance = (props: Props): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -361,42 +361,48 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     return <></>;
     return <></>;
   }
   }
 
 
+  return (
+    <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
+      <div className="page-editor-editor-container flex-expand-vert border-end">
+        <CodeMirrorEditorMain
+          isEditorMode={editorMode === EditorMode.Editor}
+          onSave={saveWithShortcut}
+          onUpload={uploadHandler}
+          acceptedUploadFileType={acceptedUploadFileType}
+          onScroll={scrollEditorHandlerThrottle}
+          indentSize={currentIndentSize ?? defaultIndentSize}
+          user={user ?? undefined}
+          pageId={pageId ?? undefined}
+          initialValue={initialValue}
+          editorSettings={editorSettings}
+          onEditorsUpdated={onEditorsUpdated}
+          cmProps={cmProps}
+        />
+      </div>
+      <div
+        ref={previewRef}
+        onScroll={scrollPreviewHandlerThrottle}
+        className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
+      >
+        <Preview
+          rendererOptions={rendererOptions}
+          markdown={markdownToPreview}
+          pagePath={currentPagePath}
+          expandContentWidth={shouldExpandContent}
+          style={pastEndStyle}
+        />
+      </div>
+    </div>
+  );
+};
+
+export const PageEditor = React.memo((props: Props): JSX.Element => {
   return (
   return (
     <div data-testid="page-editor" id="page-editor" className={`flex-expand-vert ${props.visibility ? '' : 'd-none'}`}>
     <div data-testid="page-editor" id="page-editor" className={`flex-expand-vert ${props.visibility ? '' : 'd-none'}`}>
 
 
       <EditorNavbar />
       <EditorNavbar />
 
 
-      <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
-        <div className="page-editor-editor-container flex-expand-vert border-end">
-          <CodeMirrorEditorMain
-            isEditorMode={editorMode === EditorMode.Editor}
-            onSave={saveWithShortcut}
-            onUpload={uploadHandler}
-            acceptedUploadFileType={acceptedUploadFileType}
-            onScroll={scrollEditorHandlerThrottle}
-            indentSize={currentIndentSize ?? defaultIndentSize}
-            user={user ?? undefined}
-            pageId={pageId ?? undefined}
-            initialValue={initialValue}
-            editorSettings={editorSettings}
-            onEditorsUpdated={onEditorsUpdated}
-            cmProps={cmProps}
-          />
-        </div>
-        <div
-          ref={previewRef}
-          onScroll={scrollPreviewHandlerThrottle}
-          className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
-        >
-          <Preview
-            rendererOptions={rendererOptions}
-            markdown={markdownToPreview}
-            pagePath={currentPagePath}
-            expandContentWidth={shouldExpandContent}
-            style={pastEndStyle}
-          />
-        </div>
-      </div>
+      <PageEditorSubstance visibility={props.visibility} />
 
 
       <EditorNavbarBottom />
       <EditorNavbarBottom />
 
 

+ 8 - 0
apps/app/src/client/components/PagePathNavSticky/PagePathNavSticky.module.scss

@@ -13,3 +13,11 @@
     }
     }
   }
   }
 }
 }
+
+@media print {
+  .grw-page-path-nav-sticky :global {
+    .sticky-inner-wrapper {
+      position: static !important;
+    }
+  }
+}

+ 1 - 3
apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx

@@ -13,7 +13,7 @@ import {
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
 
 
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
-import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { usePageSelectModal } from '~/stores/modal';
 import { usePageSelectModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 
 
@@ -38,7 +38,6 @@ export const PageSelectModal: FC = () => {
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
-  const { data: targetAndAncestorsData } = useTargetAndAncestors();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
 
 
   const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
   const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
@@ -96,7 +95,6 @@ export const PageSelectModal: FC = () => {
                 isReadOnlyUser={!!isReadOnlyUser}
                 isReadOnlyUser={!!isReadOnlyUser}
                 targetPath={targetPath}
                 targetPath={targetPath}
                 targetPathOrId={targetPathOrId}
                 targetPathOrId={targetPathOrId}
-                targetAndAncestorsData={targetAndAncestorsData}
                 onClickTreeItem={onClickTreeItem}
                 onClickTreeItem={onClickTreeItem}
               />
               />
             </div>
             </div>

+ 4 - 7
apps/app/src/client/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -5,10 +5,10 @@ import React, {
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
-import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
+import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import {
 import {
-  mutatePageTree, mutateRecentlyUpdated, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
+  mutatePageTree, mutateRecentlyUpdated, useSWRxRootPage, useSWRxV5MigrationStatus,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 import { useSidebarScrollerRef } from '~/stores/ui';
 import { useSidebarScrollerRef } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -99,14 +99,12 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetId } = useCurrentPageId();
-  const { data: targetAndAncestorsData } = useTargetAndAncestors();
 
 
   const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
   const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
 
 
   const targetPathOrId = targetId || currentPath;
   const targetPathOrId = targetId || currentPath;
   const path = currentPath || '/';
   const path = currentPath || '/';
 
 
-  const { data: ancestorsChildrenResult } = useSWRxPageAncestorsChildren(path, { suspense: true });
   const { data: rootPageResult } = useSWRxRootPage({ suspense: true });
   const { data: rootPageResult } = useSWRxRootPage({ suspense: true });
   const { data: sidebarScrollerRef } = useSidebarScrollerRef();
   const { data: sidebarScrollerRef } = useSidebarScrollerRef();
   const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
   const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
@@ -144,7 +142,7 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
   const scrollOnInitDebounced = useMemo(() => debounce(500, scrollOnInit), [scrollOnInit]);
   const scrollOnInitDebounced = useMemo(() => debounce(500, scrollOnInit), [scrollOnInit]);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (isInitialScrollCompleted || ancestorsChildrenResult == null || rootPageResult == null) {
+    if (isInitialScrollCompleted || rootPageResult == null) {
       return;
       return;
     }
     }
 
 
@@ -166,7 +164,7 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
     return () => {
     return () => {
       observer.disconnect();
       observer.disconnect();
     };
     };
-  }, [isInitialScrollCompleted, scrollOnInitDebounced, ancestorsChildrenResult, rootPageResult]);
+  }, [isInitialScrollCompleted, scrollOnInitDebounced, rootPageResult]);
   // *******************************  end  *******************************
   // *******************************  end  *******************************
 
 
 
 
@@ -189,7 +187,6 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
         isWipPageShown={isWipPageShown}
         isWipPageShown={isWipPageShown}
         targetPath={path}
         targetPath={path}
         targetPathOrId={targetPathOrId}
         targetPathOrId={targetPathOrId}
-        targetAndAncestorsData={targetAndAncestorsData}
         CustomTreeItem={PageTreeItem}
         CustomTreeItem={PageTreeItem}
       />
       />
 
 

+ 2 - 1
apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -34,7 +34,7 @@ const moduleClass = styles['page-tree-item'] ?? '';
 
 
 const logger = loggerFactory('growi:cli:Item');
 const logger = loggerFactory('growi:cli:Item');
 
 
-export const PageTreeItem: FC<TreeItemProps> = (props) => {
+export const PageTreeItem = (props:TreeItemProps): JSX.Element => {
   const router = useRouter();
   const router = useRouter();
 
 
   const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
   const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
@@ -186,6 +186,7 @@ export const PageTreeItem: FC<TreeItemProps> = (props) => {
   return (
   return (
     <TreeItemLayout
     <TreeItemLayout
       className={moduleClass}
       className={moduleClass}
+      targetPath={props.targetPath}
       targetPathOrId={props.targetPathOrId}
       targetPathOrId={props.targetPathOrId}
       itemLevel={props.itemLevel}
       itemLevel={props.itemLevel}
       itemNode={props.itemNode}
       itemNode={props.itemNode}

+ 8 - 6
apps/app/src/client/components/TreeItem/TreeItemLayout.tsx

@@ -22,21 +22,21 @@ type TreeItemLayoutProps = TreeItemProps & {
   indentSize?: number,
   indentSize?: number,
 }
 }
 
 
-export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
+export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
   const {
   const {
     className, itemClassName,
     className, itemClassName,
     indentSize = 10,
     indentSize = 10,
     itemLevel: baseItemLevel = 1,
     itemLevel: baseItemLevel = 1,
-    itemNode, targetPathOrId, isOpen: _isOpen = false,
+    itemNode, targetPath, targetPathOrId, isOpen: _isOpen = false,
     onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, onWheelClick,
     onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, onWheelClick,
     isEnableActions, isReadOnlyUser, isWipPageShown = true,
     isEnableActions, isReadOnlyUser, isWipPageShown = true,
     itemRef, itemClass,
     itemRef, itemClass,
     showAlternativeContent,
     showAlternativeContent,
   } = props;
   } = props;
 
 
-  const { page, children } = itemNode;
+  const { page } = itemNode;
 
 
-  const [currentChildren, setCurrentChildren] = useState<ItemNode[]>(children);
+  const [currentChildren, setCurrentChildren] = useState<ItemNode[]>([]);
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [isOpen, setIsOpen] = useState(_isOpen);
 
 
   const { data } = useSWRxPageChildren(isOpen ? page._id : null);
   const { data } = useSWRxPageChildren(isOpen ? page._id : null);
@@ -84,8 +84,9 @@ export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
 
 
   // didMount
   // didMount
   useEffect(() => {
   useEffect(() => {
-    if (hasChildren()) setIsOpen(true);
-  }, [hasChildren]);
+    const isPathToTarget = page.path != null && targetPath.startsWith(page.path) && targetPath !== page.path; // Target Page does not need to be opened
+    if (isPathToTarget) setIsOpen(true);
+  }, [targetPath, page.path]);
 
 
   /*
   /*
    * When swr fetch succeeded
    * When swr fetch succeeded
@@ -108,6 +109,7 @@ export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
     isReadOnlyUser,
     isReadOnlyUser,
     isOpen: false,
     isOpen: false,
     isWipPageShown,
     isWipPageShown,
+    targetPath,
     targetPathOrId,
     targetPathOrId,
     onRenamed,
     onRenamed,
     onClickDuplicateMenuItem,
     onClickDuplicateMenuItem,

+ 2 - 2
apps/app/src/client/components/TreeItem/interfaces/index.ts

@@ -1,5 +1,4 @@
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import type { IPageToDeleteWithMeta } from '@growi/core';
-import type { Nullable } from 'vitest';
 
 
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForPageDuplicateModal } from '~/stores/modal';
 import type { IPageForPageDuplicateModal } from '~/stores/modal';
@@ -23,7 +22,8 @@ export type TreeItemToolProps = TreeItemBaseProps & {
 };
 };
 
 
 export type TreeItemProps = TreeItemBaseProps & {
 export type TreeItemProps = TreeItemBaseProps & {
-  targetPathOrId?: Nullable<string>,
+  targetPath: string,
+  targetPathOrId?:string,
   isOpen?: boolean,
   isOpen?: boolean,
   isWipPageShown?: boolean,
   isWipPageShown?: boolean,
   itemClass?: React.FunctionComponent<TreeItemProps>,
   itemClass?: React.FunctionComponent<TreeItemProps>,

+ 1 - 1
apps/app/src/client/util/bookmark-utils.ts

@@ -16,7 +16,7 @@ export const addNewFolder = async(name: string, parent: string | null): Promise<
 
 
 // Put bookmark to a folder
 // Put bookmark to a folder
 export const addBookmarkToFolder = async(pageId: string, folderId: string | null): Promise<void> => {
 export const addBookmarkToFolder = async(pageId: string, folderId: string | null): Promise<void> => {
-  await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId, folderId });
+  await apiv3Post('/bookmark-folder/add-bookmark-to-folder', { pageId, folderId });
 };
 };
 
 
 // Delete bookmark folder
 // Delete bookmark folder

+ 1 - 1
apps/app/src/components/Layout/BasicLayout.tsx

@@ -46,7 +46,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
   return (
     <RawLayout className={`${moduleClass} ${className ?? ''}`}>
     <RawLayout className={`${moduleClass} ${className ?? ''}`}>
       <div className="page-wrapper flex-row">
       <div className="page-wrapper flex-row">
-        <div className="z-2">
+        <div className="z-2 d-print-none">
           <Sidebar />
           <Sidebar />
         </div>
         </div>
 
 

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

@@ -1,7 +1,7 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
 
 .page-content-footer :global {
 .page-content-footer :global {
-  border-top: solid 1px transparent;
+  border-top: solid 1px var(--bs-border-color);
   .page-meta {
   .page-meta {
     font-size: 0.95em;
     font-size: 0.95em;
   }
   }

+ 4 - 6
apps/app/src/components/PageView/PageContentFooter.tsx

@@ -22,12 +22,10 @@ export const PageContentFooter = (props: PageContentFooterProps): JSX.Element =>
   }
   }
 
 
   return (
   return (
-    <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
-      <div className="container-lg grw-container-convertible">
-        <div className="page-meta">
-          <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
-          <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="footer" />
-        </div>
+    <div className={`${styles['page-content-footer']} my-4 pt-4 d-edit-none d-print-none}`}>
+      <div className="page-meta">
+        <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
+        <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="footer" />
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 1 - 1
apps/app/src/components/PageView/PageViewLayout.tsx

@@ -51,7 +51,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
       </div>
       </div>
 
 
       { footerContents != null && (
       { footerContents != null && (
-        <footer className={`footer d-edit-none wide-gutter-x-lg ${fluidLayoutClass}`}>
+        <footer className={`footer d-edit-none container-lg wide-gutter-x-lg ${fluidLayoutClass}`}>
           {footerContents}
           {footerContents}
         </footer>
         </footer>
       ) }
       ) }

+ 1 - 1
apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx

@@ -42,7 +42,7 @@ const ShareLinkAlert: FC<Props> = (props: Props) => {
   return (
   return (
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
       <span className="material-symbols-outlined me-1">link</span>
       <span className="material-symbols-outlined me-1">link</span>
-      {(expiredAt === null ? <span>{t('page_page.notice.no_deadline')}</span>
+      {(expiredAt == null ? <span>{t('page_page.notice.no_deadline')}</span>
       // eslint-disable-next-line react/no-danger
       // eslint-disable-next-line react/no-danger
         : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
         : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
       )}
       )}

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

@@ -30,7 +30,7 @@ interface AuthorizedRequest extends Request {
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
@@ -216,14 +216,14 @@ module.exports = (crowi: Crowi): Router => {
 
 
   router.get('/ldap/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/ldap/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
     const settings = {
     const settings = {
-      ldapGroupSearchBase: configManager?.getConfig('crowi', 'external-user-group:ldap:groupSearchBase'),
-      ldapGroupMembershipAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute'),
-      ldapGroupMembershipAttributeType: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType'),
-      ldapGroupChildGroupAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute'),
-      autoGenerateUserOnLdapGroupSync: configManager?.getConfig('crowi', 'external-user-group:ldap:autoGenerateUserOnGroupSync'),
-      preserveDeletedLdapGroups: configManager?.getConfig('crowi', 'external-user-group:ldap:preserveDeletedGroups'),
-      ldapGroupNameAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute'),
-      ldapGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute'),
+      ldapGroupSearchBase: configManager.getConfig('external-user-group:ldap:groupSearchBase'),
+      ldapGroupMembershipAttribute: configManager.getConfig('external-user-group:ldap:groupMembershipAttribute'),
+      ldapGroupMembershipAttributeType: configManager.getConfig('external-user-group:ldap:groupMembershipAttributeType'),
+      ldapGroupChildGroupAttribute: configManager.getConfig('external-user-group:ldap:groupChildGroupAttribute'),
+      autoGenerateUserOnLdapGroupSync: configManager.getConfig('external-user-group:ldap:autoGenerateUserOnGroupSync'),
+      preserveDeletedLdapGroups: configManager.getConfig('external-user-group:ldap:preserveDeletedGroups'),
+      ldapGroupNameAttribute: configManager.getConfig('external-user-group:ldap:groupNameAttribute'),
+      ldapGroupDescriptionAttribute: configManager.getConfig('external-user-group:ldap:groupDescriptionAttribute'),
     };
     };
 
 
     return res.apiv3(settings);
     return res.apiv3(settings);
@@ -231,14 +231,14 @@ module.exports = (crowi: Crowi): Router => {
 
 
   router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
     const settings = {
     const settings = {
-      keycloakHost: configManager?.getConfig('crowi', 'external-user-group:keycloak:host'),
-      keycloakGroupRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm'),
-      keycloakGroupSyncClientRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm'),
-      keycloakGroupSyncClientID: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID'),
-      keycloakGroupSyncClientSecret: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret'),
-      autoGenerateUserOnKeycloakGroupSync: configManager?.getConfig('crowi', 'external-user-group:keycloak:autoGenerateUserOnGroupSync'),
-      preserveDeletedKeycloakGroups: configManager?.getConfig('crowi', 'external-user-group:keycloak:preserveDeletedGroups'),
-      keycloakGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute'),
+      keycloakHost: configManager.getConfig('external-user-group:keycloak:host'),
+      keycloakGroupRealm: configManager.getConfig('external-user-group:keycloak:groupRealm'),
+      keycloakGroupSyncClientRealm: configManager.getConfig('external-user-group:keycloak:groupSyncClientRealm'),
+      keycloakGroupSyncClientID: configManager.getConfig('external-user-group:keycloak:groupSyncClientID'),
+      keycloakGroupSyncClientSecret: configManager.getConfig('external-user-group:keycloak:groupSyncClientSecret'),
+      autoGenerateUserOnKeycloakGroupSync: configManager.getConfig('external-user-group:keycloak:autoGenerateUserOnGroupSync'),
+      preserveDeletedKeycloakGroups: configManager.getConfig('external-user-group:keycloak:preserveDeletedGroups'),
+      keycloakGroupDescriptionAttribute: configManager.getConfig('external-user-group:keycloak:groupDescriptionAttribute'),
     };
     };
 
 
     return res.apiv3(settings);
     return res.apiv3(settings);
@@ -269,7 +269,7 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
 
 
     try {
     try {
-      await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
+      await configManager.updateConfigs(params, { skipPubsub: true });
       return res.apiv3({}, 204);
       return res.apiv3({}, 204);
     }
     }
     catch (err) {
     catch (err) {
@@ -301,7 +301,7 @@ module.exports = (crowi: Crowi): Router => {
       };
       };
 
 
       try {
       try {
-        await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
+        await configManager.updateConfigs(params, { skipPubsub: true });
         return res.apiv3({}, 204);
         return res.apiv3({}, 204);
       }
       }
       catch (err) {
       catch (err) {
@@ -319,7 +319,7 @@ module.exports = (crowi: Crowi): Router => {
       );
       );
     }
     }
 
 
-    const isLdapEnabled = await configManager.getConfig('crowi', 'security:passport-ldap:isEnabled');
+    const isLdapEnabled = await configManager.getConfig('security:passport-ldap:isEnabled');
     if (!isLdapEnabled) {
     if (!isLdapEnabled) {
       return res.apiv3Err(
       return res.apiv3Err(
         new ErrorV3('Authentication using ldap is not set', 'external_user_group.ldap.auth_not_set'), 422,
         new ErrorV3('Authentication using ldap is not set', 'external_user_group.ldap.auth_not_set'), 422,
@@ -349,25 +349,25 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
 
 
     const getAuthProviderType = () => {
     const getAuthProviderType = () => {
-      let kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
+      let kcHost = configManager.getConfig('external-user-group:keycloak:host');
       if (kcHost?.endsWith('/')) {
       if (kcHost?.endsWith('/')) {
         kcHost = kcHost.slice(0, -1);
         kcHost = kcHost.slice(0, -1);
       }
       }
-      const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
+      const kcGroupRealm = configManager.getConfig('external-user-group:keycloak:groupRealm');
 
 
       // starts with kcHost, contains kcGroupRealm in path
       // starts with kcHost, contains kcGroupRealm in path
       // see: https://regex101.com/r/3ihDmf/1
       // see: https://regex101.com/r/3ihDmf/1
       const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
       const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
 
 
-      const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
-      const oidcIssuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
+      const isOidcEnabled = configManager.getConfig('security:passport-oidc:isEnabled');
+      const oidcIssuerHost = configManager.getConfig('security:passport-oidc:issuerHost');
 
 
-      if (isOidcEnabled && regex.test(oidcIssuerHost)) return 'oidc';
+      if (isOidcEnabled && oidcIssuerHost != null && regex.test(oidcIssuerHost)) return 'oidc';
 
 
-      const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
-      const samlEntryPoint = configManager.getConfig('crowi', 'security:passport-saml:entryPoint');
+      const isSamlEnabled = configManager.getConfig('security:passport-saml:isEnabled');
+      const samlEntryPoint = configManager.getConfig('security:passport-saml:entryPoint');
 
 
-      if (isSamlEnabled && regex.test(samlEntryPoint)) return 'saml';
+      if (isSamlEnabled && samlEntryPoint != null && regex.test(samlEntryPoint)) return 'saml';
 
 
       return null;
       return null;
     };
     };

+ 7 - 6
apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts

@@ -3,19 +3,20 @@ import type { IUserHasId } from '@growi/core';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import ExternalAccount from '~/server/models/external-account';
 import ExternalAccount from '~/server/models/external-account';
 import S2sMessage from '~/server/models/vo/s2s-message';
 import S2sMessage from '~/server/models/vo/s2s-message';
-import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
-import { S2sMessageHandlable } from '~/server/service/s2s-messaging/handlable';
+import type { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import type { S2sMessageHandlable } from '~/server/service/s2s-messaging/handlable';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
 import { configManager } from '../../../../server/service/config-manager';
 import { configManager } from '../../../../server/service/config-manager';
 import { externalAccountService } from '../../../../server/service/external-account';
 import { externalAccountService } from '../../../../server/service/external-account';
-import {
+import type {
   ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, IExternalUserGroupHasId,
   ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, IExternalUserGroupHasId,
 } from '../../interfaces/external-user-group';
 } from '../../interfaces/external-user-group';
 import ExternalUserGroup from '../models/external-user-group';
 import ExternalUserGroup from '../models/external-user-group';
 import ExternalUserGroupRelation from '../models/external-user-group-relation';
 import ExternalUserGroupRelation from '../models/external-user-group-relation';
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 
 const logger = loggerFactory('growi:service:external-user-group-sync-service');
 const logger = loggerFactory('growi:service:external-user-group-sync-service');
 
 
@@ -37,7 +38,7 @@ abstract class ExternalUserGroupSyncService implements S2sMessageHandlable {
 
 
   groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
   groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
 
 
-  authProviderType: string | null; // auth provider type (e.g: ldap, oidc). Has to be set before syncExternalUserGroups execution.
+  authProviderType: IExternalAuthProviderType | null; // auth provider type (e.g: ldap, oidc). Has to be set before syncExternalUserGroups execution.
 
 
   socketIoService: any;
   socketIoService: any;
 
 
@@ -93,7 +94,7 @@ abstract class ExternalUserGroupSyncService implements S2sMessageHandlable {
     if (this.authProviderType == null) throw new Error('auth provider type is not set');
     if (this.authProviderType == null) throw new Error('auth provider type is not set');
     if (this.syncStatus.isExecutingSync) throw new Error('External user group sync is already being executed');
     if (this.syncStatus.isExecutingSync) throw new Error('External user group sync is already being executed');
 
 
-    const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
+    const preserveDeletedLdapGroups = configManager.getConfig(`external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     const existingExternalUserGroupIds: string[] = [];
     const existingExternalUserGroupIds: string[] = [];
 
 
     const socket = this.socketIoService?.getAdminSocket();
     const socket = this.socketIoService?.getAdminSocket();
@@ -183,7 +184,7 @@ abstract class ExternalUserGroupSyncService implements S2sMessageHandlable {
     const authProviderType = this.authProviderType;
     const authProviderType = this.authProviderType;
     if (authProviderType == null) throw new Error('auth provider type is not set');
     if (authProviderType == null) throw new Error('auth provider type is not set');
 
 
-    const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
+    const autoGenerateUserOnGroupSync = configManager.getConfig(`external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
 
 
     const getExternalAccount = async() => {
     const getExternalAccount = async() => {
       if (autoGenerateUserOnGroupSync && externalAccountService != null) {
       if (autoGenerateUserOnGroupSync && externalAccountService != null) {

+ 2 - 1
apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.integ.ts

@@ -146,7 +146,8 @@ describe('KeycloakUserGroupSyncService.generateExternalUserGroupTrees', () => {
   };
   };
 
 
   beforeAll(async() => {
   beforeAll(async() => {
-    await configManager.updateConfigsInTheSameNamespace('crowi', configParams, true);
+    await configManager.loadConfigs();
+    await configManager.updateConfigs(configParams, { skipPubsub: true });
     keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(null, null);
     keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(null, null);
     keycloakUserGroupSyncService.init('oidc');
     keycloakUserGroupSyncService.init('oidc');
   });
   });

+ 14 - 13
apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.ts

@@ -1,13 +1,14 @@
 import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
 import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
-import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';
-import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';
+import type GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';
+import type UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';
 
 
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
-import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import type { S2sMessagingService } from '~/server/service/s2s-messaging/base';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
-import { ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
+import type { ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
+import { ExternalGroupProviderType } from '../../interfaces/external-user-group';
 
 
 import ExternalUserGroupSyncService from './external-user-group-sync';
 import ExternalUserGroupSyncService from './external-user-group-sync';
 
 
@@ -22,9 +23,9 @@ export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
 
 
   kcAdminClient: KeycloakAdminClient;
   kcAdminClient: KeycloakAdminClient;
 
 
-  realm: string; // realm that contains the groups
+  realm: string | undefined; // realm that contains the groups
 
 
-  groupDescriptionAttribute: string; // attribute to map to group description
+  groupDescriptionAttribute: string | undefined; // attribute to map to group description
 
 
   isInitialized = false;
   isInitialized = false;
 
 
@@ -34,10 +35,10 @@ export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
   init(authProviderType: 'oidc' | 'saml'): void {
   init(authProviderType: 'oidc' | 'saml'): void {
-    const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
-    const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
-    const kcGroupSyncClientRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm');
-    const kcGroupDescriptionAttribute = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute');
+    const kcHost = configManager.getConfig('external-user-group:keycloak:host');
+    const kcGroupRealm = configManager.getConfig('external-user-group:keycloak:groupRealm');
+    const kcGroupSyncClientRealm = configManager.getConfig('external-user-group:keycloak:groupSyncClientRealm');
+    const kcGroupDescriptionAttribute = configManager.getConfig('external-user-group:keycloak:groupDescriptionAttribute');
 
 
     this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
     this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
     this.realm = kcGroupRealm;
     this.realm = kcGroupRealm;
@@ -70,12 +71,12 @@ export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
    * Authenticate to group sync client using client credentials grant type
    * Authenticate to group sync client using client credentials grant type
    */
    */
   private async auth(): Promise<void> {
   private async auth(): Promise<void> {
-    const kcGroupSyncClientID: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID');
-    const kcGroupSyncClientSecret: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret');
+    const kcGroupSyncClientID = configManager.getConfig('external-user-group:keycloak:groupSyncClientID');
+    const kcGroupSyncClientSecret = configManager.getConfig('external-user-group:keycloak:groupSyncClientSecret');
 
 
     await this.kcAdminClient.auth({
     await this.kcAdminClient.auth({
       grantType: 'client_credentials',
       grantType: 'client_credentials',
-      clientId: kcGroupSyncClientID,
+      clientId: kcGroupSyncClientID ?? '',
       clientSecret: kcGroupSyncClientSecret,
       clientSecret: kcGroupSyncClientSecret,
     });
     });
   }
   }

+ 12 - 10
apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts

@@ -1,12 +1,14 @@
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
-import { ldapService, SearchResultEntry } from '~/server/service/ldap';
-import PassportService from '~/server/service/passport';
-import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import type { SearchResultEntry } from '~/server/service/ldap';
+import { ldapService } from '~/server/service/ldap';
+import type PassportService from '~/server/service/passport';
+import type { S2sMessagingService } from '~/server/service/s2s-messaging/base';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
+import type { ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
 import {
 import {
-  ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, LdapGroupMembershipAttributeType,
+  ExternalGroupProviderType, LdapGroupMembershipAttributeType,
 } from '../../interfaces/external-user-group';
 } from '../../interfaces/external-user-group';
 
 
 import ExternalUserGroupSyncService from './external-user-group-sync';
 import ExternalUserGroupSyncService from './external-user-group-sync';
@@ -47,11 +49,11 @@ export class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
   override async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
   override async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
-    const groupChildGroupAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute');
-    const groupMembershipAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute');
-    const groupNameAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute');
-    const groupDescriptionAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute');
-    const groupBase: string = ldapService.getGroupSearchBase();
+    const groupChildGroupAttribute = configManager.getConfig('external-user-group:ldap:groupChildGroupAttribute');
+    const groupMembershipAttribute = configManager.getConfig('external-user-group:ldap:groupMembershipAttribute');
+    const groupNameAttribute = configManager.getConfig('external-user-group:ldap:groupNameAttribute');
+    const groupDescriptionAttribute = configManager.getConfig('external-user-group:ldap:groupDescriptionAttribute');
+    const groupBase = ldapService.getGroupSearchBase();
 
 
     const groupEntries = await ldapService.searchGroupDir();
     const groupEntries = await ldapService.searchGroupDir();
 
 
@@ -117,7 +119,7 @@ export class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
   private async getUserInfo(userId: string): Promise<ExternalUserInfo | null> {
   private async getUserInfo(userId: string): Promise<ExternalUserInfo | null> {
-    const groupMembershipAttributeType = configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType');
+    const groupMembershipAttributeType = configManager.getConfig('external-user-group:ldap:groupMembershipAttributeType');
     const attrMapUsername = this.passportService.getLdapAttrNameMappedToUsername();
     const attrMapUsername = this.passportService.getLdapAttrNameMappedToUsername();
     const attrMapName = this.passportService.getLdapAttrNameMappedToName();
     const attrMapName = this.passportService.getLdapAttrNameMappedToName();
     const attrMapMail = this.passportService.getLdapAttrNameMappedToMail();
     const attrMapMail = this.passportService.getLdapAttrNameMappedToMail();

+ 2 - 2
apps/app/src/features/openai/server/routes/middlewares/certify-ai-service.ts

@@ -8,7 +8,7 @@ import { OpenaiServiceTypes } from '../../../interfaces/ai';
 const logger = loggerFactory('growi:middlewares:certify-ai-service');
 const logger = loggerFactory('growi:middlewares:certify-ai-service');
 
 
 export const certifyAiService = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
 export const certifyAiService = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
-  const aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+  const aiEnabled = configManager.getConfig('app:aiEnabled');
 
 
   if (!aiEnabled) {
   if (!aiEnabled) {
     const message = 'AI_ENABLED is not true';
     const message = 'AI_ENABLED is not true';
@@ -16,7 +16,7 @@ export const certifyAiService = (req: Request, res: Response & { apiv3Err }, nex
     return res.apiv3Err(message, 403);
     return res.apiv3Err(message, 403);
   }
   }
 
 
-  const openaiServiceType = configManager.getConfig('crowi', 'openai:serviceType');
+  const openaiServiceType = configManager.getConfig('openai:serviceType');
   if (openaiServiceType == null || !OpenaiServiceTypes.includes(openaiServiceType)) {
   if (openaiServiceType == null || !OpenaiServiceTypes.includes(openaiServiceType)) {
     const message = 'AI_SERVICE_TYPE is missing or contains an invalid value';
     const message = 'AI_SERVICE_TYPE is missing or contains an invalid value';
     logger.error(message);
     logger.error(message);

+ 16 - 5
apps/app/src/features/openai/server/services/assistant/assistant.ts

@@ -15,9 +15,20 @@ const AssistantDefaultModelMap: Record<AssistantType, OpenAI.Chat.ChatModel> = {
   [AssistantType.CHAT]: 'gpt-4o-mini',
   [AssistantType.CHAT]: 'gpt-4o-mini',
 };
 };
 
 
+const isValidChatModel = (model: string): model is OpenAI.Chat.ChatModel => {
+  return model.startsWith('gpt-');
+};
+
 const getAssistantModelByType = (type: AssistantType): OpenAI.Chat.ChatModel => {
 const getAssistantModelByType = (type: AssistantType): OpenAI.Chat.ChatModel => {
-  const configKey = `openai:assistantModel:${type.toLowerCase()}`;
-  return configManager.getConfig('crowi', configKey) ?? AssistantDefaultModelMap[type];
+  const configValue = type === AssistantType.SEARCH
+    ? undefined // TODO: add the value for 'openai:assistantModel:search' to config-definition.ts
+    : configManager.getConfig('openai:assistantModel:chat');
+
+  if (typeof configValue === 'string' && isValidChatModel(configValue)) {
+    return configValue;
+  }
+
+  return AssistantDefaultModelMap[type];
 };
 };
 
 
 type AssistantType = typeof AssistantType[keyof typeof AssistantType];
 type AssistantType = typeof AssistantType[keyof typeof AssistantType];
@@ -45,7 +56,7 @@ const findAssistantByName = async(assistantName: string): Promise<OpenAI.Beta.As
 };
 };
 
 
 const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Promise<OpenAI.Beta.Assistant> => {
 const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Promise<OpenAI.Beta.Assistant> => {
-  const appSiteUrl = configManager.getConfig('crowi', 'app:siteUrl');
+  const appSiteUrl = configManager.getConfig('app:siteUrl');
   const assistantName = `GROWI ${type} Assistant for ${appSiteUrl}${nameSuffix != null ? ` ${nameSuffix}` : ''}`;
   const assistantName = `GROWI ${type} Assistant for ${appSiteUrl}${nameSuffix != null ? ` ${nameSuffix}` : ''}`;
   const assistantModel = getAssistantModelByType(type);
   const assistantModel = getAssistantModelByType(type);
 
 
@@ -57,7 +68,7 @@ const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Pr
       }));
       }));
 
 
   // update instructions
   // update instructions
-  const instructions = configManager.getConfig('crowi', 'openai:chatAssistantInstructions');
+  const instructions = configManager.getConfig('openai:chatAssistantInstructions');
   openaiClient.beta.assistants.update(assistant.id, {
   openaiClient.beta.assistants.update(assistant.id, {
     instructions,
     instructions,
     model: assistantModel,
     model: assistantModel,
@@ -75,7 +86,7 @@ const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Pr
 
 
 //   searchAssistant = await getOrCreateAssistant(AssistantType.SEARCH);
 //   searchAssistant = await getOrCreateAssistant(AssistantType.SEARCH);
 //   openaiClient.beta.assistants.update(searchAssistant.id, {
 //   openaiClient.beta.assistants.update(searchAssistant.id, {
-//     instructions: configManager.getConfig('crowi', 'openai:searchAssistantInstructions'),
+//     instructions: configManager.getConfig('openai:searchAssistantInstructions'),
 //     tools: [{ type: 'file_search' }],
 //     tools: [{ type: 'file_search' }],
 //   });
 //   });
 
 

+ 1 - 1
apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts

@@ -13,7 +13,7 @@ export class OpenaiClientDelegator implements IOpenaiClientDelegator {
 
 
   constructor() {
   constructor() {
     // Retrieve OpenAI related values from environment variables
     // Retrieve OpenAI related values from environment variables
-    const apiKey = configManager.getConfig('crowi', 'openai:apiKey');
+    const apiKey = configManager.getConfig('openai:apiKey');
 
 
     const isValid = [apiKey].every(value => value != null);
     const isValid = [apiKey].every(value => value != null);
     if (!isValid) {
     if (!isValid) {

+ 1 - 1
apps/app/src/features/openai/server/services/client.ts

@@ -3,5 +3,5 @@ import OpenAI from 'openai';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
 export const openaiClient = new OpenAI({
 export const openaiClient = new OpenAI({
-  apiKey: configManager?.getConfig('crowi', 'openai:apiKey'), // This is the default and can be omitted
+  apiKey: configManager.getConfig('openai:apiKey'), // This is the default and can be omitted
 });
 });

+ 4 - 4
apps/app/src/features/openai/server/services/cron/thread-deletion-cron.ts

@@ -37,10 +37,10 @@ export class ThreadDeletionCronService {
     }
     }
 
 
     this.openaiService = openaiService;
     this.openaiService = openaiService;
-    this.threadDeletionCronExpression = configManager.getConfig('crowi', 'openai:threadDeletionCronExpression');
-    this.threadDeletionCronMaxMinutesUntilRequest = configManager.getConfig('crowi', 'app:openaiThreadDeletionCronMaxMinutesUntilRequest');
-    this.threadDeletionBarchSize = configManager.getConfig('crowi', 'openai:threadDeletionBarchSize');
-    this.threadDeletionApiCallInterval = configManager.getConfig('crowi', 'openai:threadDeletionApiCallInterval');
+    this.threadDeletionCronExpression = configManager.getConfig('openai:threadDeletionCronExpression');
+    this.threadDeletionCronMaxMinutesUntilRequest = configManager.getConfig('app:openaiThreadDeletionCronMaxMinutesUntilRequest');
+    this.threadDeletionBarchSize = configManager.getConfig('openai:threadDeletionBarchSize');
+    this.threadDeletionApiCallInterval = configManager.getConfig('openai:threadDeletionApiCallInterval');
 
 
     this.cronJob?.stop();
     this.cronJob?.stop();
     this.cronJob = this.generateCronJob();
     this.cronJob = this.generateCronJob();

+ 4 - 4
apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts

@@ -36,10 +36,10 @@ export class VectorStoreFileDeletionCronService {
     }
     }
 
 
     this.openaiService = openaiService;
     this.openaiService = openaiService;
-    this.vectorStoreFileDeletionCronExpression = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionCronExpression');
-    this.vectorStoreFileDeletionCronMaxMinutesUntilRequest = configManager.getConfig('crowi', 'app:openaiVectorStoreFileDeletionCronMaxMinutesUntilRequest');
-    this.vectorStoreFileDeletionBarchSize = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionBarchSize');
-    this.vectorStoreFileDeletionApiCallInterval = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionApiCallInterval');
+    this.vectorStoreFileDeletionCronExpression = configManager.getConfig('openai:vectorStoreFileDeletionCronExpression');
+    this.vectorStoreFileDeletionCronMaxMinutesUntilRequest = configManager.getConfig('app:openaiVectorStoreFileDeletionCronMaxMinutesUntilRequest');
+    this.vectorStoreFileDeletionBarchSize = configManager.getConfig('openai:vectorStoreFileDeletionBarchSize');
+    this.vectorStoreFileDeletionApiCallInterval = configManager.getConfig('openai:vectorStoreFileDeletionApiCallInterval');
 
 
     this.cronJob?.stop();
     this.cronJob?.stop();
     this.cronJob = this.generateCronJob();
     this.cronJob = this.generateCronJob();

+ 1 - 1
apps/app/src/features/openai/server/services/is-ai-enabled.ts

@@ -1,3 +1,3 @@
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
-export const isAiEnabled = (): boolean => configManager.getConfig('crowi', 'app:aiEnabled');
+export const isAiEnabled = (): boolean => configManager.getConfig('app:aiEnabled');

+ 11 - 10
apps/app/src/features/openai/server/services/openai.ts

@@ -2,6 +2,7 @@ import assert from 'node:assert';
 import { Readable, Transform } from 'stream';
 import { Readable, Transform } from 'stream';
 import { pipeline } from 'stream/promises';
 import { pipeline } from 'stream/promises';
 
 
+import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { PageGrant, isPopulated } from '@growi/core';
 import { PageGrant, isPopulated } from '@growi/core';
 import type { HydratedDocument, Types } from 'mongoose';
 import type { HydratedDocument, Types } from 'mongoose';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
@@ -20,7 +21,7 @@ import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { OpenaiServiceTypes } from '../../interfaces/ai';
 import { OpenaiServiceTypes } from '../../interfaces/ai';
-import { sanitizeMarkdown } from '../utils/sanitize-markdown';
+import { convertMarkdownToHtml } from '../utils/convert-markdown-to-html';
 
 
 import { getClient } from './client-delegator';
 import { getClient } from './client-delegator';
 // import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
 // import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
@@ -49,7 +50,7 @@ export interface IOpenaiService {
 class OpenaiService implements IOpenaiService {
 class OpenaiService implements IOpenaiService {
 
 
   private get client() {
   private get client() {
-    const openaiServiceType = configManager.getConfig('crowi', 'openai:serviceType');
+    const openaiServiceType = configManager.getConfig('openai:serviceType');
     return getClient({ openaiServiceType });
     return getClient({ openaiServiceType });
   }
   }
 
 
@@ -157,9 +158,9 @@ class OpenaiService implements IOpenaiService {
   //   }
   //   }
   // }
   // }
 
 
-  private async uploadFile(pageId: Types.ObjectId, body: string): Promise<OpenAI.Files.FileObject> {
-    const sanitizedMarkdown = await sanitizeMarkdown(body);
-    const file = await toFile(Readable.from(sanitizedMarkdown), `${pageId}.md`);
+  private async uploadFile(pageId: Types.ObjectId, pagePath: string, revisionBody: string): Promise<OpenAI.Files.FileObject> {
+    const convertedHtml = await convertMarkdownToHtml({ pagePath, revisionBody });
+    const file = await toFile(Readable.from(convertedHtml), `${pageId}.html`);
     const uploadedFile = await this.client.uploadFile(file);
     const uploadedFile = await this.client.uploadFile(file);
     return uploadedFile;
     return uploadedFile;
   }
   }
@@ -183,17 +184,17 @@ class OpenaiService implements IOpenaiService {
   async createVectorStoreFile(pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
   async createVectorStoreFile(pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
     const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
     const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
     const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
     const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
-    const processUploadFile = async(page: PageDocument) => {
+    const processUploadFile = async(page: HydratedDocument<PageDocument>) => {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
-          const uploadedFile = await this.uploadFile(page._id, page.revision.body);
+          const uploadedFile = await this.uploadFile(page._id, page.path, page.revision.body);
           prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           return;
           return;
         }
         }
 
 
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
-          const uploadedFile = await this.uploadFile(page._id, pagePopulatedToShowRevision.revision.body);
+          const uploadedFile = await this.uploadFile(page._id, page.path, pagePopulatedToShowRevision.revision.body);
           prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
         }
         }
       }
       }
@@ -363,8 +364,8 @@ export const getOpenaiService = (): IOpenaiService | undefined => {
     return instance;
     return instance;
   }
   }
 
 
-  const aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
-  const openaiServiceType = configManager.getConfig('crowi', 'openai:serviceType');
+  const aiEnabled = configManager.getConfig('app:aiEnabled');
+  const openaiServiceType = configManager.getConfig('openai:serviceType');
   if (aiEnabled && openaiServiceType != null && OpenaiServiceTypes.includes(openaiServiceType)) {
   if (aiEnabled && openaiServiceType != null && OpenaiServiceTypes.includes(openaiServiceType)) {
     instance = new OpenaiService();
     instance = new OpenaiService();
     return instance;
     return instance;

+ 89 - 0
apps/app/src/features/openai/server/utils/convert-markdown-to-html.ts

@@ -0,0 +1,89 @@
+import { dynamicImport } from '@cspell/dynamic-import';
+import type { Root, Code } from 'mdast';
+import type * as RehypeMeta from 'rehype-meta';
+import type * as RehypeStringify from 'rehype-stringify';
+import type * as RemarkParse from 'remark-parse';
+import type * as RemarkRehype from 'remark-rehype';
+import type * as Unified from 'unified';
+import type * as UnistUtilVisit from 'unist-util-visit';
+
+interface ModuleCache {
+  unified?: typeof Unified.unified;
+  visit?: typeof UnistUtilVisit.visit;
+  remarkParse?: typeof RemarkParse.default;
+  remarkRehype?: typeof RemarkRehype.default;
+  rehypeMeta?: typeof RehypeMeta.default;
+  rehypeStringify?: typeof RehypeStringify.default;
+}
+
+let moduleCache: ModuleCache = {};
+
+const initializeModules = async(): Promise<void> => {
+  if (moduleCache.unified != null
+    && moduleCache.visit != null
+    && moduleCache.remarkParse != null
+    && moduleCache.remarkRehype != null
+    && moduleCache.rehypeMeta != null
+    && moduleCache.rehypeStringify != null
+  ) {
+    return;
+  }
+
+  const [
+    { unified },
+    { visit },
+    { default: remarkParse },
+    { default: remarkRehype },
+    { default: rehypeMeta },
+    { default: rehypeStringify },
+  ] = await Promise.all([
+    dynamicImport<typeof Unified>('unified', __dirname),
+    dynamicImport<typeof UnistUtilVisit>('unist-util-visit', __dirname),
+    dynamicImport<typeof RemarkParse>('remark-parse', __dirname),
+    dynamicImport<typeof RemarkRehype>('remark-rehype', __dirname),
+    dynamicImport<typeof RehypeMeta>('rehype-meta', __dirname),
+    dynamicImport<typeof RehypeStringify>('rehype-stringify', __dirname),
+  ]);
+
+  moduleCache = {
+    unified,
+    visit,
+    remarkParse,
+    remarkRehype,
+    rehypeMeta,
+    rehypeStringify,
+  };
+};
+
+export const convertMarkdownToHtml = async({ pagePath, revisionBody }: { pagePath: string, revisionBody: string }): Promise<string> => {
+  await initializeModules();
+
+  const {
+    unified, visit, remarkParse, remarkRehype, rehypeMeta, rehypeStringify,
+  } = moduleCache;
+
+  if (unified == null || visit == null || remarkParse == null || remarkRehype == null || rehypeMeta == null || rehypeStringify == null) {
+    throw new Error('Failed to initialize required modules');
+  }
+
+  const sanitizeMarkdown = () => {
+    return (tree: Root) => {
+      visit(tree, 'code', (node: Code) => {
+        if (node.lang === 'drawio') {
+          node.value = '<!-- drawio content replaced -->';
+        }
+      });
+    };
+  };
+
+  const processor = unified()
+    .use(remarkParse)
+    .use(sanitizeMarkdown)
+    .use(remarkRehype)
+    .use(rehypeMeta, {
+      title: pagePath,
+    })
+    .use(rehypeStringify);
+
+  return processor.processSync(revisionBody).toString();
+};

+ 0 - 65
apps/app/src/features/openai/server/utils/sanitize-markdown.ts

@@ -1,65 +0,0 @@
-import { dynamicImport } from '@cspell/dynamic-import';
-import type { Root, Code } from 'mdast';
-import type * as RemarkParse from 'remark-parse';
-import type * as RemarkStringify from 'remark-stringify';
-import type * as Unified from 'unified';
-import type * as UnistUtilVisit from 'unist-util-visit';
-
-interface ModuleCache {
-  remarkParse?: typeof RemarkParse.default;
-  remarkStringify?: typeof RemarkStringify.default;
-  unified?: typeof Unified.unified;
-  visit?: typeof UnistUtilVisit.visit;
-}
-
-let moduleCache: ModuleCache = {};
-
-const initializeModules = async(): Promise<void> => {
-  if (moduleCache.remarkParse != null && moduleCache.remarkStringify != null && moduleCache.unified != null && moduleCache.visit != null) {
-    return;
-  }
-
-  const [{ default: remarkParse }, { default: remarkStringify }, { unified }, { visit }] = await Promise.all([
-    dynamicImport<typeof RemarkParse>('remark-parse', __dirname),
-    dynamicImport<typeof RemarkStringify>('remark-stringify', __dirname),
-    dynamicImport<typeof Unified>('unified', __dirname),
-    dynamicImport<typeof UnistUtilVisit>('unist-util-visit', __dirname),
-  ]);
-
-  moduleCache = {
-    remarkParse,
-    remarkStringify,
-    unified,
-    visit,
-  };
-};
-
-export const sanitizeMarkdown = async(markdown: string): Promise<string> => {
-  await initializeModules();
-
-  const {
-    remarkParse, remarkStringify, unified, visit,
-  } = moduleCache;
-
-
-  if (remarkParse == null || remarkStringify == null || unified == null || visit == null) {
-    throw new Error('Failed to initialize required modules');
-  }
-
-  const sanitize = () => {
-    return (tree: Root) => {
-      visit(tree, 'code', (node: Code) => {
-        if (node.lang === 'drawio') {
-          node.value = '<!-- drawio content replaced -->';
-        }
-      });
-    };
-  };
-
-  const processor = unified()
-    .use(remarkParse)
-    .use(sanitize)
-    .use(remarkStringify);
-
-  return processor.processSync(markdown).toString();
-};

+ 1 - 0
apps/app/src/features/opentelemetry/server/index.ts

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

+ 76 - 0
apps/app/src/features/opentelemetry/server/logger.ts

@@ -0,0 +1,76 @@
+import { diag, type DiagLogger } from '@opentelemetry/api';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:diag');
+
+
+class DiagLoggerBunyanAdapter implements DiagLogger {
+
+  private parseMessage(message: string, args: unknown[]): [logMessage: string, data: object] {
+    let logMessage = message;
+    let data = {};
+
+    // check whether the message is a JSON string
+    try {
+      const parsedMessage = JSON.parse(message);
+      if (typeof parsedMessage === 'object' && parsedMessage !== null) {
+        data = parsedMessage;
+        // if parsed successfully, use 'message' property as log message
+        logMessage = 'message' in data && typeof data.message === 'string'
+          ? data.message
+          : message;
+      }
+    }
+    catch (e) {
+      // do nothing if the message is not a JSON string
+    }
+
+    // merge additional data
+    if (args.length > 0) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const argsData = (args as any).reduce((acc, arg) => {
+        if (typeof arg === 'string') {
+          try {
+            const parsed = JSON.parse(arg);
+            return { ...acc, ...parsed };
+          }
+          catch (e) {
+            return { ...acc, additionalInfo: arg };
+          }
+        }
+        return { ...acc, ...arg };
+      }, {});
+      data = { ...data, ...argsData };
+    }
+
+    return [logMessage, data];
+  }
+
+  error(message: string, ...args): void {
+    logger.error(...this.parseMessage(message, args));
+  }
+
+  warn(message: string, ...args): void {
+    logger.warn(...this.parseMessage(message, args));
+  }
+
+  info(message: string, ...args): void {
+    logger.info(...this.parseMessage(message, args));
+  }
+
+  debug(message: string, ...args): void {
+    logger.debug(...this.parseMessage(message, args));
+  }
+
+  verbose(message: string, ...args): void {
+    logger.trace(...this.parseMessage(message, args));
+  }
+
+}
+
+
+export const initLogger = (): void => {
+  // Enable global logger for OpenTelemetry
+  diag.setLogger(new DiagLoggerBunyanAdapter());
+};

+ 67 - 0
apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts

@@ -0,0 +1,67 @@
+import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
+import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
+import { Resource, type IResource } from '@opentelemetry/resources';
+import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
+import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
+import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions';
+
+import { getGrowiVersion } from '~/utils/growi-version';
+
+type Configuration = Partial<NodeSDKConfiguration> & {
+  resource: IResource;
+};
+
+let resource: Resource;
+let configuration: Configuration;
+
+export const generateNodeSDKConfiguration = (serviceInstanceId?: string): Configuration => {
+  if (configuration == null) {
+    const version = getGrowiVersion();
+
+    resource = new Resource({
+      [ATTR_SERVICE_NAME]: 'growi',
+      [ATTR_SERVICE_VERSION]: version,
+    });
+
+    configuration = {
+      resource,
+      traceExporter: new OTLPTraceExporter(),
+      metricReader: new PeriodicExportingMetricReader({
+        exporter: new OTLPMetricExporter(),
+      }),
+      instrumentations: [getNodeAutoInstrumentations({
+        '@opentelemetry/instrumentation-bunyan': {
+          enabled: false,
+        },
+        // disable fs instrumentation since this generates very large amount of traces
+        // see: https://opentelemetry.io/docs/languages/js/libraries/#registration
+        '@opentelemetry/instrumentation-fs': {
+          enabled: false,
+        },
+      })],
+    };
+  }
+
+  if (serviceInstanceId != null) {
+    configuration.resource = resource.merge(new Resource({
+      [SEMRESATTRS_SERVICE_INSTANCE_ID]: serviceInstanceId,
+    }));
+  }
+
+  return configuration;
+};
+
+// public async shutdownInstrumentation(): Promise<void> {
+//   await this.sdkInstance.shutdown();
+
+//   // メモ: 以下の restart コードは動かない
+//   // span/metrics ともに何も出なくなる
+//   // そもそも、restart するような使い方が出来なさそう?
+//   // see: https://github.com/open-telemetry/opentelemetry-specification/issues/27/
+//   // const sdk = new NodeSDK({...});
+//   // sdk.start();
+//   // await sdk.shutdown().catch(console.error);
+//   // const newSdk = new NodeSDK({...});
+//   // newSdk.start();
+// }

+ 103 - 0
apps/app/src/features/opentelemetry/server/node-sdk.ts

@@ -0,0 +1,103 @@
+import { ConfigSource } from '@growi/core/dist/interfaces';
+import type { NodeSDK } from '@opentelemetry/sdk-node';
+
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:server');
+
+
+let sdkInstance: NodeSDK;
+
+/**
+ * Overwrite "OTEL_SDK_DISABLED" env var before sdk.start() is invoked if needed.
+ * Since otel library sees it.
+ */
+function overwriteSdkDisabled(): void {
+  const instrumentationEnabled = configManager.getConfig('otel:enabled', ConfigSource.env);
+
+  if (instrumentationEnabled && (
+    process.env.OTEL_SDK_DISABLED === 'true'
+    || process.env.OTEL_SDK_DISABLED === '1'
+  )) {
+    logger.warn("OTEL_SDK_DISABLED overwritten with 'false' since GROWI's 'otel:enabled' config is true.");
+    process.env.OTEL_SDK_DISABLED = 'false';
+    return;
+  }
+
+  if (!instrumentationEnabled && (
+    process.env.OTEL_SDK_DISABLED === 'false'
+    || process.env.OTEL_SDK_DISABLED === '0'
+  )) {
+    logger.warn("OTEL_SDK_DISABLED is overwritten with 'true' since GROWI's 'otel:enabled' config is false.");
+    process.env.OTEL_SDK_DISABLED = 'true';
+    return;
+  }
+
+}
+
+export const startInstrumentation = async(): Promise<void> => {
+  if (sdkInstance != null) {
+    logger.warn('OpenTelemetry instrumentation already started');
+    return;
+  }
+
+  // load configs from env
+  await configManager.loadConfigs({ source: ConfigSource.env });
+
+  overwriteSdkDisabled();
+
+  const instrumentationEnabled = configManager.getConfig('otel:enabled', ConfigSource.env);
+  if (instrumentationEnabled) {
+
+    logger.info(`GROWI now collects anonymous telemetry.
+
+This data is used to help improve GROWI, but you can opt-out at any time.
+
+For more information, see https://docs.growi.org/en/admin-guide/telemetry.html.
+`);
+
+    // initialize global logger for development
+    const isDev = process.env.NODE_ENV === 'development';
+    if (isDev) {
+      const { initLogger } = await import('./logger');
+      initLogger();
+    }
+
+    // instanciate NodeSDK
+    const { NodeSDK } = await import('@opentelemetry/sdk-node');
+    const { generateNodeSDKConfiguration } = await import('./node-sdk-configuration');
+
+    sdkInstance = new NodeSDK(generateNodeSDKConfiguration());
+    sdkInstance.start();
+  }
+};
+
+export const initServiceInstanceId = async(): Promise<void> => {
+  const instrumentationEnabled = configManager.getConfig('otel:enabled', ConfigSource.env);
+
+  if (instrumentationEnabled) {
+    const { generateNodeSDKConfiguration } = await import('./node-sdk-configuration');
+
+    const serviceInstanceId = configManager.getConfig('otel:serviceInstanceId')
+      ?? configManager.getConfig('app:serviceInstanceId');
+
+    // overwrite resource
+    const updatedResource = generateNodeSDKConfiguration(serviceInstanceId).resource;
+    (sdkInstance as any).resource = updatedResource;
+  }
+};
+
+// public async shutdownInstrumentation(): Promise<void> {
+//   await this.sdkInstance.shutdown();
+
+//   // メモ: 以下の restart コードは動かない
+//   // span/metrics ともに何も出なくなる
+//   // そもそも、restart するような使い方が出来なさそう?
+//   // see: https://github.com/open-telemetry/opentelemetry-specification/issues/27/
+//   // const sdk = new NodeSDK({...});
+//   // sdk.start();
+//   // await sdk.shutdown().catch(console.error);
+//   // const newSdk = new NodeSDK({...});
+//   // newSdk.start();
+// }

+ 2 - 2
apps/app/src/features/questionnaire/interfaces/condition.ts

@@ -1,7 +1,7 @@
 import type { HasObjectId } from '@growi/core';
 import type { HasObjectId } from '@growi/core';
+import type { GrowiServiceType } from '@growi/core/dist/consts';
 
 
-import { GrowiServiceType } from './growi-info';
-import { UserType } from './user-info';
+import type { UserType } from './user-info';
 
 
 
 
 interface UserCondition {
 interface UserCondition {

+ 18 - 0
apps/app/src/features/questionnaire/interfaces/growi-app-info.ts

@@ -0,0 +1,18 @@
+import type { IGrowiAdditionalInfo, IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { AttachmentMethodType } from '~/interfaces/attachment';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
+
+
+export type IGrowiAppAdditionalInfo = IGrowiAdditionalInfo & {
+  attachmentType: AttachmentMethodType
+  activeExternalAccountTypes?: IExternalAuthProviderType[]
+}
+
+// legacy properties (extracted from additionalInfo for growi-questionnaire)
+// see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
+export type IGrowiAppInfoLegacy = Omit<IGrowiInfo<IGrowiAppAdditionalInfo>, 'additionalInfo'>
+  & IGrowiAppAdditionalInfo
+  & {
+    appSiteUrlHashed: string,
+  };

+ 0 - 58
apps/app/src/features/questionnaire/interfaces/growi-info.ts

@@ -1,58 +0,0 @@
-import * as os from 'node:os';
-
-import { IExternalAuthProviderType } from '@growi/core';
-
-export const GrowiServiceType = {
-  cloud: 'cloud',
-  privateCloud: 'private-cloud',
-  onPremise: 'on-premise',
-  others: 'others',
-} as const;
-export const GrowiWikiType = { open: 'open', closed: 'closed' } as const;
-export const GrowiAttachmentType = {
-  aws: 'aws',
-  gcs: 'gcs',
-  gcp: 'gcp',
-  azure: 'azure',
-  gridfs: 'gridfs',
-  mongo: 'mongo',
-  mongodb: 'mongodb',
-  local: 'local',
-  none: 'none',
-} as const;
-export const GrowiDeploymentType = {
-  officialHelmChart: 'official-helm-chart',
-  growiDockerCompose: 'growi-docker-compose',
-  node: 'node',
-  others: 'others',
-} as const;
-export const GrowiExternalAuthProviderType = IExternalAuthProviderType;
-
-export type GrowiServiceType = typeof GrowiServiceType[keyof typeof GrowiServiceType]
-type GrowiWikiType = typeof GrowiWikiType[keyof typeof GrowiWikiType]
-export type GrowiAttachmentType = typeof GrowiAttachmentType[keyof typeof GrowiAttachmentType]
-export type GrowiDeploymentType = typeof GrowiDeploymentType[keyof typeof GrowiDeploymentType]
-export type GrowiExternalAuthProviderType = typeof GrowiExternalAuthProviderType[keyof typeof GrowiExternalAuthProviderType]
-
-interface IGrowiOSInfo {
-  type?: ReturnType<typeof os.type>
-  platform?: ReturnType<typeof os.platform>
-  arch?: ReturnType<typeof os.arch>
-  totalmem?: ReturnType<typeof os.totalmem>
-}
-
-export interface IGrowiInfo {
-  version: string
-  appSiteUrl?: string
-  appSiteUrlHashed: string
-  installedAt: Date
-  installedAtByOldestUser: Date
-  type: GrowiServiceType
-  currentUsersCount: number
-  currentActiveUsersCount: number
-  wikiType: GrowiWikiType
-  attachmentType: GrowiAttachmentType
-  activeExternalAccountTypes?: GrowiExternalAuthProviderType[]
-  osInfo?: IGrowiOSInfo
-  deploymentType?: GrowiDeploymentType
-}

+ 16 - 3
apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts

@@ -1,11 +1,24 @@
-import { IGrowiInfo } from './growi-info';
-import { IUserInfo } from './user-info';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
+import type { IUserInfo } from './user-info';
 
 
 
 
 export interface IProactiveQuestionnaireAnswer {
 export interface IProactiveQuestionnaireAnswer {
   satisfaction: number,
   satisfaction: number,
   commentText: string,
   commentText: string,
-  growiInfo: IGrowiInfo,
+  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>,
+  userInfo: IUserInfo,
+  answeredAt: Date,
+  lengthOfExperience?: string,
+  position?: string,
+  occupation?: string,
+}
+
+export interface IProactiveQuestionnaireAnswerLegacy {
+  satisfaction: number,
+  commentText: string,
+  growiInfo: IGrowiAppInfoLegacy,
   userInfo: IUserInfo,
   userInfo: IUserInfo,
   answeredAt: Date,
   answeredAt: Date,
   lengthOfExperience?: string,
   lengthOfExperience?: string,

+ 14 - 4
apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts

@@ -1,11 +1,21 @@
-import { IAnswer } from './answer';
-import { IGrowiInfo } from './growi-info';
-import { IUserInfo } from './user-info';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { IAnswer } from './answer';
+import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
+import type { IUserInfo } from './user-info';
 
 
 export interface IQuestionnaireAnswer<ID = string> {
 export interface IQuestionnaireAnswer<ID = string> {
   answers: IAnswer[]
   answers: IAnswer[]
   answeredAt: Date
   answeredAt: Date
-  growiInfo: IGrowiInfo
+  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>
+  userInfo: IUserInfo
+  questionnaireOrder: ID
+}
+
+export interface IQuestionnaireAnswerLegacy<ID = string> {
+  answers: IAnswer[]
+  answeredAt: Date
+  growiInfo: IGrowiAppInfoLegacy,
   userInfo: IUserInfo
   userInfo: IUserInfo
   questionnaireOrder: ID
   questionnaireOrder: ID
 }
 }

+ 3 - 2
apps/app/src/features/questionnaire/server/models/questionnaire-order.ts

@@ -1,8 +1,9 @@
-import { Model, Schema, Document } from 'mongoose';
+import type { Model, Document } from 'mongoose';
+import { Schema } from 'mongoose';
 
 
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
 
-import { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
+import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
 
 
 import conditionSchema from './schema/condition';
 import conditionSchema from './schema/condition';
 import questionSchema from './schema/question';
 import questionSchema from './schema/question';

+ 2 - 2
apps/app/src/features/questionnaire/server/models/schema/condition.ts

@@ -1,7 +1,7 @@
+import { GrowiServiceType } from '@growi/core/dist/consts';
 import { Schema } from 'mongoose';
 import { Schema } from 'mongoose';
 
 
-import { ICondition } from '../../../interfaces/condition';
-import { GrowiServiceType } from '../../../interfaces/growi-info';
+import type { ICondition } from '../../../interfaces/condition';
 import { UserType } from '../../../interfaces/user-info';
 import { UserType } from '../../../interfaces/user-info';
 
 
 const conditionSchema = new Schema<ICondition>({
 const conditionSchema = new Schema<ICondition>({

+ 26 - 10
apps/app/src/features/questionnaire/server/models/schema/growi-info.ts

@@ -1,21 +1,27 @@
+import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+import { GrowiWikiType } from '@growi/core/dist/interfaces';
 import { Schema } from 'mongoose';
 import { Schema } from 'mongoose';
 
 
-import {
-  GrowiAttachmentType, GrowiDeploymentType, GrowiExternalAuthProviderType, GrowiServiceType, GrowiWikiType, IGrowiInfo,
-} from '../../../interfaces/growi-info';
+import type { IGrowiAppAdditionalInfo } from '~/features/questionnaire/interfaces/growi-app-info';
+import { AttachmentMethodType } from '~/interfaces/attachment';
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 
-export const growiInfoSchema = new Schema<IGrowiInfo>({
-  version: { type: String, required: true },
-  appSiteUrl: { type: String },
-  appSiteUrlHashed: { type: String, required: true },
+const growiAdditionalInfoSchema = new Schema<IGrowiAppAdditionalInfo>({
   installedAt: { type: Date, required: true },
   installedAt: { type: Date, required: true },
   installedAtByOldestUser: { type: Date, required: true },
   installedAtByOldestUser: { type: Date, required: true },
-  type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
   currentUsersCount: { type: Number, required: true },
   currentUsersCount: { type: Number, required: true },
   currentActiveUsersCount: { type: Number, required: true },
   currentActiveUsersCount: { type: Number, required: true },
+  attachmentType: { type: String, required: true, enum: Object.values(AttachmentMethodType) },
+  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
+});
+
+export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
+  version: { type: String, required: true },
+  appSiteUrl: { type: String },
+  serviceInstanceId: { type: String, required: true },
+  type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
   wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
   wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
-  attachmentType: { type: String, required: true, enum: Object.values(GrowiAttachmentType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(GrowiExternalAuthProviderType) }],
   osInfo: {
   osInfo: {
     type: { type: String },
     type: { type: String },
     platform: String,
     platform: String,
@@ -23,4 +29,14 @@ export const growiInfoSchema = new Schema<IGrowiInfo>({
     totalmem: Number,
     totalmem: Number,
   },
   },
   deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
   deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
+  additionalInfo: growiAdditionalInfoSchema,
+
+  // legacy properties (extracted from additionalInfo for growi-questionnaire)
+  // see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
+  installedAt: { type: Date },
+  installedAtByOldestUser: { type: Date },
+  currentUsersCount: { type: Number },
+  currentActiveUsersCount: { type: Number },
+  attachmentType: { type: String, enum: Object.values(AttachmentMethodType) },
+  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
 });
 });

+ 20 - 11
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -6,6 +6,8 @@ import { body, validationResult } from 'express-validator';
 import type Crowi from '~/server/crowi';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import axios from '~/utils/axios';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -16,6 +18,7 @@ import { StatusType } from '../../../interfaces/questionnaire-answer-status';
 import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
 import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
 import QuestionnaireAnswer from '../../models/questionnaire-answer';
 import QuestionnaireAnswer from '../../models/questionnaire-answer';
 import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
 import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
+import { convertToLegacyFormat, getSiteUrlHashed } from '../../util/convert-to-legacy-format';
 
 
 
 
 const logger = loggerFactory('growi:routes:apiv3:questionnaire');
 const logger = loggerFactory('growi:routes:apiv3:questionnaire');
@@ -59,8 +62,8 @@ module.exports = (crowi: Crowi): Router => {
   };
   };
 
 
   router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-    const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
+    const growiInfo = await growiInfoService.getGrowiInfo(true);
+    const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
 
 
     try {
     try {
       const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
       const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
@@ -74,15 +77,16 @@ module.exports = (crowi: Crowi): Router => {
   });
   });
 
 
   router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const isEnabled = crowi.configManager!.getConfig('crowi', 'questionnaire:isQuestionnaireEnabled');
+    const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
     return res.apiv3({ isEnabled });
     return res.apiv3({ isEnabled });
   });
   });
 
 
   router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const sendQuestionnaireAnswer = async() => {
     const sendQuestionnaireAnswer = async() => {
-      const questionnaireServerOrigin = crowi.configManager?.getConfig('crowi', 'app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-      const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
+      const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
+      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
+      const growiInfo = await growiInfoService.getGrowiInfo(true);
+      const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
 
 
       const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
       const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
         satisfaction: req.body.satisfaction,
         satisfaction: req.body.satisfaction,
@@ -95,8 +99,10 @@ module.exports = (crowi: Crowi): Router => {
         answeredAt: new Date(),
         answeredAt: new Date(),
       };
       };
 
 
+      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
+
       try {
       try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswer);
+        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
       }
       }
       catch (err) {
       catch (err) {
         if (err.request != null) {
         if (err.request != null) {
@@ -126,9 +132,10 @@ module.exports = (crowi: Crowi): Router => {
 
 
   router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
     const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
-      const questionnaireServerOrigin = crowi.configManager?.getConfig('crowi', 'app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-      const userInfo = crowi.questionnaireService!.getUserInfo(user, growiInfo.appSiteUrlHashed);
+      const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
+      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
+      const growiInfo = await growiInfoService.getGrowiInfo(true);
+      const userInfo = crowi.questionnaireService.getUserInfo(user, getSiteUrlHashed(growiInfo.appSiteUrl));
 
 
       const questionnaireAnswer: IQuestionnaireAnswer = {
       const questionnaireAnswer: IQuestionnaireAnswer = {
         growiInfo,
         growiInfo,
@@ -138,8 +145,10 @@ module.exports = (crowi: Crowi): Router => {
         questionnaireOrder: req.body.questionnaireOrderId,
         questionnaireOrder: req.body.questionnaireOrderId,
       };
       };
 
 
+      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
+
       try {
       try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswer);
+        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);
       }
       }
       catch (err) {
       catch (err) {
         if (err.request != null) {
         if (err.request != null) {

+ 22 - 11
apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts

@@ -1,14 +1,17 @@
 import axiosRetry from 'axios-retry';
 import axiosRetry from 'axios-retry';
 
 
+import type Crowi from '~/server/crowi';
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { getRandomIntInRange } from '~/utils/rand';
 import { getRandomIntInRange } from '~/utils/rand';
 
 
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
+import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
 import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
 import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
 import QuestionnaireAnswer from '../models/questionnaire-answer';
 import QuestionnaireAnswer from '../models/questionnaire-answer';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
 import QuestionnaireOrder from '../models/questionnaire-order';
 import QuestionnaireOrder from '../models/questionnaire-order';
+import { convertToLegacyFormat } from '../util/convert-to-legacy-format';
 
 
 const logger = loggerFactory('growi:service:questionnaire-cron');
 const logger = loggerFactory('growi:service:questionnaire-cron');
 
 
@@ -26,20 +29,19 @@ axiosRetry(axios, { retries: 3 });
  */
  */
 class QuestionnaireCronService {
 class QuestionnaireCronService {
 
 
-  crowi: any;
+  crowi: Crowi;
 
 
   cronJob: any;
   cronJob: any;
 
 
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
   sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
   sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
 
 
   startCron(): void {
   startCron(): void {
-    const cronSchedule = this.crowi.configManager?.getConfig('crowi', 'app:questionnaireCronSchedule');
-    const maxHoursUntilRequest = this.crowi.configManager?.getConfig('crowi', 'app:questionnaireCronMaxHoursUntilRequest');
+    const cronSchedule = this.crowi.configManager.getConfig('app:questionnaireCronSchedule');
+    const maxHoursUntilRequest = this.crowi.configManager.getConfig('app:questionnaireCronMaxHoursUntilRequest');
 
 
     const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
     const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
 
 
@@ -53,7 +55,8 @@ class QuestionnaireCronService {
   }
   }
 
 
   async executeJob(): Promise<void> {
   async executeJob(): Promise<void> {
-    const questionnaireServerOrigin = this.crowi.configManager?.getConfig('crowi', 'app:questionnaireServerOrigin');
+    const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
+    const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
 
 
     const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
     const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
       const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
       const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
@@ -75,15 +78,23 @@ class QuestionnaireCronService {
 
 
     const resendQuestionnaireAnswers = async() => {
     const resendQuestionnaireAnswers = async() => {
       const questionnaireAnswers = await QuestionnaireAnswer.find()
       const questionnaireAnswers = await QuestionnaireAnswer.find()
-        .select('-_id -answers._id  -growiInfo._id -userInfo._id');
+        .select('-_id -answers._id  -growiInfo._id -userInfo._id')
+        .lean();
       const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
       const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
-        .select('-_id -growiInfo._id -userInfo._id');
+        .select('-_id -growiInfo._id -userInfo._id')
+        .lean();
 
 
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, { questionnaireAnswers })
+      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
+        // convert to legacy format
+        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
+      })
         .then(async() => {
         .then(async() => {
           await QuestionnaireAnswer.deleteMany();
           await QuestionnaireAnswer.deleteMany();
         });
         });
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, { proactiveQuestionnaireAnswers })
+      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
+        // convert to legacy format
+        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
+      })
         .then(async() => {
         .then(async() => {
           await ProactiveQuestionnaireAnswer.deleteMany();
           await ProactiveQuestionnaireAnswer.deleteMany();
         });
         });

+ 301 - 0
apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts

@@ -0,0 +1,301 @@
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+import { mock } from 'vitest-mock-extended';
+
+import pkg from '^/package.json';
+
+
+import type UserEvent from '~/server/events/user';
+import { configManager } from '~/server/service/config-manager';
+
+import type Crowi from '../../../../server/crowi';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
+import { StatusType } from '../../interfaces/questionnaire-answer-status';
+import { UserType } from '../../interfaces/user-info';
+import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
+import QuestionnaireOrder from '../models/questionnaire-order';
+
+import QuestionnaireService from './questionnaire';
+
+
+describe('QuestionnaireService', () => {
+  const appVersion = pkg.version;
+
+  let questionnaireService: QuestionnaireService;
+
+  let User;
+  let user;
+
+  beforeAll(async() => {
+
+    await configManager.loadConfigs();
+
+    const crowiMock = mock<Crowi>({
+      version: appVersion,
+      event: vi.fn().mockImplementation((eventName) => {
+        if (eventName === 'user') {
+          return mock<UserEvent>({
+            on: vi.fn(),
+          });
+        }
+      }),
+    });
+    const userModelFactory = (await import('~/server/models/user')).default;
+    User = userModelFactory(crowiMock);
+
+    await User.deleteMany({}); // clear users
+    user = await User.create({
+      name: 'Example for Questionnaire Service Test',
+      username: 'questionnaire test user',
+      email: 'questionnaireTestUser@example.com',
+      password: 'usertestpass',
+      createdAt: '2000-01-01',
+    });
+
+    questionnaireService = new QuestionnaireService(crowiMock);
+  });
+
+  describe('getUserInfo', () => {
+    test('Should get correct user info when user given', () => {
+      const userInfo = questionnaireService.getUserInfo(user, 'growiurlhashfortest');
+      expect(userInfo).not.toBeNull();
+      assert(userInfo != null);
+
+      expect(userInfo.type).equal(UserType.general);
+      assert(userInfo.type === UserType.general);
+
+      expect(userInfo.userIdHash).toBeTruthy();
+      expect(userInfo.userIdHash).not.toBe(user._id);
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      delete (userInfo as any).userIdHash;
+
+      expect(userInfo).toEqual({ type: 'general', userCreatedAt: new Date('2000-01-01') });
+    });
+
+    test('Should get correct user info when user is null', () => {
+      const userInfo = questionnaireService.getUserInfo(null, '');
+      expect(userInfo).toEqual({ type: 'guest' });
+    });
+  });
+
+  describe('getQuestionnaireOrdersToShow', () => {
+    let doc1;
+    let doc2;
+    let doc3;
+    let doc4;
+    let doc5;
+    let doc6;
+    let doc7;
+    let doc8;
+    let doc9;
+    let doc10;
+    let doc11;
+    let doc12;
+
+    beforeAll(async() => {
+      const questionnaireToBeShown = {
+        shortTitle: {
+          ja_JP: 'GROWI に関するアンケート',
+          en_US: 'Questions about GROWI',
+        },
+        title: {
+          ja_JP: 'GROWI に関するアンケート',
+          en_US: 'Questions about GROWI',
+        },
+        showFrom: '2022-12-11',
+        showUntil: '2100-12-12',
+        condition: {
+          user: {
+            types: ['general'],
+            daysSinceCreation: {
+              moreThanOrEqualTo: 365,
+              lessThanOrEqualTo: 365 * 1000,
+            },
+          },
+          growi: {
+            types: ['on-premise'],
+            versionRegExps: [appVersion],
+          },
+        },
+        createdAt: '2023-01-01',
+        updatedAt: '2023-01-01',
+      };
+
+      // insert initial db data
+      doc1 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert finished data
+      doc2 = await QuestionnaireOrder.create({
+        ...questionnaireToBeShown,
+        showFrom: '2020-12-11',
+        showUntil: '2021-12-12',
+      });
+      // insert data for admin or guest
+      doc3 = await QuestionnaireOrder.create({
+        ...questionnaireToBeShown,
+        condition: {
+          user: {
+            types: ['admin', 'guest'],
+          },
+          growi: {
+            types: ['on-premise'],
+            versionRegExps: [appVersion],
+          },
+        },
+      });
+      // insert answered data
+      doc4 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert skipped data
+      doc5 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert denied data
+      doc6 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert data for different growi type
+      doc7 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+            },
+            growi: {
+              types: ['cloud'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+      // insert data for different growi version
+      doc8 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: ['1.0.0-alpha'],
+            },
+          },
+        },
+      );
+      // insert data for users that used GROWI for less than or equal to a year
+      doc9 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                lessThanOrEqualTo: 365,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+      // insert data for users that used GROWI for more than or equal to 1000 years
+      doc10 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                moreThanOrEqualTo: 365 * 1000,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+      // insert data for users that used GROWI for more than a month and less than 6 months
+      doc11 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                moreThanOrEqualTo: 30,
+                lessThanOrEqualTo: 30 * 6,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+
+      await QuestionnaireAnswerStatus.insertMany([
+        {
+          user: user._id,
+          questionnaireOrderId: doc4._id,
+          status: StatusType.answered,
+        },
+        {
+          user: user._id,
+          questionnaireOrderId: doc5._id,
+          status: StatusType.skipped,
+        },
+        {
+          user: user._id,
+          questionnaireOrderId: doc6._id,
+          status: StatusType.skipped,
+        },
+      ]);
+    });
+
+    test('Should get questionnaire orders to show', async() => {
+      const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
+        type: 'on-premise',
+        version: appVersion,
+      });
+      const userInfo = questionnaireService.getUserInfo(user, 'appSiteUrlHashed');
+
+      const questionnaireOrderDocuments = await questionnaireService.getQuestionnaireOrdersToShow(userInfo, growiInfo, user._id);
+
+      expect(questionnaireOrderDocuments[0].toObject()).toMatchObject(
+        {
+          __v: 0,
+          shortTitle: {
+            ja_JP: 'GROWI に関するアンケート',
+            en_US: 'Questions about GROWI',
+          },
+          title: {
+            ja_JP: 'GROWI に関するアンケート',
+            en_US: 'Questions about GROWI',
+          },
+          showFrom: new Date('2022-12-11'),
+          showUntil: new Date('2100-12-12'),
+          questions: [],
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                moreThanOrEqualTo: 365,
+                lessThanOrEqualTo: 365 * 1000,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+          createdAt: new Date('2023-01-01'),
+          updatedAt: new Date('2023-01-01'),
+        },
+      );
+
+    });
+
+  });
+
+});

+ 8 - 70
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -1,18 +1,13 @@
 import crypto from 'crypto';
 import crypto from 'crypto';
-import * as os from 'node:os';
 
 
 import type { IUserHasId } from '@growi/core';
 import type { IUserHasId } from '@growi/core';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
 
 
+import type Crowi from '~/server/crowi';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-// eslint-disable-next-line import/no-named-as-default
-import { Config } from '~/server/models/config';
-import { aclService } from '~/server/service/acl';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import type { IGrowiInfo } from '../../interfaces/growi-info';
-import {
-  GrowiWikiType, GrowiExternalAuthProviderType, GrowiServiceType, GrowiAttachmentType, GrowiDeploymentType,
-} from '../../interfaces/growi-info';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { type IUserInfo, UserType } from '../../interfaces/user-info';
 import { type IUserInfo, UserType } from '../../interfaces/user-info';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
@@ -25,72 +20,13 @@ const logger = loggerFactory('growi:service:questionnaire');
 
 
 class QuestionnaireService {
 class QuestionnaireService {
 
 
-  crowi: any;
+  crowi: Crowi;
 
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
-  async getGrowiInfo(): Promise<IGrowiInfo> {
-    const User = this.crowi.model('User');
-
-    const appSiteUrl = this.crowi.appService.getSiteUrl();
-    const hasher = crypto.createHash('sha256');
-    hasher.update(appSiteUrl);
-    const appSiteUrlHashed = hasher.digest('hex');
-
-    // Get the oldest user who probably installed this GROWI.
-    // https://mongoosejs.com/docs/6.x/docs/api.html#model_Model-findOne
-    // https://stackoverflow.com/questions/13443069/mongoose-findone-with-sorting
-    const user = await User.findOne({ createdAt: { $ne: null } }).sort({ createdAt: 1 });
-
-    const installedAtByOldestUser = user ? user.createdAt : null;
-
-    const appInstalledConfig = await Config.findOne({ key: 'app:installed' });
-    const installedAt = appInstalledConfig != null && appInstalledConfig.createdAt != null ? appInstalledConfig.createdAt : installedAtByOldestUser;
-
-    const currentUsersCount = await User.countDocuments();
-    const currentActiveUsersCount = await User.countActiveUsers();
-
-    const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
-    const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
-
-    const activeExternalAccountTypes: GrowiExternalAuthProviderType[] = Object.values(GrowiExternalAuthProviderType).filter((type) => {
-      return this.crowi.configManager.getConfig('crowi', `security:passport-${type}:isEnabled`);
-    });
-
-    const typeStr = this.crowi.configManager.getConfig('crowi', 'app:serviceType');
-    const type = Object.values(GrowiServiceType).includes(typeStr) ? typeStr : null;
-
-    const attachmentTypeStr = this.crowi.configManager.getConfig('crowi', 'app:fileUploadType');
-    const attachmentType = Object.values(GrowiAttachmentType).includes(attachmentTypeStr) ? attachmentTypeStr : null;
-
-    const deploymentTypeStr = this.crowi.configManager.getConfig('crowi', 'app:deploymentType');
-    const deploymentType = Object.values(GrowiDeploymentType).includes(deploymentTypeStr) ? deploymentTypeStr : null;
-
-    return {
-      version: this.crowi.version,
-      osInfo: {
-        type: os.type(),
-        platform: os.platform(),
-        arch: os.arch(),
-        totalmem: os.totalmem(),
-      },
-      appSiteUrl: this.crowi.configManager.getConfig('crowi', 'questionnaire:isAppSiteUrlHashed') ? null : appSiteUrl,
-      appSiteUrlHashed,
-      installedAt,
-      installedAtByOldestUser,
-      type,
-      currentUsersCount,
-      currentActiveUsersCount,
-      wikiType,
-      attachmentType,
-      activeExternalAccountTypes,
-      deploymentType,
-    };
-  }
-
   getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
   getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
     if (user != null) {
     if (user != null) {
       const hasher = crypto.createHmac('sha256', appSiteUrlHashed);
       const hasher = crypto.createHmac('sha256', appSiteUrlHashed);
@@ -106,7 +42,9 @@ class QuestionnaireService {
     return { type: UserType.guest };
     return { type: UserType.guest };
   }
   }
 
 
-  async getQuestionnaireOrdersToShow(userInfo: IUserInfo, growiInfo: IGrowiInfo, userId: ObjectIdLike | null): Promise<QuestionnaireOrderDocument[]> {
+  async getQuestionnaireOrdersToShow(
+      userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>, userId: ObjectIdLike | null,
+  ): Promise<QuestionnaireOrderDocument[]> {
     const currentDate = new Date();
     const currentDate = new Date();
 
 
     let questionnaireOrders = await QuestionnaireOrder.find({
     let questionnaireOrders = await QuestionnaireOrder.find({

+ 9 - 6
apps/app/src/features/questionnaire/server/util/condition.ts

@@ -1,7 +1,10 @@
-import { ICondition } from '../../interfaces/condition';
-import { IGrowiInfo } from '../../interfaces/growi-info';
-import { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import { IUserInfo, UserType } from '../../interfaces/user-info';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { ICondition } from '../../interfaces/condition';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
+import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
+import type { IUserInfo } from '../../interfaces/user-info';
+import { UserType } from '../../interfaces/user-info';
 
 
 
 
 const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
 const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
@@ -39,7 +42,7 @@ const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
   return true;
   return true;
 };
 };
 
 
-const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo): boolean => {
+const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
   const { growi: { types, versionRegExps } } = condition;
   const { growi: { types, versionRegExps } } = condition;
 
 
   if (!types.includes(growiInfo.type)) {
   if (!types.includes(growiInfo.type)) {
@@ -53,7 +56,7 @@ const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo): boolean =
   return true;
   return true;
 };
 };
 
 
-export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo): boolean => {
+export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
   const { condition } = order;
   const { condition } = order;
 
 
   if (!checkUserInfo(condition, userInfo)) {
   if (!checkUserInfo(condition, userInfo)) {

+ 128 - 0
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts

@@ -0,0 +1,128 @@
+import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+import { GrowiWikiType } from '@growi/core/dist/interfaces';
+import {
+  describe, test, expect,
+} from 'vitest';
+import { mock } from 'vitest-mock-extended';
+
+import { AttachmentMethodType } from '../../../../interfaces/attachment';
+import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
+
+import { convertToLegacyFormat } from './convert-to-legacy-format';
+
+describe('convertToLegacyFormat', () => {
+  test('should return same object when input is already in legacy format', () => {
+    const growiInfoLegacy: IGrowiAppInfoLegacy = {
+      version: '1.0.0',
+      appSiteUrl: 'https://example.com',
+      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
+      serviceInstanceId: 'service-instance-id',
+      type: GrowiServiceType.cloud,
+      wikiType: GrowiWikiType.open,
+      deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
+
+      // legacy properties
+      installedAt: new Date(),
+      installedAtByOldestUser: new Date(),
+      currentUsersCount: 1,
+      currentActiveUsersCount: 1,
+      attachmentType: AttachmentMethodType.local,
+    };
+
+    const legacyData = {
+      someData: 'test',
+      growiInfo: growiInfoLegacy,
+    };
+
+    const result = convertToLegacyFormat(legacyData);
+    expect(result).toStrictEqual(legacyData);
+  });
+
+  test('should convert new format to legacy format', () => {
+    const installedAt = new Date();
+    const installedAtByOldestUser = new Date();
+
+    const growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo> = {
+      version: '1.0.0',
+      appSiteUrl: 'https://example.com',
+      serviceInstanceId: 'service-instance-id',
+      type: GrowiServiceType.cloud,
+      wikiType: GrowiWikiType.open,
+      deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
+      additionalInfo: {
+        installedAt,
+        installedAtByOldestUser,
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+        attachmentType: AttachmentMethodType.local,
+      },
+    };
+    const newFormatData = {
+      someData: 'test',
+      growiInfo,
+    };
+
+    const growiInfoLegacy: IGrowiAppInfoLegacy = {
+      version: '1.0.0',
+      appSiteUrl: 'https://example.com',
+      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
+      serviceInstanceId: 'service-instance-id',
+      type: GrowiServiceType.cloud,
+      wikiType: GrowiWikiType.open,
+      deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
+
+      // legacy properties
+      installedAt,
+      installedAtByOldestUser,
+      currentUsersCount: 1,
+      currentActiveUsersCount: 1,
+      attachmentType: AttachmentMethodType.local,
+    };
+    const expected = {
+      someData: 'test',
+      growiInfo: growiInfoLegacy,
+    };
+
+    const result = convertToLegacyFormat(newFormatData);
+    expect(result).toStrictEqual(expected);
+  });
+
+  test('should convert new format and omit appSiteUrl', () => {
+    // arrange
+    const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
+      appSiteUrl: 'https://example.com',
+      additionalInfo: {
+        installedAt: new Date(),
+        installedAtByOldestUser: new Date(),
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+        attachmentType: AttachmentMethodType.local,
+      },
+    });
+
+    // act
+    const result = convertToLegacyFormat({ growiInfo }, true);
+
+    // assert
+    expect(result.growiInfo.appSiteUrl).toBeUndefined();
+  });
+});

+ 40 - 0
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts

@@ -0,0 +1,40 @@
+import assert from 'assert';
+import crypto from 'crypto';
+
+import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
+
+
+type IHasGrowiAppInfoLegacy<T> = T & {
+  growiInfo: IGrowiAppInfoLegacy;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppInfoLegacy<T> {
+  return !('additionalInfo' in data.growiInfo);
+}
+
+export function getSiteUrlHashed(siteUrl: string): string {
+  const hasher = crypto.createHash('sha256');
+  hasher.update(siteUrl);
+  return hasher.digest('hex');
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T, isAppSiteUrlHashed = false): IHasGrowiAppInfoLegacy<T> {
+  if (isLegacy(questionnaireAnswer)) {
+    return questionnaireAnswer;
+  }
+
+  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
+  assert(additionalInfo != null);
+
+  return {
+    ...questionnaireAnswer,
+    growiInfo: {
+      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
+      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
+      ...rest,
+      ...additionalInfo,
+    },
+  };
+}

+ 2 - 1
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -8,6 +8,7 @@ import { param, query } from 'express-validator';
 
 
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
+import type Crowi from '~/server/crowi';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -32,7 +33,7 @@ const validator = {
 let presetTemplateSummaries: TemplateSummary[];
 let presetTemplateSummaries: TemplateSummary[];
 
 
 
 
-module.exports = (crowi) => {
+module.exports = (crowi: Crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
 
   router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
   router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {

+ 1 - 0
apps/app/src/interfaces/activity.ts

@@ -365,6 +365,7 @@ export const ActionGroupSize = {
   Medium: 'MEDIUM',
   Medium: 'MEDIUM',
   Large: 'LARGE',
   Large: 'LARGE',
 } as const;
 } as const;
+export type ActionGroupSize = typeof ActionGroupSize[keyof typeof ActionGroupSize];
 
 
 export const SmallActionGroup = {
 export const SmallActionGroup = {
   ACTION_USER_LOGIN_WITH_LOCAL,
   ACTION_USER_LOGIN_WITH_LOCAL,

+ 13 - 1
apps/app/src/interfaces/attachment.ts

@@ -1,7 +1,19 @@
-import type { IAttachmentHasId } from '@growi/core';
+import type { IAttachmentHasId } from '@growi/core/dist/interfaces';
 
 
 import type { PaginateResult } from './mongoose-utils';
 import type { PaginateResult } from './mongoose-utils';
 
 
+export const AttachmentMethodType = {
+  aws: 'aws',
+  gcs: 'gcs',
+  gcp: 'gcp',
+  azure: 'azure',
+  gridfs: 'gridfs',
+  mongo: 'mongo',
+  mongodb: 'mongodb',
+  local: 'local',
+  none: 'none',
+} as const;
+export type AttachmentMethodType = typeof AttachmentMethodType[keyof typeof AttachmentMethodType]
 
 
 export type IResAttachmentList = {
 export type IResAttachmentList = {
   paginateResult: PaginateResult<IAttachmentHasId>
   paginateResult: PaginateResult<IAttachmentHasId>

+ 3 - 2
apps/app/src/interfaces/crowi-request.ts

@@ -2,13 +2,14 @@ import type { IUser } from '@growi/core';
 import type { Request } from 'express';
 import type { Request } from 'express';
 import type { HydratedDocument } from 'mongoose';
 import type { HydratedDocument } from 'mongoose';
 
 
+import type Crowi from '~/server/crowi';
+
 
 
 export interface CrowiProperties {
 export interface CrowiProperties {
 
 
   user?: HydratedDocument<IUser>,
   user?: HydratedDocument<IUser>,
 
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  crowi: any,
+  crowi: Crowi,
 
 
   session: any,
   session: any,
 
 

+ 9 - 0
apps/app/src/interfaces/external-auth-provider.ts

@@ -0,0 +1,9 @@
+export const IExternalAuthProviderType = {
+  ldap: 'ldap',
+  saml: 'saml',
+  oidc: 'oidc',
+  google: 'google',
+  github: 'github',
+} as const;
+
+export type IExternalAuthProviderType = typeof IExternalAuthProviderType[keyof typeof IExternalAuthProviderType]

+ 0 - 7
apps/app/src/interfaces/page-listing-results.ts

@@ -18,13 +18,6 @@ export interface ChildrenResult {
   children: Partial<IPageForItem>[]
   children: Partial<IPageForItem>[]
 }
 }
 
 
-
-export interface TargetAndAncestors {
-  targetAndAncestors: Partial<IPageForItem>[]
-  rootPage: Partial<IPageForItem>,
-}
-
-
 export interface V5MigrationStatus {
 export interface V5MigrationStatus {
   isV5Compatible : boolean,
   isV5Compatible : boolean,
   migratablePagesCount: number
   migratablePagesCount: number

+ 38 - 0
apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js

@@ -0,0 +1,38 @@
+import mongoose from 'mongoose';
+
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:remove-index-for-ns-from-configs');
+
+async function dropIndexIfExists(db, collectionName, indexName) {
+  // check existence of the collection
+  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  if (items.length === 0) {
+    return;
+  }
+
+  const collection = await db.collection(collectionName);
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    await mongoose.connect(getMongoUri(), mongoOptions);
+
+    // drop index
+    await dropIndexIfExists(db, 'configs', 'ns_1_key_1');
+
+    // create index
+    const collection = await db.collection('configs');
+    await collection.createIndex({ key: 1 }, { unique: true });
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 24 - 0
apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js

@@ -0,0 +1,24 @@
+import mongoose from 'mongoose';
+import { v4 as uuidv4 } from 'uuid';
+
+import { configManager } from '~/server/service/config-manager';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:generate-service-instance-id');
+
+module.exports = {
+  async up(db) {
+    logger.info('Generate serviceInstanceId for the system');
+    await mongoose.connect(getMongoUri(), mongoOptions);
+
+    await configManager.loadConfigs();
+
+    await configManager.updateConfig('app:serviceInstanceId', uuidv4(), { skipPubsub: true });
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 2 - 6
apps/app/src/migrations/20180927102719-init-serverurl.js

@@ -24,7 +24,6 @@ module.exports = {
 
 
     // find 'app:siteUrl'
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
     const siteUrlConfig = await Config.findOne({
-      ns: 'crowi',
       key: 'app:siteUrl',
       key: 'app:siteUrl',
     });
     });
     // exit if exists
     // exit if exists
@@ -35,7 +34,6 @@ module.exports = {
 
 
     // find all callbackUrls
     // find all callbackUrls
     const configs = await Config.find({
     const configs = await Config.find({
-      ns: 'crowi',
       $or: [
       $or: [
         { key: 'security:passport-github:callbackUrl' },
         { key: 'security:passport-github:callbackUrl' },
         { key: 'security:passport-google:callbackUrl' },
         { key: 'security:passport-google:callbackUrl' },
@@ -63,11 +61,10 @@ module.exports = {
     }
     }
 
 
     if (siteUrl != null) {
     if (siteUrl != null) {
-      const ns = 'crowi';
       const key = 'app:siteUrl';
       const key = 'app:siteUrl';
       await Config.findOneAndUpdate(
       await Config.findOneAndUpdate(
-        { ns, key },
-        { ns, key, value: JSON.stringify(siteUrl) },
+        { key },
+        { key, value: JSON.stringify(siteUrl) },
         { upsert: true },
         { upsert: true },
       );
       );
       logger.info('Migration has successfully applied');
       logger.info('Migration has successfully applied');
@@ -80,7 +77,6 @@ module.exports = {
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({
-      ns: 'crowi',
       key: 'app:siteUrl',
       key: 'app:siteUrl',
     });
     });
 
 

+ 6 - 6
apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -15,18 +15,18 @@ module.exports = {
     // enable passport and delete configs for crowi classic auth
     // enable passport and delete configs for crowi classic auth
     await Promise.all([
     await Promise.all([
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
-        { ns: 'crowi', key: 'security:isEnabledPassport' },
-        { ns: 'crowi', key: 'security:isEnabledPassport', value: JSON.stringify(true) },
+        { key: 'security:isEnabledPassport' },
+        { key: 'security:isEnabledPassport', value: JSON.stringify(true) },
         { upsert: true },
         { upsert: true },
       ),
       ),
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
-        { ns: 'crowi', key: 'google:clientId' },
-        { ns: 'crowi', key: 'google:clientId', value: JSON.stringify(null) },
+        { key: 'google:clientId' },
+        { key: 'google:clientId', value: JSON.stringify(null) },
         { upsert: true },
         { upsert: true },
       ),
       ),
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
-        { ns: 'crowi', key: 'google:clientSecret' },
-        { ns: 'crowi', key: 'google:clientSecret', value: JSON.stringify(null) },
+        { key: 'google:clientSecret' },
+        { key: 'google:clientSecret', value: JSON.stringify(null) },
         { upsert: true },
         { upsert: true },
       ),
       ),
     ]);
     ]);

+ 3 - 6
apps/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -9,9 +9,9 @@ const logger = loggerFactory('growi:migrate:add-config-app-installed');
 
 
 /**
 /**
  * BEFORE
  * BEFORE
- *   - Config document { ns: 'crowi', key: 'app:installed' } does not exist
+ *   - Config document { key: 'app:installed' } does not exist
  * AFTER
  * AFTER
- *   - Config document { ns: 'crowi', key: 'app:installed' } is created
+ *   - Config document { key: 'app:installed' } is created
  *     - value will be true if one or more users exist
  *     - value will be true if one or more users exist
  *     - value will be false if no users exist
  *     - value will be false if no users exist
  */
  */
@@ -23,9 +23,8 @@ module.exports = {
 
 
     const User = userModelFactory();
     const User = userModelFactory();
 
 
-    // find 'app:siteUrl'
+    // find 'app:installed'
     const appInstalled = await Config.findOne({
     const appInstalled = await Config.findOne({
-      ns: 'crowi',
       key: 'app:installed',
       key: 'app:installed',
     });
     });
     // exit if exists
     // exit if exists
@@ -38,7 +37,6 @@ module.exports = {
 
 
     if (userCount > 0) {
     if (userCount > 0) {
       await Config.create({
       await Config.create({
-        ns: 'crowi',
         key: 'app:installed',
         key: 'app:installed',
         value: true,
         value: true,
       });
       });
@@ -53,7 +51,6 @@ module.exports = {
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({
-      ns: 'crowi',
       key: 'app:installed',
       key: 'app:installed',
     });
     });
 
 

+ 0 - 1
apps/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -23,7 +23,6 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'customize:behavior',
       key: 'customize:behavior',
       value: JSON.stringify('growi'),
       value: JSON.stringify('growi'),
     });
     });

+ 0 - 1
apps/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -45,7 +45,6 @@ module.exports = {
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'customize:layout',
       key: 'customize:layout',
       value: JSON.stringify(insertLayoutType),
       value: JSON.stringify(insertLayoutType),
     });
     });

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