Browse Source

Merge pull request #9866 from weseek/master

Release v7.2.1
Yuki Takei 11 months ago
parent
commit
d1aa048ca2
100 changed files with 886 additions and 141 deletions
  1. 8 4
      .devcontainer/app/devcontainer.json
  2. 9 0
      .devcontainer/app/initializeCommand.sh
  3. 5 0
      .devcontainer/app/postCreateCommand.sh
  4. 12 0
      .devcontainer/compose.extend.template.yml
  5. 3 1
      .devcontainer/compose.yml
  6. 30 0
      .devcontainer/pdf-converter/devcontainer.json
  7. 9 0
      .devcontainer/pdf-converter/initializeCommand.sh
  8. 22 0
      .devcontainer/pdf-converter/postCreateCommand.sh
  9. 1 1
      .github/workflows/auto-approve.yml
  10. 1 1
      .github/workflows/auto-labeling.yml
  11. 168 0
      .github/workflows/ci-pdf-converter.yml
  12. 1 1
      .github/workflows/draft-release.yml
  13. 120 0
      .github/workflows/release-pdf-converter.yml
  14. 1 6
      .github/workflows/release-slackbot-proxy.yml
  15. 1 1
      .github/workflows/reusable-app-create-manifests.yml
  16. 23 5
      .github/workflows/reusable-app-prod.yml
  17. 1 1
      .github/workflows/reusable-app-reg-suit.yml
  18. 2 0
      .gitignore
  19. 16 1
      apps/app/bin/swagger-jsdoc/definition-apiv3.js
  20. 5 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh
  21. 5 1
      apps/app/docker/docker-entrypoint.sh
  22. 9 4
      apps/app/package.json
  23. 2 0
      apps/app/playwright.config.ts
  24. 39 5
      apps/app/playwright/20-basic-features/use-tools.spec.ts
  25. 9 2
      apps/app/public/static/locales/en_US/admin.json
  26. 17 2
      apps/app/public/static/locales/en_US/translation.json
  27. 9 2
      apps/app/public/static/locales/fr_FR/admin.json
  28. 17 2
      apps/app/public/static/locales/fr_FR/translation.json
  29. 9 2
      apps/app/public/static/locales/ja_JP/admin.json
  30. 17 2
      apps/app/public/static/locales/ja_JP/translation.json
  31. 9 2
      apps/app/public/static/locales/zh_CN/admin.json
  32. 17 2
      apps/app/public/static/locales/zh_CN/translation.json
  33. 1 1
      apps/app/src/client/components/Admin/AdminHome/EnvVarsTable.tsx
  34. 11 0
      apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx
  35. 3 0
      apps/app/src/client/components/Admin/App/AwsSetting.tsx
  36. 3 0
      apps/app/src/client/components/Admin/App/AzureSetting.tsx
  37. 10 6
      apps/app/src/client/components/Admin/App/FileUploadSetting.tsx
  38. 3 0
      apps/app/src/client/components/Admin/App/GcsSetting.tsx
  39. 1 1
      apps/app/src/client/components/Admin/App/MaskedInput.tsx
  40. 136 0
      apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx
  41. 43 29
      apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx
  42. 1 1
      apps/app/src/client/components/Admin/Common/AdminInstallButtonRow.tsx
  43. 1 1
      apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx
  44. 1 1
      apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx
  45. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx
  46. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeFunctionOption.tsx
  47. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeFunctionSetting.tsx
  48. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeLayoutSetting.tsx
  49. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeLogoSetting.tsx
  50. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  51. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizePresentationSetting.tsx
  52. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx
  53. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeSidebarSetting.tsx
  54. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeThemeOptions.tsx
  55. 3 1
      apps/app/src/client/components/Admin/Customize/CustomizeThemeSetting.tsx
  56. 1 1
      apps/app/src/client/components/Admin/Customize/ThemeColorBox.tsx
  57. 1 1
      apps/app/src/client/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx
  58. 1 1
      apps/app/src/client/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  59. 1 1
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  60. 1 1
      apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  61. 3 1
      apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  62. 3 1
      apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx
  63. 1 1
      apps/app/src/client/components/Admin/ForbiddenPage.tsx
  64. 1 1
      apps/app/src/client/components/Admin/FullTextSearchManagement.tsx
  65. 1 1
      apps/app/src/client/components/Admin/G2GDataTransfer.tsx
  66. 1 1
      apps/app/src/client/components/Admin/G2GDataTransferExportForm.tsx
  67. 1 1
      apps/app/src/client/components/Admin/G2GDataTransferStatusIcon.tsx
  68. 1 1
      apps/app/src/client/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  69. 1 1
      apps/app/src/client/components/Admin/ManageExternalAccount.tsx
  70. 1 1
      apps/app/src/client/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  71. 1 1
      apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx
  72. 1 1
      apps/app/src/client/components/Admin/NotFoundPage.tsx
  73. 1 1
      apps/app/src/client/components/Admin/Notification/ManageGlobalNotification.tsx
  74. 1 1
      apps/app/src/client/components/Admin/Notification/NotificationTypeIcon.tsx
  75. 1 1
      apps/app/src/client/components/Admin/Security/LdapAuthTest.tsx
  76. 1 1
      apps/app/src/client/components/Admin/SlackIntegration/BotTypeCard.tsx
  77. 3 0
      apps/app/src/client/components/Admin/SlackIntegration/Bridge.tsx
  78. 1 1
      apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithProxyConnectionStatus.tsx
  79. 1 1
      apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxyConnectionStatus.tsx
  80. 2 1
      apps/app/src/client/components/Admin/SlackIntegration/MessageBasedOnConnection.jsx
  81. 3 0
      apps/app/src/client/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx
  82. 3 1
      apps/app/src/client/components/Admin/SlackIntegration/SlackIntegration.tsx
  83. 2 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupDropdown.tsx
  84. 1 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupTable.tsx
  85. 1 1
      apps/app/src/client/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  86. 3 1
      apps/app/src/client/components/Admin/UserGroupDetail/UserGroupPageList.tsx
  87. 1 1
      apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserModal.tsx
  88. 1 1
      apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  89. 1 1
      apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx
  90. 1 1
      apps/app/src/client/components/Admin/Users/GrantAdminButton.tsx
  91. 1 1
      apps/app/src/client/components/Admin/Users/GrantReadOnlyButton.tsx
  92. 1 1
      apps/app/src/client/components/Admin/Users/RevokeAdminButton.tsx
  93. 1 1
      apps/app/src/client/components/Admin/Users/RevokeAdminMenuItem.tsx
  94. 1 1
      apps/app/src/client/components/Admin/Users/RevokeReadOnlyMenuItem.tsx
  95. 1 1
      apps/app/src/client/components/Admin/Users/SortIcons.tsx
  96. 1 1
      apps/app/src/client/components/Admin/Users/StatusSuspendMenuItem.tsx
  97. 2 0
      apps/app/src/client/components/AlertSiteUrlUndefined.tsx
  98. 1 1
      apps/app/src/client/components/AuthorInfo/AuthorInfo.tsx
  99. 1 1
      apps/app/src/client/components/Bookmarks/BookmarkFolderItemControl.tsx
  100. 3 1
      apps/app/src/client/components/Bookmarks/BookmarkFolderMenu.tsx

+ 8 - 4
.devcontainer/devcontainer.json → .devcontainer/app/devcontainer.json

