Преглед изворни кода

Merge pull request #9343 from weseek/master

Release v7.1.0
Yuki Takei пре 1 година
родитељ
комит
9b07e4637c
100 измењених фајлова са 2095 додато и 1221 уклоњено
  1. 5 2
      .changeset/config.json
  2. 0 59
      .devcontainer/Dockerfile
  3. 3 22
      .devcontainer/compose.yml
  4. 34 33
      .devcontainer/devcontainer.json
  5. 18 0
      .devcontainer/postCreateCommand.sh
  6. 1 1
      .github/dependabot.yml
  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. 34 80
      .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. 19 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. 1 1
      apps/app/config/logger/config.dev.js
  26. 0 37
      apps/app/config/swagger-definition.js
  27. 39 75
      apps/app/docker/Dockerfile
  28. 1 0
      apps/app/docker/Dockerfile.dockerignore
  29. 3 4
      apps/app/docker/README.md
  30. 1 3
      apps/app/docker/codebuild/buildspec.yml
  31. 13 1
      apps/app/next.config.js
  32. 4 0
      apps/app/nodemon.json
  33. 99 66
      apps/app/package.json
  34. 1 1
      apps/app/playwright.config.ts
  35. 1 1
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  36. 1 1
      apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts
  37. 1 1
      apps/app/playwright/utils/Login.ts
  38. 17 0
      apps/app/public/static/locales/en_US/admin.json
  39. 21 1
      apps/app/public/static/locales/en_US/translation.json
  40. 17 0
      apps/app/public/static/locales/fr_FR/admin.json
  41. 20 1
      apps/app/public/static/locales/fr_FR/translation.json
  42. 17 0
      apps/app/public/static/locales/ja_JP/admin.json
  43. 20 1
      apps/app/public/static/locales/ja_JP/translation.json
  44. 17 0
      apps/app/public/static/locales/zh_CN/admin.json
  45. 20 1
      apps/app/public/static/locales/zh_CN/translation.json
  46. 0 13
      apps/app/resource/locales/en_US/notifications/notActiveUser.ejs
  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. 0 13
      apps/app/resource/locales/fr_FR/notifications/notActiveUser.ejs
  51. 3 3
      apps/app/resource/locales/fr_FR/sandbox-diagrams.md
  52. 1 1
      apps/app/resource/locales/fr_FR/sandbox-math.md
  53. 262 112
      apps/app/resource/locales/fr_FR/sandbox.md
  54. 0 13
      apps/app/resource/locales/ja_JP/notifications/notActiveUser.ejs
  55. 3 3
      apps/app/resource/locales/ja_JP/sandbox-diagrams.md
  56. 182 187
      apps/app/resource/locales/ja_JP/sandbox.md
  57. 0 13
      apps/app/resource/locales/zh_CN/notifications/notActiveUser.ejs
  58. 3 3
      apps/app/resource/locales/zh_CN/sandbox-diagrams.md
  59. 1 1
      apps/app/resource/locales/zh_CN/sandbox-math.md
  60. 232 86
      apps/app/resource/locales/zh_CN/sandbox.md
  61. 4 0
      apps/app/resource/search/mappings-es7.json
  62. 4 0
      apps/app/resource/search/mappings-es8.json
  63. 4 4
      apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx
  64. 34 1
      apps/app/src/client/components/Admin/Security/SecuritySetting.jsx
  65. 0 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  66. 12 11
      apps/app/src/client/components/Bookmarks/DragAndDropWrapper.tsx
  67. 0 19
      apps/app/src/client/components/InAppNotification/InAppNotificationDropdown.tsx
  68. 7 2
      apps/app/src/client/components/InAppNotification/InAppNotificationElm.tsx
  69. 4 22
      apps/app/src/client/components/InAppNotification/InAppNotificationPage.tsx
  70. 1 1
      apps/app/src/client/components/InfiniteScroll.tsx
  71. 3 2
      apps/app/src/client/components/Me/InAppNotificationSettings.tsx
  72. 92 0
      apps/app/src/client/components/NotAvailableForReadOnlyUser.spec.tsx
  73. 24 1
      apps/app/src/client/components/NotAvailableForReadOnlyUser.tsx
  74. 1 1
      apps/app/src/client/components/Page/SlideRenderer.tsx
  75. 3 3
      apps/app/src/client/components/PageComment.tsx
  76. 16 11
      apps/app/src/client/components/PageComment/CommentControl.tsx
  77. 4 5
      apps/app/src/client/components/PageComment/CommentEditor.tsx
  78. 5 1
      apps/app/src/client/components/PageControls/PageControls.tsx
  79. 0 1
      apps/app/src/client/components/PageEditor/MarkdownTableDataImportForm.tsx
  80. 34 1
      apps/app/src/client/components/PageEditor/OptionsSelector.tsx
  81. 3 3
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  82. 1 1
      apps/app/src/client/components/PageEditor/markdown-drawio-util-for-editor.ts
  83. 1 1
      apps/app/src/client/components/PageEditor/markdown-table-util-for-editor.ts
  84. 1 1
      apps/app/src/client/components/PagePresentationModal.tsx
  85. 5 6
      apps/app/src/client/components/ReactMarkdownComponents/Header.tsx
  86. 5 5
      apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx
  87. 2 3
      apps/app/src/client/components/SearchPage/SearchResultList.tsx
  88. 3 3
      apps/app/src/client/components/Sidebar/AppTitle/AppTitle.module.scss
  89. 1 17
      apps/app/src/client/components/Sidebar/InAppNotification/PrimaryItemForNotification.tsx
  90. 11 0
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  91. 3 3
      apps/app/src/client/services/AdminHomeContainer.js
  92. 24 5
      apps/app/src/client/services/renderer/renderer.tsx
  93. 31 16
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  94. 2 1
      apps/app/src/components/Layout/BasicLayout.tsx
  95. 23 6
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  96. 78 0
      apps/app/src/features/callout/components/CalloutViewer.module.scss
  97. 68 0
      apps/app/src/features/callout/components/CalloutViewer.tsx
  98. 1 0
      apps/app/src/features/callout/components/index.ts
  99. 2 0
      apps/app/src/features/callout/index.ts
  100. 23 0
      apps/app/src/features/callout/services/callout.ts

+ 5 - 2
.changeset/config.json

@@ -17,8 +17,11 @@
     "@growi/custom-icons",
     "@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"
 }

+ 18 - 0
.devcontainer/postCreateCommand.sh

@@ -0,0 +1,18 @@
+sudo chown -R vscode:vscode /workspace;
+
+# Instal additional packages
+sudo apt update
+sudo apt-get install -y --no-install-recommends \
+  git-lfs \
+  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

+ 1 - 1
.github/dependabot.yml

@@ -30,5 +30,5 @@ updates:
       - dependency-name: handsontable
       - dependency-name: typeorm
       - dependency-name: mysql2
-
+      - dependency-name: "@codemirror/*"
 

+ 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

+ 34 - 80
.github/workflows/reusable-app-prod.yml

@@ -22,40 +22,25 @@ jobs:
 
     steps:
     - uses: actions/checkout@v4
-      with:
-        # retrieve local font files
-        lfs: true
+
+    - name: Install Git LFS
+      run: |
+        git lfs install
+
+    - 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 +61,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 +79,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 +94,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 +126,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
@@ -199,8 +165,6 @@ jobs:
   run-playwright:
     needs: [build-prod]
 
-    if: ${{ !inputs.skip-e2e-test && startsWith(github.head_ref, 'mergify/merge-queue/') }}
-
     runs-on: ubuntu-latest
     container:
       # Match the Playwright version
@@ -228,37 +192,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 +225,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 +239,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 +253,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": [

+ 19 - 1
CHANGELOG.md

@@ -1,9 +1,27 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.21...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.23...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [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}"

+ 1 - 1
apps/app/config/logger/config.dev.js

@@ -43,5 +43,5 @@ module.exports = {
   // 'growi:cli:StickyStretchableScroller': 'debug',
   // 'growi:cli:ItemsTree': 'debug',
   'growi:searchResultList': 'debug',
-
+  'growi:service:openai': 'debug',
 };

+ 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',
-      },
-    },
-  },
-};

+ 39 - 75
apps/app/docker/Dockerfile

@@ -1,4 +1,4 @@
-# syntax = docker/dockerfile:1.4
+# syntax = docker/dockerfile:1
 
 
 ##
@@ -6,89 +6,51 @@
 ##
 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 git and git-lfs
+RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash \
+  && apt-get update && apt-get install -y git git-lfs --no-install-recommends \
+  && git lfs install
 
-# setup (with network-timeout = 1 hour)
-RUN yarn config set network-timeout 3600000
-RUN yarn global add node-gyp
-RUN yarn --frozen-lockfile
+# 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"
 
