Просмотр исходного кода

Merge branch 'feat/page-bulk-export' into feat/135772-pdf-page-bulk-export

Futa Arai 1 год назад
Родитель
Сommit
07dec1cfc7
100 измененных файлов с 2140 добавлено и 1177 удалено
  1. 5 3
      .changeset/config.json
  2. 0 59
      .devcontainer/Dockerfile
  3. 3 22
      .devcontainer/compose.yml
  4. 34 33
      .devcontainer/devcontainer.json
  5. 17 0
      .devcontainer/postCreateCommand.sh
  6. 0 2
      .gitattributes
  7. 2 3
      .github/workflows/ci-app-prod.yml
  8. 16 47
      .github/workflows/ci-app.yml
  9. 23 57
      .github/workflows/ci-slackbot-proxy.yml
  10. 6 6
      .github/workflows/release-slackbot-proxy.yml
  11. 14 34
      .github/workflows/release-subpackages.yml
  12. 13 15
      .github/workflows/release.yml
  13. 30 78
      .github/workflows/reusable-app-prod.yml
  14. 5 15
      .github/workflows/reusable-app-reg-suit.yml
  15. 3 0
      .gitignore
  16. 0 0
      .npmrc
  17. 2 1
      .vscode/launch.json
  18. 63 1
      CHANGELOG.md
  19. 7 7
      README.md
  20. 7 7
      README_JP.md
  21. 28 0
      apps/app/bin/swagger-jsdoc/definition-apiv1.js
  22. 93 0
      apps/app/bin/swagger-jsdoc/definition-apiv3.js
  23. 15 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv1.sh
  24. 14 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh
  25. 0 37
      apps/app/config/swagger-definition.js
  26. 28 76
      apps/app/docker/Dockerfile
  27. 1 0
      apps/app/docker/Dockerfile.dockerignore
  28. 3 4
      apps/app/docker/README.md
  29. 1 8
      apps/app/docker/codebuild/buildspec.yml
  30. 5 2
      apps/app/next.config.js
  31. 4 0
      apps/app/nodemon.json
  32. 84 56
      apps/app/package.json
  33. 1 1
      apps/app/playwright.config.ts
  34. 1 1
      apps/app/playwright/utils/Login.ts
  35. 4 4
      apps/app/public/static/locales/en_US/admin.json
  36. 6 1
      apps/app/public/static/locales/en_US/translation.json
  37. 4 4
      apps/app/public/static/locales/fr_FR/admin.json
  38. 5 1
      apps/app/public/static/locales/fr_FR/translation.json
  39. 4 4
      apps/app/public/static/locales/ja_JP/admin.json
  40. 5 1
      apps/app/public/static/locales/ja_JP/translation.json
  41. 4 4
      apps/app/public/static/locales/zh_CN/admin.json
  42. 5 1
      apps/app/public/static/locales/zh_CN/translation.json
  43. BIN
      apps/app/resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2
  44. BIN
      apps/app/resource/fonts/PressStart2P-latin.woff2
  45. BIN
      apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2
  46. BIN
      apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2
  47. 3 3
      apps/app/resource/locales/en_US/sandbox-diagrams.md
  48. 1 1
      apps/app/resource/locales/en_US/sandbox-math.md
  49. 188 26
      apps/app/resource/locales/en_US/sandbox.md
  50. 3 3
      apps/app/resource/locales/fr_FR/sandbox-diagrams.md
  51. 1 1
      apps/app/resource/locales/fr_FR/sandbox-math.md
  52. 262 112
      apps/app/resource/locales/fr_FR/sandbox.md
  53. 3 3
      apps/app/resource/locales/ja_JP/sandbox-diagrams.md
  54. 182 187
      apps/app/resource/locales/ja_JP/sandbox.md
  55. 3 3
      apps/app/resource/locales/zh_CN/sandbox-diagrams.md
  56. 1 1
      apps/app/resource/locales/zh_CN/sandbox-math.md
  57. 232 86
      apps/app/resource/locales/zh_CN/sandbox.md
  58. 4 4
      apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx
  59. 4 4
      apps/app/src/client/components/Admin/Security/SecuritySetting.jsx
  60. 0 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  61. 12 11
      apps/app/src/client/components/Bookmarks/DragAndDropWrapper.tsx
  62. 1 1
      apps/app/src/client/components/InfiniteScroll.tsx
  63. 3 2
      apps/app/src/client/components/Me/InAppNotificationSettings.tsx
  64. 1 2
      apps/app/src/client/components/PageComment/CommentEditor.tsx
  65. 0 1
      apps/app/src/client/components/PageEditor/MarkdownTableDataImportForm.tsx
  66. 3 3
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  67. 1 1
      apps/app/src/client/components/PageEditor/markdown-drawio-util-for-editor.ts
  68. 1 1
      apps/app/src/client/components/PageEditor/markdown-table-util-for-editor.ts
  69. 2 3
      apps/app/src/client/components/SearchPage/SearchResultList.tsx
  70. 3 3
      apps/app/src/client/services/AdminHomeContainer.js
  71. 14 3
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  72. 6 18
      apps/app/src/features/callout/components/CalloutViewer.module.scss
  73. 1 1
      apps/app/src/features/callout/services/callout.ts
  74. 55 10
      apps/app/src/features/openai/chat/components/AiChatModal/AiChatModal.tsx
  75. 6 0
      apps/app/src/features/openai/interfaces/message-error.ts
  76. 57 0
      apps/app/src/features/openai/server/models/thread-relation.ts
  77. 31 4
      apps/app/src/features/openai/server/models/vector-store-file-relation.ts
  78. 16 3
      apps/app/src/features/openai/server/models/vector-store.ts
  79. 17 2
      apps/app/src/features/openai/server/routes/message.ts
  80. 1 1
      apps/app/src/features/openai/server/routes/rebuild-vector-store.ts
  81. 8 23
      apps/app/src/features/openai/server/routes/thread.ts
  82. 22 0
      apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts
  83. 4 0
      apps/app/src/features/openai/server/services/client-delegator/interfaces.ts
  84. 22 0
      apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts
  85. 13 0
      apps/app/src/features/openai/server/services/getStreamErrorCode.ts
  86. 0 0
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.spec.ts
  87. 14 7
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.ts
  88. 0 0
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.spec.ts
  89. 2 2
      apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.ts
  90. 29 0
      apps/app/src/features/openai/server/services/openai-api-error-handler.ts
  91. 187 25
      apps/app/src/features/openai/server/services/openai.ts
  92. 68 0
      apps/app/src/features/openai/server/services/thread-deletion-cron.ts
  93. 68 0
      apps/app/src/features/openai/server/services/vector-store-file-deletion-cron.ts
  94. 8 7
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  95. 4 4
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  96. 1 1
      apps/app/src/migrations/20180926134048-make-email-unique.js
  97. 2 3
      apps/app/src/migrations/20180927102719-init-serverurl.js
  98. 2 2
      apps/app/src/migrations/20181019114028-abolish-page-group-relation.js
  99. 1 1
      apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  100. 2 2
      apps/app/src/migrations/20190618104011-add-config-app-installed.js

+ 5 - 3
.changeset/config.json

@@ -15,11 +15,13 @@
     "@growi/app",
     "@growi/slackbot-proxy",
     "@growi/custom-icons",
-    "@growi/markdown-splitter",
     "@growi/editor",
     "@growi/presentation",
-    "@growi/preset-*",
-    "@growi/remark-*",
+    "@growi/preset-templates",
+    "@growi/preset-themes",
+    "@growi/remark-attachment-refs",
+    "@growi/remark-drawio",
+    "@growi/remark-lsx",
     "@growi/slack",
     "@growi/ui"
   ]

+ 0 - 59
.devcontainer/Dockerfile

@@ -1,59 +0,0 @@
-#-------------------------------------------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
-#-------------------------------------------------------------------------------------------------------------
-
-FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-20
-
-# The node image includes a non-root user with sudo access. Use the
-# "remoteUser" property in devcontainer.json to use it. On Linux, update
-# these values to ensure the container user's UID/GID matches your local values.
-# See https://aka.ms/vscode-remote/containers/non-root-user for details.
-ARG USERNAME=node
-ARG USER_UID=1000
-ARG USER_GID=$USER_UID
-
-RUN mkdir -p /workspace/growi/node_modules
-RUN mkdir -p /workspace/growi/apps/app/node_modules
-RUN mkdir -p /workspace/growi/apps/slackbot-proxy/node_modules
-RUN mkdir -p /workspace/growi/apps/app/.next
-
-# [Optional] Update UID/GID if needed
-RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
-        groupmod --gid $USER_GID $USERNAME \
-        && usermod --uid $USER_UID --gid $USER_GID $USERNAME; \
-    fi
-RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
-
-# *************************************************************
-# * Uncomment this section to use RUN instructions to install *
-# * any needed dependencies after executing "apt-get update". *
-# * See https://docs.docker.com/engine/reference/builder/#run *
-# *************************************************************
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Prepare to install Chrome for VRT
-RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
-RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
-
-RUN apt-get update \
-    && apt-get -y install --no-install-recommends \
-      git-lfs \
-      iputils-ping net-tools dnsutils \
-
-    # Uncomment below lines to install Chromium
-    # --- works only on AMD64 ---
-    # && apt-get -y install --no-install-recommends chromium \
-    #    libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk \
-
-    # Clean up
-    && apt-get autoremove -y \
-    && apt-get clean -y \
-    && rm -rf /var/lib/apt/lists/*
-ENV DEBIAN_FRONTEND=dialog
-
-RUN yarn global add turbo
-RUN yarn global add node-gyp
-
-# Uncomment to default to non-root user
-# USER $USER_UID

+ 3 - 22
.devcontainer/docker-compose.yml → .devcontainer/compose.yml

@@ -1,30 +1,12 @@
-#-------------------------------------------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
-#-------------------------------------------------------------------------------------------------------------
-
-version: '3'
 services:
   node:
-    # Uncomment the next line to use a non-root user for all processes. You can also
-    # simply use the "remoteUser" property in devcontainer.json if you just want VS Code
-    # and its sub-processes (terminals, tasks, debugging) to execute as the user. On Linux,
-    # you may need to update USER_UID and USER_GID in .devcontainer/Dockerfile to match your
-    # user if not 1000. See https://aka.ms/vscode-remote/containers/non-root for details.
-    user: node
-
-    build:
-      context: .
-      dockerfile: Dockerfile
-
+    image: mcr.microsoft.com/devcontainers/base:ubuntu
     volumes:
       - ..:/workspace/growi:delegated
+      - pnpm-store:/workspace/growi/.pnpm-store
       - node_modules:/workspace/growi/node_modules
-      - node_modules_app:/workspace/growi/apps/app/node_modules
-      - node_modules_slackbot-proxy:/workspace/growi/apps/slackbot-proxy/node_modules
       - buildcache_app:/workspace/growi/apps/app/.next
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
-
     tty: true
 
   mongo:
@@ -59,7 +41,6 @@ services:
       - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
 volumes:
+  pnpm-store:
   node_modules:
-  node_modules_app:
-  node_modules_slackbot-proxy:
   buildcache_app:

+ 34 - 33
.devcontainer/devcontainer.json

@@ -1,44 +1,45 @@
-// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
-// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/javascript-node-12-mongo
-// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
 {
   "name": "GROWI-Dev",
-  "dockerComposeFile": "docker-compose.yml",
+  "dockerComposeFile": "compose.yml",
   "service": "node",
   "workspaceFolder": "/workspace/growi",
 
-  // Set *default* container specific settings.json values on container create.
-  "settings": {
-    "terminal.integrated.defaultProfile.linux": "bash"
+  "features": {
+    "ghcr.io/devcontainers/features/node:1": {
+      "version": "20.18.0"
+    }
   },
 
-  // Add the IDs of extensions you want installed when the container is created.
-  "extensions": [
-    "dbaeumer.vscode-eslint",
-    "mhutchie.git-graph",
-    "eamodio.gitlens",
-    "github.vscode-pull-request-github",
-    "cschleiden.vscode-github-actions",
-    "cweijan.vscode-database-client2",
-    "mongodb.mongodb-vscode",
-    "msjsdiag.debugger-for-chrome",
-    "firefox-devtools.vscode-firefox-debug",
-    "editorconfig.editorconfig",
-    "shinnn.stylelint",
-    "stylelint.vscode-stylelint",
-    "vitest.explorer",
-    "ms-playwright.playwright"
-  ],
-
-  // Uncomment the next line if you want start specific services in your Docker Compose config.
-  // "runServices": [],
-
-  // Uncomment the line below if you want to keep your containers running after VS Code shuts down.
-  // "shutdownAction": "none",
+  // Use 'forwardPorts' to make a list of ports inside the container available locally.
+  // "forwardPorts": [],
 
   // Use 'postCreateCommand' to run commands after the container is created.
-  "postCreateCommand": "git-lfs pull & turbo run bootstrap",
+  "postCreateCommand": "/bin/bash ./.devcontainer/postCreateCommand.sh",
+
+  // Configure tool-specific properties.
+  "customizations": {
+    "vscode": {
+      "extensions": [
+        "dbaeumer.vscode-eslint",
+        "mhutchie.git-graph",
+        "eamodio.gitlens",
+        "github.vscode-pull-request-github",
+        "cschleiden.vscode-github-actions",
+        "cweijan.vscode-database-client2",
+        "mongodb.mongodb-vscode",
+        "msjsdiag.debugger-for-chrome",
+        "firefox-devtools.vscode-firefox-debug",
+        "editorconfig.editorconfig",
+        "shinnn.stylelint",
+        "stylelint.vscode-stylelint",
+        "vitest.explorer",
+        "ms-playwright.playwright"
+      ],
+    }
+  },
 
-  // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
-  "remoteUser": "node"
+  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+  // "remoteUser": "root"
 }

+ 17 - 0
.devcontainer/postCreateCommand.sh

@@ -0,0 +1,17 @@
+sudo chown -R vscode:vscode /workspace;
+
+# Instal additional packages
+sudo apt update
+sudo apt-get install -y --no-install-recommends \
+  iputils-ping net-tools dnsutils
+sudo apt-get clean -y
+
+# Setup pnpm
+SHELL=bash pnpm setup
+eval "$(cat /home/vscode/.bashrc)"
+
+# Install turbo
+pnpm install turbo --global
+
+# Install dependencies
+turbo run bootstrap

+ 0 - 2
.gitattributes

@@ -1,2 +0,0 @@
-*.gz filter=lfs diff=lfs merge=lfs -text
-*.woff2 filter=lfs diff=lfs merge=lfs -text

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

@@ -5,7 +5,6 @@ on:
     branches:
       - master
       - dev/7.*.x
-      - dev/6.*.x
     paths:
       - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
@@ -13,7 +12,7 @@ on:
       - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - apps/app/**
       - '!apps/app/docker/**'
@@ -26,7 +25,7 @@ on:
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - turbo.json
       - package.json
       - apps/app/**

+ 16 - 47
.github/workflows/ci-app.yml

@@ -12,7 +12,7 @@ on:
       - .eslint*
       - tsconfig.base.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - apps/app/**
       - '!apps/app/docker/**'
@@ -34,21 +34,12 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - uses: pnpm/action-setup@v4
+
       - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
-
-      - name: Cache/Restore node_modules
-        uses: actions/cache@v4
-        with:
-          path: |
-            **/node_modules
-            !**/node_modules/.cache/turbo
-          key: node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-          restore-keys: |
-            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
+          cache: 'pnpm'
 
       - name: Cache/Restore dist
         uses: actions/cache@v4
@@ -63,9 +54,8 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
-          yarn global add node-gyp
-          yarn --frozen-lockfile
+          pnpm add turbo --global
+          pnpm install --frozen-lockfile
 
       - name: Lint
         run: |
@@ -98,21 +88,12 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - uses: pnpm/action-setup@v4
+
       - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
-
-      - name: Cache/Restore node_modules
-        uses: actions/cache@v4
-        with:
-          path: |
-            **/node_modules
-            !**/node_modules/.cache/turbo
-          key: node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-          restore-keys: |
-            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
+          cache: 'pnpm'
 
       - name: Cache/Restore dist
         uses: actions/cache@v4
@@ -127,9 +108,8 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
-          yarn global add node-gyp
-          yarn --frozen-lockfile
+          pnpm add turbo --global
+          pnpm install --frozen-lockfile
 
       - name: Test
         run: |
@@ -172,22 +152,12 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - uses: pnpm/action-setup@v4
+
       - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
-
-      - name: Cache/Restore node_modules
-        uses: actions/cache@v4
-        with:
-          path: |
-            **/node_modules
-            !**/node_modules/.cache/turbo
-          key: node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-          restore-keys: |
-            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
-            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
+          cache: 'pnpm'
 
       - name: Cache/Restore dist
         uses: actions/cache@v4
@@ -202,9 +172,8 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
-          yarn global add node-gyp
-          yarn --frozen-lockfile
+          pnpm add turbo --global
+          pnpm install --frozen-lockfile
 
       - name: turbo run launch-dev:ci
         working-directory: ./apps/app

+ 23 - 57
.github/workflows/ci-slackbot-proxy.yml

@@ -12,7 +12,7 @@ on:
       - .eslint*
       - tsconfig.base.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - apps/slackbot-proxy/**
       - '!apps/slackbot-proxy/docker/**'
@@ -35,21 +35,12 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Cache/Restore node_modules
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/slackbot-proxy/package.json') }}
-        restore-keys: |
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+        cache: 'pnpm'
 
     - name: Restore dist
       uses: actions/cache/restore@v4
@@ -63,9 +54,8 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Lint
       run: |
@@ -110,21 +100,12 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Cache/Restore node_modules
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/slackbot-proxy/package.json') }}
-        restore-keys: |
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+        cache: 'pnpm'
 
     - name: Restore dist
       uses: actions/cache/restore@v4
@@ -138,11 +119,10 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
-    - name: yarn dev:ci
+    - name: turbo run dev:ci
       working-directory: ./apps/slackbot-proxy
       run: |
         cp config/ci/.env.local.for-ci .env.development.local
@@ -198,36 +178,20 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install turbo
       run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune @growi/slackbot-proxy
-        rm -rf apps packages
-        mv out/* .
-
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-slackbot-prxy-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-slackbot-proxy-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+        pnpm add turbo --global
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
     - name: Restore dist
       uses: actions/cache/restore@v4
@@ -244,15 +208,17 @@ jobs:
       run: |
         turbo run build
 
-    - name: Install dependencies for production
+    - name: Assembling all dependencies
       run: |
-        yarn --production
+        rm -rf out
+        pnpm deploy out --prod --filter @growi/slackbot-proxy
+        rm -rf apps/slackbot-proxy/node_modules && mv out/node_modules apps/slackbot-proxy/node_modules
 
-    - name: yarn start:prod:ci
+    - name: pnpm run start:prod:ci
       working-directory: ./apps/slackbot-proxy
       run: |
         cp config/ci/.env.local.for-ci .env.production.local
-        yarn start:prod:ci
+        pnpm run start:prod:ci
       env:
         SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql

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

@@ -93,21 +93,21 @@ jobs:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '18'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Bump versions for next RC
       run: |
-        turbo run version --filter=@growi/slackbot-proxy -- --prerelease
+        turbo run version:prerelease --filter=@growi/slackbot-proxy
 
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@2.0.1

+ 14 - 34
.github/workflows/release-subpackages.yml

@@ -28,27 +28,17 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Setup .npmrc
       run: |
@@ -61,14 +51,14 @@ jobs:
     - name: Retrieve changesets information
       id: changesets-status
       run: |
-        yarn changeset status --output status.json
+        pnpm changeset status --output status.json
         echo "CHANGESETS_LENGTH=$(jq -r '.changesets | length' status.json)" >> $GITHUB_OUTPUT
         rm status.json
 
     - name: Snapshot release to npm
       if: steps.changesets-status.outputs.CHANGESETS_LENGTH > 0
       run: |
-        yarn release-subpackages:snapshot
+        pnpm run release-subpackages:snapshot
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -81,35 +71,25 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Create Release Pull Request or Publish to npm
       id: changesets
       uses: changesets/action@v1
       with:
         title: Release Subpackages
-        version: yarn version-subpackages
-        publish: yarn release-subpackages
+        version: pnpm run version-subpackages
+        publish: pnpm run release-subpackages
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

+ 13 - 15
.github/workflows/release.yml

@@ -22,22 +22,21 @@ jobs:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Bump versions
       run: |
-        turbo run version --filter=@growi/app -- --patch
-        yarn upgrade --scope=@growi
+        turbo run version:patch --filter=@growi/app
         sh ./apps/app/bin/github-actions/update-readme.sh
 
     - name: Retrieve information from package.json
@@ -162,23 +161,22 @@ jobs:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Bump versions for next RC
       run: |
-        turbo run version --filter=@growi/app -- --prepatch
-        turbo run version --filter=@growi/slackbot-proxy -- --prepatch
-        yarn upgrade --scope=@growi
+        turbo run version:prepatch --filter=@growi/app
+        turbo run version:prepatch --filter=@growi/slackbot-proxy
 
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@2.0.1

+ 30 - 78
.github/workflows/reusable-app-prod.yml

@@ -22,40 +22,21 @@ jobs:
 
     steps:
     - uses: actions/checkout@v4
-      with:
-        # retrieve local font files
-        lfs: true
+
+    - uses: pnpm/action-setup@v4
 
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install turbo
       run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune @growi/app
-        rm -rf apps packages
-        mv out/* .
-
-    - name: Cache/Restore node_modules
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-          !**/node_modules/.cache/turbo
-        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+        pnpm add turbo --global
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
     - name: Cache/Restore dist
       uses: actions/cache@v4