@@ -2,8 +2,8 @@
 // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
 {
   "name": "GROWI-Dev",
-  "dockerComposeFile": "compose.yml",
-  "service": "node",
+  "dockerComposeFile": ["../compose.yml", "../compose.extend.yml"],
+  "service": "app",
   "workspaceFolder": "/workspace/growi",
 
   "features": {
@@ -15,8 +15,9 @@
   // Use 'forwardPorts' to make a list of ports inside the container available locally.
   // "forwardPorts": [],
 
+  "initializeCommand": "/bin/bash .devcontainer/pdf-converter/initializeCommand.sh",
   // Use 'postCreateCommand' to run commands after the container is created.
-  "postCreateCommand": "/bin/bash ./.devcontainer/postCreateCommand.sh",
+  "postCreateCommand": "/bin/bash ./.devcontainer/app/postCreateCommand.sh",
 
   // Configure tool-specific properties.
   "customizations": {
@@ -37,7 +38,10 @@
         "vitest.explorer",
         "ms-playwright.playwright"
       ],
-    }
+      "settings": {
+        "terminal.integrated.defaultProfile.linux": "bash"
+      }
+    },
   },
 
   // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.

+ 9 - 0
.devcontainer/app/initializeCommand.sh

@@ -0,0 +1,9 @@
+# prevent file not found error on docker compose up
+if [ ! -f ".devcontainer/compose.extend.yml" ]; then
+
+cat > ".devcontainer/compose.extend.yml" <<EOF
+services:
+  {}
+EOF
+
+fi

+ 5 - 0
.devcontainer/postCreateCommand.sh → .devcontainer/app/postCreateCommand.sh

@@ -6,6 +6,11 @@ sudo apt-get install -y --no-install-recommends \
   iputils-ping net-tools dnsutils
 sudo apt-get clean -y
 
+# Set permissions for shared directory for bulk export
+mkdir -p /tmp/page-bulk-export
+sudo chown -R vscode:vscode /tmp/page-bulk-export
+sudo chmod 700 /tmp/page-bulk-export
+
 # Setup pnpm
 SHELL=bash pnpm setup
 eval "$(cat /home/vscode/.bashrc)"

+ 12 - 0
.devcontainer/compose.extend.template.yml

@@ -0,0 +1,12 @@
+# A template of the file for extending the primary docker compose configuration.
+# To actually use this file, create a `compose.extend.yml` file and copy the contents of this file into it.
+services:
+  pdf-converter:
+    # enabling devcontainer 'features' was not working for secondary devcontainer (https://github.com/devcontainers/features/issues/1175)
+    image: mcr.microsoft.com/vscode/devcontainers/javascript-node:1-20
+    volumes:
+      - ..:/workspace/growi:delegated
+      - pnpm-store:/workspace/growi/.pnpm-store
+      - node_modules:/workspace/growi/node_modules
+      - page_bulk_export_tmp:/tmp/page-bulk-export
+    tty: true

+ 3 - 1
.devcontainer/compose.yml

@@ -1,5 +1,5 @@
 services:
-  node:
+  app:
     image: mcr.microsoft.com/devcontainers/base:ubuntu
     volumes:
       - ..:/workspace/growi:delegated
@@ -8,6 +8,7 @@ services:
       - buildcache_app:/workspace/growi/apps/app/.next
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../share:/workspace/share:delegated
+      - page_bulk_export_tmp:/tmp/page-bulk-export
     tty: true
     networks:
     - default
@@ -48,6 +49,7 @@ volumes:
   pnpm-store:
   node_modules:
   buildcache_app:
+  page_bulk_export_tmp:
 
 networks:
   default:

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

@@ -0,0 +1,30 @@
+{
+  "name": "GROWI-PDF-Converter",
+  "dockerComposeFile": ["../compose.yml", "../compose.extend.yml"],
+  "service": "pdf-converter",
+  "workspaceFolder": "/workspace/growi",
+
+  // Use 'forwardPorts' to make a list of ports inside the container available locally.
+  // "forwardPorts": [],
+
+  "initializeCommand": "/bin/bash .devcontainer/pdf-converter/initializeCommand.sh",
+  // Use 'postCreateCommand' to run commands after the container is created.
+  "postCreateCommand": "/bin/bash ./.devcontainer/pdf-converter/postCreateCommand.sh",
+
+  // Configure tool-specific properties.
+  "customizations": {
+    "vscode": {
+      "extensions": [
+        "dbaeumer.vscode-eslint",
+        "mhutchie.git-graph",
+        "eamodio.gitlens"
+      ],
+      "settings": {
+        "terminal.integrated.defaultProfile.linux": "bash"
+      }
+    }
+  }
+
+  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+  // "remoteUser": "root"
+}

+ 9 - 0
.devcontainer/pdf-converter/initializeCommand.sh

@@ -0,0 +1,9 @@
+# prevent file not found error on docker compose up
+if [ ! -f ".devcontainer/compose.extend.yml" ]; then
+
+cat > ".devcontainer/compose.extend.yml" <<EOF
+services:
+  {}
+EOF
+
+fi

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

@@ -0,0 +1,22 @@
+# Instal additional packages
+sudo apt update
+sudo apt-get install -y --no-install-recommends \
+  chromium fonts-lato fonts-ipafont-gothic fonts-noto-cjk
+sudo apt-get clean -y
+
+# Set permissions for shared directory for bulk export
+mkdir -p /tmp/page-bulk-export
+sudo chown -R node:node /tmp/page-bulk-export
+sudo chmod 700 /tmp/page-bulk-export
+
+# Setup pnpm
+SHELL=bash pnpm setup
+eval "$(cat /home/node/.bashrc)"
+# Update pnpm
+pnpm i -g pnpm
+
+# Install turbo
+pnpm install turbo --global
+
+# Install dependencies
+turbo run bootstrap

+ 1 - 1
.github/workflows/auto-approve.yml

@@ -16,7 +16,7 @@ jobs:
     steps:
       - name: Dependabot metadata
         id: dependabot-metadata
-        uses: dependabot/fetch-metadata@v1
+        uses: dependabot/fetch-metadata@v2
         with:
           github-token: '${{ secrets.GITHUB_TOKEN }}'
       - name: Approve a PR

+ 1 - 1
.github/workflows/auto-labeling.yml

@@ -25,7 +25,7 @@ jobs:
         && !startsWith( github.head_ref, 'mergify/merge-queue/' ))
 
     steps:
-      - uses: release-drafter/release-drafter@v5
+      - uses: release-drafter/release-drafter@v6
         with:
           disable-releaser: true
         env:

+ 168 - 0
.github/workflows/ci-pdf-converter.yml