-# 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 +61,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 +69,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 +85,20 @@ RUN set -eux; \
 # verify that the binary works
 	gosu nobody true
 
-COPY --from=deps-resolver-prod --chown=node:node \
-  ${optDir}/node_modules.tar ${appDir}/
+# Add pnpm for 'node' user
+RUN apt-get update && apt-get install -y sudo ca-certificates wget --no-install-recommends \
+  && wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sudo -u node sh - \
+  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false
+ENV PNPM_HOME="/home/node/.local/share/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+
 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 +109,4 @@ VOLUME /data
 EXPOSE 3000
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
-CMD ["yarn migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"]
+CMD ["pnpm 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 - 3
apps/app/docker/codebuild/buildspec.yml

@@ -27,6 +27,4 @@ phases:
 
 cache:
   paths:
-    - node_modules/**/*
-    - apps/*/node_modules/**/*
-    - packages/*/node_modules/**/*
+    - .pnpm-store/**/*

+ 13 - 1
apps/app/next.config.js

@@ -48,6 +48,14 @@ const getTranspilePackages = () => {
     'emoticon',
     'direction', // for hast-util-select
     'bcp-47-match', // for hast-util-select
+    'parse-entities',
+    'character-reference-invalid',
+    'is-hexadecimal',
+    'is-alphabetical',
+    'is-alphanumerical',
+    'github-slugger',
+    'html-url-attributes',
+    'estree-util-is-identifier-name',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
 
@@ -151,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",

+ 99 - 66
apps/app/package.json

@@ -1,38 +1,41 @@
 {
   "name": "@growi/app",
-  "version": "7.0.21",
+  "version": "7.1.0-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",
+    "pre:lfs": "git lfs pull",
+    "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,48 +43,49 @@
     "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": "cross-env NODE_ENV=development pnpm run ts-node --experimental-repl-await src/server/console.js",
+    "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": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
-    "@azure/identity": "^4.3.0",
+    "@azure/identity": "^4.4.1",
+    "@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/jest": "^29.5.2",
-    "@types/ldapjs": "^2.2.5",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
@@ -92,12 +96,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",
@@ -105,6 +110,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",
@@ -118,21 +124,31 @@
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
-    "hast-util-select": "^5.0.5",
+    "hast-util-sanitize": "^5.0.1",
+    "hast-util-select": "^6.0.2",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "i18next": "^23.10.1",
     "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",
-    "mermaid": "^10.1.0",
+    "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",
@@ -149,6 +165,7 @@
     "node-cron": "^3.0.2",
     "nodemailer": "^6.9.15",
     "nodemailer-ses-transport": "~1.5.0",
+    "openai": "^4.56.0",
     "openid-client": "^5.4.0",
     "p-retry": "^4.0.0",
     "passport": "^0.6.0",
@@ -157,6 +174,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",
@@ -168,7 +186,7 @@
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^14.1.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.7",
+    "react-markdown": "^9.0.1",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
@@ -177,27 +195,34 @@
     "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
-    "rehype-katex": "^6.0.2",
-    "rehype-raw": "^6.1.1",
-    "rehype-sanitize": "^5.0.1",
-    "rehype-slug": "^5.0.1",
+    "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": "^3.0.2",
-    "remark-emoji": "^3.0.2",
-    "remark-frontmatter": "^4.0.1",
-    "remark-gfm": "^3.0.1",
-    "remark-math": "^5.1.1",
-    "remark-toc": "^8.0.1",
-    "remark-wiki-link": "^1.0.4",
+    "remark-breaks": "^4.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",
     "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",
@@ -217,10 +242,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/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",
@@ -231,12 +257,19 @@
     "@testing-library/react": "^16.0.1",
     "@testing-library/user-event": "^14.5.2",
     "@types/express": "^4.17.21",
+    "@types/hast": "^3.0.4",
     "@types/jest": "^29.5.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",
@@ -259,23 +292,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",
-    "rehype-rewrite": "^3.0.6",
-    "replacestream": "^4.0.3",
+    "rehype-rewrite": "^4.0.2",
+    "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/20-basic-features/access-to-page.spec.ts

@@ -23,7 +23,7 @@ test('/Sandbox/Math is successfully loaded', async({ page }) => {
   await page.goto('/Sandbox/Math');
 
   // Expect the Math-specific elements to be present
-  await expect(page.locator('.math').first()).toBeVisible();
+  await expect(page.locator('.katex').first()).toBeVisible();
 });
 
 test('Sandbox with edit is successfully loaded', async({ page }) => {

+ 1 - 1
apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts

@@ -15,7 +15,7 @@ test('/Sandbox/math is successfully loaded', async({ page }) => {
   await page.goto('/Sandbox/Math');
 
   // Check if the math elements are visible
-  await expect(page.locator('.math').first()).toBeVisible();
+  await expect(page.locator('.katex').first()).toBeVisible();
 });
 
 test('Access to /me page', async({ page }) => {

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

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

@@ -15,6 +15,7 @@
     "scope_of_page_disclosure": "Scope of page disclosure",
     "set_point": "Set point",
     "Guest Users Access": "Guest users access",
+    "readonly_users_access": "Read only users' access",
     "always_hidden": "Always hidden",
     "always_displayed": "Always displayed",
     "displayed_or_hidden": "Hidden / Displayed",
@@ -37,6 +38,7 @@
     "page_delete_rights": "Delete rights",
     "page_delete": "Page Delete",
     "page_delete_completely": "Page Delete Completely",
+    "comment_manage_rights": "Comment management rights",
     "other_options": "Other options",
     "deletion_explanation": "Restricts users who can trash the selected single page.",
     "complete_deletion_explanation": "Restricts users who can completely delete  selected single page.",
@@ -85,6 +87,10 @@
       "deny": "Deny (Registered users only)",
       "readonly": "Accept (Guests can read only)"
     },
+    "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)",
       "restricted": "Restricted (Requires approval by administrators)",
@@ -1132,5 +1138,16 @@
   },
   "forbidden_page": {
     "do_not_have_admin_permission": "Users without administrative rights cannot access the administration screen"
+  },
+  "ai_integration": {
+    "ai_integration": "AI Integration",
+    "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, please set the environment variable <code>AI_ENABLED</code> to true.",
+    "ai_search_management": "AI search management",
+    "rebuild_vector_store": "Rebuild Vector Store",
+    "rebuild_vector_store_label": "Rebuild",
+    "rebuild_vector_store_explanation1": "Delete the existing Vector Store and recreate the Vector Store on the public page.",
+    "rebuild_vector_store_explanation2": "This process may take several minutes.",
+    "rebuild_vector_store_requested": "Vector Store rebuild has been requested",
+    "rebuild_vector_store_failed": "Vector Store rebuild failed"
   }
 }

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

@@ -332,6 +332,12 @@
     "theme": "Theme",
     "keymap": "Keymap",
     "indent": "Indent",
+    "paste": {
+      "title": "Paste behavior",
+      "both": "Both",
+      "text": "Text only",
+      "file": "File only"
+    },
     "editor_config": "Editor Config",
     "Show active line": "Show active line",
     "auto_format_table": "Auto format table",
@@ -350,7 +356,8 @@
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "No user found",
     "reply": "Reply",
-    "delete_comment": "Delete comment?"
+    "delete_comment": "Delete comment?",
+    "comment_management_is_not_allowed": "Comment management is not allowed."
   },
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
@@ -478,6 +485,19 @@
     "latest_revision": "theirs",
     "selected_editable_revision": "Selected Page Body (Editable)"
   },
+  "modal_aichat": {
+    "title": "Knowledge Assistant",
+    "title_beta_label": "(Beta)",
+    "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",
+    "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",
     "set_link_and_label": "Set link and label",

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

@@ -15,6 +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 lecture seule",
     "always_hidden": "Toujours caché",
     "always_displayed": "Toujours affiché",
     "displayed_or_hidden": "Caché / Affiché",
@@ -37,6 +38,7 @@
     "page_delete_rights": "Droits de suppression",
     "page_delete": "Suppression de page",
     "page_delete_completely": "Suppression complète de page",
+    "comment_manage_rights": "Droits de gestion des commentaires",
     "other_options": "Paramètres supplémentaires",
     "deletion_explanation": "Restreindre les utilisateurs pouvant supprimer une page.",
     "complete_deletion_explanation": "Restreindre les utilisateurs pouvant supprimer complètement une page.",
@@ -85,6 +87,10 @@
       "deny": "Refuser (Utilisateurs inscrits seulement)",
       "readonly": "Autoriser (Lecture seule)"
     },
+    "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)",
       "restricted": "Restreint (Requiert l'approbation d'administrateurs)",
@@ -1131,5 +1137,16 @@
   },
   "forbidden_page": {
     "do_not_have_admin_permission": "Seul les administrateurs peuvent accéder à cette page."
+  },
+  "ai_integration": {
+    "ai_integration": "Intégration de l'IA",
+    "disable_mode_explanation": "Actuellement, l'intégration de l'IA est désactivée. Pour l'activer, veuillez définir la variable d'environnement <code>AI_ENABLED</code> sur true",
+    "ai_search_management": "Gestion de la recherche par l'IA",
+    "rebuild_vector_store": "Reconstruire le magasin Vector",
+    "rebuild_vector_store_label": "Reconstruire",
+    "rebuild_vector_store_explanation1": "Supprimez le Vector Store existant et recréez le Vector Store sur la page publique.",
+    "rebuild_vector_store_explanation2": "Ce processus peut prendre plusieurs minutes.",
+    "rebuild_vector_store_requested": "La reconstruction du magasin Vector a été demandée",
+    "rebuild_vector_store_failed": "Échec de la reconstruction du magasin de vecteurs"
   }
 }

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