@@ -76,10 +57,16 @@ jobs:
       env:
         ANALYZE: 1
 
+    - name: Assembling all dependencies
+      run: |
+        rm -rf out
+        pnpm deploy out --prod --filter @growi/app
+        rm -rf apps/app/node_modules && mv out/node_modules apps/app/node_modules
+
     - name: Archive production files
       id: archive-prod-files
       run: |
-        tar -zcf production.tar.gz \
+        tar -zcf production.tar.gz --exclude ./apps/app/.next/cache \
           package.json \
           apps/app/.next \
           apps/app/config \
@@ -88,9 +75,8 @@ jobs:
           apps/app/resource \
           apps/app/tmp \
           apps/app/.env.production* \
-          apps/app/package.json \
-          packages/*/dist \
-          packages/*/package.json
+          apps/app/node_modules \
+          apps/app/package.json
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
     - name: Upload production files as artifact
@@ -104,8 +90,7 @@ jobs:
       with:
         name: Bundle Analyzing Report (node${{ inputs.node-version }})
         path: |
-          apps/app/.next/analyze/client.html
-          apps/app/.next/analyze/server.html
+          apps/app/.next/analyze
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
@@ -137,50 +122,27 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Install turbo
-      run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune @growi/app
-        rm -rf apps packages
-        mv out/* .
-
-    - name: Restore node_modules
-      uses: actions/cache/restore@v4
-      with:
-        path: |
-          **/node_modules
-        # shared key with build-prod
-        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
-
-    - name: Install dependencies
-      run: |
-        yarn --production
+        cache: 'pnpm'
 
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       with:
         name: Production Files (node${{ inputs.node-version }})
 
-    - name: Extract procution files artifact
+    - name: Extract procution files
       run: |
         tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
 
-    - name: yarn server:ci
+    - name: pnpm run server:ci
       working-directory: ./apps/app
       run: |
         cp config/ci/.env.local.for-ci .env.production.local
-        yarn server:ci
+        pnpm run server:ci
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
@@ -228,37 +190,27 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Restore node_modules
-      uses: actions/cache/restore@v4
-      with:
-        path: |
-          **/node_modules
-        # saved key by build-prod
-        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
     - name: Install Playwright browsers
       run: |
-        yarn playwright install --with-deps ${{ matrix.browser }}
+        pnpm playwright install --with-deps ${{ matrix.browser }}
 
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       with:
         name: Production Files (node${{ inputs.node-version }})
 
-    - name: Extract procution files artifact
+    - name: Extract procution files
       run: |
         tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
 
@@ -271,7 +223,7 @@ jobs:
       if: ${{ matrix.browser == 'chromium' }}
       working-directory: ./apps/app
       run: |
-        yarn playwright test --project=chromium/installer
+        pnpm playwright test --project=chromium/installer
       env:
         HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
         MONGO_URI: mongodb://mongodb:27017/growi-playwright-installer
@@ -285,7 +237,7 @@ jobs:
     - name: Playwright Run
       working-directory: ./apps/app
       run: |
-        yarn playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
+        pnpm playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
       env:
         HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
         MONGO_URI: mongodb://mongodb:27017/growi-playwright
@@ -299,7 +251,7 @@ jobs:
     - name: Playwright Run (--project=${browser}/guest-mode)
       working-directory: ./apps/app
       run: |
-        yarn playwright test --project=${{ matrix.browser }}/guest-mode --shard=${{ matrix.shard }}
+        pnpm playwright test --project=${{ matrix.browser }}/guest-mode --shard=${{ matrix.shard }}
       env:
         HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
         MONGO_URI: mongodb://mongodb:27017/growi-playwright-guest-mode

+ 5 - 15
.github/workflows/reusable-app-reg-suit.yml

@@ -54,26 +54,16 @@ jobs:
         ref: ${{ inputs.checkout-ref }}
         fetch-depth: 0
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Restore node_modules
-      uses: actions/cache/restore@v4
-      with:
-        path: |
-          **/node_modules
-        # saved key by launch-prod
-        key: node_modules-app-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
     - name: Download screenshots taken by cypress
       uses: actions/download-artifact@v4
@@ -85,7 +75,7 @@ jobs:
     - name: Run reg-suit
       working-directory: ./apps/app
       run: |
-        yarn reg:run
+        pnpm run reg:run
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master

+ 3 - 0
.gitignore

@@ -4,6 +4,7 @@
 node_modules
 /.pnp
 .pnp.js
+.pnpm-store
 
 # testing
 coverage
@@ -40,4 +41,6 @@ yarn-error.log*
 
 # turborepo
 .turbo
+
+# pnpm deploy target dir
 out


+ 2 - 1
.vscode/launch.json

@@ -31,8 +31,9 @@
         "request": "launch",
         "name": "Debug: Server",
         "cwd": "${workspaceFolder}/apps/app",