@@ -0,0 +1,168 @@
+name: Node CI for pdf-converter
+
+on:
+  push:
+    branches-ignore:
+      - release/**
+      - rc/**
+      - support/prepare-v**
+    paths:
+      - .github/mergify.yml
+      - .github/workflows/ci-pdf-converter.yml
+      - .eslint*
+      - tsconfig.base.json
+      - turbo.json
+      - pnpm-lock.yaml
+      - package.json
+      - apps/pdf-converter/**
+      - '!apps/pdf-converter/docker/**'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+
+jobs:
+
+  ci-pdf-converter-lint:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [20.x]
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'pnpm'
+
+    - name: Install dependencies
+      run: |
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
+
+    - name: Lint
+      run: |
+        turbo run lint --filter=@growi/pdf-converter
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-pdf-converter - test (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+  ci-pdf-converter-launch-dev:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [20.x]
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'pnpm'
+
+    - name: Install dependencies
+      run: |
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
+
+    - name: turbo run dev:pdf-converter:ci
+      working-directory: ./apps/pdf-converter
+      run: turbo run dev:pdf-converter:ci
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-pdf-converter - launch-dev (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+  ci-pdf-converter-launch-prod:
+
+    if: startsWith(github.head_ref, 'mergify/merge-queue/')
+
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [20.x]
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'pnpm'
+
+    - name: Install turbo
+      run: |
+        pnpm add turbo --global
+
+    - name: Install dependencies
+      run: |
+        pnpm install --frozen-lockfile
+
+    - name: Restore dist
+      uses: actions/cache/restore@v4
+      with:
+        path: |
+          **/.turbo
+          **/dist
+        key: dist-pdf-converter-prod-${{ runner.OS }}-node${{ matrix.node-version }}-${{ github.sha }}
+        restore-keys: |
+          dist-pdf-converter-prod-${{ runner.OS }}-node${{ matrix.node-version }}-
+
+    - name: Build
+      working-directory: ./apps/pdf-converter
+      run: |
+        turbo run build
+
+    - name: Assembling all dependencies
+      run: |
+        rm -rf out
+        pnpm deploy out --prod --filter @growi/pdf-converter
+        rm -rf apps/pdf-converter/node_modules && mv out/node_modules apps/pdf-converter/node_modules
+
+    - name: pnpm run start:prod:ci
+      working-directory: ./apps/pdf-converter
+      run: pnpm run start:prod:ci
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-pdf-converter - launch-prod (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+    - name: Cache dist
+      uses: actions/cache/save@v4
+      with:
+        path: |
+          **/.turbo
+          **/dist
+        key: dist-pdf-converter-prod-${{ runner.OS }}-node${{ matrix.node-version }}-${{ github.sha }}

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

@@ -29,7 +29,7 @@ jobs:
         uses: myrotvorets/info-from-package-json-action@v2.0.2
         id: package-json
 
-      - uses: release-drafter/release-drafter@v5
+      - uses: release-drafter/release-drafter@v6
         id: release-drafter
         with:
           config-name: release-drafter.yml

+ 120 - 0
.github/workflows/release-pdf-converter.yml

@@ -0,0 +1,120 @@
+name: Release Docker Image for @growi/pdf-converter
+
+on:
+  pull_request:
+    branches:
+      - release/pdf-converter/**
+    types: [closed]
+
+jobs:
+  build-and-push-image:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        ref: ${{ github.event.pull_request.base.ref }}
+
+    - name: Retrieve information from package.json
+      uses: myrotvorets/info-from-package-json-action@2.0.1
+      id: package-json
+      with:
+        workingDir: apps/pdf-converter
+
+    - name: Docker meta
+      id: meta
+      uses: docker/metadata-action@v4
+      with:
+        images: growilabs/pdf-converter
+        tags: |
+          type=raw,value=latest
+          type=raw,value=${{ steps.package-json.outputs.packageVersion }}
+
+    - name: Login to docker.io registry
+      run: |
+        echo ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }} | docker login --username growimoogle --password-stdin
+
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v3
+
+    - name: Build and push
+      uses: docker/build-push-action@v6
+      with:
+        context: .
+        file: ./apps/pdf-converter/docker/Dockerfile
+        platforms: linux/amd64,linux/arm64
+        push: true
+        builder: ${{ steps.buildx.outputs.name }}
+        cache-from: type=gha
+        cache-to: type=gha,mode=max
+        tags: ${{ steps.meta.outputs.tags }}
+
+    - name: Add tag
+      uses: anothrNick/github-tag-action@v1
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        CUSTOM_TAG: pdf-converter/v${{ steps.package-json.outputs.packageVersion }}
+        VERBOSE : true
+
+    - name: Update Docker Hub Description
+      uses: peter-evans/dockerhub-description@v3
+      with:
+        username: growimoogle
+        password: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
+        repository: growilabs/pdf-converter
+        readme-filepath: ./apps/pdf-converter/docker/README.md
+
+
+  create-pr-for-next-rc:
+    needs: build-and-push-image
+
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [20.x]
+
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        ref: ${{ github.event.pull_request.base.ref }}
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'pnpm'
+
+    - name: Install dependencies
+      run: |
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
+
+    - name: Bump versions for next RC
+      run: |
+        turbo run version:prerelease --filter=@growi/pdf-converter
+
+    - name: Retrieve information from package.json
+      uses: myrotvorets/info-from-package-json-action@2.0.1
+      id: package-json
+      with:
+        workingDir: apps/pdf-converter
+
+    - name: Commit
+      uses: github-actions-x/commit@v2.9
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
+        push-branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
+        commit-message: 'Bump version'
+        name: GitHub Action
+
+    - name: Create PR
+      uses: repo-sync/pull-request@v2
+      with:
+        source_branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
+        destination_branch: master
+        pr_title: Prepare pdf-converter v${{ steps.package-json.outputs.packageVersion }}
+        pr_label: flag/exclude-from-changelog,type/prepare-next-version
+        pr_body: "An automated PR generated by ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+        github_token: ${{ secrets.GITHUB_TOKEN }}

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

@@ -51,7 +51,7 @@ jobs:
       uses: docker/setup-buildx-action@v3
 
     - name: Build and push
-      uses: docker/build-push-action@v4
+      uses: docker/build-push-action@v6
       with:
         context: .
         file: ./apps/slackbot-proxy/docker/Dockerfile
@@ -62,11 +62,6 @@ jobs:
         cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
 
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache
-
     - name: Add tag
       uses: anothrNick/github-tag-action@v1
       env:

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

@@ -45,7 +45,7 @@ jobs:
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
 
     - name: Create and push manifest images
-      uses: Noelware/docker-manifest-action@master
+      uses: Noelware/docker-manifest-action@0.4.3
       with:
         base-image: ${{ inputs.tags }}
         extra-images: ${{ steps.meta-extra-images.outputs.tags }}

+ 23 - 5
.github/workflows/reusable-app-prod.yml

@@ -262,12 +262,19 @@ jobs:
         MONGO_URI: mongodb://mongodb:27017/growi-playwright-guest-mode
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
 
-    - name: Upload test results
+    - name: Generate shard ID
+      id: shard-id
       if: always()
+      run: |
+        SHARD_ID=$(echo "${{ matrix.shard }}" | tr '/' '-')
+        echo "shard_id=${SHARD_ID}" >> $GITHUB_OUTPUT
+
+    - name: Upload test results
       uses: actions/upload-artifact@v4
+      if: always()
       with:
-        name: blob-report-${{ matrix.shard }}
-        path: blob-report
+        name: blob-report-${{ matrix.browser }}-${{ steps.shard-id.outputs.shard_id }}
+        path: ./apps/app/blob-report
         retention-days: 30
 
     - name: Slack Notification
@@ -302,10 +309,21 @@ jobs:
       run: |
         pnpm install --frozen-lockfile
 
+    - name: Download blob reports
+      uses: actions/download-artifact@v4
+      with:
+        pattern: blob-report-*
+        path: all-blob-reports
+        merge-multiple: true
+
     - name: Merge into HTML Report
       run: |
-        mkdir -p all-blob-reports
-        pnpm playwright merge-reports --reporter html ./all-blob-reports
+        mkdir -p playwright-report
+        if [ -z "$(ls all-blob-reports/*.zip all-blob-reports/*.blob 2>/dev/null || true)" ]; then
+          echo "<html><body><h1>No test results available</h1><p>This could be because tests were skipped or all artifacts were not available.</p></body></html>" > playwright-report/index.html
+        else
+          pnpm playwright merge-reports --reporter html all-blob-reports
+        fi
 
     - name: Upload HTML report
       uses: actions/upload-artifact@v4

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

@@ -1,4 +1,4 @@
-name: Reusable build app workflow for production
+name: Reusable VRT reporting workflow for production
 
 on:
   workflow_call:

+ 2 - 0
.gitignore

@@ -44,3 +44,5 @@ yarn-error.log*
 
 # pnpm deploy target dir
 out
+
+.devcontainer/compose.extend.yml

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

@@ -28,6 +28,11 @@ module.exports = {
         in: 'cookie',
         name: 'connect.sid',
       },
+      transferHeaderAuth: {
+        type: 'apiKey',
+        in: 'header',
+        name: 'x-growi-transfer-key',
+      },
     },
   },
   'x-tagGroups': [
@@ -39,10 +44,11 @@ module.exports = {
         'BookmarkFolders',
         'Page',
         'Pages',
+        'PageListing',
         'Revisions',
         'ShareLinks',
         'Users',
-        '',
+        'UserUISettings',
         '',
       ],
     },
@@ -63,20 +69,29 @@ module.exports = {
       name: 'System Management API',
       tags: [
         'Home',
+        'Activity',
         'AdminHome',
         'AppSettings',
+        'ExternalUserGroups',
         'SecuritySetting',
         'MarkDownSetting',
         'CustomizeSetting',
         'Import',
         'Export',
+        'GROWI to GROWI Transfer',
         'MongoDB',
         'NotificationSetting',
+        'Plugins',
+        'Questionnaire',
+        'QuestionnaireSetting',
+        'SlackIntegration',
         'SlackIntegrationSettings',
         'SlackIntegrationSettings (with proxy)',
         'SlackIntegrationSettings (without proxy)',
         'SlackIntegrationLegacySetting',
         'ShareLink Management',
+        'Templates',
+        'Staff',
         'UserGroupRelations',
         'UserGroups',
         'Users Management',

+ 5 - 0
apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh

@@ -10,5 +10,10 @@ OUT=${OUT:-"${APP_PATH}/tmp/openapi-spec-apiv3.json"}
 swagger-jsdoc \
   -o "${OUT}" \
   -d "${APP_PATH}/bin/swagger-jsdoc/definition-apiv3.js" \
+  "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
+  "${APP_PATH}/src/features/questionnaire/server/routes/apiv3/*.ts" \
+  "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
+  "${APP_PATH}/src/features/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \
+  "${APP_PATH}/src/server/routes/login.js" \
   "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"

+ 5 - 1
apps/app/docker/docker-entrypoint.sh

@@ -7,8 +7,12 @@ mkdir -p /data/uploads
 if [ ! -e "./public/uploads" ]; then
   ln -s /data/uploads ./public/uploads
 fi
-
 chown -R node:node /data/uploads
 chown -h node:node ./public/uploads
 
+# Set permissions for shared directory for bulk export
+mkdir -p /tmp/page-bulk-export
+chown -R node:node /tmp/page-bulk-export
+chmod 700 /tmp/page-bulk-export
+
 exec gosu node /bin/bash -c "$@"

+ 9 - 4
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.2.0",
+  "version": "7.2.1-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -31,7 +31,7 @@
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:swagger2openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
     "lint:swagger2openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
-    "lint": "run-p lint:*",
+    "lint": "run-p lint:**",
     "prelint:swagger2openapi:apiv3": "pnpm run swagger2openapi:apiv3",
     "prelint:swagger2openapi:apiv1": "pnpm run swagger2openapi:apiv1",
     "test": "run-p test:*",
@@ -82,6 +82,7 @@
     "@growi/remark-growi-directive": "workspace:^",
     "@growi/remark-lsx": "workspace:^",
     "@growi/slack": "workspace:^",
+    "@growi/pdf-converter-client": "workspace:^",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@opentelemetry/api": "^1.9.0",
     "@opentelemetry/auto-instrumentations-node": "^0.55.1",
@@ -94,12 +95,14 @@
     "@opentelemetry/sdk-trace-node": "^1.28.0",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
+    "@types/async": "^3.2.24",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
+    "babel-plugin-superjson-next": "^0.4.2",
     "body-parser": "^1.20.3",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
@@ -164,7 +167,7 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
-    "next": "^14.2.21",
+    "next": "^14.2.26",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
     "next-superjson": "^0.0.4",
@@ -214,6 +217,7 @@
     "remark-directive": "^3.0.0",
     "remark-frontmatter": "^5.0.0",
     "remark-gfm": "^4.0.0",
+    "remark-html": "^16.0.1",
     "remark-math": "^6.0.0",
     "remark-parse": "^11.0.0",
     "remark-rehype": "^11.1.1",
@@ -223,7 +227,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.2.8",
-    "swr": "^2.2.2",
+    "swr": "^2.3.2",
     "throttle-debounce": "^5.0.0",
     "ts-deepmerge": "^6.2.0",
     "tslib": "^2.8.0",
@@ -265,6 +269,7 @@
     "@testing-library/jest-dom": "^6.5.0",
     "@testing-library/react": "^16.0.1",
     "@testing-library/user-event": "^14.5.2",
+    "@types/archiver": "^6.0.2",
     "@types/bunyan": "^1.8.11",
     "@types/express": "^4.17.21",
     "@types/hast": "^3.0.4",

+ 2 - 0
apps/app/playwright.config.ts

@@ -72,6 +72,8 @@ export default defineConfig({
     trace: 'on-first-retry',
 
     viewport: { width: 1400, height: 1024 },
+
+    screenshot: 'only-on-failure',
   },
 
   /* Configure projects for major browsers */

+ 39 - 5
apps/app/playwright/20-basic-features/use-tools.spec.ts

@@ -1,8 +1,44 @@
 import { test, expect, type Page } from '@playwright/test';
 
 const openPageItemControl = async(page: Page): Promise<void> => {
-  await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
-  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  const nav = page.getByTestId('grw-contextual-sub-nav');
+  const button = nav.getByTestId('open-page-item-control-btn');
+
+  // Wait for navigation element to be visible and attached
+  await expect(nav).toBeVisible();
+  await nav.waitFor({ state: 'visible' });
+
+  // Wait for button to be visible, enabled and attached
+  await expect(button).toBeVisible();
+  await expect(button).toBeEnabled();
+  await button.waitFor({ state: 'visible' });
+
+  // Add a small delay to ensure the button is fully interactive
+  await page.waitForTimeout(100);
+
+  await button.click();
+};
+
+const openPutBackPageModal = async(page: Page): Promise<void> => {
+  const alert = page.getByTestId('trash-page-alert');
+  const button = alert.getByTestId('put-back-button');
+
+  // Wait for alert element to be visible and attached
+  await expect(alert).toBeVisible();
+  await alert.waitFor({ state: 'visible' });
+
+  // Wait for button to be visible, enabled and attached
+  await expect(button).toBeVisible();
+  await expect(button).toBeEnabled();
+  await button.waitFor({ state: 'visible' });
+
+  // Scroll to the top of the page to prevent the subnav hide the button
+  await page.evaluate(() => {
+    window.scrollTo(0, 0);
+  });
+
+  await button.click();
+  await expect(page.getByTestId('put-back-page-modal')).toBeVisible();
 };
 
 test('Page Deletion and PutBack is executed successfully', async({ page }) => {
@@ -15,9 +51,7 @@ test('Page Deletion and PutBack is executed successfully', async({ page }) => {
   await page.getByTestId('delete-page-button').click();
 
   // PutBack
-  await expect(page.getByTestId('trash-page-alert')).toBeVisible();
-  await page.getByTestId('put-back-button').click();
-  await expect(page.getByTestId('put-back-page-modal')).toBeVisible();
+  await openPutBackPageModal(page);
   await page.getByTestId('put-back-execution-button').click();
   await expect(page.getByTestId('trash-page-alert')).not.toBeVisible();
 });

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

@@ -10,6 +10,7 @@
   "only_me": "Only me",
   "only_inside_the_group": "Only inside the group",
   "optional": "Optional",
+  "days": "days",
   "security_settings": {
     "security_settings": "Security Settings",
     "scope_of_page_disclosure": "Scope of page disclosure",
@@ -359,6 +360,11 @@
     "file_uploading": "File uploading",
     "enable_files_except_image": "Enabling this option will allow upload of any file type. Without this option, only image file upload is supported.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
+    "page_bulk_export_settings": "Page Bulk Export Settings",
+    "enable_page_bulk_export": "Enable bulk export",
+    "page_bulk_export_explanation": "Enables a feature that allows all users to export a page and all it's child pages at once from the menu. Exported data will be automatically deleted after the storage period has passed.",
+    "page_bulk_export_warning": "The bulk page export feature is available to all users. In order to maintain system resources, we ask for your cooperation in using the minimum amount necessary. If you are an administrator, please inform all users of this.",
+    "page_bulk_export_storage_period": "Storage period",
     "update": "Update",
     "mail_settings": "E-mail Settings",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
@@ -382,7 +388,7 @@
     "file_delivery_method_relay":"Internal System Relay",
     "file_delivery_method_redirect_info":"Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
     "file_delivery_method_relay_info":"Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
-    "fixed_by_env_var": "This is fixed by the env var <code>FILE_UPLOAD={{fileUploadType}}</code>.",
+    "fixed_by_env_var": "This is fixed by the env var <code>{{envKey}}={{envVar}}</code>.",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "local_label": "Local",
@@ -1062,7 +1068,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "Add User to User Group",
     "ADMIN_SEARCH_CONNECTION": "Attempting to reconnect to Elasticsearch",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Normalize of Elasticsearch indexes",
-    "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes"
+    "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "Update Page Bulk Export Settings"
   },
   "g2g": {
     "transfer_success": "Completed GROWI to GROWI transfer successfully",

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

@@ -748,11 +748,26 @@
     "discription_heading": "Create Account",
     "discription": "Create an your account with the invited email address"
   },
-  "export_bulk": {
+  "page_export": {
     "failed_to_export": "Failed to export",
     "failed_to_count_pages": "Failed to count pages",
     "export_page_markdown": "Export page as Markdown",
-    "export_page_pdf": "Export page as PDF"
+    "export_page_pdf": "Export page as PDF",
+    "bulk_export": "Export page and all child pages",
+    "bulk_export_download_explanation": "A notification will be sent when the export is complete. To download the exported file, click the notification.",
+    "bulk_export_exec_time_warning": "If the number of pages is large, it may take a while to export",
+    "large_bulk_export_warning": "To conserve system resources, please refrain from exporting a large number of pages consecutively",
+    "markdown": "Markdown",
+    "choose_export_format": "Select export format",
+    "bulk_export_started": "Please wait a moment...",
+    "bulk_export_download_expired": "Download period has expired",
+    "bulk_export_job_expired": "Export process was canceled because it took too long",
+    "export_in_progress": "Export in progress",
+    "export_in_progress_explanation": "Export with the same format is already in progress. Would you like to restart to export the latest page contents?",
+    "export_cancel_warning": "The following export in progress will be canceled",
+    "restart": "Restart",
+    "format": "Format",
+    "started_on": "Started on"
   },
   "message": {
     "successfully_connected": "Successfully Connected!",

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

@@ -10,6 +10,7 @@
   "only_me": "Seulement moi",
   "only_inside_the_group": "Utilisateurs du groupe",
   "optional": "Optionnel",
+  "days": "jours",
   "security_settings": {
     "security_settings": "Sécurité",
     "scope_of_page_disclosure": "Confidentialité de la page",
@@ -359,6 +360,11 @@
     "file_uploading": "Téléversement de fichiers",
     "enable_files_except_image": "Autoriser tout les types de fichiers",
     "attach_enable": "Autorise le téléversement de tout les types de fichiers.",
+    "page_bulk_export_settings": "Paramètres d'exportation de pages par lots",
+    "enable_page_bulk_export": "Activer l'exportation groupée",
+    "page_bulk_export_explanation": "Active une fonctionnalité qui permet à tous les utilisateurs d'exporter simultanément toutes les pages sélectionnées dans le menu des pages et leurs pages subordonnées. Les données exportées seront automatiquement supprimées une fois la période de conservation écoulée.",
+    "page_bulk_export_warning": "La fonctionnalité d’exportation de pages en masse est disponible pour tous les utilisateurs. Afin de maintenir les ressources du système, nous demandons votre coopération pour utiliser le montant minimum nécessaire. Si vous êtes administrateur, veuillez en informer tous les utilisateurs.",
+    "page_bulk_export_storage_period": "Date limite de téléchargement",
     "update": "Sauvegarder",
     "mail_settings": "SMTP",
     "mailer_is_not_set_up": "Paramètres d'envoi de courriels non configurés.",
@@ -382,7 +388,7 @@
     "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>{{envKey}}={{envVar}}</code>.",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "local_label": "Local",
@@ -1061,7 +1067,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "Ajouter l'utilisateur au groupe",
     "ADMIN_SEARCH_CONNECTION": "Essai de reconnexion Elasticsearch",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Nomarliser l'index Elasticsearch",
-    "ADMIN_SEARCH_INDICES_REBUILD": "Reconstruire l'index Elasticsearch"
+    "ADMIN_SEARCH_INDICES_REBUILD": "Reconstruire l'index Elasticsearch",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "Mettre à jour les paramètres d'exportation groupée de la page"
   },
   "g2g": {
     "transfer_success": "Transfert de GROWI vers GROWI complété!",

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

@@ -743,11 +743,26 @@
     "discription_heading": "Créer un compte",
     "discription": "Créer un compte avec votre adresse courriel invitée"
   },
-  "export_bulk": {
+  "page_export": {
     "failed_to_export": "Échec de l'export",
     "failed_to_count_pages": "Échec du compte des pages",
     "export_page_markdown": "Exporter la page en Markdown",
-    "export_page_pdf": "Exporter la page en PDF"
+    "export_page_pdf": "Exporter la page en PDF",
+    "bulk_export": "Exporter la page et toutes les pages enfants",
+    "bulk_export_download_explanation": "Une notification sera envoyée lorsque l’exportation sera terminée. Pour télécharger le fichier exporté, cliquez sur la notification.",
+    "bulk_export_exec_time_warning": "Si le nombre de pages est important, l'exportation peut prendre un certain temps.",
+    "large_bulk_export_warning": "Pour préserver les ressources du système, veuillez éviter d'exporter un grand nombre de pages consécutivement",
+    "markdown": "Markdown",
+    "choose_export_format": "Sélectionnez le format d'exportation",
+    "bulk_export_started": "Patientez s'il-vous-plait...",
+    "bulk_export_download_expired": "La période de téléchargement a expiré",
+    "bulk_export_job_expired": "Le traitement a été interrompu car le temps d'exportation était trop long",
+    "export_in_progress": "Exportation en cours",
+    "export_in_progress_explanation": "L'exportation avec le même format est déjà en cours. Souhaitez-vous redémarrer pour exporter le dernier contenu de la page ?",
+    "export_cancel_warning": "Les exportations suivantes en cours seront annulées",
+    "restart": "Redémarrage",
+    "format": "Format",
+    "started_on": "Commencé le"
   },
   "message": {
     "successfully_connected": "Connecté!",

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

@@ -19,6 +19,7 @@
   "only_me": "自分のみ",
   "only_inside_the_group": "特定グループのみ",
   "optional": "オプション",
+  "days": "日",
   "security_settings": {
     "security_settings": "セキュリティ設定",
     "scope_of_page_disclosure": "ページの公開範囲",
@@ -368,6 +369,11 @@
     "file_uploading": "ファイルアップロード",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
+    "page_bulk_export_settings": "ページ一括エクスポート設定",
+    "enable_page_bulk_export": "一括エクスポートを有効にする",
+    "page_bulk_export_explanation": "すべてのユーザーが、ページメニューから選択したページとその配下ページをまとめてエクスポートできる機能を有効化します。エクスポートされたデータは保存期間経過後に自動的に削除されます。",
+    "page_bulk_export_warning": "ページ一括エクスポート機能は全ユーザーが利用可能です。システムリソースの維持のため、必要最小限の利用にご協力をお願いいたします。管理者の方は、この旨をユーザーの皆様にご周知ください。",
+    "page_bulk_export_storage_period": "保存期間",
     "update": "更新",
     "mail_settings": "メールの設定",
     "mailer_is_not_set_up": "メール設定がセットアップされていません。",
@@ -402,7 +408,7 @@
     "azure_storage_account_name": "ストレージアカウント名",
     "azure_storage_container_name": "コンテナ名",
     "azure_note_for_the_only_env_option": "現在Azure設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
-    "fixed_by_env_var": "環境変数 <code>FILE_UPLOAD={{fileUploadType}}</code> により固定されています。",
+    "fixed_by_env_var": "環境変数 <code>{{envKey}}={{envVar}}</code> により固定されています。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
     "test_connection": "接続テスト",
     "change_setting": "この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。",
@@ -1071,7 +1077,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "ユーザーグループにユーザーを追加",
     "ADMIN_SEARCH_CONNECTION": "Elasticsearch の再接続の試行",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch のインデックスの正規化",
-    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
+    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "ページ一括エクスポート設定の更新"
   },
   "g2g": {
     "transfer_success": "G2G移行が完了しました",

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

@@ -781,11 +781,26 @@
     "discription_heading": "アカウント作成",
     "discription": "招待を受け取ったメールアドレスでアカウントを作成します"
   },
-  "export_bulk": {
+  "page_export": {
     "failed_to_export": "ページのエクスポートに失敗しました",
     "failed_to_count_pages": "ページ数の取得に失敗しました",
     "export_page_markdown": "マークダウン形式でページをエクスポート",
-    "export_page_pdf": "PDF形式でページをエクスポート"
+    "export_page_pdf": "PDF形式でページをエクスポート",
+    "bulk_export": "ページとその配下のページを全てエクスポート",
+    "bulk_export_download_explanation": "エクスポート完了後に通知が届きます。通知をクリックし、ファイルをダウンロードしてください。",
+    "bulk_export_exec_time_warning": "ページ数が多いと、エクスポートに時間がかかる場合があります",
+    "large_bulk_export_warning": "システムリソースの維持のため、ページ数の多いエクスポートを連続して実行することはご遠慮ください",
+    "markdown": "マークダウン",
+    "choose_export_format": "エクスポート形式を選択してください",
+    "bulk_export_started": "ただいま準備中です...",
+    "bulk_export_download_expired": "ダウンロード期限が切れました",
+    "bulk_export_job_expired": "エクスポート時間が長すぎるため、処理が中断されました",
+    "export_in_progress": "エクスポート進行中",
+    "export_in_progress_explanation": "既に同じ形式でのエクスポートが進行中です。最新のページ内容でエクスポートを最初からやり直しますか?",
+    "export_cancel_warning": "進行中の以下のエクスポートはキャンセルされます",
+    "restart": "やり直す",
+    "format": "形式",
+    "started_on": "開始日時"
   },
   "message": {
     "successfully_connected": "接続に成功しました!",

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

@@ -19,6 +19,7 @@
   "only_me": "只有我",
   "only_inside_the_group": "仅组内",
   "optional": "可选的",
+  "days": "天",
   "security_settings": {
     "security_settings": "安全设置",
     "scope_of_page_disclosure": "页面公开范围",
@@ -368,6 +369,11 @@
     "file_uploading": "文件上传",
     "enable_files_except_image": "启用此选项将允许上传任何文件类型。如果没有此选项,则仅支持图像文件上载。",
     "attach_enable": "如果启用此选项,则可以附加图像文件以外的文件。",
+    "page_bulk_export_settings": "页面批量导出设置",
+    "enable_page_bulk_export": "启用批量导出",
+    "page_bulk_export_explanation": "启用一项功能,允许所有用户一次性导出从页面菜单中选择的所有页面及其下级页面。保留期限过后,导出的数据将自动删除。",
+    "page_bulk_export_warning": "批量页面导出功能可供所有用户使用。为了维护系统资源,请您配合使用最低限度的资源。如果您是管理员,请将此事实告知所有用户。",
+    "page_bulk_export_storage_period": "储存期限",
     "update": "更新",
     "mail_settings": "邮件设置",
     "mailer_is_not_set_up": "邮件设置尚未完成。",
@@ -402,7 +408,7 @@
     "azure_storage_account_name": "Storage Account Name",
     "azure_storage_container_name": "Container Name",
     "azure_note_for_the_only_env_option": "The Azure Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-    "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
+    "fixed_by_env_var": "这是由env var 修复的 <code>{{envKey}}={{envVar}}</code>.",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "test_connection": "测试邮件服务器连接",
     "change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
@@ -1071,7 +1077,8 @@
     "ADMIN_USER_GROUP_ADD_USER": "添加用户到用户组",
     "ADMIN_SEARCH_CONNECTION": "重试Elasticsearch连接",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "试图重新连接Elasticsearch",
-    "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
+    "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "更新页面批量导出设置"
   },
   "g2g": {
     "transfer_success": "Completed GROWI to GROWI transfer successfully",

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

@@ -752,11 +752,26 @@
     "discription_heading": "创建账户",
     "discription": "用被邀请的电子邮件地址创建一个你的账户"
   },
-  "export_bulk": {
+  "page_export": {
     "failed_to_export": "导出失败",
     "failed_to_count_pages": "页面计数失败",
     "export_page_markdown": "以Markdown格式导出页面",
-    "export_page_pdf": "以PDF格式导出页面"
+    "export_page_pdf": "以PDF格式导出页面",
+    "bulk_export": "导出页面及其下的所有页面",
+    "bulk_export_download_explanation": "导出完成后将发送通知。要下载导出的文件,请单击通知。",
+    "bulk_export_exec_time_warning": "如果页数较多,导出可能需要一段时间",
+    "large_bulk_export_warning": "为了节省系统资源,请避免连续导出大量页面",
+    "markdown": "Markdown",
+    "choose_export_format": "选择导出格式",
+    "bulk_export_started": "目前我们正在准备...",
+    "bulk_export_download_expired": "下载期限已过",
+    "bulk_export_job_expired": "由于导出时间太长,处理被中断",
+    "export_in_progress": "导出正在进行中",
+    "export_in_progress_explanation": "已在进行相同格式的导出。您要重新启动以导出最新的页面内容吗?",
+    "export_cancel_warning": "以下正在进行的导出将被取消",
+    "restart": "重新开始",
+    "format": "格式",
+    "started_on": "开始于"
   },
   "message": {
     "successfully_connected": "连接成功!",

+ 1 - 1
apps/app/src/client/components/Admin/AdminHome/EnvVarsTable.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 type EnvVarsTableProps = {
   envVars: Record<string, string | number | boolean>,

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

@@ -14,6 +14,7 @@ import AppSetting from './AppSetting';
 import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
 import { MaintenanceMode } from './MaintenanceMode';
+import PageBulkExportSettings from './PageBulkExportSettings';
 import QuestionnaireSettings from './QuestionnaireSettings';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
@@ -108,6 +109,16 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
       </div>
 
+      {/* TODO: Enable configuring bulk export for GROWI.cloud when it can be relased for cloud (https://redmine.weseek.co.jp/issues/163220) */}
+      {!adminAppContainer.state.isBulkExportDisabledForCloud && (
+        <div className="row mt-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">{t('admin:app_setting.page_bulk_export_settings')}</h2>
+            <PageBulkExportSettings />
+          </div>
+        </div>
+      )}
+
       <div className="row mt-5">
         <div className="col-lg-12">
           <h2 className="admin-setting-header">{t('admin:app_setting.questionnaire_settings')}</h2>

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

@@ -1,5 +1,8 @@
+import type { JSX } from 'react';
+
 import { useTranslation } from 'next-i18next';
 
+
 export type AwsSettingMoleculeProps = {
   s3ReferenceFileWithRelayMode
   s3Region

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

@@ -1,7 +1,10 @@
+import type { JSX } from 'react';
+
 import { useTranslation } from 'next-i18next';
 
 import MaskedInput from './MaskedInput';
 
+
 export type AzureSettingMoleculeProps = {
   azureReferenceFileWithRelayMode
   azureUseOnlyEnvVars

+ 10 - 6
apps/app/src/client/components/Admin/App/FileUploadSetting.tsx

@@ -1,10 +1,11 @@
-import type { ChangeEvent } from 'react';
+import type { ChangeEvent, JSX } from 'react';
 import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import { FileUploadType } from '~/interfaces/file-uploader';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
@@ -16,9 +17,6 @@ import type { AzureSettingMoleculeProps } from './AzureSetting';
 import { GcsSettingMolecule } from './GcsSetting';
 import type { GcsSettingMoleculeProps } from './GcsSetting';
 
-
-const fileUploadTypes = ['aws', 'gcs', 'azure', 'gridfs', 'local'] as const;
-
 type FileUploadSettingMoleculeProps = {
   fileUploadType: string
   isFixedFileUploadByEnvVar: boolean
@@ -45,7 +43,7 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
         </label>
 
         <div className="col-md-6 py-2">
-          {fileUploadTypes.map((type) => {
+          {Object.values(FileUploadType).map((type) => {
             return (
               <div key={type} className="form-check form-check-inline">
                 <input
@@ -67,7 +65,13 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
             <span className="material-symbols-outlined">help</span>
             <b>FIXED</b><br />
             {/* eslint-disable-next-line react/no-danger */}
-            <b dangerouslySetInnerHTML={{ __html: t('admin:app_setting.fixed_by_env_var', { fileUploadType: props.envFileUploadType }) }} />
+            <b dangerouslySetInnerHTML={{
+              __html: t('admin:app_setting.fixed_by_env_var', {
+                envKey: 'FILE_UPLOAD',
+                envVar: props.envFileUploadType,
+              }),
+            }}
+            />
           </p>
         )}
       </div>

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

@@ -1,5 +1,8 @@
+import type { JSX } from 'react';
+
 import { useTranslation } from 'next-i18next';
 
+
 export type GcsSettingMoleculeProps = {
   gcsReferenceFileWithRelayMode
   gcsUseOnlyEnvVars

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

@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, type JSX } from 'react';
 
 import styles from './MaskedInput.module.scss';
 

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

@@ -0,0 +1,136 @@
+import {
+  useState, useCallback, useEffect, type JSX,
+} from 'react';
+
+import { LoadingSpinner } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRxAppSettings } from '~/stores/admin/app-settings';
+
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const PageBulkExportSettings = (): JSX.Element => {
+  const { t } = useTranslation(['admin', 'commons']);
+
+  const { data, error, mutate } = useSWRxAppSettings();
+
+  const [isBulkExportPagesEnabled, setIsBulkExportPagesEnabled] = useState(data?.isBulkExportPagesEnabled);
+  const [bulkExportDownloadExpirationSeconds, setBulkExportDownloadExpirationSeconds] = useState(data?.bulkExportDownloadExpirationSeconds);
+
+  const changeBulkExportDownloadExpirationSeconds = (bulkExportDownloadExpirationDays: number) => {
+    const bulkExportDownloadExpirationSeconds = bulkExportDownloadExpirationDays * 24 * 60 * 60;
+    setBulkExportDownloadExpirationSeconds(bulkExportDownloadExpirationSeconds);
+  };
+
+  const onSubmitHandler = useCallback(async() => {
+    try {
+      await apiv3Put('/app-settings/page-bulk-export-settings', {
+        isBulkExportPagesEnabled,
+        bulkExportDownloadExpirationSeconds,
+      });
+      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.questionnaire_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+    mutate();
+  }, [isBulkExportPagesEnabled, bulkExportDownloadExpirationSeconds, mutate, t]);
+
+  useEffect(() => {
+    if (data?.useOnlyEnvVarForFileUploadType) {
+      setIsBulkExportPagesEnabled(data?.envIsBulkExportPagesEnabled);
+    }
+    else {
+      setIsBulkExportPagesEnabled(data?.isBulkExportPagesEnabled);
+    }
+    setBulkExportDownloadExpirationSeconds(data?.bulkExportDownloadExpirationSeconds);
+  }, [data]);
+
+  const isLoading = data === undefined && error === undefined;
+
+  return (
+    <>
+      {isLoading && (
+        <div className="text-muted text-center mb-5">
+          <LoadingSpinner className="me-1 fs-3" />
+        </div>
+      )}
+
+      {!isLoading && (
+        <>
+          <p className="card custom-card bg-warning-subtle my-3">
+            {t('admin:app_setting.page_bulk_export_explanation')} <br />
+            <span className="text-danger mt-1">
+              {t('admin:app_setting.page_bulk_export_warning')}
+            </span>
+          </p>
+
+          <div className="my-4 row">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+            </label>
+
+            <div className="col-md-6">
+              <div className="form-check form-switch form-check-info">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="cbIsPageBulkExportEnabled"
+                  checked={isBulkExportPagesEnabled}
+                  disabled={data?.useOnlyEnvVarsForIsBulkExportPagesEnabled}
+                  onChange={e => setIsBulkExportPagesEnabled(e.target.checked)}
+                />
+                <label className="form-label form-check-label" htmlFor="cbIsPageBulkExportEnabled">
+                  {t('app_setting.enable_page_bulk_export')}
+                </label>
+              </div>
+              {data?.useOnlyEnvVarsForIsBulkExportPagesEnabled && (
+                <p className="form-text text-muted">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <b dangerouslySetInnerHTML={{
+                    __html: t('admin:app_setting.fixed_by_env_var', {
+                      envKey: 'BULK_EXPORT_PAGES_ENABLED',
+                      envVar: isBulkExportPagesEnabled,
+                    }),
+                  }}
+                  />
+                </p>
+              )}
+            </div>
+          </div>
+
+          <div className="mb-4">
+            <div className="row">
+              <label
+                className="text-start text-md-end col-md-3 col-form-label"
+              >
+                {t('app_setting.page_bulk_export_storage_period')}
+              </label>
+
+              <div className="col-md-2">
+                <select
+                  className="form-select"
+                  value={(bulkExportDownloadExpirationSeconds ?? 0) / (24 * 60 * 60)}
+                  onChange={(e) => { changeBulkExportDownloadExpirationSeconds(Number(e.target.value)) }}
+                >
+                  {Array.from({ length: 7 }, (_, i) => i + 1).map(number => (
+                    <option key={`be-download-expiration-option-${number}`} value={number}>
+                      {number} {t('admin:days')}
+                    </option>
+                  ))}
+                </select>
+              </div>
+            </div>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onSubmitHandler} />
+        </>
+      )}
+    </>
+  );
+};
+
+export default PageBulkExportSettings;

+ 43 - 29
apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx

@@ -1,5 +1,5 @@
 import {
-  useState, useCallback, useEffect,
+  useState, useCallback, useEffect, type JSX,
 } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';
@@ -72,37 +72,51 @@ const QuestionnaireSettings = (): JSX.Element => {
 
       {!isLoading && (
         <>
-          <div className="my-4">
-            <div className="form-check form-switch form-check-info">
-              <input
-                type="checkbox"
-                className="form-check-input"
-                id="isQuestionnaireEnabled"
-                checked={isQuestionnaireEnabled}
-                onChange={onChangeIsQuestionnaireEnabledHandler}
-              />
-              <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                {t('app_setting.enable_questionnaire')}
-              </label>
+          <div className="my-4 row">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+            </label>
+
+            <div className="col-md-6">
+              <div className="form-check form-switch form-check-info">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="isQuestionnaireEnabled"
+                  checked={isQuestionnaireEnabled}
+                  onChange={onChangeIsQuestionnaireEnabledHandler}
+                />
+                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
+                  {t('app_setting.enable_questionnaire')}
+                </label>
+              </div>
             </div>
           </div>
 
-          <div className="my-4">
-            <div className="form-check form-check-info">
-              <input
-                type="checkbox"
-                className="form-check-input"
-                id="isAppSiteUrlHashed"
-                checked={isAppSiteUrlHashed}
-                onChange={onChangeisAppSiteUrlHashedHandler}
-                disabled={!isQuestionnaireEnabled}
-              />
-              <label className="form-label form-check-label" htmlFor="isAppSiteUrlHashed">
-                {t('app_setting.anonymize_app_site_url')}
-              </label>
-              <p className="form-text text-muted small">
-                {t('app_setting.url_anonymization_explanation')}
-              </p>
+          <div className="my-4 row">
+            <label
+              className="text-start text-md-end col-md-3 col-form-label"
+            >
+            </label>
+
+            <div className="col-md-6">
+              <div className="form-check form-check-info">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="isAppSiteUrlHashed"
+                  checked={isAppSiteUrlHashed}
+                  onChange={onChangeisAppSiteUrlHashedHandler}
+                  disabled={!isQuestionnaireEnabled}
+                />
+                <label className="form-label form-check-label" htmlFor="isAppSiteUrlHashed">
+                  {t('app_setting.anonymize_app_site_url')}
+                </label>
+                <p className="form-text text-muted small">
+                  {t('app_setting.url_anonymization_explanation')}
+                </p>
+              </div>
             </div>
           </div>
 

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 type Props = {
   onClick: () => void,

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { Progress } from 'reactstrap';
 

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 type Props = {
   optionId: string

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';

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

@@ -1,5 +1,5 @@
 import React, {
-  useCallback, useEffect, useState,
+  useCallback, useEffect, useState, type JSX,
 } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';

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

@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useState, type JSX } from 'react';
 
 import { useTranslation } from 'react-i18next';
 

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { PrismAsyncLight } from 'react-syntax-highlighter';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { PrismAsyncLight } from 'react-syntax-highlighter';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, type JSX } from 'react';
 
 import { type GrowiThemeMetadata, GrowiThemeSchemeType } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,6 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+  useCallback, useEffect, useState, type JSX,
+} from 'react';
 
 import { PresetThemes, PresetThemesMetadatas } from '@growi/preset-themes';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import type { GrowiThemeMetadata } from '@growi/core';
 

+ 1 - 1
apps/app/src/client/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

+ 1 - 1
apps/app/src/client/components/Admin/ElasticsearchManagement/ReconnectControls.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';

+ 1 - 1
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';

+ 1 - 1
apps/app/src/client/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

+ 3 - 1
apps/app/src/client/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -1,4 +1,6 @@
-import React, { useCallback, useState, useEffect } from 'react';
+import React, {
+  useCallback, useState, useEffect, type JSX,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
 import {

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

@@ -1,4 +1,6 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+  useCallback, useEffect, useState, type JSX,
+} from 'react';
 
 import { useTranslation } from 'react-i18next';
 

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import DefaultErrorPage from 'next/error';
 import { useTranslation } from 'react-i18next';

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

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

@@ -1,5 +1,5 @@
 import React, {
-  useCallback, useEffect, useState,
+  useCallback, useEffect, useState, type JSX,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';

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

@@ -1,5 +1,5 @@
 import React, {
-  useState, useEffect, useCallback, useMemo,
+  useState, useEffect, useCallback, useMemo, type JSX,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React, { type ComponentPropsWithoutRef } from 'react';
+import React, { type ComponentPropsWithoutRef, type JSX } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';
 

+ 1 - 1
apps/app/src/client/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 

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

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';

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

@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';

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

@@ -1,4 +1,4 @@
-import { useCallback, useRef } from 'react';
+import { useCallback, useRef, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

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

@@ -1,5 +1,5 @@
 import React, {
-  useCallback, useMemo, useEffect, useState,
+  useCallback, useMemo, useEffect, useState, type JSX,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';

+ 1 - 1
apps/app/src/client/components/Admin/Notification/NotificationTypeIcon.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { UncontrolledTooltip } from 'reactstrap';
 

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

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { SlackbotType } from '@growi/slack';
 import { useTranslation } from 'next-i18next';

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

@@ -1,7 +1,10 @@
 
+import type { JSX } from 'react';
+
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 
+
 const ProxyCircle = () => (
   <div className="grw-bridge-proxy-circle position-relative">
     <div className="circle position-absolute m-auto z-1 bg-primary border-light rounded-circle">

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import type { ConnectionStatus } from '@growi/slack';
 import Image from 'next/image';

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import type { ConnectionStatus } from '@growi/slack';
 import Image from 'next/image';

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

@@ -1,6 +1,7 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 const MessageBasedOnConnection = (props) => {

+ 3 - 0
apps/app/src/client/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx

@@ -1,6 +1,9 @@
 
+import type { JSX } from 'react';
+
 import { useTranslation } from 'next-i18next';
 
+
 type Props = {
   slackAppIntegration: {
     _id: string,

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

@@ -1,4 +1,6 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, {
+  useState, useEffect, useCallback, type JSX,
+} from 'react';
 
 import { SlackbotType } from '@growi/slack';
 import { LoadingSpinner } from '@growi/ui/dist/components';

+ 2 - 1
apps/app/src/client/components/Admin/UserGroup/UserGroupDropdown.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useCallback } from 'react';
 
 import type { IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

+ 1 - 1
apps/app/src/client/components/Admin/UserGroup/UserGroupTable.tsx

@@ -1,4 +1,4 @@
-import type { FC } from 'react';
+import type { FC, JSX } from 'react';
 import React, { useState, useEffect } from 'react';
 
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';

+ 1 - 1
apps/app/src/client/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useState, useCallback, useEffect,
+  useState, useCallback, useEffect, type JSX,
 } from 'react';
 
 import {

+ 3 - 1
apps/app/src/client/components/Admin/UserGroupDetail/UserGroupPageList.tsx

@@ -1,4 +1,6 @@
-import React, { useEffect, useState, useCallback } from 'react';
+import React, {
+  useEffect, useState, useCallback, type JSX,
+} from 'react';
 
 import type { IPageHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

+ 1 - 1
apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserModal.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import type { IUserGroupHasId, IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

+ 1 - 1
apps/app/src/client/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import { UserPicture } from '@growi/ui/dist/components';
 import { format as dateFnsFormat } from 'date-fns/format';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IAdminExternalAccount } from '@growi/core';
 import { format as dateFnsFormat } from 'date-fns/format';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 type SortIconsProps = {
   onClick: (sortOrder: string) => void,

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';

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

@@ -1,3 +1,5 @@
+import type { JSX } from 'react';
+
 import { useTranslation } from 'next-i18next';
 
 import { useSiteUrl } from '~/stores-universal/context';

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

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { type JSX } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { isPopulated, type IUser, type Ref } from '@growi/core';

+ 1 - 1
apps/app/src/client/components/Bookmarks/BookmarkFolderItemControl.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, type JSX } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import {

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

@@ -1,4 +1,6 @@
-import React, { useCallback, useMemo, useState } from 'react';
+import React, {
+  useCallback, useMemo, useState, type JSX,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { DropdownItem, DropdownMenu, UncontrolledDropdown } from 'reactstrap';

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