@@ -332,6 +332,12 @@
     "theme": "Thème",
     "keymap": "Touches",
     "indent": "Indentation",
+    "paste": {
+      "title": "Comportement du collage",
+      "both": "Les deux",
+      "text": "Texte seulement",
+      "file": "Fichier seulement"
+    },
     "editor_config": "Configuration de l'éditeur",
     "Show active line": "Montrer la ligne active",
     "auto_format_table": "Formattage les tables",
@@ -350,7 +356,8 @@
     "display_the_page_when_posting_this_comment": "Afficher la page en postant le commentaire",
     "no_user_found": "Aucun utilisateur trouvé",
     "reply": "Répondre",
-    "delete_comment": "Supprimer?"
+    "delete_comment": "Supprimer?",
+    "comment_management_is_not_allowed": "La gestion des commentaires n'est pas autorisée."
   },
   "page_api_error": {
     "notfound_or_forbidden": "Page originale introuvable ou accès restreint.",
@@ -472,6 +479,18 @@
     "latest_revision": "les autres",
     "selected_editable_revision": "Corps de page sélectionné (Modifiable)"
   },
+  "modal_aichat": {
+    "title": "Assistant de Connaissance",
+    "title_beta_label": "(Bêta)",
+    "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",
+    "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",
     "set_link_and_label": "Ajouter lien et étiquette",

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

@@ -24,6 +24,7 @@
     "scope_of_page_disclosure": "ページの公開範囲",
     "set_point": "設定値",
     "Guest Users Access":"ゲストユーザーのアクセス",
+    "readonly_users_access": "閲覧のみユーザーのアクセス",
     "always_hidden": "非表示 (固定)",
     "always_displayed": "表示 (固定)",
     "displayed_or_hidden": "非表示 / 表示",
@@ -46,6 +47,7 @@
     "page_delete_rights": "ページの削除権限",
     "page_delete": "ゴミ箱に入れる",
     "page_delete_completely": "完全に削除する",
+    "comment_manage_rights": "コメントの操作権限",
     "other_options": "その他のオプション",
     "deletion_explanation": "ページをゴミ箱に入れることができるユーザーを制限します。",
     "complete_deletion_explanation": "ページを完全削除することができるユーザーを制限します。",
@@ -94,6 +96,10 @@
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
     },
+    "read_only_users_comment": {
+      "deny": "拒否 (閲覧のみユーザーのコメント操作を禁止)",
+      "accept": "許可 (閲覧のみユーザーもコメント操作可能)"
+    },
     "registration_mode": {
       "open": "公開 (だれでも登録可能)",
       "restricted": "制限 (登録完了には管理者の承認が必要)",
@@ -1142,5 +1148,16 @@
   },
   "forbidden_page": {
     "do_not_have_admin_permission": "管理者権限のないユーザーでは管理画面にはアクセスできません"
+  },
+  "ai_integration": {
+    "ai_integration": "AI 連携",
+    "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> を true に設定してください。",
+    "ai_search_management": "AI 検索管理",
+    "rebuild_vector_store": "Vector Store のリビルド",
+    "rebuild_vector_store_label": "リビルド",
+    "rebuild_vector_store_explanation1": "既存の Vector Store を削除し、公開ページの Vector Store を再作成します。",
+    "rebuild_vector_store_explanation2": "この作業には数分かかる可能性があります。",
+    "rebuild_vector_store_requested": "Vector Store のリビルドを受け付けました",
+    "rebuild_vector_store_failed": "Vector Store のリビルドに失敗しました"
   }
 }

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

@@ -365,6 +365,12 @@
     "theme": "テーマ",
     "keymap": "キーマップ",
     "indent": "インデント",
+    "paste": {
+      "title": "ペースト時の動作",
+      "both": "両方",
+      "text": "テキストのみ",
+      "file": "ファイルのみ"
+    },
     "editor_config": "エディタ設定",
     "Show active line": "アクティブ行をハイライト",
     "auto_format_table": "表の自動整形",
@@ -383,7 +389,8 @@
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
     "no_user_found": "ユーザー名が見つかりません",
     "reply": "返信",
-    "delete_comment": "コメントを削除しますか?"
+    "delete_comment": "コメントを削除しますか?",
+    "comment_management_is_not_allowed": "コメントの操作が許可されていません。"
   },
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
@@ -511,6 +518,18 @@
     "latest_revision": "最新の本文",
     "selected_editable_revision": "保存するページ本文(編集可能)"
   },
+  "modal_aichat": {
+    "title": "ナレッジアシスタント",
+    "title_beta_label": "(ベータ)",
+    "placeholder": "ききたいことを入力してください",
+    "caution_against_hallucination": "情報が正しいか出典を確認しましょう",
+    "progress_label": "回答を生成しています",
+    "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": "リンク編集",
     "set_link_and_label": "リンク情報",

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

@@ -27,6 +27,7 @@
     "always_hidden": "总是隐藏",
     "displayed_or_hidden": "隐藏 / 显示",
     "Guest Users Access": "来宾用户访问",
+    "readonly_users_access": "只浏览用户的访问",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"register_limitation": "注册限制",
 		"register_limitation_desc": "限制新用户注册",
@@ -46,6 +47,7 @@
     "page_delete_rights": "删除权限",
     "page_delete": "删除",
     "page_delete_completely": "彻底删除",
+    "comment_manage_rights": "评论管理权限",
     "other_options": "其他选项",
     "deletion_explanation": "限制用户对选定的单一页面进行垃圾处理。",
     "complete_deletion_explanation": "限制可以完全删除所选单页的用户。",
@@ -94,6 +96,10 @@
 			"deny": "拒绝(仅限注册用户)",
 			"readonly": "接受(来宾可以只读)"
 		},
+    "read_only_users_comment": {
+      "deny": "拒绝 (禁止只浏览用户操作评论)",
+      "accept": "允许 (只浏览用户可以管理评论)"
+    },
 		"registration_mode": {
 			"open": "打开(任何人都可以注册)",
 			"restricted": "受限(需要管理员批准)",
@@ -1141,5 +1147,16 @@
   },
   "forbidden_page": {
     "do_not_have_admin_permission": "没有管理权限的用户无法访问管理屏幕"
+  },
+  "ai_integration": {
+    "ai_integration": "AI 集成",
+    "disable_mode_explanation": "目前,AI 集成已禁用。要启用它,请将环境变量 <code>AI_ENABLED</code> 设置为 true",
+    "ai_search_management": "AI 搜索管理",
+    "rebuild_vector_store": "重建矢量商店",
+    "rebuild_vector_store_label": "重建",
+    "rebuild_vector_store_explanation1": "删除现有的矢量存储,在公共页面上重新创建矢量存储。",
+    "rebuild_vector_store_explanation2": "这个过程可能需要几分钟。",
+    "rebuild_vector_store_requested": "已要求重建矢量存储库",
+    "rebuild_vector_store_failed": "向量存储区重建失败"
   }
 }

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

@@ -322,6 +322,12 @@
     "theme": "主题",
     "keymap": "键表",
     "indent": "缩进",
+    "paste": {
+      "title": "粘贴行为",
+      "both": "两者",
+      "text": "仅文本",
+      "file": "仅文件"
+    },
     "editor_config": "编辑器配置",
 		"Show active line": "显示活动行",
 		"auto_format_table": "自动格式化表格",
@@ -340,7 +346,8 @@
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "未找到用户名",
     "reply": "Reply",
-    "delete_comment": "Delete comment?"
+    "delete_comment": "Delete comment?",
+    "comment_management_is_not_allowed": "不允许操作评论。"
   },
   "page_api_error": {
     "notfound_or_forbidden": "未找到或禁止原始页。",
@@ -467,6 +474,18 @@
     "latest_revision": "最新页面正文",
     "selected_editable_revision": "选定的可编辑页面正文"
   },