-        "runtimeExecutable": "yarn",
+        "runtimeExecutable": "pnpm",
         "runtimeArgs": [
+          "run",
           "dev"
         ],
         "skipFiles": [

+ 63 - 1
CHANGELOG.md

@@ -1,9 +1,71 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.21...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.1.0...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.1.0](https://github.com/weseek/growi/compare/v7.0.23...v7.1.0) - 2024-10-31
+
+### BREAKING CHANGES
+
+* imprv: Update default value for S3\_OBJECT\_ACL (#9332) @yuki-takei
+
+### 💎 Features
+
+* feat: GROWI OpenAI Integration (#9246) @yuki-takei
+
+### 🚀 Improvement
+
+* imprv: Add GitHub Markdown alerts  (#9127) @reiji-h
+* imprv: Upgrade unified and remark-growi-directive (#9048) @reiji-h
+* imprv: ROM users can manage comments (#9101) @WNomunomu
+* imprv: Update default value for S3\_OBJECT\_ACL (#9332) @yuki-takei
+* imprv: Sandbox (#9330) @yuki-takei
+* support: JSDoc for OpenAPI document (#9311) @yuki-takei
+
+
+### 🐛 Bug Fixes
+
+* fix: Couldn't show old revision (#9296) @yuki-takei
+* fix: Replace the word ROM (#9295) @satof3
+* fix: forgot-password API (#9257) @reiji-h
+* fix: Edit button appear for the side of header (#9270) @yuki-takei
+* fix: Ensure text-only paste for mixed content from various sources (#9096) @reiji-h
+* fix: Notification count badge (#9124) @shironegi39
+* fix(ogp): Set an unknown label when the user is not found (#9232) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Migrate to pnpm from yarn v1 (#9249) @yuki-takei
+* support: Omit MongoDB 4.x compatible code (#9334) @yuki-takei
+* support: Pull LFS files with turbo (#9325) @yuki-takei
+* support: Use `pnpm deploy` instead of `turbo prune` (#9323) @yuki-takei
+* support: Maintenance API docs generation (#9302) @yuki-takei
+* support: Improve typings for PageService (#9220) @yuki-takei
+* support: Typescriptize accessTokenParser (#9320) @yuki-takei
+* support: Migrate to pnpm from yarn v1 (#9249) @yuki-takei
+* support: JSDoc for OpenAPI document (#9311) @yuki-takei
+* support: Maintenance API docs generation (#9302) @yuki-takei
+* support: Omit docs route (#9299) @yuki-takei
+
+## [v7.0.23](https://github.com/weseek/growi/compare/v7.0.22...v7.0.23) - 2024-10-24
+
+### 🐛 Bug Fixes
+
+* fix: Couln't show old revision (#9296) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Maintenance API docs generation (#9302) @yuki-takei
+* support: Omit docs route (#9299) @yuki-takei
+
+## [v7.0.22](https://github.com/weseek/growi/compare/v7.0.21...v7.0.22) - 2024-10-21
+
+### 🐛 Bug Fixes
+
+* fix: Edit button appear for the side of header (#9270) @yuki-takei
+* fix: Collaborative editing occurs unstable behavior (#9267) @yuki-takei
+
 ## [v7.0.21](https://github.com/weseek/growi/compare/v7.0.20...v7.0.21) - 2024-10-15
 
 ### 🚀 Improvement

+ 7 - 7
README.md

@@ -81,9 +81,9 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 - Node.js v18.x or v20.x
 - npm 6.x
-- yarn
+- pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 4.4 or above
+- MongoDB 6.0 or above
 
 ### Optional Dependencies
 
@@ -95,11 +95,11 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 ## Command details
 
-| command           | desc                                                    |
-| ------------------| ------------------------------------------------------- |
-| `yarn app:build`  | Build GROWI app client                                  |
-| `yarn app:server` | Launch GROWI app server                                 |
-| `yarn start`      | Invoke `yarn app:build` and `yarn app:server`           |
+| command               | desc                                                    |
+| --------------------- | ------------------------------------------------------- |
+| `npm run app:build`   | Build GROWI app client                                  |
+| `npm run app:server`  | Launch GROWI app server                                 |
+| `npm run start`       | Invoke `npm run app:build` and `npm run app:server`     |
 
 For more info, see [GROWI Docs: List of npm Scripts](https://docs.growi.org/en/dev/startup-v5/start-development.html#list-of-npm-scripts).
 

+ 7 - 7
README_JP.md

@@ -80,9 +80,9 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 
 - Node.js v18.x or v20.x
 - npm 6.x
-- yarn
+- pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 4.4 以上
+- MongoDB 6.0 以上
 
 ### オプションの依存関係
 
@@ -94,11 +94,11 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 
 ## コマンド詳細
 
-| コマンド          | 説明                                                    |
-| ------------------| ------------------------------------------------------- |
-| `yarn app:build`  | GROWI app クライアントをビルドします。                  |
-| `yarn app:server` | GROWI app サーバーを起動します。                        |
-| `yarn start`      | `yarn app:build` と `yarn app:server` を呼び出します。  |
+| コマンド              | 説明                                                            |
+| --------------------- | --------------------------------------------------------------- |
+| `npm run app:build`   | GROWI app クライアントをビルドします。                          |
+| `npm run app:server`  | GROWI app サーバーを起動します。                                |
+| `npm run start`       | `npm run app:build` と `npm run app:server` を呼び出します。    |
 
 詳しくは [GROWI Docs: npm スクリプトリスト](https://docs.growi.org/ja/dev/startup-v5/start-development.html#npm-%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%95%E3%82%9A%E3%83%88%E3%83%AA%E3%82%B9%E3%83%88)をご覧ください。
 

+ 28 - 0
apps/app/bin/swagger-jsdoc/definition-apiv1.js

@@ -0,0 +1,28 @@
+const pkg = require('../../package.json');
+
+module.exports = {
+  openapi: '3.0.1',
+  info: {
+    title: 'GROWI REST API v1',
+    version: pkg.version,
+  },
+  servers: [
+    {
+      url: 'https://demo.growi.org/_api',
+    },
+  ],
+  security: [
+    {
+      api_key: [],
+    },
+  ],
+  components: {
+    securitySchemes: {
+      api_key: {
+        type: 'apiKey',
+        name: 'access_token',
+        in: 'query',
+      },
+    },
+  },
+};

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

@@ -0,0 +1,93 @@
+const pkg = require('../../package.json');
+
+module.exports = {
+  openapi: '3.0.1',
+  info: {
+    title: 'GROWI REST API v3',
+    version: pkg.version,
+  },
+  servers: [
+    {
+      url: 'https://demo.growi.org/_api/v3',
+    },
+  ],
+  security: [
+    {
+      api_key: [],
+    },
+  ],
+  components: {
+    securitySchemes: {
+      api_key: {
+        type: 'apiKey',
+        name: 'access_token',
+        in: 'query',
+      },
+    },
+  },
+  'x-tagGroups': [
+    {
+      name: 'User API',
+      tags: [
+        'Attachment',
+        'Bookmarks',
+        'Page',
+        'Pages',
+        'Revisions',
+        'ShareLinks',
+        'Users',
+        '',
+        '',
+      ],
+    },
+    {
+      name: 'User Personal Settings API',
+      tags: [
+        'GeneralSetting',
+        'EditorSetting',
+        'InAppNotificationSettings',
+        '',
+        '',
+        '',
+        '',
+        '',
+      ],
+    },
+    {
+      name: 'System Management API',
+      tags: [
+        'Home',
+        'AppSettings',
+        'SecuritySetting',
+        'MarkDownSetting',
+        'CustomizeSetting',
+        'Import',
+        'Export',
+        'MongoDB',
+        'NotificationSetting',
+        'SlackIntegrationSettings',
+        'SlackIntegrationSettings (with proxy)',
+        'SlackIntegrationSettings (without proxy)',
+        'SlackIntegrationLegacySetting',
+        'ShareLink Management',
+        'UserGroupRelations',
+        'UserGroups',
+        'Users Management',
+        'FullTextSearch Management',
+      ],
+    },
+    {
+      name: 'Public API',
+      tags: [
+        'Healthcheck',
+        'Statistics',
+        '',
+        '',
+        '',
+        '',
+        '',
+        '',
+      ],
+    },
+  ],
+};

+ 15 - 0
apps/app/bin/swagger-jsdoc/generate-spec-apiv1.sh

@@ -0,0 +1,15 @@
+# USAGE:
+#   cd apps/app && sh bin/swagger-jsdoc/generate-spec-apiv1.sh
+#   APP_PATH=/path/to/apps/app sh bin/swagger-jsdoc/generate-spec-apiv1.sh
+#   APP_PATH=/path/to/apps/app OUT=/path/to/output sh bin/swagger-jsdoc/generate-spec-apiv1.sh
+
+APP_PATH=${APP_PATH:-"."}
+
+OUT=${OUT:-"${APP_PATH}/tmp/openapi-spec-apiv1.json"}
+
+swagger-jsdoc \
+  -o "${OUT}" \
+  -d "${APP_PATH}/bin/swagger-jsdoc/definition-apiv1.js" \
+  "${APP_PATH}/src/server/routes/*.{js,ts}" \
+  "${APP_PATH}/src/server/routes/attachment/**/*.{js,ts}" \
+  "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"

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

@@ -0,0 +1,14 @@
+# USAGE:
+#   cd apps/app && sh bin/swagger-jsdoc/generate-spec-apiv3.sh
+#   APP_PATH=/path/to/apps/app sh bin/swagger-jsdoc/generate-spec-apiv3.sh
+#   APP_PATH=/path/to/apps/app OUT=/path/to/output sh bin/swagger-jsdoc/generate-spec-apiv3.sh
+
+APP_PATH=${APP_PATH:-"."}
+
+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/server/routes/apiv3/**/*.{js,ts}" \
+  "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"

+ 0 - 37
apps/app/config/swagger-definition.js

@@ -1,37 +0,0 @@
-const pkg = require('../package.json');
-
-const apiVersion = process.env.API_VERSION || '3';
-const basePath = (apiVersion === '1' ? '/_api' : `/_api/v${apiVersion}`);
-
-module.exports = {
-  openapi: '3.0.1',
-  info: {
-    title: `GROWI REST API v${apiVersion}`,
-    version: pkg.version,
-  },
-  servers: [
-    {
-      url: 'https://demo.growi.org{basePath}',
-      variables: {
-        basePath: {
-          default: basePath,
-          description: 'base path',
-        },
-      },
-    },
-  ],
-  security: [
-    {
-      api_key: [],
-    },
-  ],
-  components: {
-    securitySchemes: {
-      api_key: {
-        type: 'apiKey',
-        name: 'access_token',
-        in: 'query',
-      },
-    },
-  },
-};

+ 28 - 76
apps/app/docker/Dockerfile

@@ -1,4 +1,4 @@
-# syntax = docker/dockerfile:1.4
+# syntax = docker/dockerfile:1
 
 
 ##
@@ -6,89 +6,46 @@
 ##
 FROM node:20-slim AS base
 
-ENV optDir /opt
+ENV optDir=/opt
 
 WORKDIR ${optDir}
 
-RUN yarn global add turbo
-COPY . .
-RUN turbo prune @growi/app --docker
-
-
-##
-## deps-resolver
-##
-FROM node:20-slim AS deps-resolver
-
-ENV optDir /opt
+# install tools
+RUN apt-get update && apt-get install -y ca-certificates wget curl --no-install-recommends
 
-WORKDIR ${optDir}
-
-RUN set -eux; \
-	apt-get update; \
-	apt-get install -y python3 build-essential;
-
-# copy files
-COPY --from=base ${optDir}/out/json/ .
-COPY --from=base ${optDir}/out/yarn.lock ./yarn.lock
+# install pnpm
+RUN wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh -
+ENV PNPM_HOME="/root/.local/share/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
 
-# setup (with network-timeout = 1 hour)
-RUN yarn config set network-timeout 3600000
-RUN yarn global add node-gyp
-RUN yarn --frozen-lockfile
-
-# make artifacts
-RUN tar -cf node_modules.tar \
-  node_modules \
-  apps/*/node_modules \
-  packages/*/node_modules
-
-
-
-##
-## deps-resolver-prod
-##
-FROM deps-resolver AS deps-resolver-prod
-
-RUN yarn --production
-# make artifacts
-RUN tar -cf node_modules.tar \
-  node_modules \
-  apps/*/node_modules \
-  packages/*/node_modules
+# install turbo
+RUN pnpm add turbo --global
 
 
 
 ##
 ## builder
 ##
-FROM node:20-slim AS builder
+FROM base AS builder
 
-ENV optDir /opt
+ENV optDir=/opt
 
 WORKDIR ${optDir}
 
-RUN yarn global add turbo
-
-# copy files
-COPY --from=base ${optDir}/out/full/ .
-COPY --from=base ${optDir}/out/yarn.lock ./yarn.lock
-COPY ["tsconfig.base.json", "./"]
-
-# copy dependent packages
-COPY --from=deps-resolver \
-  ${optDir}/node_modules.tar ${optDir}/
+COPY . .
 
-# extract node_modules.tar
-RUN tar -xf node_modules.tar
-RUN rm node_modules.tar
+RUN pnpm add node-gyp --global
+RUN pnpm install ---frozen-lockfile
 
 # build
 RUN turbo run clean
-RUN turbo run build
+RUN turbo run build --filter @growi/app
 
 # make artifacts
-RUN tar -cf packages.tar \
+RUN pnpm deploy out --prod --filter @growi/app
+RUN rm -rf apps/app/node_modules && mv out/node_modules apps/app/node_modules
+RUN rm -rf apps/app/.next/cache
+RUN tar -zcf packages.tar.gz \
   package.json \
   apps/app/.next \
   apps/app/config \
@@ -99,8 +56,7 @@ RUN tar -cf packages.tar \
   apps/app/.env.production* \
   apps/app/next.config.js \
   apps/app/package.json \
-  packages/*/package.json \
-  packages/*/dist
+  apps/app/node_modules
 
 
 
@@ -108,12 +64,12 @@ RUN tar -cf packages.tar \
 ## release
 ##
 FROM node:20-slim
-LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+LABEL maintainer="Yuki Takei <yuki@weseek.co.jp>"
 
-ENV NODE_ENV production
+ENV NODE_ENV="production"
 
-ENV optDir /opt
-ENV appDir ${optDir}/growi
+ENV optDir=/opt
+ENV appDir=${optDir}/growi
 
 # Add gosu
 # see: https://github.com/tianon/gosu/blob/1.13/INSTALL.md
@@ -124,17 +80,13 @@ RUN set -eux; \
 # verify that the binary works
 	gosu nobody true
 
-COPY --from=deps-resolver-prod --chown=node:node \
-  ${optDir}/node_modules.tar ${appDir}/
 COPY --from=builder --chown=node:node \
-  ${optDir}/packages.tar ${appDir}/
+  ${optDir}/packages.tar.gz ${appDir}/
 
 # extract artifacts as 'node' user
 USER node
 WORKDIR ${appDir}
-RUN tar -xf node_modules.tar
-RUN tar -xf packages.tar
-RUN rm node_modules.tar packages.tar
+RUN tar -zxf packages.tar.gz && rm packages.tar.gz
 
 COPY --chown=node:node --chmod=700 apps/app/docker/docker-entrypoint.sh /
 
@@ -145,4 +97,4 @@ VOLUME /data
 EXPOSE 3000
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
-CMD ["yarn migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"]
+CMD ["npm run migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"]

+ 1 - 0
apps/app/docker/Dockerfile.dockerignore

@@ -2,6 +2,7 @@
 **/coverage
 **/Dockerfile
 **/*.dockerignore
+**/.pnpm-store
 **/.next
 **/.turbo
 out

+ 3 - 4
apps/app/docker/README.md

@@ -10,10 +10,9 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`7.0.21`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.21/apps/app/docker/Dockerfile)
+* [`7.1.0`, `7.1`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.1.0/apps/app/docker/Dockerfile)
+* [`7.0.23`, `7.0` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.23/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
-* [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
-* [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 
 
 What is GROWI?
@@ -27,7 +26,7 @@ see: [weseek/growi](https://github.com/weseek/growi)
 Requirements
 -------------
 
-* MongoDB (>= 4.4)
+* MongoDB (>= 6.0)
 
 ### Optional Dependencies
 

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

@@ -10,11 +10,6 @@ env:
 phases:
   pre_build:
     commands:
-      # install Git LFS
-      - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | bash
-      - yum install -y git-lfs
-      # fetch LFS files
-      - git-lfs pull
       # login to docker.io
       - echo ${DOCKER_REGISTRY_PASSWORD} | docker login --username wsmoogle --password-stdin
   build:
@@ -27,6 +22,4 @@ phases:
 
 cache:
   paths:
-    - node_modules/**/*
-    - apps/*/node_modules/**/*
-    - packages/*/node_modules/**/*
+    - .pnpm-store/**/*

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

@@ -73,7 +73,6 @@ const getTranspilePackages = () => {
 const optimizePackageImports = [
   '@growi/core',
   '@growi/editor',
-  '@growi/markdown-splitter',
   '@growi/pluginkit',
   '@growi/presentation',
   '@growi/preset-themes',
@@ -160,7 +159,11 @@ module.exports = async(phase, { defaultConfig }) => {
   }
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
-    enabled: phase === PHASE_PRODUCTION_BUILD && process.env.ANALYZE === 'true',
+    enabled: phase === PHASE_PRODUCTION_BUILD
+      && (
+        process.env.ANALYZE === 'true'
+          || process.env.ANALYZE === '1'
+      ),
   });
 
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 4 - 0
apps/app/nodemon.json

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

+ 84 - 56
apps/app/package.json

@@ -1,38 +1,40 @@
 {
   "name": "@growi/app",
-  "version": "7.1.0-RC.0",
+  "version": "7.1.1-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
     "//// for production": "",
     "build": "run-p build:*",
-    "start": "yarn next start",
-    "build:client": "yarn next build",
-    "build:server": "yarn cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
+    "start": "next start",
+    "build:client": "next build",
+    "build:server": "cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
     "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
     "clean": "shx rm -rf dist transpiled",
-    "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
-    "server:ci": "yarn server --ci",
-    "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
-    "styles-prebuilt": "vite build -c vite.styles-prebuilt.config.ts",
-    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up -f config/migrate-mongo-config.js",
+    "server": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
+    "server:ci": "pnpm run server --ci",
+    "preserver": "cross-env NODE_ENV=production pnpm run migrate",
+    "pre:styles": "vite build -c vite.styles-prebuilt.config.ts",
+    "migrate": "node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js",
     "//// for development": "",
-    "dev": "yarn cross-env NODE_ENV=development nodemon --exec yarn ts-node --inspect src/server/app.ts",
-    "dev:styles-prebuilt": "yarn styles-prebuilt --mode dev",
-    "dev:migrate-mongo": "yarn cross-env NODE_ENV=development yarn ts-node node_modules/.bin/migrate-mongo",
-    "dev:migrate": "yarn dev:migrate:status > tmp/cache/migration-status.out && yarn dev:migrate:up",
-    "dev:migrate:create": "yarn dev:migrate-mongo create -f config/migrate-mongo-config.js",
-    "dev:migrate:status": "yarn dev:migrate-mongo status -f config/migrate-mongo-config.js",
-    "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
-    "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
+    "dev": "cross-env NODE_ENV=development nodemon --exec pnpm run ts-node --inspect src/server/app.ts",
+    "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": "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: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",
     "//// for CI": "",
-    "launch-dev:ci": "yarn cross-env NODE_ENV=development yarn dev:migrate && yarn ts-node src/server/app.ts --ci",
+    "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "lint:typecheck": "npx -y tspc",
-    "lint:eslint": "yarn eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
+    "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
-    "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
+    "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:*",
-    "prelint:swagger2openapi": "yarn openapi:v3",
+    "prelint:swagger2openapi:apiv3": "pnpm run swagger2openapi:apiv3",
+    "prelint:swagger2openapi:apiv1": "pnpm run swagger2openapi:apiv1",
     "test": "run-p test:*",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
     "test:vitest": "vitest run --coverage",
@@ -40,20 +42,22 @@
     "reg:run": "reg-suit run",
     "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
-    "console": "yarn repl",
-    "repl": "yarn cross-env NODE_ENV=development yarn ts-node src/server/repl.ts",
-    "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
-    "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
-    "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
+    "console": "npm run repl",
+    "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
+    "swagger2openapi:apiv3": "sh bin/swagger-jsdoc/generate-spec-apiv3.sh",
+    "swagger2openapi:apiv1": "sh bin/swagger-jsdoc/generate-spec-apiv1.sh",
     "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config",
-    "version": "yarn version --no-git-tag-version --non-interactive --preid=RC"
+    "version:patch": "pnpm version patch",
+    "version:prerelease": "pnpm version prerelease --preid=RC",
+    "version:prepatch": "pnpm version prepatch --preid=RC",
+    "version:preminor": "pnpm version preminor --preid=RC",
+    "version:premajor": "pnpm version premajor --preid=RC"
   },
   "// comments for dependencies": {
     "@aws-skd/*": "fix version above 3.186.0 that is required by mongodb@4.16.0",
     "@keycloak/keycloak-admin-client": "19.0.0 or above exports only ESM.",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "next-themes": "0.3.0 causes a type error: https://github.com/pacocoursey/next-themes/issues/122",
-    "remark-wiki-link": "!!DO NOT REMOVE!! including 'mdast-util-wiki-link' and 'micromark-extension-wiki-link' required by pukiwiki-like-linker",
     "string-width": "5.0.0 or above exports only ESM."
   },
   "dependencies": {
@@ -64,26 +68,25 @@
     "@azure/openai": "^2.0.0-beta.2",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
+    "@cspell/dynamic-import": "^8.15.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/core": "link:../../packages/core",
-    "@growi/pluginkit": "link:../../packages/pluginkit",
-    "@growi/presentation": "link:../../packages/presentation",
-    "@growi/preset-templates": "link:../../packages/preset-templates",
-    "@growi/preset-themes": "link:../../packages/preset-themes",
-    "@growi/remark-attachment-refs": "link:../../packages/remark-attachment-refs",
-    "@growi/remark-drawio": "link:../../packages/remark-drawio",
-    "@growi/remark-growi-directive": "link:../../packages/remark-growi-directive",
-    "@growi/remark-lsx": "link:../../packages/remark-lsx",
-    "@growi/slack": "link:../../packages/slack",
+    "@growi/core": "workspace:^",
+    "@growi/pluginkit": "workspace:^",
+    "@growi/presentation": "workspace:^",
+    "@growi/preset-templates": "workspace:^",
+    "@growi/preset-themes": "workspace:^",
+    "@growi/remark-attachment-refs": "workspace:^",
+    "@growi/remark-drawio": "workspace:^",
+    "@growi/remark-growi-directive": "workspace:^",
+    "@growi/remark-lsx": "workspace:^",
+    "@growi/slack": "workspace:^",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@types/async": "^3.2.24",
-    "@types/jest": "^29.5.2",
-    "@types/ldapjs": "^2.2.5",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
@@ -94,12 +97,13 @@
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
-    "check-node-version": "^4.1.0",
+    "check-node-version": "^4.2.1",
     "compression": "^1.7.4",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^4.6.0",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
+    "cross-env": "^7.0.0",
     "csurf": "^1.11.0",
     "csv-to-markdown-table": "^1.4.1",
     "date-fns": "^3.6.0",
@@ -107,6 +111,7 @@
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
+    "dotenv-flow": "^3.2.0",
     "ejs": "^3.1.10",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "^4.0.0",
@@ -120,6 +125,7 @@
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
+    "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
@@ -127,14 +133,23 @@
     "i18next-resources-to-backend": "^1.2.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
+    "js-tiktoken": "^1.0.15",
+    "js-yaml": "^4.1.0",
+    "katex": "^0.16.11",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
+    "mdast-util-from-markdown": "^2.0.1",
+    "mdast-util-gfm-table": "^2.0.0",
+    "mdast-util-wiki-link": "^0.1.2",
     "mermaid": "^11.2.0",
     "method-override": "^3.0.0",
+    "micromark-extension-gfm-table": "^2.1.0",
+    "micromark-extension-wiki-link": "^0.0.4",
     "migrate-mongo": "^11.0.0",
     "mkdirp": "^1.0.3",
+    "mongodb": "^4.17.2",
     "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
@@ -160,6 +175,7 @@
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
+    "prop-types": "^15.8.1",
     "qs": "^6.11.1",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
@@ -180,27 +196,34 @@
     "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
-    "rehype-katex": "^7.0.0",
+    "rehype-katex": "^7.0.1",
     "rehype-raw": "^7.0.0",
     "rehype-sanitize": "^6.0.0",
     "rehype-slug": "^6.0.0",
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^4.0.0",
-    "remark-emoji": "^5.0.0",
+    "remark-directive": "^3.0.0",
     "remark-frontmatter": "^5.0.0",
     "remark-gfm": "^4.0.0",
     "remark-math": "^6.0.0",
+    "remark-parse": "^11.0.0",
+    "remark-rehype": "^11.1.1",
+    "remark-stringify": "^11.0.0",
     "remark-toc": "^9.0.0",
-    "remark-wiki-link": "^2.0.1",
     "sanitize-filename": "^1.6.3",
     "socket.io": "^4.7.5",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
-    "swagger-jsdoc": "^6.1.0",
+    "swagger-jsdoc": "^6.2.8",
     "swr": "^2.2.2",
     "throttle-debounce": "^5.0.0",
+    "ts-deepmerge": "^6.2.0",
+    "tslib": "^2.8.0",
     "uglifycss": "^0.0.29",
+    "uid-safe": "^2.1.5",
+    "unified": "^11.0.0",
+    "unist-util-visit": "^5.0.0",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
     "unzip-stream": "^0.3.2",
@@ -220,11 +243,11 @@
     "mongodb": "mongoose which is used requires mongo@4.16.0."
   },
   "devDependencies": {
-    "@growi/core-styles": "link:../../packages/core-styles",
-    "@growi/custom-icons": "link:../../packages/custom-icons",
-    "@growi/editor": "link:../../packages/editor",
-    "@growi/markdown-splitter": "link:../../packages/markdown-splitter",
-    "@growi/ui": "link:../../packages/ui",
+    "@emoji-mart/data": "^1.2.1",
+    "@growi/core-styles": "workspace:^",
+    "@growi/custom-icons": "workspace:^",
+    "@growi/editor": "workspace:^",
+    "@growi/ui": "workspace:^",
     "@handsontable/react": "=2.1.0",
     "@next/bundle-analyzer": "^14.1.3",
     "@popperjs/core": "^2.11.8",
@@ -236,13 +259,19 @@
     "@testing-library/user-event": "^14.5.2",
     "@types/archiver": "^6.0.2",
     "@types/express": "^4.17.21",
+    "@types/hast": "^3.0.4",
     "@types/jest": "^29.5.2",
-    "@types/node-cron": "^3.0.2",
+    "@types/ldapjs": "^2.2.5",
+    "@types/mdast": "^4.0.4",
+    "@types/node-cron": "^3.0.11",
+    "@types/react": "^18.2.14",
+    "@types/react-dom": "^18.2.6",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
     "@types/testing-library__dom": "^7.5.0",
     "@types/throttle-debounce": "^5.0.1",
+    "@types/unist": "^3.0.3",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
     "babel-loader": "^8.2.5",
@@ -265,24 +294,23 @@
     "jest-localstorage-mock": "^2.4.14",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
-    "mongodb": "4.16.0",
+    "mdast-util-directive": "^3.0.0",
+    "mdast-util-find-and-replace": "^3.0.1",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
-    "plantuml-encoder": "^1.2.5",
     "pretty-bytes": "^6.1.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dnd": "^14.0.5",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dropzone": "^14.2.3",
+    "react-hook-form": "^7.45.4",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "react-toastify": "^9.1.3",
-    "remark-github-admonitions-to-directives": "^2.0.0",
     "rehype-rewrite": "^4.0.2",
-    "replacestream": "^4.0.3",
+    "remark-github-admonitions-to-directives": "^2.0.0",
     "sass": "^1.53.0",
-    "simple-load-script": "^1.0.2",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",

+ 1 - 1
apps/app/playwright.config.ts

@@ -51,7 +51,7 @@ export default defineConfig({
   reporter: process.env.CI ? 'github' : 'list',
 
   webServer: {
-    command: 'yarn server',
+    command: 'pnpm run server',
     url: 'http://localhost:3000',
     reuseExistingServer: !process.env.CI,
     stdout: 'ignore',

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

@@ -8,7 +8,7 @@ export const login = async(page: Page): Promise<void> => {
   // Perform authentication steps. Replace these actions with your own.
   await page.goto('/admin');
 
-  const loginForm = await page.$('form#login-form');
+  const loginForm = await page.getByRole('form');
 
   if (loginForm != null) {
     await page.getByLabel('Username or E-mail').fill('admin');

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

@@ -15,7 +15,7 @@
     "scope_of_page_disclosure": "Scope of page disclosure",
     "set_point": "Set point",
     "Guest Users Access": "Guest users access",
-    "readonly_users_access": "ROM users' access",
+    "readonly_users_access": "Read only users' access",
     "always_hidden": "Always hidden",
     "always_displayed": "Always displayed",
     "displayed_or_hidden": "Hidden / Displayed",
@@ -87,9 +87,9 @@
       "deny": "Deny (Registered users only)",
       "readonly": "Accept (Guests can read only)"
     },
-    "rom_users_comment": {
-      "deny": "Deny (Prohibit ROM users from comment management)",
-      "accept": "Allow (ROM users can manage comments)"
+    "read_only_users_comment": {
+      "deny": "Deny (Prohibit reead only users from comment management)",
+      "accept": "Allow (Read only users can manage comments)"
     },
     "registration_mode": {
       "open": "Open (Anyone can register)",

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

@@ -491,7 +491,12 @@
     "placeholder": "Ask me anything.",
     "caution_against_hallucination": "Please verify the information and check the sources.",
     "progress_label": "Generating answers",
-    "failed_to_create_or_retrieve_thread": "Failed to create or retrieve thread"
+    "failed_to_create_or_retrieve_thread": "Failed to create or retrieve thread",
+    "budget_exceeded": "You have reached your usage limit for OpenAI's API. To use the Knowledge Assistant again, please add credits from the OpenAI billing page.",
+    "budget_exceeded_for_growi_cloud": "You have reached your OpenAI API usage limit. To use the Knowledge Assistant again, please add credits from the GROWI.cloud admin page for Hosted users or from the OpenAI billing page for Owned users.",
+    "error_message": "An error has occurred",
+    "show_error_detail": "Show error details"
+
   },
   "link_edit": {
     "edit_link": "Edit Link",

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

@@ -15,7 +15,7 @@
     "scope_of_page_disclosure": "Confidentialité de la page",
     "set_point": "Valeur",
     "Guest Users Access": "Accès invité",
-    "readonly_users_access": "Accès des utilisateurs ROM",
+    "readonly_users_access": "Accès des utilisateurs lecture seule",
     "always_hidden": "Toujours caché",
     "always_displayed": "Toujours affiché",
     "displayed_or_hidden": "Caché / Affiché",
@@ -87,9 +87,9 @@
       "deny": "Refuser (Utilisateurs inscrits seulement)",
       "readonly": "Autoriser (Lecture seule)"
     },
-    "rom_users_comment": {
-      "deny": "Refuser (Interdire la gestion des commentaires aux utilisateurs ROM)",
-      "accept": "Autoriser (Les utilisateurs ROM peuvent gérer les commentaires)"
+    "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)"
     },
     "registration_mode": {
       "open": "Ouvert (Tout le monde peut s'inscrire)",

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

@@ -485,7 +485,11 @@
     "placeholder": "Demandez-moi n'importe quoi.",
     "caution_against_hallucination": "Veuillez vérifier les informations et consulter les sources.",
     "progress_label": "Génération des réponses",
-    "failed_to_create_or_retrieve_thread": "Échec de la création ou de la récupération du fil de discussion"
+    "failed_to_create_or_retrieve_thread": "Échec de la création ou de la récupération du fil de discussion",
+    "budget_exceeded": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page de facturation d'OpenAI.",
+    "budget_exceeded_for_growi_cloud": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page d'administration de GROWI.cloud pour les utilisateurs hébergés ou à partir de la page de facturation de l'OpenAI pour les utilisateurs propriétaires.",
+    "error_message": "Erreur",
+    "show_error_detail": "Détails de l'exposition"
   },
   "link_edit": {
     "edit_link": "Modifier lien",

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

@@ -24,7 +24,7 @@
     "scope_of_page_disclosure": "ページの公開範囲",
     "set_point": "設定値",
     "Guest Users Access":"ゲストユーザーのアクセス",
-    "readonly_users_access": "ROMユーザーのアクセス",
+    "readonly_users_access": "閲覧のみユーザーのアクセス",
     "always_hidden": "非表示 (固定)",
     "always_displayed": "表示 (固定)",
     "displayed_or_hidden": "非表示 / 表示",
@@ -96,9 +96,9 @@
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
     },
-    "rom_users_comment": {
-      "deny": "拒否 (ROMユーザーのコメント操作を禁止)",
-      "accept": "許可 (ROMユーザーもコメント操作可能)"
+    "read_only_users_comment": {
+      "deny": "拒否 (閲覧のみユーザーのコメント操作を禁止)",
+      "accept": "許可 (閲覧のみユーザーもコメント操作可能)"
     },
     "registration_mode": {
       "open": "公開 (だれでも登録可能)",

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

@@ -524,7 +524,11 @@
     "placeholder": "ききたいことを入力してください",
     "caution_against_hallucination": "情報が正しいか出典を確認しましょう",
     "progress_label": "回答を生成しています",
-    "failed_to_create_or_retrieve_thread": "スレッドの作成または取得に失敗しました"
+    "failed_to_create_or_retrieve_thread": "スレッドの作成または取得に失敗しました",
+    "budget_exceeded": "OpenAI の API の利用上限に達しました。ナレッジアシスタントを再度利用するには OpenAI の請求ページからクレジットを追加してください。",
+    "budget_exceeded_for_growi_cloud": "OpenAI の API の利用上限に達しました。ナレッジアシスタントを再度利用するには Hosted の場合は GROWI.cloud の管理画面から Owned の場合は OpenAI の請求ページからクレジットを追加してください。",
+    "error_message": "エラーが発生しました",
+    "show_error_detail": "詳細を表示"
   },
   "link_edit": {
     "edit_link": "リンク編集",

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

@@ -27,7 +27,7 @@
     "always_hidden": "总是隐藏",
     "displayed_or_hidden": "隐藏 / 显示",
     "Guest Users Access": "来宾用户访问",
-    "readonly_users_access": "ROM用户的访问",
+    "readonly_users_access": "只浏览用户的访问",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"register_limitation": "注册限制",
 		"register_limitation_desc": "限制新用户注册",
@@ -96,9 +96,9 @@
 			"deny": "拒绝(仅限注册用户)",
 			"readonly": "接受(来宾可以只读)"
 		},
-    "rom_users_comment": {
-      "deny": "拒绝 (禁止ROM用户操作评论)",
-      "accept": "允许 (ROM用户可以管理评论)"
+    "read_only_users_comment": {
+      "deny": "拒绝 (禁止只浏览用户操作评论)",
+      "accept": "允许 (只浏览用户可以管理评论)"
     },
 		"registration_mode": {
 			"open": "打开(任何人都可以注册)",

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

@@ -480,7 +480,11 @@
     "placeholder": "问我任何问题。",
     "caution_against_hallucination": "请核实信息并检查来源。",
     "progress_label": "生成答案中",
-    "failed_to_create_or_retrieve_thread": "创建或获取线程失败"
+    "failed_to_create_or_retrieve_thread": "创建或获取线程失败",
+    "budget_exceeded": "您已达到 OpenAI API 的使用上限。要再次使用知识助手,请从 OpenAI 账单页面添加点数。",
+    "budget_exceeded_for_growi_cloud": "您已达到 OpenAI API 使用上限。如需再次使用知识助手,请从GROWI.cloud管理页面为托管用户添加点数,或从OpenAI计费页面为自有用户添加点数。",
+    "error_message": "错误",
+    "show_error_detail": "显示详情"
   },
   "link_edit": {
     "edit_link": "Edit Link",

BIN
apps/app/resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2


BIN
apps/app/resource/fonts/PressStart2P-latin.woff2


BIN
apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2


BIN
apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2


+ 3 - 3
apps/app/resource/locales/en_US/sandbox-diagrams.md

@@ -1,4 +1,4 @@
-# :pencil: diagrams.net(Draw.io)
+# :pencil2: diagrams.net(Draw.io)
 
 See [diagrams.net](https://diagrams.net)
 
@@ -23,7 +23,7 @@ See [diagrams.net](https://diagrams.net)
 
 
 
-# :pencil: PlantUML
+# :pencil2: PlantUML
 
 See [PlantUML](http://plantuml.com/).
 
@@ -151,7 +151,7 @@ State3 --> [*] : Aborted
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 
 ## Pie graph
 

+ 1 - 1
apps/app/resource/locales/en_US/sandbox-math.md

@@ -1,4 +1,4 @@
-# :pencil: Math
+# :pencil2: Math
 
 See [KaTeX](https://katex.org/).
 

+ 188 - 26
apps/app/resource/locales/en_US/sandbox.md

@@ -1,18 +1,17 @@
 # What is Sandbox?
-- In this page, you will find tips that help you to master GROWI 
-- Feel free to enrich the content of your pages with the references under this hierarchy
+- On this page, you will find tips that help you to master GROWI 
+- Feel free to enrich the content of your pages with the references under this page hierarchy
 
 
-# :closed_book:Headings & Paragraphs
+# :closed_book: Headings & Paragraphs
 - By inserting headings and paragraphs, you can make the text on the page easier to read
 
 ## Headers
 - Add `#` before the heading text to create a heading 
     - Depending on the number of `#`, the typeface size of headings would be different shown in the View screen 
-    - Check the View screen on the right side to understand the effect of headings
 - The number of `#` will decide the hierarchy level and help you to organize the contents
 
-```
+```markdown
 # First-level heading
 ## Second-level heading
 ### Third-level heading
@@ -26,26 +25,43 @@
     - You can also change this in the Setting to break the line without half-width spaces
         - Change the line break setting in the `Markdown Settings` sector of the admin page
 
-#### Without line break
+#### Example: Without line break
 Paragraph 1
 Paragraph 2
 
-#### With line break
+#### Example: With line break
 Paragraph 1  
 Paragraph 2
 
 ## Block
-- Paragraphs can be created by inserting a blank table in the text
+- Paragraphs can be created by inserting a blank line in the text
 - Passage can be broken into sentences and make them easier to read
 
-#### Without paragraph
-Paragraph 1  
-Paragraph 2
+#### Example: Without paragraph
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 
-#### With paragraph
-Paragraph 1  
+#### Example: With paragraph
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
 
-Paragraph 2
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+## Horizontal lines
+- Insert the horizontal line with three or more consecutive asterisks `*` or underscores `_`
+
+#### Example
+Below is a horizontal line
+***
+
+Below is a horizontal line
+___
+
+```markdown
+Below is a horizontal line
+***
+
+Below is a horizontal line
+___
+```
 
 
 # :green_book: Styling Text
@@ -59,6 +75,11 @@ Paragraph 2
 - This sentence indicates emphasis with *Italic*
 - This sentence indicates emphasis with _Italic_ 
 
+```markdown
+- This sentence indicates emphasis with *Italic*
+- This sentence indicates emphasis with _Italic_ 
+```
+
 ## Bold
 - Enclose the text with two asterisks `*` or two underscores `_`
 
@@ -66,6 +87,11 @@ Paragraph 2
 - This sentence indicates emphasis with **Bold** 
 - This sentence indicates emphasis with __Bold__
 
+```markdown
+- This sentence indicates emphasis with **Bold** 
+- This sentence indicates emphasis with __Bold__
+```
+
 ## Italic & Bold
 - Enclose the text with three asterisks `*` or three underscores `_`
 
@@ -73,6 +99,10 @@ Paragraph 2
 - This sentence indicates emphasis with ***Italic & Bold***
 - This sentence indicates emphasis witH ___Italic & Bold___
 
+```markdown
+- This sentence indicates emphasis with ***Italic & Bold***
+- This sentence indicates emphasis witH ___Italic & Bold___
+```
 
 # :orange_book: Insert Lists
 ## Bulleted List
@@ -88,6 +118,8 @@ Paragraph 2
 
 ## Numbered List
 - `Number.` at the beginning of a line to insert a numbered list
+    - Numbers are automatically assigned
+
 - Numbered list and bulleted list can also be combined for use
 
 #### Example
@@ -110,7 +142,46 @@ Paragraph 2
 - [x] Task 2
 
 
-# :blue_book: Others
+# :blue_book: Link
+
+## Auto link
+Just write the URL and the link will be generated automatically.
+
+### Example
+
+https://www.google.co.jp
+
+```markdown
+https://www.google.co.jp
+```
+
+## Label and link
+Insert a link by writing `[label](URL)`
+
+### Example
+- [Google](https://www.google.co.jp/)
+- [Sandbox is here](/Sandbox)
+
+```markdown
+- [Google](https://www.google.co.jp/)
+- [Sandbox is here](/Sandbox)
+```
+
+## Flexible link syntax
+
+Flexible link syntax make it easy to write a link by page path, a relative page link and link label and URL.
+
+- [[/Sandbox]]
+- [[./Math]]
+- [[How to write formulas?>./Math]]
+
+```markdown
+- [[/Sandbox]]
+- [[./Math]]
+- [[How to write formulas?>./Math]]
+```
+
+# :notebook: Others
 ## Blockquotes
 - Use quoted expressions by putting `>` at the beginning of the paragraph
     - Multiple quotations can be expressed by using a sequence of `>` characters
@@ -121,16 +192,34 @@ Paragraph 2
 > - Quotation
 >> Multiple quotations need to insert more `>`
 
+```markdown
+> - Quotation
+> - Quotation
+>> Multiple quotations need to insert more `>`
+```
+
 ## Code
 - It is possible to express the code by adding it in three `` ` ``
 
 #### Example
-```
+
+```markdown
 Add codes here  
-Line breaks and paragraphs can be reflected in the code
 
-- List also can be used in code
-    - List also can be used in code
+Line breaks and paragraphs can be reflected in the code as-is
+```
+
+#### Example (source code)
+
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
 ```
 
 ## Inline Code
@@ -139,20 +228,93 @@ Line breaks and paragraphs can be reflected in the code
 #### Example
 Here is the `inline code` 
 
-## Horizontal lines
-- Insert the horizontal line with three or more consecutive asterisks `*` or underscores `_`
+
+## Table
+
+### General syntax
 
 #### Example
-Below is a horizontal line
-***
 
-Below is a horizontal line
-___
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+```markdown
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+```
+
+### CSV / TSV
+
+#### Example
+
+``` tsv
+Content Cell	Content Cell
+Content Cell	Content Cell
+```
+
+~~~
+``` csv
+Content Cell,Content Cell
+Content Cell,Content Cell
+```
+~~~
+
+~~~
+``` tsv
+Content Cell	Content Cell
+Content Cell	Content Cell
+```
+~~~
+
+
+### CSV / TSV (with header)
+
+
+#### Example
+
+``` tsv-h
+First Header	Second Header
+Content Cell	Content Cell
+Content Cell	Content Cell
+```
+
+~~~
+``` csv-h
+First Header,Second Header
+Content Cell,Content Cell
+Content Cell,Content Cell
+```
+~~~
+
+~~~
+``` tsv-h
+First Header	Second Header
+Content Cell	Content Cell
+Content Cell	Content Cell
+```
+~~~
 
 
 # :ledger: More Applications
-- [Bootstrap5](/Sandbox/Bootstrap5)
+- [Bootstrap](/Sandbox/Bootstrap)
 
 - [Diagrams](/Sandbox/Diagrams)
 
 - [Math](/Sandbox/Math)
+
+
+
+
+

+ 3 - 3
apps/app/resource/locales/fr_FR/sandbox-diagrams.md

@@ -1,4 +1,4 @@
-# :pencil: diagrams.net(Draw.io)
+# :pencil2: diagrams.net(Draw.io)
 
 See [diagrams.net](https://diagrams.net)
 
@@ -23,7 +23,7 @@ See [diagrams.net](https://diagrams.net)
 
 
 
-# :pencil: PlantUML
+# :pencil2: PlantUML
 
 See [PlantUML](http://plantuml.com/).
 
@@ -151,7 +151,7 @@ State3 --> [*] : Aborted
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 
 ## Pie graph
 

+ 1 - 1
apps/app/resource/locales/fr_FR/sandbox-math.md

@@ -1,4 +1,4 @@
-# :pencil: Math
+# :pencil2: Math
 
 See [KaTeX](https://katex.org/).
 

+ 262 - 112
apps/app/resource/locales/fr_FR/sandbox.md

@@ -1,158 +1,308 @@
-# What is Sandbox?
-- In this page, you will find tips that help you to master GROWI 
-- Feel free to enrich the content of your pages with the references under this hierarchy
+# Qu'est-ce que Sandbox ?
+- Sur cette page, vous trouverez des conseils qui vous aideront à maîtriser GROWI
+- N'hésitez pas à enrichir le contenu de vos pages avec les références sous cette hiérarchie de pages
+
+# :closed_book: Titres et paragraphes
+- En insérant des titres et des paragraphes, vous pouvez rendre le texte de la page plus facile à lire
+
+## En-têtes
+- Ajoutez `#` avant le texte du titre pour créer un titre
+    - En fonction du nombre de `#`, la taille de la police des titres sera différente de celle affichée dans l'écran d'affichage
+- Le nombre de `#` déterminera le niveau de hiérarchie et vous aidera à organiser le contenu
+
+```markdown
+# Titre de premier niveau
+## Titre de deuxième niveau
+### Titre de troisième niveau
+#### Titre de quatrième niveau
+##### Titre de cinquième niveau
+###### Titre de sixième niveau
+```
+
+## Saut
+- Insérez deux espaces de demi-largeur à la fin de la phrase que vous souhaitez couper
+    - Vous pouvez également modifier cela dans le paramètre pour couper la ligne sans demi-largeur espaces
+        - Modifiez le paramètre de saut de ligne dans le secteur « Paramètres Markdown » de la page d'administration
+
+#### Exemple : Sans saut de ligne
+Paragraphe 1
+Paragraphe 2
+
+#### Exemple : Avec saut de ligne
+Paragraphe 1  
+Paragraphe 2
+
+## Bloc
+- Les paragraphes peuvent être créés en insérant une ligne vide dans le texte
+- Le passage peut être divisé en phrases et les rendre plus faciles à lire
+
+#### Exemple : Sans paragraphe
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 
+#### Exemple : Avec paragraphe
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
 
-# :closed_book:Headings & Paragraphs
-- By inserting headings and paragraphs, you can make the text on the page easier to read
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 
-## Headers
-- Add `#` before the heading text to create a heading 
-    - Depending on the number of `#`, the typeface size of headings would be different shown in the View screen 
-    - Check the View screen on the right side to understand the effect of headings
-- The number of `#` will decide the hierarchy level and help you to organize the contents
+## Lignes horizontales
+- Insérer la ligne horizontale avec trois astérisques consécutifs ou plus `*` ou des traits de soulignement `_`
+
+#### Exemple
+Ci-dessous se trouve une ligne horizontale
+***
+
+Ci-dessous se trouve une ligne horizontale
+___
+
+```markdown
+Ci-dessous se trouve une ligne horizontale
+***
 
+Ci-dessous se trouve une ligne horizontale
+___
 ```
-# First-level heading
-## Second-level heading
-### Third-level heading
-#### Forth-level heading
-##### Fifth-level heading
-###### Sixth-level heading
+
+# :green_book: Style du texte
+- Différents styles peuvent être appliqués pour enrichir l'expression textuelle d'une phrase
+    - Ces styles peuvent également être facilement appliqués en sélectionnant l'icône de la barre d'outils en bas de l'écran d'édition
+
+## Italique
+- Entourez le texte d'un astérisque `*` ou d'un trait de soulignement `_`.
+
+#### Exemples
+- Cette phrase indique l'emphase avec *Italique*
+- Cette phrase indique l'emphase avec _Italique_
+
+```markdown
+- Cette phrase indique l'emphase avec *Italique*
+- Cette phrase indique l'emphase avec _Italique_
 ```
 
-## Break
-- Insert two half-width spaces at the end of the sentence you want to break
-    - You can also change this in the Setting to break the line without half-width spaces
-        - Change the line break setting in the `Markdown Settings` sector of the admin page
+## Gras
+- Entourez le texte de deux astérisques `*` ou de deux traits de soulignement `_`
+
+#### Exemple
+- Cette phrase indique l'emphase avec **Gras**
+- Cette phrase indique l'emphase avec __Gras__
 
-#### Without line break
-Paragraph 1
-Paragraph 2
+```markdown
+- Cette phrase indique l'emphase avec **Gras**
+- Cette phrase indique l'emphase avec __Gras__
+```
 
-#### With line break
-Paragraph 1  
-Paragraph 2
+## Italique et Gras
+- Entourez le texte de trois astérisques `*` ou de trois traits de soulignement `_`
 
-## Block
-- Paragraphs can be created by inserting a blank table in the text
-- Passage can be broken into sentences and make them easier to read
+#### Exemple
+- Cette phrase indique l'emphase avec ***Italique et Gras***
+- Cette phrase indique l'emphase avec ___Italique et Gras___
 
-#### Without paragraph
-Paragraph 1  
-Paragraph 2
+```markdown
+- Cette phrase indique l'emphase avec ***Italique et gras***
+- Cette phrase indique l'emphase avec ___Italique et gras___
+```
 
-#### With paragraph
-Paragraph 1  
+# :orange_book: Insérer des listes
+## Liste à puces
+- Insérer une liste à puces en commençant une ligne par un trait d'union `-`, un plus `+` ou un astérisque `*`
 
-Paragraph 2
+#### Exemple
+- Cette phrase est présente dans la liste à puces
+    - Cette phrase est présente dans la liste à puces
+        - Cette phrase est présente dans la liste à puces
+        - Cette phrase est présente dans la liste à puces
+- Cette phrase est présente dans la liste à puces
+    - Cette phrase est présente dans la liste à puces
 
+## Liste numérotée
+- `Number.` au début d'une ligne pour insérer une liste numérotée
+    - Les numéros sont automatiquement attribués
 
-# :green_book: Styling Text
-- Various styles can be applied to enrich the textual expression of a sentence
-    - These styles also can be easily applied by selecting the toolbar icon at the bottom of the Edit screen
+- La liste numérotée et la liste à puces peuvent également être combinées pour être utilisées
 
-## Italic
-- Enclose the text with an asterisk `*` or an underscore `_`.
+#### Exemple
+1. Cette phrase est présente dans la liste numérotée
+    1. Cette phrase est présente dans la liste numérotée
+    1. Cette phrase est présente dans la liste numérotée
+    1. Cette phrase est présente dans la liste numérotée
+        - Cette phrase est présente dans la liste à puces
+1. Cette phrase est présente dans la liste à puces
+    - Cette phrase est présente dans la liste à puces
 
-#### Examples
-- This sentence indicates emphasis with *Italic*
-- This sentence indicates emphasis with _Italic_ 
+## Liste des tâches
+- Insérer une liste de cases à cocher non cochées en écrivant `[]`
+    - Cocher la case à cocher en écrivant `[x]`
 
-## Bold
-- Enclose the text with two asterisks `*` or two underscores `_`
+#### Exemple
+- [ ] Tâche 1
+    - [x] Tâche 1-1
+    - [ ] Tâche 1-2
+- [x] Tâche 2
 
-#### Example
-- This sentence indicates emphasis with **Bold** 
-- This sentence indicates emphasis with __Bold__
+# :blue_book: Lien
 
-## Italic & Bold
-- Enclose the text with three asterisks `*` or three underscores `_`
+## Lien automatique
+Il suffit d'écrire l'URL et le lien sera généré automatiquement.
 
-#### Example
-- This sentence indicates emphasis with ***Italic & Bold***
-- This sentence indicates emphasis witH ___Italic & Bold___
+### Exemple
 
+https://www.google.co.jp
 
-# :orange_book: Insert Lists
-## Bulleted List
-- Insert a bulleted list by starting a line with a hyphen `-`, a plus `+`, or an asterisk `*`
+```markdown
+https://www.google.co.jp
+```
 
-#### Example
-- This sentence is present in the bulleted list
-    - This sentence is present in the bulleted list
-        - This sentence is present in the bulleted list
-        - This sentence is present in the bulleted list
-- This sentence is present in the bulleted list
-    - This sentence is present in the bulleted list
+## Libellé et lien
+Insérez un lien en écrivant `[label](URL)`
 
-## Numbered List
-- `Number.` at the beginning of a line to insert a numbered list
-- Numbered list and bulleted list can also be combined for use
+### Exemple
+- [Google](https://www.google.co.jp/)
+- [Sandbox est ici](/Sandbox)
 
-#### Example
-1. This sentence is present in the numbered list
-    1. This sentence is present in the numbered list
-    1. This sentence is present in the numbered list
-    1. This sentence is present in the numbered list
-        - This sentence is present in the bulleted list 
-1. This sentence is present in the bulleted list
-    - This sentence is present in the bulleted list
+```markdown
+- [Google](https://www.google.co.jp/)
+- [Sandbox est ici](/Sandbox)
+```
 
-## Task List
-- Insert an unchecked checkbox list by writing `[] `
-    - Check the checkbox by writing `[x]`
+## Syntaxe de lien flexible
 
-#### Example
-- [ ] Task 1
-    - [x] Task 1-1
-    - [ ] Task 1-2
-- [x] Task 2
+La syntaxe de lien flexible permet d'écrire facilement un lien par chemin de page, un lien de page relatif et un libellé de lien et une URL.
 
+- [[/Sandbox]]
+- [[./Math]]
+- [[Comment écrire des formules ?>./Math]]
 
-# :blue_book: Others
-## Blockquotes
-- Use quoted expressions by putting `>` at the beginning of the paragraph
-    - Multiple quotations can be expressed by using a sequence of `>` characters
-- Lists and other elements can be used together within the blockquotes
+```markdown
+- [[/Sandbox]]
+- [[./Math]]
+- [[Comment écrire des formules ?>./Math]]
+```
 
-#### Example
-> - Quotation
-> - Quotation
->> Multiple quotations need to insert more `>`
+# :notebook: Autres
+## Citations
+- Utilisez des expressions entre guillemets en mettant `>` au début du paragraphe
+- Plusieurs citations peuvent être exprimées en utilisant une séquence de caractères `>`
+- Des listes et d'autres éléments peuvent être utilisés ensemble dans les citations
+
+#### Exemple
+> - Citation
+> - Citation
+>> Plusieurs citations doivent insérer plus de `>`
+
+```markdown
+> - Citation
+> - Citation
+>> Plusieurs citations doivent insérer plus de `>`
+```
 
 ## Code
-- It is possible to express the code by adding it in three `` ` ``
+- Il est possible d'exprimer le code en l'ajoutant en trois `` ` ``
 
-#### Example
+#### Exemple
+
+```markdown
+Ajoutez des codes ici
+
+Les sauts de ligne et les paragraphes peuvent être reflétés dans le code tel quel
 ```
-Add codes here  
-Line breaks and paragraphs can be reflected in the code
 
-- List also can be used in code
-    - List also can be used in code
+#### Exemple (code source)
+
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
 ```
 
-## Inline Code
-- Enclose words in `` ` `` to make inline code
+## Code en ligne
+- Entourez les mots de `` ` `` pour créer du code en ligne
 
-#### Example
-Here is the `inline code` 
+#### Exemple
+Voici le `code en ligne`
 
-## Horizontal lines
-- Insert the horizontal line with three or more consecutive asterisks `*` or underscores `_`
+## Tableau
 
-#### Example
-Below is a horizontal line
-***
+### Syntaxe générale
 
-Below is a horizontal line
-___
+#### Exemple
+
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+```markdown
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+```
+
+### CSV / TSV
+
+#### Exemple
+
+``` tsv
+Cellule de contenu Cellule de contenu
+Cellule de contenu Cellule de contenu
+```
+
+~~~
+``` csv
+Cellule de contenu,Cellule de contenu
+Cellule de contenu,Cellule de contenu
+```
+~~~
+
+~~~
+``` tsv
+Cellule de contenu Cellule de contenu
+Cellule de contenu Cellule de contenu
+```
+~~~
+
+### CSV / TSV (avec en-tête)
 
+#### Exemple
+
+``` tsv-h
+Premier en-tête Deuxième en-tête
+Cellule de contenu Cellule de contenu
+Cellule de contenu Cellule de contenu
+```
+
+~~~
+``` csv-h
+Premier en-tête Deuxième en-tête
+Cellule de contenu,Cellule de contenu
+Cellule de contenu,Cellule de contenu
+```
+~~~
+
+~~~
+``` tsv-h
+Premier en-tête Deuxième en-tête
+Cellule de contenu Cellule de contenu
+Cellule de contenu Contenu Cellule
+```
+~~~
 
-# :ledger: More Applications
-- [Bootstrap5](/Sandbox/Bootstrap5)
+# :ledger: Autres applications
+- [Bootstrap](/Sandbox/Bootstrap)
 
-- [Diagrams](/Sandbox/Diagrams)
+- [Diagrammes](/Sandbox/Diagrammes)
 
-- [Math](/Sandbox/Math)
+- [Math](/Sandbox/Math)

+ 3 - 3
apps/app/resource/locales/ja_JP/sandbox-diagrams.md

@@ -2,7 +2,7 @@
 - GROWI では各種機能を活用することで様々な図形の表現が可能です
   - 各種機能の特色を活かして図形の表現をしましょう
 
-# :pencil: Diagrams.net(旧 Draw.io)
+# :pencil2: Diagrams.net(旧 Draw.io)
 - 図形の挿入時に全般的にご利用いただきやすい図形の挿入方法となります
   - サービスの詳細は [こちら](https://www.drawio.com/) をご確認ください
 - Edit 画面下部のツールバーより専用の編集画面を用いて図形を編集することが可能です
@@ -28,7 +28,7 @@ pLzXsqNMti76NH0Pwggu8d4Jzx3eCO/R05/M+XevvTtWnIgTcapK0wCCNGN8ZmSq/oVxwy2t6dwYU1H2
 ```
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 - Mermaidとは、Markdownテキストでグラフを作成できるダイアグラムツールです
   - サービスの詳細は [こちら](https://mermaid.js.org/) をご確認ください
 
@@ -79,7 +79,7 @@ mindmap
 ```
 
 
-# :pencil: PlantUML
+# :pencil2: PlantUML
 - PlantUML はオープンソースの UML 描画ツールです
   - サービスの詳細は [こちら](https://plantuml.com/) をご確認ください
 

+ 182 - 187
apps/app/resource/locales/ja_JP/sandbox.md

@@ -1,18 +1,16 @@
-# Sandbox(サンドボックスとは
-- この階層下では、GROWI をより便利に活用するための活用術や活用ヒントを掲載しています
-- この階層下のページ内容を組織内で自由に書き換えて GROWI の理解度を深めるために活用しましょう!
+# サンドボックスとは?
+- このページでは、GROWI を使いこなすためのヒントを紹介します
+- このページと下の階層にある参考記述を利用して、ページのコンテンツを充実させることができます
 
+# :closed_book: 見出しと段落
+- 見出しと段落を挿入すると、ページ上のテキストを読みやすくすることができます
 
-# :memo:見出しや段落
-- 見出しや段落を挿入することで、ページ内の文章にメリハリがつき読みやすい文章を作成することが可能です
+## ヘッダー
+- 見出しを作成するには、見出しテキストの前に `#` を追加します
+- `#` の数に応じて、View 画面に表示される見出しの書体サイズが変わります
+- このページ内にもたくさんの見出しが活用されており、`#` の数に応じて内容をグルーピングすることができます
 
-## 見出し(Headers)
-- 行頭に `#` をレベルの数だけ記述することで見出しを作成することが可能です
-    - 各見出しに応じて View 画面に表示される際のデザインも異なります
-    - 各見出しに応じて View 画面右側に表示される目次が生成されます
-- このページ内にもたくさんの見出しが活用されており、`#` の数に応じて内容をグルーピングすることで可能です
-
-```
+```markdown
 # 見出し1
 ## 見出し2
 ### 見出し3
@@ -23,63 +21,94 @@
 
 ## 改行(Br)
 - 改行したい文章の行末に半角スペースを2つ挿入することで改行をすることができます
-    - こちらの挙動は、設定画面から半角スペースなしで改行が反映されるように設定を変更することが可能です
-        - 「マークダウン設定_Line Break設定(/admin/markdown)」から変更が可能です
+    - 管理画面から半角スペースなしで改行が反映されるように設定を変更することも可能です
+        - 「マークダウン設定」から変更できま
 
-#### 改行がない場合
+#### 例: 改行なし
 文章 1 の内容が入ります
 文章 2 の内容が入ります
 
-#### 改行がある場合
+#### 例: 改行あり
 文章 1 の内容が入ります  
 文章 2 の内容が入ります
 
-## 段落(Block)
-- 文章内で空白表を挿入することで段落を作成することが可能で
-- 段落を作成することで文章の節目を作成し読みやすい文章を作成することができます
+## ブロック
+- テキストに空白行を挿入することで段落を作成できま
+- 文章を文に分割して読みやすくすることができます
 
-#### 段落がない場合
-文章 1 の内容が入ります  
-文章 2 の内容が入ります
+#### 例: 段落なし
+あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモリーオ市、郊外のぎらぎらひかる草の波。
+またそのなかでいっしょになったたくさんのひとたち、ファゼーロとロザーロ、羊飼のミーロや、顔の赤いこどもたち、地主のテーモ、山猫博士のボーガント・デストゥパーゴなど、いまこの暗い巨きな石の建物のなかで考えていると、みんなむかし風のなつかしい青い幻燈のように思われます。
 
-#### 段落がある場合
-文章 1 の内容が入ります  
+#### 例: 段落がある場合
 
-文章 2 の内容が入ります
+あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモリーオ市、郊外のぎらぎらひかる草の波。
 
+またそのなかでいっしょになったたくさんのひとたち、ファゼーロとロザーロ、羊飼のミーロや、顔の赤いこどもたち、地主のテーモ、山猫博士のボーガント・デストゥパーゴなど、いまこの暗い巨きな石の建物のなかで考えていると、みんなむかし風のなつかしい青い幻燈のように思われます。
 
-# :memo:文字の強調
-- 各種記述方法を適用させることで文内の文字の表現を豊かにすることが可能です
-    - これらの表現は Edit 画面下部のツールバーから該当のアイコンを選択することで簡単に適用させることも可能です
+## 水平線
+- 3 つ以上の連続したアスタリスク `*` またはアンダースコア `_` で水平線を挿入します
 
-## 斜体(Italic)
-- アスタリスク `*` もしくはアンダースコア `_` 1つで該当の文字列を囲みます
+#### 例
+以下は水平線です
+***
 
-#### 活用例
-- この文章は *斜体が適用* されます  
-- この文章は _斜体が適用_ されます
+以下は水平線です
+___
 
-## 太字(Bold)
-- アスタリスク `*` もしくはアンダースコア `_` 2つで該当の文字列を囲みます
+```markdown
+以下は水平線です
+***
+
+以下は水平線です
+___
+```
 
-#### 活用例
-- この文章は **強調が適用** されます  
-- この文章は __強調が適用__ されます
+# :green_book: テキストのスタイル設定
+- さまざまなスタイルを適用して、文章のテキスト表現を豊かにすることができます
+- これらのスタイルは、編集画面の下部にあるツールバー アイコンを選択して簡単に適用することもできます
 
-## 斜体 & 太字(Italic & Bold)
-- アスタリスク `*` もしくはアンダースコア `_` 3つで該当の文字列を囲みます
+## 斜体
+- テキストをアスタリスク `*` またはアンダースコア `_` で囲みます。
+
+#### 例
+- この文は *斜体* で強調を示します
+- この文は _斜体_ で強調を示します
+
+```markdown
+- この文は *斜体* で強調を示します
+- この文は _斜体_ で強調を示します
+```
 
-#### 活用例
-- この文章は ***斜体 & 太字が適用*** されます  
-- この文章は ___斜体 & 太字が適用___ されます
+## 太字
+- テキストを 2 つのアスタリスク `*` または 2 つのアンダースコア `_` で囲みます
 
+#### 例
+- この文は **太字** で強調を示します
+- この文は __太字__ で強調を示します
 
-# :memo:リストの挿入
+```markdown
+- この文は **太字** で強調を示します
+- この文は __太字__ で強調を示します
+```
+
+## 斜体と太字
+- テキストを 3 つのアスタリスク `*` または 3 つのアンダースコア `_` で囲みます
+
+#### 例
+- この文は ***斜体と太字*** で強調を示します
+- この文は ___斜体と太字で強調を示します太字___
+
+```markdown
+- この文は ***斜体 & 太字*** で強調を示します
+- この文は ___斜体 & 太字___ で強調を示します
+```
+
+# :orange_book: リストの挿入
 ## 箇条書きリスト
 - ハイフン `-`、プラス `+`、アスタリスク `*` を行頭に記述することで、箇条書きのリストを挿入することでができます
-    - タブを活用することで前の行のリストに紐づくリストを挿入することも可能です
 
-#### 活用例
+#### 例
 - この文章は箇条書きリストで表現しています
     - この文章は箇条書きリストで表現しています
         - この文章は箇条書きリストで表現しています
@@ -89,10 +118,10 @@
 
 ## 番号付きリスト
 - `番号.` を行頭に記述することで、番号付きのリストを挿入することができます
-    - タブを活用することで前の行のリストに紐づくリストを挿入することも可能で
-- 番号付きリストと箇条書きリストを組み合わせて活用することも可能で
+    - 番号は自動で採番されま
+- 番号付きリストと箇条書きリストを組み合わせて使用​​することもできま
 
-#### 活用
+#### 例
 1. この文章は番号付きリストで表現しています
     1. この文章は番号付きリストで表現しています
     1. この文章は番号付きリストで表現しています
@@ -101,138 +130,83 @@
 1. この文章は箇条書きリストで表現しています
     - この文章は箇条書きリストで表現しています  
 
-## タスクリスト
+## タスク リスト
 - `[] ` を記述することでリストに対して未チェックのチェックボックスを挿入することができます
     - `[x] ` を記述することでチェック済みのチェックボックスを挿入することができます
 
-#### 活用
+#### 例
 - [ ] タスク 1
     - [x] タスク 1-1
     - [ ] タスク 1-2
 - [x] タスク2
 
+# :blue_book: リンク
 
-# :memo:表の挿入
-## Markdown 標準
-- Markdown で記載できる標準的な形式の表です
+## 自動リンク
+URL を記述するだけで、リンクが自動的に生成されます。
 
-#### 活用例
-| 左揃え               |               右揃え |        中央揃え        |
-| :------------------- | -------------------: | :--------------------: |
-| この列は             |             この列は |        この列は        |
-| 左揃えで表示されます | 右揃えで表示されます | 中央揃えで表示されます |
+### 例
 
-## TSV
-#### 活用例
-``` tsv
-10:00	集合
-10:20	移動
-```
-
-## TSV(ヘッダー付き)
-#### 活用例
-``` tsv-h
-時間	行動
-10:00	集合
-10:20	移動
-```
-
-## CSV
-#### 活用例
-``` csv
-11:00,MTG
-12:00,昼食
-```
+https://www.google.co.jp
 
-## CSV(ヘッダー付き)
-#### 活用例
-``` csv-h
-時間,行動
-11:00,MTG
-12:00,昼食
+```markdown
+https://www.google.co.jp
 ```
 
+## ラベルとリンク
+`[label](URL)` と記述してリンクを挿入します
 
-# :memo:リンクの挿入
-## Markdown 標準
-- Markdown で記載できる標準的な形式のリンクです
-- `[表示されるテキスト](リンク先のURL)`でリンクに変換されます
-
-#### 活用例
-[Google](https://www.google.co.jp/)
-
-## Pukiwiki like linker
-- もっとも柔軟なリンクの形式です
-- 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます
+### 例
+- [Google](https://www.google.co.jp/)
+- [砂場ページはこちら](/Sandbox)
 
-#### 活用例
-Bootstrap によるページの装飾方法の記述方法は [[こちらをご確認ください>./Bootstrap5]]
-
-
-# :memo:画像の挿入
-## 画像(Images)の挿入
-- `![Alt文字列](URL)` で`<img>`タグを挿入できます
-
-#### 活用例
-![Minion](https://octodex.github.com/images/minion.png)
-
-## 画像のサイズ指定
-- 画像の大きさなどを指定する場合はimgタグを使用します
-
-#### 活用例
-<img src="https://octodex.github.com/images/dojocat.jpg" width="500px">
+```markdown
+- [Google](https://www.google.co.jp/)
+- [砂場ページはこちら](/Sandbox)
+```
 
+## 柔軟なリンク構文
 
-# :memo:コンテンツやページの表示
-## 目次(ToC)
-- いくつかの `#` 記号に続けて `ToC` を記述することでページ内に目次を生成することができます
-    - `ToC` は `Table of Contents` または `Table-of-Contents` でも適用されます
-- 生成される目次は、ページ内で `ToC` を記述した以降の部分の目次となります
+柔軟なリンク構文により、ページパスによるリンク、相対ページリンク、リンクラベルとURLによるリンクを簡単に記述できます。
 
-#### 活用例
-##### ToC
+- [[/Sandbox]]
+- [[./Math]]
+- [[数式の書き方は?>./Math]]
 
-## 配下ページの表示(lsx)
-- ページ内に `$lsx()` を記述することで配下に作成されているページを表示することができます
-- 各種オプションを指定することで表示される配下ページを操作することができます
-    - lsx の詳細は [GROWI 公式ドキュメント](https://docs.growi.org/ja/guide/features/lsx.html) をご確認ください
+```markdown
+- [[/Sandbox]]
+- [[./Math]]
+- [[数式の書き方は?>./Math]]
+```
 
-#### 活用例
-$lsx()
+# :notebook: その他
+## 引用符
+- 行頭に `>` を記述することで引用表現を記述できます
+    - 多重引用の際は `>` を複数個連続で記述することで表現できます
 
-# :memo:その他の基本的な表現
-## 引用(Blockquotes)
-- 行頭に `>` を記述することで引用表現をすることが可能です
-    - 多重引用の際は `>` を複数個連続で記述することで表現が可能です
-- 引用内でリストなどの要素を併用することも可能です
+#### 例
+> - 引用符
+> - 引用符
+>> 複数の引用符にはさらに `>` を挿入する必要があります
 
-#### 活用例
+```markdown
 > - 引用する文章が入ります
 > - 引用する文章が入ります
->> 多重引用したい文章の場合は複数個の挿入が必要です
-
-## コード(Code)
-- `` ` `` 3つで囲むことでコードの表現をすることが可能です
-
-#### 活用例
+>> 多重引用を表現するにはさらに `>` を挿入します
 ```
-コードが入ります  
-改行や段落をコード内で反映させることが可能です
 
-- リストもコード内での表現が可能です
-    - リストもコード内での表現が可能です
-```
+## コード
+- `` ` `` 3つで囲むことでコードの表現をすることが可能です
 
-## インラインコード
-- `` ` `` で単語を囲むとインラインコードになります
+#### 例
 
-#### 活用例
-こちらは `インラインコード` です
+```markdown
+ここにコードを追加
 
-## シンタックスハイライトとファイル名
-- [highlight.js Demo](https://highlightjs.org/static/demo/) の common カテゴリ内の言語に対応しています
+改行と段落はそのまま反映されます
+```
+#### 例 (ソースコード)
 
-#### 活用例 
 ```javascript:mersenne-twister.js
 function MersenneTwister(seed) {
   if (arguments.length == 0) {
@@ -244,57 +218,78 @@ function MersenneTwister(seed) {
 }
 ```
 
-## pre 整形済みテキスト
-- 半角スペース4個もしくはタブで、コードブロックを pre 表示できます
-
-#### 活用例
-    class Hoge
-        def hoge
-            print 'hoge'
-        end
-    end
-
-## 水平線(Hr)
-- アスタリスク `*` もしくはアンダースコア `_` を3つ以上連続して記述することで水平線を挿入できます
-
-#### 活用例
-以下に水平線が挿入されます
-***
-
-以下に水平線が挿入されます
-___
-
-## 脚注(Footnote)
-- 脚注 `[^1]` と脚注への参照 `[^1]:` を作成することができます
+## インライン コード
+- `` ` `` で単語を囲むとインラインコードになります
 
-#### 活用
-脚注への参照[^1]を書くことができます。
+#### 例
+こちらは `インラインコード` です
 
-長い脚注は[^longnote]のように書くことができます。
 
-[^1]: 1つめの脚注への参照です。
 
-[^longnote]: 脚注を複数ブロックで書く例です。
+# :memo:表の挿入
+## Markdown 標準
+- Markdown で記載できる標準的な形式の表です
 
-    後続の段落はインデントされて、前の脚注に属します。
+#### 例
+| 左揃え               |               右揃え |        中央揃え        |
+| :------------------- | -------------------: | :--------------------: |
+| この列は             |             この列は |        この列は        |
+| 左揃えで表示されます | 右揃えで表示されます | 中央揃えで表示されます |
 
-## 絵文字(Emoji)
-:smiley: :smile: :laughing: :innocent: :drooling_face:
+```markdown
+| 左揃え               |               右揃え |        中央揃え        |
+| :------------------- | -------------------: | :--------------------: |
+| この列は             |             この列は |        この列は        |
+| 左揃えで表示されます | 右揃えで表示されます | 中央揃えで表示されます |
+```
 
-:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
+### CSV / TSV
+#### 例
 
-:+1: :-1: :open_hands: :raised_hands: :point_right:
+``` tsv
+10:00	集合
+10:20	移動
+```
 
-:apple: :green_apple: :strawberry: :cake: :hamburger:
+~~~
+``` csv
+11:00,MTG
+12:00,昼食
+```
+~~~
 
-:basketball: :football: :baseball: :volleyball: :8ball:
+~~~
+``` tsv
+10:00	集合
+10:20	移動
+```
+~~~
 
-:hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
+### CSV / TSV(ヘッダー付き)
+#### 例
+``` tsv-h
+時間	行動
+10:00	集合
+10:20	移動
+```
 
-:watch: :gear: :gem: :wrench: :email:
+~~~
+``` csv-h
+時間,行動
+11:00,MTG
+12:00,昼食
+```
+~~~
 
+~~~
+``` tsv-h
+時間	行動
+10:00	集合
+10:20	移動
+```
+~~~
 
-# :memo:さらに応用的な表現
+# :ledger: さらにアプリケーションを利用
 - [ページの装飾方法(Bootstrap5)](/Sandbox/Bootstrap5)
 
 - [図形の表現方法(Diagrams)](/Sandbox/Diagrams)

+ 3 - 3
apps/app/resource/locales/zh_CN/sandbox-diagrams.md

@@ -1,4 +1,4 @@
-# :pencil: diagrams.net(Draw.io)
+# :pencil2: diagrams.net(Draw.io)
 
 See [diagrams.net](https://diagrams.net)
 
@@ -23,7 +23,7 @@ See [diagrams.net](https://diagrams.net)
 
 
 
-# :pencil: PlantUML
+# :pencil2: PlantUML
 
 See [PlantUML](http://plantuml.com/).
 
@@ -151,7 +151,7 @@ State3 --> [*] : Aborted
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 
 ## Pie graph
 

+ 1 - 1
apps/app/resource/locales/zh_CN/sandbox-math.md

@@ -1,4 +1,4 @@
-# :pencil: Math
+# :pencil2: Math
 
 See [KaTeX](https://katex.org/).
 

+ 232 - 86
apps/app/resource/locales/zh_CN/sandbox.md

@@ -1,107 +1,135 @@
 # 什么是沙盒?
-- 在本页中,您可以找到帮助您掌握 GROWI 的技巧。
-- 您可以在此层级下的参考资料中丰富您的网页内容
+- 在此页面上,您将找到帮助您掌握 GROWI 的技巧
+- 使用此页面层次结构下的参考资料随意丰富页面内容
 
-
-# :closed_book:标题和段落
-- 通过插入标题和段落,可以使页面上的文字更易于阅读
+# :closed_book: 标题和段落
+- 通过插入标题和段落,您可以使页面上的文本更易于阅读
 
 ## 标题
-- 在标题文字前添加 `#` 以创建标题 
-    - 在 "视图 "屏幕中,标题的字体大小会因 "#"的数量而异 
-    - 查看右侧的 "视图 "屏幕,了解标题的效果
-- `#`的数量将决定层次结构的级别,并帮助您组织内容
-
-```
-# 一级标题
-## 二级标题
-### 三级标题
+- 在标题文本前添加 `#` 以创建标题
+- 根据 `#` 的数量,标题的字体大小在视图屏幕中显示不同
+- `#` 的数量将决定层次结构级别并帮助您组织内容
+
+```markdown
+# 第一级标题
+## 第二级标题
+### 第三级标题
 #### 第四级标题
 ##### 第五级标题
 ###### 第六级标题
 ```
 
-### 断句
-- 在要换行的句子末尾插入两个半空格
-    - 您也可以在 "设置 "中进行更改,使换行不使用半宽空格
-        - 在管理页面的 "Markdown 设置 "部分更改换行设置
-
-#### 换行
-段落 1
+## 换行
+- 在要换行的句子末尾插入两个半空格
+    - 您也可以在设置中更改此设置以换行而不使用半角空格
+        - 更改换行设置在管理页面的“Markdown 设置”部分
+        
+#### 示例:没有换行
+第 1 段
 第 2 段
 
-#### 有换行符
-段落 1  
+#### 示例:有换行符
+第 1 段  
 第 2 段
 
-## 段落
-- 在文本中插入空白表格即可创建段落
-- 可将段落分成若干句子,使其更易于阅读
+## 
+- 可以通过在文本中插入空行来创建段落
+- 可以将段落分成句子,使它们更易于阅读
 
-#### 无段落
-段落 1  
-第 2 段
+#### 示例:没有段落
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 
-#### 段落
-第 1 段  
+#### 示例:用段落
+Lorem ipsum dolor sat amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut Labore et dolore magna aliqua。 Ut enim ad minim veniam, quis nostrud exeritation ullamco labouris nisi ut aliquip ex ea commodo consequat.
 
-第 2 段
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur。 Excepteur sint occaecat cupidatat non proident,sunt in culpa qui officia deserunt mollit anim id est laborum。
 
+## 水平线
+- 用三个或更多连续的星号 `*` 或下划线 `_` 插入水平线
+
+#### 示例
+下面是一条水平线
+***
+下面是一条水平线
+___
+
+```markdown
+下面是一条水平线
+***
+下面是一条水平线
+___
+```
 
 # :green_book: 文本样式
-- 可以使用各种样式来丰富句子的文字表达方式
-    - 选择 "编辑 "屏幕底部的工具栏图标,也可以轻松应用这些样式
+- 可以应用各种样式来丰富句子的文本表达
+    - 也可以通过选择编辑屏幕底部的工具栏图标轻松应用这些样式
 
-##斜体
-- 用星号`*`或下划线`_`括住文本。
+## 斜体
+- 用星号 `*` 或下划线 `_` 括住文本。
 
 #### 示例
-- 这句话用*斜体*表示强调
-- 这句话用 _Italic_ 表示强调 
+- 本句用 *Italic* 表示强调
+- 本句用 _Italic_ 表示强调
+
+```markdown
+- 本句用 *Italic* 表示强调
+- 本句用 _Italic_ 表示强调
+```
 
 ## 粗体
-- 用两个星号`*`或两个下划线`_`括住文本。
+- 用两个星号 `*` 或两个下划线 `_` 括住文本
 
 #### 示例
-- 这句话用 ** 粗体** 表示强调 
-- 这句话用__粗体__表示强调
+- 本句用 **Bold** 表示强调
+- 本句用 __Bold__ 表示强调
 
-## 斜体和粗体
-- 用三个星号`*`或三个下划线`_`括起来
+```markdown
+- 本句用 **Bold** 表示强调
+- 本句用 __Bold__ 表示强调
+```
+
+## 斜体 & 粗体
+- 用三个星号 `*` 或三个下划线 `_` 括住文本
 
 #### 示例
-- 本句用***斜体和粗体***表示强调
-- 本句用____斜体和粗体____表示强调
+- 本句用 ***Italic & 粗体*** 表示强调
+- 本句用 ___Italic & 粗体___ 表示强调
 
+```markdown
+-本句使用 ***斜体和粗体*** 表示强调
+- 本句使用 ___斜体和粗体___ 表示强调
+```
 
 # :orange_book: 插入列表
-## 缩略图列表
-- 用连字符 `-`、加号 `+` 或星号 `*` 开头一行,插入一个项目符号列表
+## 项目符号列表
+- 通过在行首使用连字符 `-`、加号 `+` 或星号 `*` 插入项目符号列表
 
 #### 示例
-- 这句话出现在项目符号列表中
-    - 这句话出现在项目符号列表中
-        - 这句话出现在项目符号列表中
-        - 这句话出现在项目符号列表中
-- 此句出现在项目符号列表中
-    - 此句子出现在项目符号列表中
+- 本句在项目符号列表中
+    - 本句在项目符号列表中
+        - 本句在项目符号列表中
+        - 本句在项目符号列表中
+- 本句在项目符号列表中
+    - 本句在项目符号列表中
 
 ## 编号列表
-- 在行首添加 `Number.` 以插入编号列表
-- 编号列表和项目符号列表也可合并使用
+- 在行首使用 `Number.` 插入编号列表
+- 编号自动分配
+
+- 编号列表和项目符号列表也可组合使用
 
 #### 示例
-1. 编号列表中有这样一句话
-    1. 编号列表中包含这句话
-    1. 该句子出现在编号表中
-    1. 此句出现在编号列表中
-        - 此句出现在项目符号列表中 
-1. 此句出现在项目符号列表中
-    - 此句出现在项目符号列表中
-
-##任务列表
-- 通过写 `[] ` 插入未选中复选框列表
-    - 通过写 `[x]` 选中复选框
+1. 本句在编号列表中
+    1. 本句在编号列表中
+    1. 此句子出现在编号列表中
+    1. 此句出现在编号列表中
+        - 此句出现在项目符号列表中
+1. 此句出现在项目符号列表中
+    - 此句出现在项目符号列表中
+
+## 任务列表
+- 通过写 `[] ` 插入未选中复选框列表
+    - 通过写 `[x]` 选中复选框
 
 #### 示例
 - [ ] 任务 1
@@ -109,51 +137,169 @@
     - [ ] 任务 1-2
 - [x] 任务 2
 
+# :blue_book: 链接
+
+## 自动链接
+只需输入 URL,链接就会自动生成。
+
+### 示例
+
+https://www.google.co.jp
+
+```markdown
+https://www.google.co.jp
+```
+
+## 标签和链接
+通过输入 `[label](URL)` 插入链接
+
+### 示例
+- [Google](https://www.google.co.jp/)
+- [Sandbox is here](/Sandbox)
+
+```markdown
+- [Google](https://www.google.co.jp/)
+- [Sandbox is here](/Sandbox)
+```
+
+## 灵活的链接语法
+
+灵活的链接语法使通过页面路径、相对页面链接和链接标签和 URL 编写链接变得容易。
 
-# :blue_book: 其他
-### 引号
-- 在段落开头加上`>`,使用引号表达式
-    - 使用`>`字符序列可表达多个引号
-- 列表和其他元素可在方括号内一起使用
+- [[/Sandbox]]
+- [[./Math]]
+- [[如何写公式?>./Math]]
+
+```markdown
+- [[/Sandbox]]
+- [[./Math]]
+- [[如何写公式?>./Math]]
+```
+
+# :notebook: 其他
+## 区块引用
+- 在段落开头放置 `>` 即可使用带引号的表达式
+    - 可以使用一系列 `>` 字符来表示多个引号
+- 列表和其他元素可以在区块引用中一起使用
 
 #### 示例
 > - 引号
 > - 引号
->> 多个引号需要插入更多的 `>` 字符
+>> 多个引号需要插入更多 `>`
+
+```markdown
+> - 引号
+> - 引号
+>> 多个引号需要插入更多 `>`
+```
 
 ## 代码
-- 可以通过将代码添加到三个 `` `` `` 中来表达代码
+- 可以通过在三个 `` ` `` 中添加代码来表示代码
 
-#### 示例
+####示例
+
+```markdown
+在此处添加代码
+
+换行符和段落可以按原样反映在代码中
 ```
-在此处添加代码  
-代码中可以体现换行和段落
 
-- 代码中也可使用列表
-    - 也可在代码中使用列表
+#### 示例(源代码)
+
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
 ```
 
 ## 内联代码
+- 将单词括在 `` ` `` 中以制作内联代码
 
+#### 示例
+以下是 `内联代码`
 
+## 表格
 
+### 通用语法
 
 #### 示例
-以下是内联代码 
 
-## 水平线
-- 用三个或三个以上连续的星号`*`或下划线`_`插入水平线
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+
+```markdown
+| Left align | Right align | Center align |
+|:-----------|------------:|:------------:|
+| This       | This        | This         |
+| column     | column      | column       |
+| will       | will        | will         |
+| be         | be          | be           |
+| left       | right       | center       |
+| aligned    | aligned     | aligned      |
+```
+
+### CSV / TSV
 
 #### 示例
-以下是水平线
-***
 
-下面是水平线
-___
+``` tsv
+内容单元格 内容单元格
+内容单元格 内容单元格
+```
+
+~~~
+``` csv
+内容单元格,内容单元格
+内容单元格,内容单元格
+```
+~~~
+
+~~~
+``` tsv
+内容单元格 内容单元格
+内容单元格 内容单元格
+```
+~~~
 
+### CSV / TSV (带标题)
+
+#### 示例
+
+``` tsv-h
+第一个标题 第二个标题
+内容单元格 内容单元格
+内容单元格 内容单元格
+```
+
+~~~
+``` csv-h
+第一个标题,第二个标题
+内容单元格,内容单元格
+内容单元格,内容单元格
+```
+~~~
+
+~~~
+``` tsv-h
+第一个标题 第二个标题
+内容单元格 内容单元格
+内容单元格 内容单元格
+```
+~~~
 
 # :ledger: 更多应用
-- [Bootstrap5](/Sandbox/Bootstrap5)
+- [Bootstrap](/Sandbox/Bootstrap)
 
 - [Diagrams](/Sandbox/Diagrams)
 

+ 4 - 4
apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -13,10 +13,10 @@ const SystemInformationTable = (props: Props) => {
   const { adminHomeContainer } = props;
 
   const {
-    growiVersion, nodeVersion, npmVersion, yarnVersion,
+    growiVersion, nodeVersion, npmVersion, pnpmVersion,
   } = adminHomeContainer.state;
 
-  if (growiVersion == null || nodeVersion == null || npmVersion == null || yarnVersion == null) {
+  if (growiVersion == null || nodeVersion == null || npmVersion == null || pnpmVersion == null) {
     return <></>;
   }
 
@@ -36,8 +36,8 @@ const SystemInformationTable = (props: Props) => {
           <td>{ npmVersion }</td>
         </tr>
         <tr>
-          <th>yarn</th>
-          <td>{ yarnVersion }</td>
+          <th>pnpm</th>
+          <td>{ pnpmVersion }</td>
         </tr>
       </tbody>
     </table>

+ 4 - 4
apps/app/src/client/components/Admin/Security/SecuritySetting.jsx

@@ -526,16 +526,16 @@ class SecuritySetting extends React.Component {
                 aria-expanded="true"
               >
                 <span className="float-start">
-                  {isRomUserAllowedToComment === true && t('security_settings.rom_users_comment.accept')}
-                  {isRomUserAllowedToComment === false && t('security_settings.rom_users_comment.deny')}
+                  {isRomUserAllowedToComment === true && t('security_settings.read_only_users_comment.accept')}
+                  {isRomUserAllowedToComment === false && t('security_settings.read_only_users_comment.deny')}
                 </span>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
                 <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(false) }}>
-                  {t('security_settings.rom_users_comment.deny')}
+                  {t('security_settings.read_only_users_comment.deny')}
                 </button>
                 <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(true) }}>
-                  {t('security_settings.rom_users_comment.accept')}
+                  {t('security_settings.read_only_users_comment.accept')}
                 </button>
               </div>
             </div>

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

@@ -120,7 +120,6 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       <select
         name="actionName"
         className="form-control"
-        placeholder="select"
         value={actionName ?? ''}
         onChange={handleActionChange}
       >

+ 12 - 11
apps/app/src/client/components/Bookmarks/DragAndDropWrapper.tsx

@@ -1,8 +1,8 @@
-import React, { ReactNode } from 'react';
+import type { ReactNode } from 'react';
 
 import { useDrag, useDrop } from 'react-dnd';
 
-import { DragItemDataType } from '~/interfaces/bookmark-info';
+import type { DragItemDataType } from '~/interfaces/bookmark-info';
 
 type DragAndDropWrapperProps = {
   item?: Partial<DragItemDataType>
@@ -53,20 +53,21 @@ export const DragAndDropWrapper = (props: DragAndDropWrapperProps): JSX.Element
     }),
   }));
 
-
-  const getRef = (c: HTMLDivElement | null) => {
+  const getCallback = (c: HTMLDivElement | null) => {
     if (useDragMode && useDropMode) {
-      return [dragRef(c), dropRef(c)];
-    } if (useDragMode) {
-      return dragRef(c);
-    } if (useDropMode) {
-      return dropRef(c);
+      dragRef(c);
+      dropRef(c);
+    }
+    else if (useDragMode) {
+      dragRef(c);
+    }
+    else if (useDropMode) {
+      dropRef(c);
     }
-    return null;
   };
 
   return (
-    <div ref={c => getRef(c)} className={`grw-drag-drop-container ${isOver ? 'grw-accept-drop-item' : ''}`}>
+    <div ref={getCallback} className={`grw-drag-drop-container ${isOver ? 'grw-accept-drop-item' : ''}`}>
       {children}
     </div>
   );

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

@@ -27,7 +27,7 @@ const useIntersection = <E extends HTMLElement>(): [boolean, Ref<E>] => {
     }
     return;
   }, [element]);
-  return [intersecting, el => el && setElement(el)];
+  return [intersecting, (el) => { if (el != null) setElement(el); }];
 };
 
 const LoadingIndicator = (): React.ReactElement => {

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

@@ -1,7 +1,6 @@
 import type { FC } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
-import pullAllBy from 'lodash/pullAllBy';
 import { useTranslation } from 'next-i18next';
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
@@ -28,7 +27,9 @@ const isCheckedRule = (ruleName: string, subscribeRules: SubscribeRule[]) => (
 
 const updateIsEnabled = (subscribeRules: SubscribeRule[], ruleName: string, isChecked: boolean) => {
   const target = [{ name: ruleName, isEnabled: isChecked }];
-  return pullAllBy(subscribeRules, target, 'name').concat(target);
+  return subscribeRules
+    .filter(rule => rule.name !== ruleName)
+    .concat(target);
 };
 
 

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

@@ -9,7 +9,6 @@ import { CodeMirrorEditorComment } from '@growi/editor/dist/client/components/Co
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useResolvedThemeForEditor } from '@growi/editor/dist/client/stores/use-resolved-theme';
 import { UserPicture } from '@growi/ui/dist/components';
-import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import {
@@ -209,7 +208,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     });
   }, [codeMirrorEditor, pageId]);
 
-  const cmProps = useMemo<ReactCodeMirrorProps>(() => ({
+  const cmProps = useMemo(() => ({
     onChange: async(value: string) => {
       const dirtyNum = await evaluateEditorDirtyMap(editorKey, value);
       mutateIsEnabledUnsavedWarning(dirtyNum > 0);

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

@@ -57,7 +57,6 @@ export const MarkdownTableDataImportForm = (props: MarkdownTableDataImportFormPr
         <select
           id="data-import-form-type-select"
           className="form-select"
-          placeholder="select"
           value={dataFormat}
           onChange={(e) => { return setDataFormat(e.target.value) }}
         >

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

@@ -6,14 +6,14 @@ import React, {
 import type EventEmitter from 'events';
 import nodePath from 'path';
 
-import { type IPageHasId, Origin } from '@growi/core';
+import { Origin } from '@growi/core';
+import type { IPageHasId } from '@growi/core/dist/interfaces';
 import { pathUtils } from '@growi/core/dist/utils';
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
 import { CodeMirrorEditorMain } from '@growi/editor/dist/client/components/CodeMirrorEditorMain';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useResolvedThemeForEditor } from '@growi/editor/dist/client/stores/use-resolved-theme';
 import { useRect } from '@growi/ui/dist/utils';
-import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
 import { throttle, debounce } from 'throttle-debounce';
@@ -267,7 +267,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   }, [codeMirrorEditor, pageId]);
 
 
-  const cmProps = useMemo<ReactCodeMirrorProps>(() => ({
+  const cmProps = useMemo(() => ({
     onChange: (value: string) => {
       setMarkdownPreviewWithDebounce(value);
     },

+ 1 - 1
apps/app/src/client/components/PageEditor/markdown-drawio-util-for-editor.ts

@@ -1,4 +1,4 @@
-import { EditorView } from '@codemirror/view';
+import type { EditorView } from '@growi/editor';
 
 const lineBeginPartOfDrawioRE = /^```(\s.*)drawio$/;
 const lineEndPartOfDrawioRE = /^```$/;

+ 1 - 1
apps/app/src/client/components/PageEditor/markdown-table-util-for-editor.ts

@@ -1,4 +1,4 @@
-import type { EditorView } from '@codemirror/view';
+import type { EditorView } from '@growi/editor';
 import { MarkdownTable } from '@growi/editor';
 
 // https://regex101.com/r/7BN2fR/10

+ 2 - 3
apps/app/src/client/components/SearchPage/SearchResultList.tsx

@@ -5,7 +5,7 @@ import React, {
 
 import {
   type IPageInfoForListing, type IPageWithMeta, isIPageInfoForListing,
-} from '@growi/core';
+} from '@growi/core/dist/interfaces';
 import { useTranslation } from 'next-i18next';
 
 import type { ISelectable, ISelectableAll } from '~/client/interfaces/selectable-all';
@@ -130,8 +130,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
         return (
           <PageListItemL
             key={page.data._id}
-            // eslint-disable-next-line no-return-assign
-            ref={c => itemsRef.current[i] = c}
+            ref={(c) => { itemsRef.current[i] = c }}
             page={page}
             isEnableActions={!isGuestUser}
             isReadOnlyUser={!!isReadOnlyUser}

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

@@ -31,7 +31,7 @@ export default class AdminHomeContainer extends Container {
       growiVersion: null,
       nodeVersion: null,
       npmVersion: null,
-      yarnVersion: null,
+      pnpmVersion: null,
       copyState: this.copyStateValues.DEFAULT,
       installedPlugins: null,
       isV5Compatible: null,
@@ -64,7 +64,7 @@ export default class AdminHomeContainer extends Container {
         growiVersion: adminHomeParams.growiVersion,
         nodeVersion: adminHomeParams.nodeVersion,
         npmVersion: adminHomeParams.npmVersion,
-        yarnVersion: adminHomeParams.yarnVersion,
+        pnpmVersion: adminHomeParams.pnpmVersion,
         envVars: adminHomeParams.envVars,
         isV5Compatible: adminHomeParams.isV5Compatible,
         isMaintenanceMode: adminHomeParams.isMaintenanceMode,
@@ -103,7 +103,7 @@ export default class AdminHomeContainer extends Container {
 |GROWI     |${this.state.growiVersion}|
 |node.js   |${this.state.nodeVersion}|
 |npm       |${this.state.npmVersion}|
-|yarn      |${this.state.yarnVersion}|
+|pnpm      |${this.state.pnpmVersion}|
 |Using Docker|yes/no|
 |Using [growi-docker-compose][growi-docker-compose]|yes/no|
 

+ 14 - 3
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -13,6 +13,17 @@ Object.entries<object>(oneDark).forEach(([key, value]) => {
 });
 
 
+type InlineCodeBlockProps = {
+  children: ReactNode,
+  className?: string,
+}
+
+const InlineCodeBlockSubstance = (props: InlineCodeBlockProps): JSX.Element => {
+  const { children, className, ...rest } = props;
+  return <code className={`code-inline ${className ?? ''}`} {...rest}>{children}</code>;
+};
+
+
 function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
 
   if (children == null) {
@@ -70,15 +81,15 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
 type CodeBlockProps = {
   children: ReactNode,
   className?: string,
-  inline?: string, // "" or undefined
+  inline?: true,
 }
 
 export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
 
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
   const { className, children, inline } = props;
-  if (inline != null) {
-    return <code className={`code-inline ${className ?? ''}`}>{children}</code>;
+  if (inline) {
+    return <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>{children}</InlineCodeBlockSubstance>;
   }
 
   const match = /language-(\w+)(:?.+)?/.exec(className || '');

+ 6 - 18
apps/app/src/features/callout/components/CalloutViewer.module.scss

@@ -1,24 +1,12 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 
 // == Colors
-@include bs.color-mode(light) {
-  .callout-viewer {
-    --callout-accent-note: hsl(212, 92%, 45%);
-    --callout-accent-tip: hsl(137, 66%, 30%);
-    --callout-accent-important: hsl(261, 69%, 59%);
-    --callout-accent-warning: hsl(40, 100%, 30%);
-    --callout-accent-caution: hsl(356, 71%, 48%);
-  }
-}
-
-@include bs.color-mode(dark) {
-  .callout-viewer {
-    --callout-accent-note: hsl(215, 93%, 58%);
-    --callout-accent-tip: hsl(128, 49%, 49%);
-    --callout-accent-important: hsl(262, 89%, 71%);
-    --callout-accent-warning: hsl(41, 72%, 48%);
-    --callout-accent-caution: hsl(3, 93%, 63%);
-  }
+.callout-viewer {
+  --callout-accent-note: var(--bs-info);
+  --callout-accent-tip: var(--bs-success);
+  --callout-accent-important: var(--bs-primary);
+  --callout-accent-warning: var(--bs-warning);
+  --callout-accent-caution: var(--bs-danger);
 }
 
 .callout-viewer :global{

+ 1 - 1
apps/app/src/features/callout/services/callout.ts

@@ -11,7 +11,7 @@ export const remarkPlugin: Plugin = () => {
         const data = node.data ?? (node.data = {});
         data.hName = 'callout';
         data.hProperties = {
-          name: node.name,
+          name: node.name.toLocaleLowerCase(),
         };
       }
     });

+ 55 - 10
apps/app/src/features/openai/chat/components/AiChatModal/AiChatModal.tsx

@@ -4,15 +4,17 @@ import React, { useCallback, useEffect, useState } from 'react';
 import { useForm, Controller } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import {
+  Collapse,
   Modal, ModalBody, ModalFooter, ModalHeader,
 } from 'reactstrap';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
+import { useGrowiCloudUri } from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 
 import { useRagSearchModal } from '../../../client/stores/rag-search';
-import { MessageErrorCode } from '../../../interfaces/message-error';
+import { MessageErrorCode, StreamErrorCode } from '../../../interfaces/message-error';
 
 import { MessageCard } from './MessageCard';
 import { ResizableTextarea } from './ResizableTextArea';
@@ -47,6 +49,10 @@ const AiChatModalSubstance = (): JSX.Element => {
   const [threadId, setThreadId] = useState<string | undefined>();
   const [messageLogs, setMessageLogs] = useState<Message[]>([]);
   const [generatingAnswerMessage, setGeneratingAnswerMessage] = useState<Message>();
+  const [errorMessage, setErrorMessage] = useState<string | undefined>();
+  const [isErrorDetailCollapsed, setIsErrorDetailCollapsed] = useState<boolean>(false);
+
+  const { data: growiCloudUri } = useGrowiCloudUri();
 
   const isGenerating = generatingAnswerMessage != null;
 
@@ -92,6 +98,7 @@ const AiChatModalSubstance = (): JSX.Element => {
 
     // reset form
     form.reset();
+    setErrorMessage(undefined);
 
     // add an empty assistant message
     const newAnswerMessage = { id: (logLength + 1).toString(), content: '' };
@@ -141,14 +148,25 @@ const AiChatModalSubstance = (): JSX.Element => {
 
         const chunk = decoder.decode(value);
 
-        // Extract text values from the chunk
-        const textValues = chunk
-          .split('\n\n')
-          .filter(line => line.trim().startsWith('data:'))
-          .map((line) => {
+        const textValues: string[] = [];
+        const lines = chunk.split('\n\n');
+        lines.forEach((line) => {
+          const trimedLine = line.trim();
+          if (trimedLine.startsWith('data:')) {
             const data = JSON.parse(line.replace('data: ', ''));
-            return data.content[0].text.value;
-          });
+            textValues.push(data.content[0].text.value);
+          }
+          else if (trimedLine.startsWith('error:')) {
+            const error = JSON.parse(line.replace('error: ', ''));
+            logger.error(error.errorMessage);
+            form.setError('input', { type: 'manual', message: error.message });
+
+            if (error.code === StreamErrorCode.BUDGET_EXCEEDED) {
+              setErrorMessage(growiCloudUri != null ? 'modal_aichat.budget_exceeded_for_growi_cloud' : 'modal_aichat.budget_exceeded');
+            }
+          }
+        });
+
 
         // append text values to the assistant message
         setGeneratingAnswerMessage((prevMessage) => {
@@ -168,7 +186,7 @@ const AiChatModalSubstance = (): JSX.Element => {
       form.setError('input', { type: 'manual', message: err.toString() });
     }
 
-  }, [form, isGenerating, messageLogs, t, threadId]);
+  }, [form, growiCloudUri, isGenerating, messageLogs, t, threadId]);
 
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
     if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
@@ -224,7 +242,34 @@ const AiChatModalSubstance = (): JSX.Element => {
         </form>
 
         {form.formState.errors.input != null && (
-          <span className="text-danger small">{form.formState.errors.input?.message}</span>
+          <div className="mt-4 bg-danger bg-opacity-10 rounded-3 p-2 w-100">
+            <div>
+              <span className="material-symbols-outlined text-danger me-2">error</span>
+              <span className="text-danger">{ errorMessage != null ? t(errorMessage) : t('modal_aichat.error_message') }</span>
+            </div>
+
+            <button
+              type="button"
+              className="btn btn-link text-secondary p-0"
+              aria-expanded={isErrorDetailCollapsed}
+              onClick={() => setIsErrorDetailCollapsed(!isErrorDetailCollapsed)}
+            >
+              <span className={`material-symbols-outlined mt-2 me-1 ${isErrorDetailCollapsed ? 'rotate-90' : ''}`}>
+                chevron_right
+              </span>
+              <span className="small">{t('modal_aichat.show_error_detail')}</span>
+            </button>
+
+            <Collapse isOpen={isErrorDetailCollapsed}>
+              <div className="ms-2">
+                <div className="">
+                  <div className="text-secondary small">
+                    {form.formState.errors.input?.message}
+                  </div>
+                </div>
+              </div>
+            </Collapse>
+          </div>
         )}
       </ModalFooter>
     </>

+ 6 - 0
apps/app/src/features/openai/interfaces/message-error.ts

@@ -1,3 +1,9 @@
 export const MessageErrorCode = {
   THREAD_ID_IS_NOT_SET: 'thread-id-is-not-set',
 } as const;
+
+export const StreamErrorCode = {
+  BUDGET_EXCEEDED: 'budget-exceeded',
+} as const;
+
+export type StreamErrorCode = typeof StreamErrorCode[keyof typeof StreamErrorCode];

+ 57 - 0
apps/app/src/features/openai/server/models/thread-relation.ts

@@ -0,0 +1,57 @@
+import type mongoose from 'mongoose';
+import { type Model, type Document, Schema } from 'mongoose';
+
+import { getOrCreateModel } from '~/server/util/mongoose-utils';
+
+const DAYS_UNTIL_EXPIRATION = 30;
+
+const generateExpirationDate = (): Date => {
+  const currentDate = new Date();
+  const expirationDate = new Date(currentDate.setDate(currentDate.getDate() + DAYS_UNTIL_EXPIRATION));
+  return expirationDate;
+};
+
+interface ThreadRelation {
+  userId: mongoose.Types.ObjectId;
+  threadId: string;
+  expiredAt: Date;
+}
+
+interface ThreadRelationDocument extends ThreadRelation, Document {
+  updateThreadExpiration(): Promise<void>;
+}
+
+interface ThreadRelationModel extends Model<ThreadRelationDocument> {
+  getExpiredThreadRelations(limit?: number): Promise<ThreadRelationDocument[] | undefined>;
+}
+
+const schema = new Schema<ThreadRelationDocument, ThreadRelationModel>({
+  userId: {
+    type: Schema.Types.ObjectId,
+    ref: 'User',
+    required: true,
+  },
+  threadId: {
+    type: String,
+    required: true,
+    unique: true,
+  },
+  expiredAt: {
+    type: Date,
+    default: generateExpirationDate,
+    required: true,
+  },
+});
+
+schema.statics.getExpiredThreadRelations = async function(limit?: number): Promise<ThreadRelationDocument[] | undefined> {
+  const currentDate = new Date();
+  const expiredThreadRelations = await this.find({ expiredAt: { $lte: currentDate } }).limit(limit ?? 100).exec();
+  return expiredThreadRelations;
+};
+
+schema.methods.updateThreadExpiration = async function(): Promise<void> {
+  this.expiredAt = generateExpirationDate();
+  await this.save();
+};
+
+export default getOrCreateModel<ThreadRelationDocument, ThreadRelationModel>('ThreadRelation', schema);

+ 31 - 4
apps/app/src/features/openai/server/models/vector-store-file-relation.ts

@@ -5,18 +5,21 @@ import { type Model, type Document, Schema } from 'mongoose';
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
 export interface VectorStoreFileRelation {
+  vectorStoreRelationId: mongoose.Types.ObjectId;
   pageId: mongoose.Types.ObjectId;
   fileIds: string[];
+  isAttachedToVectorStore: boolean;
 }
 
 interface VectorStoreFileRelationDocument extends VectorStoreFileRelation, Document {}
 
 interface VectorStoreFileRelationModel extends Model<VectorStoreFileRelation> {
   upsertVectorStoreFileRelations(vectorStoreFileRelations: VectorStoreFileRelation[]): Promise<void>;
+  markAsAttachedToVectorStore(pageIds: Types.ObjectId[]): Promise<void>;
 }
 
 export const prepareVectorStoreFileRelations = (
-    pageId: Types.ObjectId, fileId: string, relationsMap: Map<string, VectorStoreFileRelation>,
+    vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId, fileId: string, relationsMap: Map<string, VectorStoreFileRelation>,
 ): Map<string, VectorStoreFileRelation> => {
   const pageIdStr = pageId.toHexString();
   const existingData = relationsMap.get(pageIdStr);
@@ -28,8 +31,10 @@ export const prepareVectorStoreFileRelations = (
   // If the data doesn't exist, create a new one and add it to the map
   else {
     relationsMap.set(pageIdStr, {
+      vectorStoreRelationId,
       pageId,
       fileIds: [fileId],
+      isAttachedToVectorStore: false,
     });
   }
 
@@ -37,25 +42,39 @@ export const prepareVectorStoreFileRelations = (
 };
 
 const schema = new Schema<VectorStoreFileRelationDocument, VectorStoreFileRelationModel>({
+  vectorStoreRelationId: {
+    type: Schema.Types.ObjectId,
+    ref: 'VectorStore',
+    required: true,
+  },
   pageId: {
     type: Schema.Types.ObjectId,
     ref: 'Page',
     required: true,
-    unique: true,
   },
   fileIds: [{
     type: String,
     required: true,
   }],
+  isAttachedToVectorStore: {
+    type: Boolean,
+    default: false, // File is not attached to the Vector Store at the time it is uploaded
+    required: true,
+  },
 });
 
+// define unique compound index
+schema.index({ vectorStoreRelationId: 1, pageId: 1 }, { unique: true });
+
 schema.statics.upsertVectorStoreFileRelations = async function(vectorStoreFileRelations: VectorStoreFileRelation[]): Promise<void> {
   await this.bulkWrite(
     vectorStoreFileRelations.map((data) => {
       return {
         updateOne: {
-          filter: { pageId: data.pageId },
-          update: { $addToSet: { fileIds: { $each: data.fileIds } } },
+          filter: { pageId: data.pageId, vectorStoreRelationId: data.vectorStoreRelationId },
+          update: {
+            $addToSet: { fileIds: { $each: data.fileIds } },
+          },
           upsert: true,
         },
       };
@@ -63,4 +82,12 @@ schema.statics.upsertVectorStoreFileRelations = async function(vectorStoreFileRe
   );
 };
 
+// Used when attached to VectorStore
+schema.statics.markAsAttachedToVectorStore = async function(pageIds: Types.ObjectId[]): Promise<void> {
+  await this.updateMany(
+    { pageId: { $in: pageIds } },
+    { $set: { isAttachedToVectorStore: true } },
+  );
+};
+
 export default getOrCreateModel<VectorStoreFileRelationDocument, VectorStoreFileRelationModel>('VectorStoreFileRelation', schema);

+ 16 - 3
apps/app/src/features/openai/server/models/vector-store.ts

@@ -11,10 +11,13 @@ export type VectorStoreScopeType = typeof VectorStoreScopeType[keyof typeof Vect
 const VectorStoreScopeTypes = Object.values(VectorStoreScopeType);
 interface VectorStore {
   vectorStoreId: string
-  scorpeType: VectorStoreScopeType
+  scopeType: VectorStoreScopeType
+  isDeleted: boolean
 }
 
-export interface VectorStoreDocument extends VectorStore, Document {}
+export interface VectorStoreDocument extends VectorStore, Document {
+  markAsDeleted(): Promise<void>
+}
 
 type VectorStoreModel = Model<VectorStore>
 
@@ -24,11 +27,21 @@ const schema = new Schema<VectorStoreDocument, VectorStoreModel>({
     required: true,
     unique: true,
   },
-  scorpeType: {
+  scopeType: {
     enum: VectorStoreScopeTypes,
     type: String,
     required: true,
   },
+  isDeleted: {
+    type: Boolean,
+    default: false,
+    required: true,
+  },
 });
 
+schema.methods.markAsDeleted = async function(): Promise<void> {
+  this.isDeleted = true;
+  await this.save();
+};
+
 export default getOrCreateModel<VectorStoreDocument, VectorStoreModel>('VectorStore', schema);

+ 17 - 2
apps/app/src/features/openai/server/routes/message.ts

@@ -7,12 +7,14 @@ import type { MessageDelta } from 'openai/resources/beta/threads/messages.mjs';
 
 import { getOrCreateChatAssistant } from '~/features/openai/server/services/assistant';
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { MessageErrorCode } from '../../interfaces/message-error';
+import { MessageErrorCode, type StreamErrorCode } from '../../interfaces/message-error';
 import { openaiClient } from '../services';
+import { getStreamErrorCode } from '../services/getStreamErrorCode';
 
 import { certifyAiService } from './middlewares/certify-ai-service';
 
@@ -29,7 +31,6 @@ type Req = Request<undefined, Response, ReqBody>
 type PostMessageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
 export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [
@@ -80,6 +81,20 @@ export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) =>
         res.write(`data: ${JSON.stringify(delta)}\n\n`);
       };
 
+      const sendError = (message: string, code?: StreamErrorCode) => {
+        res.write(`error: ${JSON.stringify({ code, message })}\n\n`);
+      };
+
+      stream.on('event', (delta) => {
+        if (delta.event === 'thread.run.failed') {
+          const errorMessage = delta.data.last_error?.message;
+          if (errorMessage == null) {
+            return;
+          }
+          logger.error(errorMessage);
+          sendError(errorMessage, getStreamErrorCode(errorMessage));
+        }
+      });
       stream.on('messageDelta', messageDeltaHandler);
       stream.once('messageDone', () => {
         stream.off('messageDelta', messageDeltaHandler);

+ 1 - 1
apps/app/src/features/openai/server/routes/rebuild-vector-store.ts

@@ -3,6 +3,7 @@ import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
@@ -16,7 +17,6 @@ const logger = loggerFactory('growi:routes:apiv3:openai:rebuild-vector-store');
 type RebuildVectorStoreFactory = (crowi: Crowi) => RequestHandler[];
 
 export const rebuildVectorStoreHandlersFactory: RebuildVectorStoreFactory = (crowi) => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
 

+ 8 - 23
apps/app/src/features/openai/server/routes/thread.ts

@@ -1,28 +1,26 @@
+import type { IUserHasId } from '@growi/core/dist/interfaces';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
+import { filterXSS } from 'xss';
 
 import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { openaiClient } from '../services';
 import { getOpenaiService } from '../services/openai';
 
 import { certifyAiService } from './middlewares/certify-ai-service';
 
 const logger = loggerFactory('growi:routes:apiv3:openai:thread');
 
-type CreateThreadReq = Request<undefined, ApiV3Response, {
-  userMessage: string,
-  threadId?: string,
-}>
+type CreateThreadReq = Request<undefined, ApiV3Response, { threadId?: string }> & { user: IUserHasId };
 
 type CreateThreadFactory = (crowi: Crowi) => RequestHandler[];
 
 export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   const validator: ValidationChain[] = [
@@ -32,24 +30,11 @@ export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
   return [
     accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: CreateThreadReq, res: ApiV3Response) => {
-      const openaiService = getOpenaiService();
-      if (openaiService == null) {
-        return res.apiv3Err('OpenaiService is not available', 503);
-      }
-
       try {
-        const vectorStore = await openaiService.getOrCreateVectorStoreForPublicScope();
-        const threadId = req.body.threadId;
-        const thread = threadId == null
-          ? await openaiClient.beta.threads.create({
-            tool_resources: {
-              file_search: {
-                vector_store_ids: [vectorStore.vectorStoreId],
-              },
-            },
-          })
-          : await openaiClient.beta.threads.retrieve(threadId);
-
+        const openaiService = getOpenaiService();
+        const filterdThreadId = req.body.threadId != null ? filterXSS(req.body.threadId) : undefined;
+        const vectorStore = await openaiService?.getOrCreateVectorStoreForPublicScope();
+        const thread = await openaiService?.getOrCreateThread(req.user._id, vectorStore?.vectorStoreId, filterdThreadId);
         return res.apiv3({ thread });
       }
       catch (err) {

+ 22 - 0
apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts

@@ -22,6 +22,24 @@ export class AzureOpenaiClientDelegator implements IOpenaiClientDelegator {
     // TODO: initialize openaiVectorStoreId property
   }
 
+  async createThread(vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread> {
+    return this.client.beta.threads.create({
+      tool_resources: {
+        file_search: {
+          vector_store_ids: [vectorStoreId],
+        },
+      },
+    });
+  }
+
+  async retrieveThread(threadId: string): Promise<OpenAI.Beta.Threads.Thread> {
+    return this.client.beta.threads.retrieve(threadId);
+  }
+
+  async deleteThread(threadId: string): Promise<OpenAI.Beta.Threads.ThreadDeleted> {
+    return this.client.beta.threads.del(threadId);
+  }
+
   async createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore> {
     return this.client.beta.vectorStores.create({ name: `growi-vector-store-{${scopeType}` });
   }
@@ -30,6 +48,10 @@ export class AzureOpenaiClientDelegator implements IOpenaiClientDelegator {
     return this.client.beta.vectorStores.retrieve(vectorStoreId);
   }
 
+  async deleteVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStoreDeleted> {
+    return this.client.beta.vectorStores.del(vectorStoreId);
+  }
+
   async uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject> {
     return this.client.files.create({ file, purpose: 'assistants' });
   }

+ 4 - 0
apps/app/src/features/openai/server/services/client-delegator/interfaces.ts

@@ -4,8 +4,12 @@ import type { Uploadable } from 'openai/uploads';
 import type { VectorStoreScopeType } from '~/features/openai/server/models/vector-store';
 
 export interface IOpenaiClientDelegator {
+  createThread(vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread>
+  retrieveThread(threadId: string): Promise<OpenAI.Beta.Threads.Thread>
+  deleteThread(threadId: string): Promise<OpenAI.Beta.Threads.ThreadDeleted>
   retrieveVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStore>
   createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore>
+  deleteVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStoreDeleted>
   uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject>
   createVectorStoreFileBatch(vectorStoreId: string, fileIds: string[]): Promise<OpenAI.Beta.VectorStores.FileBatches.VectorStoreFileBatch>
   deleteFile(fileId: string): Promise<OpenAI.Files.FileDeleted>;

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

@@ -24,6 +24,24 @@ export class OpenaiClientDelegator implements IOpenaiClientDelegator {
     this.client = new OpenAI({ apiKey });
   }
 
+  async createThread(vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread> {
+    return this.client.beta.threads.create({
+      tool_resources: {
+        file_search: {
+          vector_store_ids: [vectorStoreId],
+        },
+      },
+    });
+  }
+
+  async retrieveThread(threadId: string): Promise<OpenAI.Beta.Threads.Thread> {
+    return this.client.beta.threads.retrieve(threadId);
+  }
+
+  async deleteThread(threadId: string): Promise<OpenAI.Beta.Threads.ThreadDeleted> {
+    return this.client.beta.threads.del(threadId);
+  }
+
   async createVectorStore(scopeType:VectorStoreScopeType): Promise<OpenAI.Beta.VectorStores.VectorStore> {
     return this.client.beta.vectorStores.create({ name: `growi-vector-store-${scopeType}` });
   }
@@ -32,6 +50,10 @@ export class OpenaiClientDelegator implements IOpenaiClientDelegator {
     return this.client.beta.vectorStores.retrieve(vectorStoreId);
   }
 
+  async deleteVectorStore(vectorStoreId: string): Promise<OpenAI.Beta.VectorStores.VectorStoreDeleted> {
+    return this.client.beta.vectorStores.del(vectorStoreId);
+  }
+
   async uploadFile(file: Uploadable): Promise<OpenAI.Files.FileObject> {
     return this.client.files.create({ file, purpose: 'assistants' });
   }

+ 13 - 0
apps/app/src/features/openai/server/services/getStreamErrorCode.ts

@@ -0,0 +1,13 @@
+import { StreamErrorCode } from '../../interfaces/message-error';
+
+const OpenaiStreamErrorMessageRegExp = {
+  BUDGET_EXCEEDED: /exceeded your current quota/i, // stream-error-message: "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors."
+} as const;
+
+export const getStreamErrorCode = (errorMessage: string): StreamErrorCode | undefined => {
+  for (const [code, regExp] of Object.entries(OpenaiStreamErrorMessageRegExp)) {
+    if (regExp.test(errorMessage)) {
+      return StreamErrorCode[code];
+    }
+  }
+};

+ 0 - 0
packages/markdown-splitter/src/services/markdown-splitter.spec.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.spec.ts


+ 14 - 7
packages/markdown-splitter/src/services/markdown-splitter.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-splitter.ts

@@ -1,12 +1,13 @@
+import { dynamicImport } from '@cspell/dynamic-import';
 import type { TiktokenModel } from 'js-tiktoken';
 import { encodingForModel } from 'js-tiktoken';
 import yaml from 'js-yaml';
-import remarkFrontmatter from 'remark-frontmatter'; // Frontmatter processing
-import remarkGfm from 'remark-gfm'; // GFM processing
-import remarkParse from 'remark-parse';
-import type { Options as StringifyOptions } from 'remark-stringify';
-import remarkStringify from 'remark-stringify';
-import { unified } from 'unified';
+import type * as RemarkFrontmatter from 'remark-frontmatter';
+import type * as RemarkGfm from 'remark-gfm';
+import type * as RemarkParse from 'remark-parse';
+import type * as RemarkStringify from 'remark-stringify';
+import type * as Unified from 'unified';
+
 
 export type MarkdownFragment = {
   label: string;
@@ -59,12 +60,18 @@ export async function splitMarkdownIntoFragments(markdownText: string, model: Ti
 
   const encoder = encodingForModel(model);
 
+  const remarkParse = (await dynamicImport<typeof RemarkParse>('remark-parse', __dirname)).default;
+  const remarkFrontmatter = (await dynamicImport<typeof RemarkFrontmatter>('remark-frontmatter', __dirname)).default;
+  const remarkGfm = (await dynamicImport<typeof RemarkGfm>('remark-gfm', __dirname)).default;
+  const remarkStringify = (await dynamicImport<typeof RemarkStringify>('remark-stringify', __dirname)).default;
+  const unified = (await dynamicImport<typeof Unified>('unified', __dirname)).unified;
+
   const parser = unified()
     .use(remarkParse)
     .use(remarkFrontmatter, ['yaml'])
     .use(remarkGfm); // Enable GFM extensions
 
-  const stringifyOptions: StringifyOptions = {
+  const stringifyOptions: RemarkStringify.Options = {
     bullet: '-', // Set list bullet to hyphen
     rule: '-', // Use hyphen for horizontal rules
   };

+ 0 - 0
packages/markdown-splitter/src/services/markdown-token-splitter.spec.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.spec.ts


+ 2 - 2
packages/markdown-splitter/src/services/markdown-token-splitter.ts → apps/app/src/features/openai/server/services/markdown-splitter/markdown-token-splitter.ts

@@ -105,7 +105,7 @@ export async function splitMarkdownIntoChunks(
 
   // Split markdown text into chunks
   const markdownFragments = await splitMarkdownIntoFragments(markdownText, model);
-  const chunks = [] as string[];
+  const chunks: string[] = [];
 
   // Group the chunks based on token count
   const fragmentGroupes = groupMarkdownFragments(markdownFragments, maxToken);
@@ -162,7 +162,7 @@ export async function splitMarkdownIntoChunks(
             const charCountForSplit = Math.floor((remainingTokenCount / fragmenTokenCount) * fragmentCharCount);
 
             // Split content based on character count
-            const splitContents = [];
+            const splitContents: string[] = [];
             for (let i = 0; i < fragment.text.length; i += charCountForSplit) {
               splitContents.push(fragment.text.slice(i, i + charCountForSplit));
             }

+ 29 - 0
apps/app/src/features/openai/server/services/openai-api-error-handler.ts

@@ -0,0 +1,29 @@
+import OpenAI from 'openai';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:service:openai');
+
+// Error Code Reference
+// https://platform.openai.com/docs/guides/error-codes/api-errors
+
+// Error Handling Reference
+// https://github.com/openai/openai-node/tree/d08bf1a8fa779e6a9349d92ddf65530dd84e686d?tab=readme-ov-file#handling-errors
+
+type ErrorHandler = {
+  notFoundError?: () => Promise<void>;
+}
+
+export const oepnaiApiErrorHandler = async(error: unknown, handler: ErrorHandler): Promise<void> => {
+  if (!(error instanceof OpenAI.APIError)) {
+    return;
+  }
+
+  logger.error(error);
+
+  if (error.status === 404 && handler.notFoundError != null) {
+    await handler.notFoundError();
+    return;
+  }
+
+};

+ 187 - 25
apps/app/src/features/openai/server/services/openai.ts

@@ -7,6 +7,7 @@ import mongoose from 'mongoose';
 import type OpenAI from 'openai';
 import { toFile } from 'openai';
 
+import ThreadRelationModel from '~/features/openai/server/models/thread-relation';
 import VectorStoreModel, { VectorStoreScopeType, type VectorStoreDocument } from '~/features/openai/server/models/vector-store';
 import VectorStoreFileRelationModel, {
   type VectorStoreFileRelation,
@@ -19,8 +20,9 @@ import loggerFactory from '~/utils/logger';
 
 import { OpenaiServiceTypes } from '../../interfaces/ai';
 
-
 import { getClient } from './client-delegator';
+// import { splitMarkdownIntoChunks } from './markdown-splitter/markdown-token-splitter';
+import { oepnaiApiErrorHandler } from './openai-api-error-handler';
 
 const BATCH_SIZE = 100;
 
@@ -28,10 +30,16 @@ const logger = loggerFactory('growi:service:openai');
 
 let isVectorStoreForPublicScopeExist = false;
 
+type VectorStoreFileRelationsMap = Map<string, VectorStoreFileRelation>
+
 export interface IOpenaiService {
+  getOrCreateThread(userId: string, vectorStoreId?: string, threadId?: string): Promise<OpenAI.Beta.Threads.Thread | undefined>;
   getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument>;
+  deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
+  deleteObsolatedVectorStoreRelations(): Promise<void> // for CronJob
   createVectorStoreFile(pages: PageDocument[]): Promise<void>;
-  deleteVectorStoreFile(pageId: Types.ObjectId): Promise<void>;
+  deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId): Promise<void>;
+  deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
   rebuildVectorStoreAll(): Promise<void>;
   rebuildVectorStore(page: HydratedDocument<PageDocument>): Promise<void>;
 }
@@ -42,52 +50,147 @@ class OpenaiService implements IOpenaiService {
     return getClient({ openaiServiceType });
   }
 
+  public async getOrCreateThread(userId: string, vectorStoreId?: string, threadId?: string): Promise<OpenAI.Beta.Threads.Thread> {
+    if (vectorStoreId != null && threadId == null) {
+      try {
+        const thread = await this.client.createThread(vectorStoreId);
+        await ThreadRelationModel.create({ userId, threadId: thread.id });
+        return thread;
+      }
+      catch (err) {
+        throw new Error(err);
+      }
+    }
+
+    const threadRelation = await ThreadRelationModel.findOne({ threadId });
+    if (threadRelation == null) {
+      throw new Error('ThreadRelation document is not exists');
+    }
+
+    // Check if a thread entity exists
+    // If the thread entity does not exist, the thread-relation document is deleted
+    try {
+      const thread = await this.client.retrieveThread(threadRelation.threadId);
+
+      // Update expiration date if thread entity exists
+      await threadRelation.updateThreadExpiration();
+
+      return thread;
+    }
+    catch (err) {
+      await oepnaiApiErrorHandler(err, { notFoundError: async() => { await threadRelation.remove() } });
+      throw new Error(err);
+    }
+  }
+
+  public async deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void> {
+    const expiredThreadRelations = await ThreadRelationModel.getExpiredThreadRelations(limit);
+    if (expiredThreadRelations == null) {
+      return;
+    }
+
+    const deletedThreadIds: string[] = [];
+    for await (const expiredThreadRelation of expiredThreadRelations) {
+      try {
+        const deleteThreadResponse = await this.client.deleteThread(expiredThreadRelation.threadId);
+        logger.debug('Delete thread', deleteThreadResponse);
+        deletedThreadIds.push(expiredThreadRelation.threadId);
+
+        // sleep
+        await new Promise(resolve => setTimeout(resolve, apiCallInterval));
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    }
+
+    await ThreadRelationModel.deleteMany({ threadId: { $in: deletedThreadIds } });
+  }
+
   public async getOrCreateVectorStoreForPublicScope(): Promise<VectorStoreDocument> {
-    const vectorStoreDocument = await VectorStoreModel.findOne({ scorpeType: VectorStoreScopeType.PUBLIC });
+    const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: VectorStoreScopeType.PUBLIC, isDeleted: false });
 
     if (vectorStoreDocument != null && isVectorStoreForPublicScopeExist) {
       return vectorStoreDocument;
     }
 
     if (vectorStoreDocument != null && !isVectorStoreForPublicScopeExist) {
-      const vectorStore = await this.client.retrieveVectorStore(vectorStoreDocument.vectorStoreId);
-      if (vectorStore != null) {
+      try {
+        // Check if vector store entity exists
+        // If the vector store entity does not exist, the vector store document is deleted
+        await this.client.retrieveVectorStore(vectorStoreDocument.vectorStoreId);
         isVectorStoreForPublicScopeExist = true;
         return vectorStoreDocument;
       }
+      catch (err) {
+        await oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
+        throw new Error(err);
+      }
     }
 
     const newVectorStore = await this.client.createVectorStore(VectorStoreScopeType.PUBLIC);
     const newVectorStoreDocument = await VectorStoreModel.create({
       vectorStoreId: newVectorStore.id,
-      scorpeType: VectorStoreScopeType.PUBLIC,
-    });
+      scopeType: VectorStoreScopeType.PUBLIC,
+    }) as VectorStoreDocument;
 
     isVectorStoreForPublicScopeExist = true;
 
     return newVectorStoreDocument;
   }
 
+  // TODO: https://redmine.weseek.co.jp/issues/156643
+  // private async uploadFileByChunks(pageId: Types.ObjectId, body: string, vectorStoreFileRelationsMap: VectorStoreFileRelationsMap) {
+  //   const chunks = await splitMarkdownIntoChunks(body, 'gpt-4o');
+  //   for await (const [index, chunk] of chunks.entries()) {
+  //     try {
+  //       const file = await toFile(Readable.from(chunk), `${pageId}-chunk-${index}.md`);
+  //       const uploadedFile = await this.client.uploadFile(file);
+  //       prepareVectorStoreFileRelations(pageId, uploadedFile.id, vectorStoreFileRelationsMap);
+  //     }
+  //     catch (err) {
+  //       logger.error(err);
+  //     }
+  //   }
+  // }
+
   private async uploadFile(pageId: Types.ObjectId, body: string): Promise<OpenAI.Files.FileObject> {
     const file = await toFile(Readable.from(body), `${pageId}.md`);
     const uploadedFile = await this.client.uploadFile(file);
     return uploadedFile;
   }
 
-  async createVectorStoreFile(pages: Array<PageDocument>): Promise<void> {
-    const vectorStoreFileRelationsMap: Map<string, VectorStoreFileRelation> = new Map();
+  private async deleteVectorStore(vectorStoreScopeType: VectorStoreScopeType): Promise<void> {
+    const vectorStoreDocument: VectorStoreDocument | null = await VectorStoreModel.findOne({ scopeType: vectorStoreScopeType, isDeleted: false });
+    if (vectorStoreDocument == null) {
+      return;
+    }
+
+    try {
+      await this.client.deleteVectorStore(vectorStoreDocument.vectorStoreId);
+      await vectorStoreDocument.markAsDeleted();
+    }
+    catch (err) {
+      await oepnaiApiErrorHandler(err, { notFoundError: vectorStoreDocument.markAsDeleted });
+      throw new Error(err);
+    }
+  }
+
+  async createVectorStoreFile(pages: Array<HydratedDocument<PageDocument>>): Promise<void> {
+    const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
+    const vectorStoreFileRelationsMap: VectorStoreFileRelationsMap = new Map();
     const processUploadFile = async(page: PageDocument) => {
       if (page._id != null && page.grant === PageGrant.GRANT_PUBLIC && page.revision != null) {
         if (isPopulated(page.revision) && page.revision.body.length > 0) {
           const uploadedFile = await this.uploadFile(page._id, page.revision.body);
-          prepareVectorStoreFileRelations(page._id, uploadedFile.id, vectorStoreFileRelationsMap);
+          prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
           return;
         }
 
         const pagePopulatedToShowRevision = await page.populateDataToShowRevision();
         if (pagePopulatedToShowRevision.revision != null && pagePopulatedToShowRevision.revision.body.length > 0) {
           const uploadedFile = await this.uploadFile(page._id, pagePopulatedToShowRevision.revision.body);
-          prepareVectorStoreFileRelations(page._id, uploadedFile.id, vectorStoreFileRelationsMap);
+          prepareVectorStoreFileRelations(vectorStore._id, page._id, uploadedFile.id, vectorStoreFileRelationsMap);
         }
       }
     };
@@ -111,43 +214,75 @@ class OpenaiService implements IOpenaiService {
       return;
     }
 
+    const pageIds = pages.map(page => page._id);
+
     try {
+      // Save vector store file relation
+      await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
+
       // Create vector store file
-      const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
       const createVectorStoreFileBatchResponse = await this.client.createVectorStoreFileBatch(vectorStore.vectorStoreId, uploadedFileIds);
       logger.debug('Create vector store file', createVectorStoreFileBatchResponse);
 
-      // Save vector store file relation
-      await VectorStoreFileRelationModel.upsertVectorStoreFileRelations(vectorStoreFileRelations);
+      // Set isAttachedToVectorStore: true when the uploaded file is attached to VectorStore
+      await VectorStoreFileRelationModel.markAsAttachedToVectorStore(pageIds);
     }
     catch (err) {
       logger.error(err);
 
       // Delete all uploaded files if createVectorStoreFileBatch fails
-      uploadedFileIds.forEach(async(fileId) => {
-        const deleteFileResponse = await this.client.deleteFile(fileId);
-        logger.debug('Delete vector store file (Due to createVectorStoreFileBatch failure)', deleteFileResponse);
-      });
+      for await (const pageId of pageIds) {
+        await this.deleteVectorStoreFile(vectorStore._id, pageId);
+      }
     }
 
   }
 
-  async deleteVectorStoreFile(pageId: Types.ObjectId): Promise<void> {
+  // Deletes all VectorStore documents that are marked as deleted (isDeleted: true) and have no associated VectorStoreFileRelation documents
+  async deleteObsolatedVectorStoreRelations(): Promise<void> {
+    const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
+    if (deletedVectorStoreRelations.length === 0) {
+      return;
+    }
+
+    const currentVectorStoreRelationIds: Types.ObjectId[] = await VectorStoreFileRelationModel.aggregate([
+      {
+        $group: {
+          _id: '$vectorStoreRelationId',
+          relationCount: { $sum: 1 },
+        },
+      },
+      { $match: { relationCount: { $gt: 0 } } },
+      { $project: { _id: 1 } },
+    ]);
+
+    if (currentVectorStoreRelationIds.length === 0) {
+      return;
+    }
+
+    await VectorStoreModel.deleteMany({ _id: { $nin: currentVectorStoreRelationIds }, isDeleted: true });
+  }
+
+  async deleteVectorStoreFile(vectorStoreRelationId: Types.ObjectId, pageId: Types.ObjectId, apiCallInterval?: number): Promise<void> {
     // Delete vector store file and delete vector store file relation
-    const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ pageId });
+    const vectorStoreFileRelation = await VectorStoreFileRelationModel.findOne({ vectorStoreRelationId, pageId });
     if (vectorStoreFileRelation == null) {
       return;
     }
 
     const deletedFileIds: string[] = [];
-    for (const fileId of vectorStoreFileRelation.fileIds) {
+    for await (const fileId of vectorStoreFileRelation.fileIds) {
       try {
-        // eslint-disable-next-line no-await-in-loop
         const deleteFileResponse = await this.client.deleteFile(fileId);
         logger.debug('Delete vector store file', deleteFileResponse);
         deletedFileIds.push(fileId);
+        if (apiCallInterval != null) {
+          // sleep
+          await new Promise(resolve => setTimeout(resolve, apiCallInterval));
+        }
       }
       catch (err) {
+        await oepnaiApiErrorHandler(err, { notFoundError: async() => { deletedFileIds.push(fileId) } });
         logger.error(err);
       }
     }
@@ -163,8 +298,34 @@ class OpenaiService implements IOpenaiService {
     await vectorStoreFileRelation.save();
   }
 
+  async deleteObsoleteVectorStoreFile(limit: number, apiCallInterval: number): Promise<void> {
+    // Retrieves all VectorStore documents that are marked as deleted
+    const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
+    if (deletedVectorStoreRelations.length === 0) {
+      return;
+    }
+
+    // Retrieves VectorStoreFileRelation documents associated with deleted VectorStore documents
+    const obsoleteVectorStoreFileRelations = await VectorStoreFileRelationModel.find(
+      { vectorStoreRelationId: { $in: deletedVectorStoreRelations.map(deletedVectorStoreRelation => deletedVectorStoreRelation._id) } },
+    ).limit(limit);
+    if (obsoleteVectorStoreFileRelations.length === 0) {
+      return;
+    }
+
+    // Delete obsolete VectorStoreFile
+    for await (const vectorStoreFileRelation of obsoleteVectorStoreFileRelations) {
+      try {
+        await this.deleteVectorStoreFile(vectorStoreFileRelation.vectorStoreRelationId, vectorStoreFileRelation.pageId, apiCallInterval);
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    }
+  }
+
   async rebuildVectorStoreAll() {
-    // TODO: https://redmine.weseek.co.jp/issues/154364
+    await this.deleteVectorStore(VectorStoreScopeType.PUBLIC);
 
     // Create all public pages VectorStoreFile
     const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
@@ -174,7 +335,7 @@ class OpenaiService implements IOpenaiService {
     const createVectorStoreFile = this.createVectorStoreFile.bind(this);
     const createVectorStoreFileStream = new Transform({
       objectMode: true,
-      async transform(chunk: PageDocument[], encoding, callback) {
+      async transform(chunk: HydratedDocument<PageDocument>[], encoding, callback) {
         await createVectorStoreFile(chunk);
         this.push(chunk);
         callback();
@@ -187,7 +348,8 @@ class OpenaiService implements IOpenaiService {
   }
 
   async rebuildVectorStore(page: HydratedDocument<PageDocument>) {
-    await this.deleteVectorStoreFile(page._id);
+    const vectorStore = await this.getOrCreateVectorStoreForPublicScope();
+    await this.deleteVectorStoreFile(vectorStore._id, page._id);
     await this.createVectorStoreFile([page]);
   }
 

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

@@ -0,0 +1,68 @@
+import nodeCron from 'node-cron';
+
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+import { getRandomIntInRange } from '~/utils/rand';
+
+import { getOpenaiService, type IOpenaiService } from './openai';
+
+const logger = loggerFactory('growi:service:thread-deletion-cron');
+
+class ThreadDeletionCronService {
+
+  cronJob: nodeCron.ScheduledTask;
+
+  openaiService: IOpenaiService;
+
+  threadDeletionCronExpression: string;
+
+  threadDeletionBarchSize: number;
+
+  threadDeletionApiCallInterval: number;
+
+  sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
+
+  startCron(): void {
+    const isAiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+    if (!isAiEnabled) {
+      return;
+    }
+
+    const openaiService = getOpenaiService();
+    if (openaiService == null) {
+      throw new Error('OpenAI service is not initialized');
+    }
+
+    this.openaiService = openaiService;
+    this.threadDeletionCronExpression = configManager.getConfig('crowi', 'openai:threadDeletionCronExpression');
+    this.threadDeletionBarchSize = configManager.getConfig('crowi', 'openai:threadDeletionBarchSize');
+    this.threadDeletionApiCallInterval = configManager.getConfig('crowi', 'openai:threadDeletionApiCallInterval');
+
+    this.cronJob?.stop();
+    this.cronJob = this.generateCronJob();
+    this.cronJob.start();
+  }
+
+  private async executeJob(): Promise<void> {
+    // Must be careful of OpenAI's rate limit
+    await this.openaiService.deleteExpiredThreads(this.threadDeletionBarchSize, this.threadDeletionApiCallInterval);
+  }
+
+  private generateCronJob() {
+    return nodeCron.schedule(this.threadDeletionCronExpression, async() => {
+      try {
+        // Sleep for a random number of minutes between 0 and 60 to distribute request load
+        const randomMilliseconds = getRandomIntInRange(0, 60) * 60 * 1000;
+        this.sleep(randomMilliseconds);
+
+        await this.executeJob();
+      }
+      catch (e) {
+        logger.error(e);
+      }
+    });
+  }
+
+}
+
+export default ThreadDeletionCronService;

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

@@ -0,0 +1,68 @@
+import nodeCron from 'node-cron';
+
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+import { getRandomIntInRange } from '~/utils/rand';
+
+import { getOpenaiService, type IOpenaiService } from './openai';
+
+const logger = loggerFactory('growi:service:vector-store-file-deletion-cron');
+
+class VectorStoreFileDeletionCronService {
+
+  cronJob: nodeCron.ScheduledTask;
+
+  openaiService: IOpenaiService;
+
+  vectorStoreFileDeletionCronExpression: string;
+
+  vectorStoreFileDeletionBarchSize: number;
+
+  vectorStoreFileDeletionApiCallInterval: number;
+
+  sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
+
+  startCron(): void {
+    const isAiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+    if (!isAiEnabled) {
+      return;
+    }
+
+    const openaiService = getOpenaiService();
+    if (openaiService == null) {
+      throw new Error('OpenAI service is not initialized');
+    }
+
+    this.openaiService = openaiService;
+    this.vectorStoreFileDeletionCronExpression = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionCronExpression');
+    this.vectorStoreFileDeletionBarchSize = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionBarchSize');
+    this.vectorStoreFileDeletionApiCallInterval = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionApiCallInterval');
+
+    this.cronJob?.stop();
+    this.cronJob = this.generateCronJob();
+    this.cronJob.start();
+  }
+
+  private async executeJob(): Promise<void> {
+    await this.openaiService.deleteObsolatedVectorStoreRelations();
+    await this.openaiService.deleteObsoleteVectorStoreFile(this.vectorStoreFileDeletionBarchSize, this.vectorStoreFileDeletionApiCallInterval);
+  }
+
+  private generateCronJob() {
+    return nodeCron.schedule(this.vectorStoreFileDeletionCronExpression, async() => {
+      try {
+        // Sleep for a random number of minutes between 0 and 60 to distribute request load
+        const randomMilliseconds = getRandomIntInRange(0, 60) * 60 * 1000;
+        this.sleep(randomMilliseconds);
+
+        await this.executeJob();
+      }
+      catch (e) {
+        logger.error(e);
+      }
+    });
+  }
+
+}
+
+export default VectorStoreFileDeletionCronService;

+ 8 - 7
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -1,15 +1,17 @@
 import type { IUserHasId } from '@growi/core';
-import { Router, Request } from 'express';
+import type { Request } from 'express';
+import { Router } from 'express';
 import { body, validationResult } from 'express-validator';
 
-import Crowi from '~/server/crowi';
-import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 
-import { IAnswer } from '../../../interfaces/answer';
-import { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
-import { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
+import type { IAnswer } from '../../../interfaces/answer';
+import type { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
+import type { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
 import { StatusType } from '../../../interfaces/questionnaire-answer-status';
 import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
 import QuestionnaireAnswer from '../../models/questionnaire-answer';
@@ -25,7 +27,6 @@ interface AuthorizedRequest extends Request {
 }
 
 module.exports = (crowi: Crowi): Router => {
-  const accessTokenParser = require('~/server/middlewares/access-token-parser')(crowi);
   const loginRequired = require('~/server/middlewares/login-required')(crowi, true);
 
   const validators = {

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

@@ -1,7 +1,7 @@
 import path from 'path';
 
 import { GrowiPluginType } from '@growi/core';
-import { TemplateSummary } from '@growi/pluginkit/dist/v4';
+import type { TemplateSummary } from '@growi/pluginkit/dist/v4';
 import { scanAllTemplates, getMarkdown } from '@growi/pluginkit/dist/v4/server/index.cjs';
 import express from 'express';
 import { param, query } from 'express-validator';
@@ -9,7 +9,7 @@ import { param, query } from 'express-validator';
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
-import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
@@ -40,7 +40,7 @@ module.exports = (crowi) => {
 
     // scan preset templates
     if (presetTemplateSummaries == null) {
-      const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
+      const presetTemplatesRoot = resolveFromRoot('node_modules/@growi/preset-templates');
 
       try {
         presetTemplateSummaries = await scanAllTemplates(presetTemplatesRoot, {
@@ -76,7 +76,7 @@ module.exports = (crowi) => {
       templateId, locale,
     } = req.params;
 
-    const presetTemplatesRoot = resolveFromRoot('../../node_modules/@growi/preset-templates');
+    const presetTemplatesRoot = resolveFromRoot('node_modules/@growi/preset-templates');
 
     try {
       const markdown = await getMarkdown(presetTemplatesRoot, templateId, locale);

+ 1 - 1
apps/app/src/migrations/20180926134048-make-email-unique.js

@@ -11,7 +11,7 @@ module.exports = {
 
   async up(db, next) {
     logger.info('Start migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
 

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

@@ -1,6 +1,5 @@
 import mongoose from 'mongoose';
 
-// eslint-disable-next-line import/no-named-as-default
 import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
@@ -21,7 +20,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
@@ -77,7 +76,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({

+ 2 - 2
apps/app/src/migrations/20181019114028-abolish-page-group-relation.js

@@ -31,7 +31,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     if (!isPagegrouprelationsExists) {
@@ -75,7 +75,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = pageModelFactory();
     const UserGroup = userGroupModelFactory();

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

@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 module.exports = {
   async up(db, next) {
     logger.info('Start migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // enable passport and delete configs for crowi classic auth
     await Promise.all([

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

@@ -19,7 +19,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = userModelFactory();
 
@@ -49,7 +49,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({

Некоторые файлы не были показаны из-за большого количества измененных файлов