+  "modal_aichat": {
+    "title": "知识助手",
+    "title_beta_label": "(测试版)",
+    "placeholder": "问我任何问题。",
+    "caution_against_hallucination": "请核实信息并检查来源。",
+    "progress_label": "生成答案中",
+    "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",
     "set_link_and_label": "Set link and label",

+ 0 - 13
apps/app/resource/locales/en_US/notifications/notActiveUser.ejs

@@ -1,13 +0,0 @@
-Password Reset
-
-Hi, <%- email %>
-
-A request has been received to change the password from <%- appTitle %>.
-However, this email is not registerd. Please try again with different email.
-
-If you did not request a password reset, you can safely ignore this email.
-
--------------------------------------------------------------------------
-
-GROWI: <%- appTitle %>
-URL: <%- url %>

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

+ 0 - 13
apps/app/resource/locales/fr_FR/notifications/notActiveUser.ejs

@@ -1,13 +0,0 @@
-Réinitialisation du mot de passe
-
-Bonjour, <%- email %>
-
-Une demande de réinitialisation de mot de passe a été demandée depuis <%- appTitle %>.
-Cette adresse courriel n'est pas enregistré. Réessayez avec une adresse courriel différente.
-
-Si vous n'avez pas demandé de réinitialisation de mot de passe, ignorez ce courriel.
-
--------------------------------------------------------------------------
-
-GROWI: <%- appTitle %>
-URL: <%- url %>

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

+ 0 - 13
apps/app/resource/locales/ja_JP/notifications/notActiveUser.ejs

@@ -1,13 +0,0 @@
-パスワードリセット
-
-こんにちは、 <%- email %>
-
-<%- appTitle %> からパスワード再設定のリクエストがありましたが、このemailは登録されておりません。
-他のemailアドレスで再度お試しください。
-
-もしこのリクエストに心当たりがない場合は、このメールを無視してください。
-
--------------------------------------------------------------------------
-
-GROWI: <%- appTitle %>
-URL: <%- url %>

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

+ 0 - 13
apps/app/resource/locales/zh_CN/notifications/notActiveUser.ejs

@@ -1,13 +0,0 @@
-重设密码
-
-嗨,<%-电子邮件%>
-
-已收到来自 <%-appTitle%> 的更改密码请求。
-但是,此电子邮件未注册。请使用其他电子邮件重试。
-
-如果您没有要求重置密码,则可以放心地忽略此电子邮件。
-
--------------------------------------------------------------------------
-
-GROWI: <%- appTitle %>
-URL: <%- url %>

+ 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 - 0
apps/app/resource/search/mappings-es7.json

@@ -64,6 +64,10 @@
           }
         }
       },
+      "body_embedded": {
+        "type": "dense_vector",
+        "dims": 768
+      },
       "comments": {
         "type": "text",
         "fields": {

+ 4 - 0
apps/app/resource/search/mappings-es8.json

@@ -64,6 +64,10 @@
           }
         }
       },
+      "body_embedded": {
+        "type": "dense_vector",
+        "dims": 768
+      },
       "comments": {
         "type": "text",
         "fields": {

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

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

@@ -333,7 +333,7 @@ class SecuritySetting extends React.Component {
     const { t, adminGeneralSecurityContainer } = this.props;
     const {
       currentRestrictGuestMode, currentPageDeletionAuthority, currentPageCompleteDeletionAuthority,
-      currentPageRecursiveDeletionAuthority, currentPageRecursiveCompleteDeletionAuthority,
+      currentPageRecursiveDeletionAuthority, currentPageRecursiveCompleteDeletionAuthority, isRomUserAllowedToComment,
     } = adminGeneralSecurityContainer.state;
 
     const isButtonDisabledForDeletion = !validateDeleteConfigs(
@@ -509,6 +509,39 @@ class SecuritySetting extends React.Component {
           </div>
         </div>
 
+        <h4 className="mb-3">{t('security_settings.comment_manage_rights')}</h4>
+        <div className="row mb-4">
+          <div className="col-md-4 text-md-end py-2">
+            <strong>{t('security_settings.readonly_users_access')}</strong>
+          </div>
+          <div className="col-md-8">
+            <div className="dropdown">
+              <button
+                className={`btn btn-outline-secondary dropdown-toggle text-end col-12
+                            col-md-auto ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
+                type="button"
+                id="dropdownMenuButton"
+                data-bs-toggle="dropdown"
+                aria-haspopup="true"
+                aria-expanded="true"
+              >
+                <span className="float-start">
+                  {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.read_only_users_comment.deny')}
+                </button>
+                <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.switchIsRomUserAllowedToComment(true) }}>
+                  {t('security_settings.read_only_users_comment.accept')}
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+
         <h4>{t('security_settings.session')}</h4>
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.max_age')}</label>

+ 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>
   );

+ 0 - 19
apps/app/src/client/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -6,19 +6,11 @@ import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 
-
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastError } from '~/client/util/toastr';
 import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 import { useDefaultSocket } from '~/stores/socket-io';
-import loggerFactory from '~/utils/logger';
 
 import InAppNotificationList from './InAppNotificationList';
 
-
-const logger = loggerFactory('growi:InAppNotificationDropdown');
-
-
 export const InAppNotificationDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
 
@@ -36,16 +28,6 @@ export const InAppNotificationDropdown = (): JSX.Element => {
   const buttonRef = useRef(null);
   useRipple(buttonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
-  const updateNotificationStatus = async() => {
-    try {
-      await apiv3Post('/in-app-notification/read');
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  };
-
   useEffect(() => {
     if (socket != null) {
       socket.on('notificationUpdated', () => {
@@ -62,7 +44,6 @@ export const InAppNotificationDropdown = (): JSX.Element => {
 
   const toggleDropdownHandler = async() => {
     if (!isOpen && inAppNotificationUnreadStatusCount != null && inAppNotificationUnreadStatusCount > 0) {
-      await updateNotificationStatus();
       mutateInAppNotificationUnreadStatusCount();
     }
 

+ 7 - 2
apps/app/src/client/components/InAppNotification/InAppNotificationElm.tsx

@@ -1,10 +1,13 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 import type { HasObjectId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { IInAppNotification, InAppNotificationStatuses } from '~/interfaces/in-app-notification';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
+import { useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 
 import { useModelNotification } from './PageNotification';
 
@@ -21,6 +24,7 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
 
   const Notification = modelNotificationUtils?.Notification;
   const publishOpen = modelNotificationUtils?.publishOpen;
+  const { mutate: mutateNotificationCount } = useSWRxInAppNotificationStatus();
 
   if (Notification == null || publishOpen == null) {
     return <></>;
@@ -31,6 +35,7 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       // set notification status "OPEND"
       await apiv3Post('/in-app-notification/open', { id: notification._id });
       onUnopenedNotificationOpend?.();
+      mutateNotificationCount();
     }
 
     publishOpen();

+ 4 - 22
apps/app/src/client/components/InAppNotification/InAppNotificationPage.tsx

@@ -1,46 +1,26 @@
 import type { FC } from 'react';
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 
-import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
+import { apiv3Put } from '~/client/util/apiv3-client';
 import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 import { useShowPageLimitationXL } from '~/stores-universal/context';
 import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
-import loggerFactory from '~/utils/logger';
 
 import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
 import PaginationWrapper from '../PaginationWrapper';
 
 import InAppNotificationList from './InAppNotificationList';
 
-
-const logger = loggerFactory('growi:InAppNotificationPage');
-
-
 export const InAppNotificationPage: FC = () => {
   const { t } = useTranslation('commons');
-  const { mutate } = useSWRxInAppNotificationStatus();
 
   const { data: showPageLimitationXL } = useShowPageLimitationXL();
 
   const limit = showPageLimitationXL != null ? showPageLimitationXL : 20;
 
-  const updateNotificationStatus = useCallback(async() => {
-    try {
-      await apiv3Post('/in-app-notification/read');
-      mutate();
-    }
-    catch (err) {
-      logger.error(err);
-    }
-  }, [mutate]);
-
-  useEffect(() => {
-    updateNotificationStatus();
-  }, [updateNotificationStatus]);
-
   const InAppNotificationCategoryByStatus = (status?: InAppNotificationStatuses) => {
     const [activePage, setActivePage] = useState(1);
     const offset = (activePage - 1) * limit;
@@ -56,6 +36,7 @@ export const InAppNotificationPage: FC = () => {
 
     const { data: notificationData, mutate: mutateNotificationData } = useSWRxInAppNotifications(limit, offset, categoryStatus);
     const { mutate: mutateAllNotificationData } = useSWRxInAppNotifications(limit, offset, undefined);
+    const { mutate: mutateNotificationCount } = useSWRxInAppNotificationStatus();
 
     const setAllNotificationPageNumber = (selectedPageNumber): void => {
       setActivePage(selectedPageNumber);
@@ -78,6 +59,7 @@ export const InAppNotificationPage: FC = () => {
       mutateNotificationData();
       // mutate notification statuses in 'ALL' Category
       mutateAllNotificationData();
+      mutateNotificationCount();
     };
 
 

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

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

@@ -0,0 +1,92 @@
+import '@testing-library/jest-dom/vitest';
+
+import { render, screen } from '@testing-library/react';
+import {
+  describe, it, expect, vi,
+} from 'vitest';
+
+import { NotAvailableIfReadOnlyUserNotAllowedToComment } from './NotAvailableForReadOnlyUser';
+
+const useIsReadOnlyUser = vi.hoisted(() => vi.fn().mockReturnValue({ data: true }));
+const useIsRomUserAllowedToComment = vi.hoisted(() => vi.fn().mockReturnValue({ data: true }));
+
+vi.mock('~/stores-universal/context', () => ({
+  useIsReadOnlyUser,
+  useIsRomUserAllowedToComment,
+}));
+
+describe('NotAvailableForReadOnlyUser.tsx', () => {
+
+  it('renders NotAvailable component as enable when user is read-only and comments by rom users is allowed', async() => {
+    useIsReadOnlyUser.mockReturnValue({ data: true });
+    useIsRomUserAllowedToComment.mockReturnValue({ data: true });
+
+    render(
+      <NotAvailableIfReadOnlyUserNotAllowedToComment>
+        <div data-testid="test-child">Test Child</div>
+      </NotAvailableIfReadOnlyUserNotAllowedToComment>,
+    );
+
+    // when
+    const element = screen.getByTestId('test-child');
+    const wrapperElement = element.parentElement;
+
+    // then
+    expect(wrapperElement).not.toHaveAttribute('aria-hidden', 'true');
+  });
+
+  it('renders NotAvailable component as disable when user is read-only and comments by rom users is not allowed', async() => {
+    useIsReadOnlyUser.mockReturnValue({ data: true });
+    useIsRomUserAllowedToComment.mockReturnValue({ data: false });
+
+    render(
+      <NotAvailableIfReadOnlyUserNotAllowedToComment>
+        <div data-testid="test-child">Test Child</div>
+      </NotAvailableIfReadOnlyUserNotAllowedToComment>,
+    );
+
+    // when
+    const element = screen.getByTestId('test-child');
+    const wrapperElement = element.parentElement;
+
+    // then
+    expect(wrapperElement).toHaveAttribute('aria-hidden', 'true');
+  });
+
+  it('renders NotAvailable component as enable when user is not read-only and comments by rom users is allowed', async() => {
+    useIsReadOnlyUser.mockReturnValue({ data: false });
+    useIsRomUserAllowedToComment.mockReturnValue({ data: true });
+
+    render(
+      <NotAvailableIfReadOnlyUserNotAllowedToComment>
+        <div data-testid="test-child">Test Child</div>
+      </NotAvailableIfReadOnlyUserNotAllowedToComment>,
+    );
+
+    // when
+    const element = screen.getByTestId('test-child');
+    const wrapperElement = element.parentElement;
+
+    // then
+    expect(wrapperElement).not.toHaveAttribute('aria-hidden', 'true');
+  });
+
+  it('renders NotAvailable component as enable when user is not read-only and comments by rom users is not allowed', async() => {
+    useIsReadOnlyUser.mockReturnValue({ data: false });
+    useIsRomUserAllowedToComment.mockReturnValue({ data: false });
+
+    render(
+      <NotAvailableIfReadOnlyUserNotAllowedToComment>
+        <div data-testid="test-child">Test Child</div>
+      </NotAvailableIfReadOnlyUserNotAllowedToComment>,
+    );
+
+    // when
+    const element = screen.getByTestId('test-child');
+    const wrapperElement = element.parentElement;
+
+    // then
+    expect(wrapperElement).not.toHaveAttribute('aria-hidden', 'true');
+  });
+
+});

+ 24 - 1
apps/app/src/client/components/NotAvailableForReadOnlyUser.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import { useIsReadOnlyUser } from '~/stores-universal/context';
+import { useIsReadOnlyUser, useIsRomUserAllowedToComment } from '~/stores-universal/context';
 
 import { NotAvailable } from './NotAvailable';
 
@@ -26,3 +26,26 @@ export const NotAvailableForReadOnlyUser: React.FC<{
   );
 });
 NotAvailableForReadOnlyUser.displayName = 'NotAvailableForReadOnlyUser';
+
+export const NotAvailableIfReadOnlyUserNotAllowedToComment: React.FC<{
+  children: JSX.Element
+}> = React.memo(({ children }) => {
+  const { t } = useTranslation();
+  const { data: isReadOnlyUser } = useIsReadOnlyUser();
+
+  const { data: isRomUserAllowedToComment } = useIsRomUserAllowedToComment();
+
+  const isDisabled = !!isReadOnlyUser && !isRomUserAllowedToComment;
+  const title = t('page_comment.comment_management_is_not_allowed');
+
+  return (
+    <NotAvailable
+      isDisabled={isDisabled}
+      title={title}
+      classNamePrefix="grw-not-available-for-read-only-user"
+    >
+      {children}
+    </NotAvailable>
+  );
+});
+NotAvailableIfReadOnlyUserNotAllowedToComment.displayName = 'NotAvailableIfReadOnlyUserNotAllowedToComment';

+ 1 - 1
apps/app/src/client/components/Page/SlideRenderer.tsx

@@ -1,4 +1,4 @@
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as ReactMarkdownOptions } from 'react-markdown';
 
 import { usePresentationViewOptions } from '~/stores/renderer';
 

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

@@ -20,7 +20,7 @@ import type { ICommentHasId, ICommentHasIdList } from '../../interfaces/comment'
 import { useSWRxPageComment } from '../../stores/comment';
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
-import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
+import { NotAvailableIfReadOnlyUserNotAllowedToComment } from './NotAvailableForReadOnlyUser';
 import { Comment } from './PageComment/Comment';
 import { CommentEditor } from './PageComment/CommentEditor';
 import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
@@ -183,7 +183,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                 {(!isReadOnly && !showEditorIds.has(comment._id)) && (
                   <div className="d-flex flex-row-reverse">
                     <NotAvailableForGuest>
-                      <NotAvailableForReadOnlyUser>
+                      <NotAvailableIfReadOnlyUserNotAllowedToComment>
                         <button
                           type="button"
                           data-testid="comment-reply-button"
@@ -193,7 +193,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                           <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-2" />
                           <span className="material-symbols-outlined me-1 fs-5 pb-1">reply</span><small>{t('page_comment.reply')}...</small>
                         </button>
-                      </NotAvailableForReadOnlyUser>
+                      </NotAvailableIfReadOnlyUserNotAllowedToComment>
                     </NotAvailableForGuest>
                   </div>
                 )}

+ 16 - 11
apps/app/src/client/components/PageComment/CommentControl.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { NotAvailableIfReadOnlyUserNotAllowedToComment } from '../NotAvailableForReadOnlyUser';
 
 type CommentControlProps = {
   onClickEditBtn: () => void,
@@ -13,17 +14,21 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
   return (
     // The page-comment-control class is imported from Comment.module.scss
     <div className="page-comment-control">
-      <button type="button" className="btn btn-link p-2 opacity-50" onClick={onClickEditBtn}>
-        <span className="material-symbols-outlined">edit</span>
-      </button>
-      <button
-        data-testid="comment-delete-button"
-        type="button"
-        className="btn btn-link p-2 me-2 opacity-50"
-        onClick={onClickDeleteBtn}
-      >
-        <span className="material-symbols-outlined">close</span>
-      </button>
+      <NotAvailableIfReadOnlyUserNotAllowedToComment>
+        <>
+          <button type="button" className="btn btn-link p-2 opacity-50" onClick={onClickEditBtn}>
+            <span className="material-symbols-outlined">edit</span>
+          </button>
+          <button
+            data-testid="comment-delete-button"
+            type="button"
+            className="btn btn-link p-2 me-2 opacity-50"
+            onClick={onClickDeleteBtn}
+          >
+            <span className="material-symbols-outlined">close</span>
+          </button>
+        </>
+      </NotAvailableIfReadOnlyUserNotAllowedToComment>
     </div>
   );
 

+ 4 - 5
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 {
@@ -32,7 +31,7 @@ import { useCommentEditorDirtyMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
-import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
+import { NotAvailableIfReadOnlyUserNotAllowedToComment } from '../NotAvailableForReadOnlyUser';
 
 import { CommentPreview } from './CommentPreview';
 import { SwitchingButtonGroup } from './SwitchingButtonGroup';
@@ -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);
@@ -330,7 +329,7 @@ export const CommentEditorPre = (props: CommentEditorProps): JSX.Element => {
     return (
       <CommentEditorLayout>
         <NotAvailableForGuest>
-          <NotAvailableForReadOnlyUser>
+          <NotAvailableIfReadOnlyUserNotAllowedToComment>
             <button
               type="button"
               className="btn btn-outline-primary w-100 text-start py-3"
@@ -341,7 +340,7 @@ export const CommentEditorPre = (props: CommentEditorProps): JSX.Element => {
               <span className="material-symbols-outlined me-1 fs-5">add_comment</span>
               <small>{t('page_comment.add_a_comment')}...</small>
             </button>
-          </NotAvailableForReadOnlyUser>
+          </NotAvailableIfReadOnlyUserNotAllowedToComment>
         </NotAvailableForGuest>
       </CommentEditorLayout>
     );

+ 5 - 1
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -16,6 +16,7 @@ import {
   toggleLike, toggleSubscribe,
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
+import RagSearchButton from '~/features/openai/client/components/RagSearchButton';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSearchPage } from '~/stores-universal/context';
 import {
   EditorMode, useEditorMode,
@@ -282,7 +283,10 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   return (
     <div className={`${styles['grw-page-controls']} hstack gap-2`} ref={pageControlsRef}>
       { isViewMode && isDeviceLargerThanMd && !isSearchPage && !isSearchPage && (
-        <SearchButton />
+        <>
+          <SearchButton />
+          <RagSearchButton />
+        </>
       )}
 
       {revisionId != null && !isViewMode && _isIPageInfoForOperation && (

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

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

@@ -3,7 +3,7 @@ import React, {
 } from 'react';
 
 import {
-  type EditorTheme, type KeyMapMode, DEFAULT_KEYMAP, DEFAULT_THEME,
+  type EditorTheme, type KeyMapMode, PasteMode, AllPasteMode, DEFAULT_KEYMAP, DEFAULT_PASTE_MODE, DEFAULT_THEME,
 } from '@growi/editor';
 import { useTranslation } from 'next-i18next';
 import Image from 'next/image';
@@ -174,6 +174,29 @@ const IndentSizeSelector = memo(({ onClickBefore }: {onClickBefore: () => void})
 IndentSizeSelector.displayName = 'IndentSizeSelector';
 
 
+const PasteSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
+
+  const { t } = useTranslation();
+  const { data: editorSettings, update } = useEditorSettings();
+  const selectedPasteMode = editorSettings?.pasteMode ?? DEFAULT_PASTE_MODE;
+
+  const listItems = useMemo(() => (
+    <>
+      { (AllPasteMode).map((pasteMode) => {
+        return (
+          <RadioListItem onClick={() => update({ pasteMode })} text={t(`page_edit.paste.${pasteMode}`) ?? ''} checked={pasteMode === selectedPasteMode} />
+        );
+      }) }
+    </>
+  ), [update, t, selectedPasteMode]);
+
+  return (
+    <Selector header={t('page_edit.paste.title')} onClickBefore={onClickBefore} items={listItems} />
+  );
+});
+PasteSelector.displayName = 'PasteSelector';
+
+
 type SwitchItemProps = {
   inputId: string,
   onChange: () => void,
@@ -269,6 +292,7 @@ const OptionsStatus = {
   Theme: 'Theme',
   Keymap: 'Keymap',
   Indent: 'Indent',
+  Paste: 'Paste',
 } as const;
 type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus];
 
@@ -330,6 +354,12 @@ export const OptionsSelector = (): JSX.Element => {
                 data={currentIndentSize.toString() ?? ''}
               />
               <hr className="my-1" />
+              <ChangeStateButton
+                onClick={() => setStatus(OptionsStatus.Paste)}
+                header={t('page_edit.paste.title')}
+                data={t(`page_edit.paste.${editorSettings.pasteMode ?? PasteMode.both}`) ?? ''}
+              />
+              <hr className="my-1" />
               <ConfigurationSelector />
             </div>
           )
@@ -346,6 +376,9 @@ export const OptionsSelector = (): JSX.Element => {
           <IndentSizeSelector onClickBefore={() => setStatus(OptionsStatus.Home)} />
         )
         }
+        { status === OptionsStatus.Paste && (
+          <PasteSelector onClickBefore={() => setStatus(OptionsStatus.Home)} />
+        )}
       </DropdownMenu>
     </Dropdown>
   );

+ 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

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

@@ -5,7 +5,7 @@ import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import dynamic from 'next/dynamic';
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as ReactMarkdownOptions } from 'react-markdown';
 import {
   Modal, ModalBody,
 } from 'reactstrap';

+ 5 - 6
apps/app/src/client/components/ReactMarkdownComponents/Header.tsx

@@ -2,8 +2,8 @@ import { useCallback, useEffect, useState } from 'react';
 
 import type EventEmitter from 'events';
 
+import type { Element } from 'hast';
 import { useRouter } from 'next/router';
-import type { Element } from 'react-markdown/lib/rehype-filter';
 
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import {
@@ -54,26 +54,25 @@ const EditLink = (props: EditLinkProps): JSX.Element => {
 type HeaderProps = {
   children: React.ReactNode,
   node: Element,
-  level: number,
   id?: string,
 }
 
 export const Header = (props: HeaderProps): JSX.Element => {
   const {
-    node, id, children, level,
+    node, id, children,
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPageYjsData } = useCurrentPageYjsData();
+  const { data: currentPageYjsData, isLoading: isLoadingCurrentPageYjsData } = useCurrentPageYjsData();
 
   const router = useRouter();
 
   const [isActive, setActive] = useState(false);
 
-  const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
+  const CustomTag = node.tagName as keyof JSX.IntrinsicElements;
 
   const activateByHash = useCallback((url: string) => {
     try {
@@ -118,7 +117,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   // It will be possible to address this TODO ySyncAnnotation become available for import.
   // Ref: https://github.com/yjs/y-codemirror.next/pull/30
   const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null
-                            && currentPageYjsData?.hasYdocsNewerThanLatestRevision === false;
+                            && (!isLoadingCurrentPageYjsData && !currentPageYjsData?.hasYdocsNewerThanLatestRevision);
 
   return (
     <>

+ 5 - 5
apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import type EventEmitter from 'events';
 
-import type { Element } from 'react-markdown/lib/rehype-filter';
+import type { Element } from 'hast';
 
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
@@ -23,8 +23,7 @@ type TableWithEditButtonProps = {
   className?: string
 }
 
-export const TableWithEditButton = React.memo((props: TableWithEditButtonProps): JSX.Element => {
-
+const TableWithEditButtonNoMemorized = (props: TableWithEditButtonProps): JSX.Element => {
   const { children, node, className } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
@@ -61,5 +60,6 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
       </table>
     </div>
   );
-});
-TableWithEditButton.displayName = 'TableWithEditButton';
+};
+TableWithEditButtonNoMemorized.displayName = 'TableWithEditButton';
+export const TableWithEditButton = React.memo(TableWithEditButtonNoMemorized) as typeof TableWithEditButtonNoMemorized;

+ 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/components/Sidebar/AppTitle/AppTitle.module.scss

@@ -39,13 +39,13 @@
 // == App title truncation
 .on-subnavigation {
   // set width for truncation
-  $grw-page-controls-width: 280px;
+  $grw-page-controls-width: 324px;
   $grw-page-editor-mode-manager-width: 90px;
   $grw-contextual-subnavigation-padding-right: 12px;
   $gap: 8px;
 
   @include bs.media-breakpoint-up(sm) {
-    width: calc(100vw - #{$grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
+    width: calc(100% - #{$grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
   }
 
   @include bs.media-breakpoint-up(md) {
@@ -53,7 +53,7 @@
     $gap: 24px;
     $grw-contextual-subnavigation-padding-right: 24px;
 
-    width: calc(100vw - #{var.$grw-sidebar-nav-width + $grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
+    width: calc(100% - #{var.$grw-sidebar-nav-width + $grw-page-controls-width + $grw-page-editor-mode-manager-width + $grw-contextual-subnavigation-padding-right + $gap * 2});
   }
 }
 

+ 1 - 17
apps/app/src/client/components/Sidebar/InAppNotification/PrimaryItemForNotification.tsx

@@ -1,15 +1,11 @@
 import { memo, useCallback, useEffect } from 'react';
 
-import { apiv3Post } from '~/client/util/apiv3-client';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 import { useDefaultSocket } from '~/stores/socket-io';
-import loggerFactory from '~/utils/logger';
 
 import { PrimaryItem, type Props } from '../SidebarNav/PrimaryItem';
 
-const logger = loggerFactory('growi:PrimaryItemsForNotification');
-
 type PrimaryItemForNotificationProps = Omit<Props, 'onClick' | 'label' | 'iconName' | 'contents' | 'badgeContents' >
 
 // TODO(after v7 release): https://redmine.weseek.co.jp/issues/138463
@@ -22,20 +18,9 @@ export const PrimaryItemForNotification = memo((props: PrimaryItemForNotificatio
 
   const badgeContents = notificationCount != null && notificationCount > 0 ? notificationCount : undefined;
 
-  const updateNotificationStatus = useCallback(async() => {
-    try {
-      await apiv3Post('/in-app-notification/read');
-      mutateNotificationCount();
-    }
-    catch (err) {
-      logger.error(err);
-    }
-  }, [mutateNotificationCount]);
-
   const itemHoverHandler = useCallback((contents: SidebarContentsType) => {
     onHover?.(contents);
-    updateNotificationStatus();
-  }, [onHover, updateNotificationStatus]);
+  }, [onHover]);
 
   useEffect(() => {
     if (socket != null) {
@@ -57,7 +42,6 @@ export const PrimaryItemForNotification = memo((props: PrimaryItemForNotificatio
       label="In-App Notification"
       iconName="notifications"
       badgeContents={badgeContents}
-      onClick={updateNotificationStatus}
       onHover={itemHoverHandler}
     />
   );

+ 11 - 0
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -41,6 +41,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isShowRestrictedByGroup: false,
       isUsersHomepageDeletionEnabled: false,
       isForceDeleteUserHomepageOnUserDeletion: false,
+      isRomUserAllowedToComment: false,
       isLocalEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
@@ -79,6 +80,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
       isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
       isForceDeleteUserHomepageOnUserDeletion: generalSetting.isForceDeleteUserHomepageOnUserDeletion,
+      isRomUserAllowedToComment: generalSetting.isRomUserAllowedToComment,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
@@ -220,6 +222,14 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ isForceDeleteUserHomepageOnUserDeletion: !this.state.isForceDeleteUserHomepageOnUserDeletion });
   }
 
+  /**
+   * switch isRomUserAllowedToComment
+   */
+  switchIsRomUserAllowedToComment(bool) {
+    this.setState({ isRomUserAllowedToComment: bool });
+  }
+
+
   /**
    * Update restrictGuestMode
    * @memberOf AdminGeneralSecuritySContainer
@@ -239,6 +249,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
       isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
       isForceDeleteUserHomepageOnUserDeletion: this.state.isForceDeleteUserHomepageOnUserDeletion,
+      isRomUserAllowedToComment: this.state.isRomUserAllowedToComment,
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);

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

+ 24 - 5
apps/app/src/client/services/renderer/renderer.tsx

@@ -10,6 +10,7 @@ import sanitize from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import type { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
+import remarkGithubAdmonitionsToDirectives from 'remark-github-admonitions-to-directives';
 import math from 'remark-math';
 import deepmerge from 'ts-deepmerge';
 import type { Pluggable } from 'unified';
@@ -19,6 +20,7 @@ import { Header } from '~/client/components/ReactMarkdownComponents/Header';
 import { LightBox } from '~/client/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/client/components/ReactMarkdownComponents/RichAttachment';
 import { TableWithEditButton } from '~/client/components/ReactMarkdownComponents/TableWithEditButton';
+import * as callout from '~/features/callout';
 import * as mermaid from '~/features/mermaid';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -26,6 +28,7 @@ import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as attachment from '~/services/renderer/remark-plugins/attachment';
+import * as codeBlock from '~/services/renderer/remark-plugins/codeblock';
 import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
@@ -64,6 +67,8 @@ export const generateViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    remarkGithubAdmonitionsToDirectives,
+    callout.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -71,15 +76,17 @@ export const generateViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
       presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
+      callout.sanitizeOption,
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -110,6 +117,7 @@ export const generateViewOptions = (
     components.drawio = DrawioViewerWithEditButton;
     components.table = TableWithEditButton;
     components.mermaid = mermaid.MermaidViewer;
+    components.callout = callout.CalloutViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
   }
@@ -129,9 +137,10 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   // add remark plugins
   // remarkPlugins.push();
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -166,6 +175,8 @@ export const generateSimpleViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    remarkGithubAdmonitionsToDirectives,
+    callout.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -176,15 +187,17 @@ export const generateSimpleViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
       presentation.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
+      callout.sanitizeOption,
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -207,6 +220,7 @@ export const generateSimpleViewOptions = (
     components.gallery = refsGrowiDirective.GalleryImmutable;
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
+    components.callout = callout.CalloutViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
   }
@@ -227,7 +241,7 @@ export const generatePresentationViewOptions = (
   const { rehypePlugins } = options;
 
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       addLineNumberAttribute.sanitizeOption,
     )]
@@ -258,6 +272,8 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    remarkGithubAdmonitionsToDirectives,
+    callout.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -265,15 +281,17 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
+      callout.sanitizeOption,
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -296,6 +314,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.gallery = refsGrowiDirective.GalleryImmutable;
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
+    components.callout = callout.CalloutViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
   }

+ 31 - 16
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -18,23 +18,36 @@ const MenuLabel = ({ menu }: { menu: string }) => {
 
   switch (menu) {
     /* eslint-disable no-multi-spaces, max-len */
-    case 'app':                      return <><span className="material-symbols-outlined me-1">settings</span>{        t('headers.app_settings', { ns: 'commons' }) }</>;
-    case 'security':                 return <><span className="material-symbols-outlined me-1">shield</span>{          t('security_settings.security_settings') }</>;
-    case 'markdown':                 return <><span className="material-symbols-outlined me-1">note</span>{            t('markdown_settings.markdown_settings') }</>;
-    case 'customize':                return <><span className="material-symbols-outlined me-1">construction</span>{          t('customize_settings.customize_settings') }</>;
-    case 'importer':                 return <><span className="material-symbols-outlined me-1">cloud_upload</span>{    t('importer_management.import_data') }</>;
-    case 'export':                   return <><span className="material-symbols-outlined me-1">cloud_download</span>{  t('export_management.export_archive_data') }</>;
+    case 'app':                      return <><span className="material-symbols-outlined me-1">settings</span>{         t('headers.app_settings', { ns: 'commons' }) }</>;
+    case 'security':                 return <><span className="material-symbols-outlined me-1">shield</span>{           t('security_settings.security_settings') }</>;
+    case 'markdown':                 return <><span className="material-symbols-outlined me-1">note</span>{             t('markdown_settings.markdown_settings') }</>;
+    case 'customize':                return <><span className="material-symbols-outlined me-1">construction</span>{     t('customize_settings.customize_settings') }</>;
+    case 'importer':                 return <><span className="material-symbols-outlined me-1">cloud_upload</span>{     t('importer_management.import_data') }</>;
+    case 'export':                   return <><span className="material-symbols-outlined me-1">cloud_download</span>{   t('export_management.export_archive_data') }</>;
     case 'data-transfer':            return <><span className="material-symbols-outlined me-1">flight</span>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
-    case 'notification':             return <><span className="material-symbols-outlined me-1">notifications</span>{            t('external_notification.external_notification')}</>;
-    case 'slack-integration':        return <><span className="material-symbols-outlined me-1">shuffle</span>{         t('slack_integration.slack_integration') }</>;
-    case 'slack-integration-legacy': return <><span className="material-symbols-outlined me-1">shuffle</span>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
-    case 'users':                    return <><span className="material-symbols-outlined me-1">person</span>{            t('user_management.user_management') }</>;
-    case 'user-groups':              return <><span className="material-symbols-outlined me-1">group</span>{          t('user_group_management.user_group_management') }</>;
-    case 'audit-log':                return <><span className="material-symbols-outlined me-1">feed</span>{            t('audit_log_management.audit_log')}</>;
-    case 'plugins':                  return <><span className="material-symbols-outlined me-1">extension</span>{          t('plugins.plugins')}</>;
-    case 'search':                   return <><span className="material-symbols-outlined me-1">search</span>{       t('full_text_search_management.full_text_search_management') }</>;
-    case 'cloud':                    return <><span className="material-symbols-outlined me-1">share</span>{       t('cloud_setting_management.to_cloud_settings')} </>;
-    default:                         return <><span className="material-symbols-outlined me-1">home</span>{            t('wiki_management_homepage') }</>;
+    case 'notification':             return <><span className="material-symbols-outlined me-1">notifications</span>{    t('external_notification.external_notification')}</>;
+    case 'slack-integration':        return <><span className="material-symbols-outlined me-1">shuffle</span>{          t('slack_integration.slack_integration') }</>;
+    case 'slack-integration-legacy': return <><span className="material-symbols-outlined me-1">shuffle</span>{          t('slack_integration_legacy.slack_integration_legacy')}</>;
+    case 'users':                    return <><span className="material-symbols-outlined me-1">person</span>{           t('user_management.user_management') }</>;
+    case 'user-groups':              return <><span className="material-symbols-outlined me-1">group</span>{            t('user_group_management.user_group_management') }</>;
+    case 'audit-log':                return <><span className="material-symbols-outlined me-1">feed</span>{             t('audit_log_management.audit_log')}</>;
+    case 'plugins':                  return <><span className="material-symbols-outlined me-1">extension</span>{        t('plugins.plugins')}</>;
+    case 'ai-integration':           return (
+      <>{/* TODO: unify sizing of growi-custom-icons so that simplify code -- 2024.10.09 Yuki Takei */}
+        <span
+          className="growi-custom-icons d-inline-block me-1"
+          style={{
+            fontSize: '18px', width: '24px', height: '24px', lineHeight: '24px', verticalAlign: 'bottom', paddingLeft: '2px',
+          }}
+        >
+          growi_ai
+        </span>
+        {t('ai_integration.ai_integration')}
+      </>
+    );
+    case 'search':                   return <><span className="material-symbols-outlined me-1">search</span>{           t('full_text_search_management.full_text_search_management') }</>;
+    case 'cloud':                    return <><span className="material-symbols-outlined me-1">share</span>{            t('cloud_setting_management.to_cloud_settings')} </>;
+    default:                         return <><span className="material-symbols-outlined me-1">home</span>{             t('wiki_management_homepage') }</>;
       /* eslint-enable no-multi-spaces, max-len */
   }
 };
@@ -106,6 +119,7 @@ export const AdminNavigation = (): JSX.Element => {
         <MenuLink menu="user-groups" isListGroupItems={isListGroupItems} isActive={isActiveMenu(['/user-groups', 'user-group-detail'])} />
         <MenuLink menu="audit-log" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="plugins" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/plugins')} />
+        <MenuLink menu="ai-integration" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/ai-integration')} />
         <MenuLink menu="search" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/search')} />
         {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (
@@ -159,6 +173,7 @@ export const AdminNavigation = (): JSX.Element => {
             {isActiveMenu('/audit-log')             && <MenuLabel menu="audit-log" />}
             {isActiveMenu('/plugins')               && <MenuLabel menu="plugins" />}
             {isActiveMenu('/data-transfer')         && <MenuLabel menu="data-transfer" />}
+            {isActiveMenu('/ai-integration')                && <MenuLabel menu="ai-integration" />}
             {/* eslint-enable no-multi-spaces */}
           </span>
         </button>

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

@@ -34,7 +34,7 @@ const DeleteBookmarkFolderModal = dynamic(
   () => import('~/client/components/DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false },
 );
 const SearchModal = dynamic(() => import('../../features/search/client/components/SearchModal'), { ssr: false });
-
+const AiChatModal = dynamic(() => import('~/features/openai/chat/components/AiChatModal').then(mod => mod.AiChatModal), { ssr: false });
 
 type Props = {
   children?: ReactNode
@@ -67,6 +67,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
       <DeleteBookmarkFolderModal />
       <PutbackPageModal />
       <SearchModal />
+      <AiChatModal />
 
       <PagePresentationModal />
       <HotkeysManager />

+ 23 - 6
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -1,6 +1,5 @@
 import type { ReactNode } from 'react';
 
-import type { CodeComponent, CodeProps } from 'react-markdown/lib/ast-to-react';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
@@ -14,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) {
@@ -45,7 +55,8 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   // see: https://github.com/weseek/growi/pull/7484
   //
   // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
-  const isSimpleString = Array.isArray(children) && children.length === 1 && typeof children[0] === 'string';
+
+  const isSimpleString = typeof children === 'string' || (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string');
   if (!isSimpleString) {
     return (
       <div style={oneDark['pre[class*="language-"]']}>
@@ -67,13 +78,19 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   );
 }
 
-export const CodeBlock: CodeComponent = ({ inline, className, children }: CodeProps) => {
+type CodeBlockProps = {
+  children: ReactNode,
+  className?: string,
+  inline?: true,
+}
 
-  if (inline) {
-    return <code className={`code-inline ${className ?? ''}`}>{children}</code>;
-  }
+export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
 
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
+  const { className, children, inline } = props;
+  if (inline) {
+    return <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>{children}</InlineCodeBlockSubstance>;
+  }
 
   const match = /language-(\w+)(:?.+)?/.exec(className || '');
   const lang = match && match[1] ? match[1] : '';

+ 78 - 0
apps/app/src/features/callout/components/CalloutViewer.module.scss

@@ -0,0 +1,78 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+// == Colors
+.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{
+
+  .callout {
+    padding: 0.5rem 1rem;
+    margin: 1rem 0rem;
+    color: inherit;
+  }
+
+  .callout-indicator {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16px;
+    line-height: 1;
+  }
+
+  .callout-hint {
+    display: inline-block;
+    margin-right: 0.3rem;
+    vertical-align: text-bottom;
+    fill: currentColor;
+  }
+
+  .callout-note {
+    .callout-indicator {
+      color: var(--callout-accent-note);
+    }
+    border-left: .25em solid var(--callout-accent-note);
+  }
+
+  .callout-tip {
+    .callout-indicator {
+      color: var(--callout-accent-tip);
+    }
+    border-left: .25em solid var(--callout-accent-tip);
+  }
+
+  .callout-warning {
+    .callout-indicator {
+      color: var(--callout-accent-warning);
+    }
+    border-left:.25em solid var(--callout-accent-warning);
+  }
+
+  .callout-caution {
+    .callout-indicator {
+      color: var(--callout-accent-caution);
+    }
+    border-left:.25em solid var(--callout-accent-caution);
+  }
+
+  .callout-important {
+    .callout-indicator {
+      color: var(--callout-accent-important);
+    }
+    border-left:.25em solid var(--callout-accent-important);
+  }
+
+  .callout-content:first-child,
+  .callout-content:only-child {
+    margin-block-start: 0;
+  }
+
+  .callout-content:last-child,
+  .callout-content:only-child {
+    margin-block-end: 0;
+  }
+}

+ 68 - 0
apps/app/src/features/callout/components/CalloutViewer.tsx

@@ -0,0 +1,68 @@
+// Ref: https://github.com/Microflash/remark-callout-directives/blob/fabe4d8adc7738469f253836f0da346591ea2a2b/README.md
+
+import type { ReactNode } from 'react';
+import React from 'react';
+
+import { type Callout } from '../services/consts';
+
+import styles from './CalloutViewer.module.scss';
+
+const moduleClass = styles['callout-viewer'];
+
+type CALLOUT_TO = {
+  [key in Callout]: string;
+}
+
+const CALLOUT_TO_TITLE: CALLOUT_TO = {
+  note: 'Note',
+  tip: 'Tip',
+  important: 'Important',
+  info: 'Important',
+  warning: 'Warning',
+  caution: 'Caution',
+  danger: 'Caution',
+};
+
+const CALLOUT_TO_ICON: CALLOUT_TO = {
+  note: 'info',
+  tip: 'lightbulb',
+  important: 'feedback',
+  info: 'feedback',
+  warning: 'warning',
+  caution: 'report',
+  danger: 'report',
+};
+
+type CalloutViewerProps = {
+  children: ReactNode,
+  node: Element,
+  name: string
+}
+
+export const CalloutViewer = React.memo((props: CalloutViewerProps): JSX.Element => {
+
+  const { node, name, children } = props;
+
+  if (node == null) {
+    return <></>;
+  }
+
+  return (
+    <div className={`${moduleClass} callout-viewer`}>
+      <div className={`callout callout-${CALLOUT_TO_TITLE[name].toLowerCase()}`}>
+        <div className="callout-indicator">
+          <div className="callout-hint">
+            <span className="material-symbols-outlined">{CALLOUT_TO_ICON[name]}</span>
+          </div>
+          <div className="callout-title">
+            {CALLOUT_TO_TITLE[name]}
+          </div>
+        </div>
+        <div className="callout-content">
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+});
+CalloutViewer.displayName = 'CalloutViewer';

+ 1 - 0
apps/app/src/features/callout/components/index.ts

@@ -0,0 +1 @@
+export { CalloutViewer } from './CalloutViewer';

+ 2 - 0
apps/app/src/features/callout/index.ts

@@ -0,0 +1,2 @@
+export * from './components';
+export * from './services';

+ 23 - 0
apps/app/src/features/callout/services/callout.ts

@@ -0,0 +1,23 @@
+import type { ContainerDirective } from 'mdast-util-directive';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+import { AllCallout } from './consts';
+
+export const remarkPlugin: Plugin = () => {
+  return (tree) => {
+    visit(tree, 'containerDirective', (node: ContainerDirective) => {
+      if (AllCallout.some(name => name === node.name.toLowerCase())) {
+        const data = node.data ?? (node.data = {});
+        data.hName = 'callout';
+        data.hProperties = {
+          name: node.name.toLocaleLowerCase(),
+        };
+      }
+    });
+  };
+};
+
+export const sanitizeOption = {
+  tagNames: ['callout'],
+};

Неке датотеке нису приказане због велике количине промена