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

Merge branch 'master' into imprv/125884-154071-eliminate-req.t()

Yuki Takei 1 год назад
Родитель
Сommit
ddb0874b23
100 измененных файлов с 2049 добавлено и 1192 удалено
  1. 5 2
      .changeset/config.json
  2. 0 59
      .devcontainer/Dockerfile
  3. 3 22
      .devcontainer/compose.yml
  4. 34 33
      .devcontainer/devcontainer.json
  5. 17 0
      .devcontainer/postCreateCommand.sh
  6. 0 2
      .gitattributes
  7. 1 1
      .github/dependabot.yml
  8. 1 1
      .github/mergify.yml
  9. 14 8
      .github/workflows/ci-app-prod.yml
  10. 16 47
      .github/workflows/ci-app.yml
  11. 23 57
      .github/workflows/ci-slackbot-proxy.yml
  12. 7 7
      .github/workflows/release-slackbot-proxy.yml
  13. 14 34
      .github/workflows/release-subpackages.yml
  14. 14 16
      .github/workflows/release.yml
  15. 30 78
      .github/workflows/reusable-app-prod.yml
  16. 5 15
      .github/workflows/reusable-app-reg-suit.yml
  17. 3 0
      .gitignore
  18. 0 0
      .npmrc
  19. 2 1
      .vscode/launch.json
  20. 115 1
      CHANGELOG.md
  21. 7 7
      README.md
  22. 7 7
      README_JP.md
  23. 28 0
      apps/app/bin/swagger-jsdoc/definition-apiv1.js
  24. 93 0
      apps/app/bin/swagger-jsdoc/definition-apiv3.js
  25. 15 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv1.sh
  26. 14 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh
  27. 1 1
      apps/app/config/logger/config.dev.js
  28. 0 37
      apps/app/config/swagger-definition.js
  29. 28 76
      apps/app/docker/Dockerfile
  30. 1 0
      apps/app/docker/Dockerfile.dockerignore
  31. 4 5
      apps/app/docker/README.md
  32. 1 8
      apps/app/docker/codebuild/buildspec.yml
  33. 1 1
      apps/app/next-env.d.ts
  34. 13 1
      apps/app/next.config.js
  35. 4 0
      apps/app/nodemon.json
  36. 106 74
      apps/app/package.json
  37. 1 1
      apps/app/playwright.config.ts
  38. 1 1
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  39. 1 1
      apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts
  40. 1 1
      apps/app/playwright/utils/Login.ts
  41. 17 0
      apps/app/public/static/locales/en_US/admin.json
  42. 1 1
      apps/app/public/static/locales/en_US/commons.json
  43. 22 1
      apps/app/public/static/locales/en_US/translation.json
  44. 17 0
      apps/app/public/static/locales/fr_FR/admin.json
  45. 1 1
      apps/app/public/static/locales/fr_FR/commons.json
  46. 21 1
      apps/app/public/static/locales/fr_FR/translation.json
  47. 17 0
      apps/app/public/static/locales/ja_JP/admin.json
  48. 1 1
      apps/app/public/static/locales/ja_JP/commons.json
  49. 21 1
      apps/app/public/static/locales/ja_JP/translation.json
  50. 17 0
      apps/app/public/static/locales/zh_CN/admin.json
  51. 1 1
      apps/app/public/static/locales/zh_CN/commons.json
  52. 21 1
      apps/app/public/static/locales/zh_CN/translation.json
  53. BIN
      apps/app/resource/fonts/MaterialSymbolsOutlined-opsz,wght,FILL@20..48,300,0..1.woff2
  54. BIN
      apps/app/resource/fonts/PressStart2P-latin.woff2
  55. BIN
      apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2
  56. BIN
      apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2
  57. 0 13
      apps/app/resource/locales/en_US/notifications/notActiveUser.ejs
  58. 3 3
      apps/app/resource/locales/en_US/sandbox-diagrams.md
  59. 1 1
      apps/app/resource/locales/en_US/sandbox-math.md
  60. 188 26
      apps/app/resource/locales/en_US/sandbox.md
  61. 0 13
      apps/app/resource/locales/fr_FR/notifications/notActiveUser.ejs
  62. 3 3
      apps/app/resource/locales/fr_FR/sandbox-diagrams.md
  63. 1 1
      apps/app/resource/locales/fr_FR/sandbox-math.md
  64. 262 112
      apps/app/resource/locales/fr_FR/sandbox.md
  65. 0 13
      apps/app/resource/locales/ja_JP/notifications/notActiveUser.ejs
  66. 3 3
      apps/app/resource/locales/ja_JP/sandbox-diagrams.md
  67. 182 187
      apps/app/resource/locales/ja_JP/sandbox.md
  68. 0 13
      apps/app/resource/locales/zh_CN/notifications/notActiveUser.ejs
  69. 3 3
      apps/app/resource/locales/zh_CN/sandbox-diagrams.md
  70. 1 1
      apps/app/resource/locales/zh_CN/sandbox-math.md
  71. 232 86
      apps/app/resource/locales/zh_CN/sandbox.md
  72. 4 0
      apps/app/resource/search/mappings-es7.json
  73. 4 0
      apps/app/resource/search/mappings-es8.json
  74. 4 4
      apps/app/src/client/components/Admin/AdminHome/SystemInfomationTable.tsx
  75. 10 1
      apps/app/src/client/components/Admin/G2GDataTransfer.tsx
  76. 8 2
      apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx
  77. 34 1
      apps/app/src/client/components/Admin/Security/SecuritySetting.jsx
  78. 0 1
      apps/app/src/client/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  79. 12 11
      apps/app/src/client/components/Bookmarks/DragAndDropWrapper.tsx
  80. 3 2
      apps/app/src/client/components/Common/Dropdown/PageItemControl.spec.tsx
  81. 0 1
      apps/app/src/client/components/CustomNavigation/CustomNav.module.scss
  82. 19 3
      apps/app/src/client/components/CustomNavigation/CustomNav.tsx
  83. 9 1
      apps/app/src/client/components/DataTransferForm.tsx
  84. 3 2
      apps/app/src/client/components/DescendantsPageList.tsx
  85. 4 0
      apps/app/src/client/components/DescendantsPageListModal.module.scss
  86. 70 0
      apps/app/src/client/components/DescendantsPageListModal.spec.tsx
  87. 25 9
      apps/app/src/client/components/DescendantsPageListModal.tsx
  88. 0 19
      apps/app/src/client/components/InAppNotification/InAppNotificationDropdown.tsx
  89. 7 2
      apps/app/src/client/components/InAppNotification/InAppNotificationElm.tsx
  90. 4 22
      apps/app/src/client/components/InAppNotification/InAppNotificationPage.tsx
  91. 1 1
      apps/app/src/client/components/InfiniteScroll.tsx
  92. 3 2
      apps/app/src/client/components/Me/InAppNotificationSettings.tsx
  93. 8 2
      apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx
  94. 2 1
      apps/app/src/client/components/NotAvailable.tsx
  95. 92 0
      apps/app/src/client/components/NotAvailableForReadOnlyUser.spec.tsx
  96. 24 1
      apps/app/src/client/components/NotAvailableForReadOnlyUser.tsx
  97. 1 1
      apps/app/src/client/components/Page/SlideRenderer.tsx
  98. 4 0
      apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.module.scss
  99. 24 9
      apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  100. 3 3
      apps/app/src/client/components/PageComment.tsx

+ 5 - 2
.changeset/config.json

@@ -17,8 +17,11 @@
     "@growi/custom-icons",
     "@growi/custom-icons",
     "@growi/editor",
     "@growi/editor",
     "@growi/presentation",
     "@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/slack",
     "@growi/ui"
     "@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:
 services:
   node:
   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:
     volumes:
       - ..:/workspace/growi:delegated
       - ..:/workspace/growi:delegated
+      - pnpm-store:/workspace/growi/.pnpm-store
       - node_modules:/workspace/growi/node_modules
       - 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
       - buildcache_app:/workspace/growi/apps/app/.next
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
-
     tty: true
     tty: true
 
 
   mongo:
   mongo:
@@ -59,7 +41,6 @@ services:
       - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
       - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
 
 volumes:
 volumes:
+  pnpm-store:
   node_modules:
   node_modules:
-  node_modules_app:
-  node_modules_slackbot-proxy:
   buildcache_app:
   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",
   "name": "GROWI-Dev",
-  "dockerComposeFile": "docker-compose.yml",
+  "dockerComposeFile": "compose.yml",
   "service": "node",
   "service": "node",
   "workspaceFolder": "/workspace/growi",
   "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.
   // Use 'postCreateCommand' to run commands after the container is created.
-  "postCreateCommand": "git-lfs pull & turbo run bootstrap",
+  "postCreateCommand": "/bin/bash ./.devcontainer/postCreateCommand.sh",
+
+  // Configure tool-specific properties.
+  "customizations": {
+    "vscode": {
+      "extensions": [
+        "dbaeumer.vscode-eslint",
+        "mhutchie.git-graph",
+        "eamodio.gitlens",
+        "github.vscode-pull-request-github",
+        "cschleiden.vscode-github-actions",
+        "cweijan.vscode-database-client2",
+        "mongodb.mongodb-vscode",
+        "msjsdiag.debugger-for-chrome",
+        "firefox-devtools.vscode-firefox-debug",
+        "editorconfig.editorconfig",
+        "shinnn.stylelint",
+        "stylelint.vscode-stylelint",
+        "vitest.explorer",
+        "ms-playwright.playwright"
+      ],
+    }
+  },
 
 
-  // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
-  "remoteUser": "node"
+  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+  // "remoteUser": "root"
 }
 }

+ 17 - 0
.devcontainer/postCreateCommand.sh

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

+ 0 - 2
.gitattributes

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

+ 1 - 1
.github/dependabot.yml

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

+ 1 - 1
.github/mergify.yml

@@ -23,8 +23,8 @@ pull_request_rules:
   - name: Automatic queue to merge
   - name: Automatic queue to merge
     conditions:
     conditions:
       - '#approved-reviews-by >= 1'
       - '#approved-reviews-by >= 1'
+      - '#changes-requested-reviews-by = 0'
       - '#review-requested = 0'
       - '#review-requested = 0'
-      - check-success = check-title
     actions:
     actions:
       queue:
       queue:
 
 

+ 14 - 8
.github/workflows/ci-app-prod.yml

@@ -5,7 +5,6 @@ on:
     branches:
     branches:
       - master
       - master
       - dev/7.*.x
       - dev/7.*.x
-      - dev/6.*.x
     paths:
     paths:
       - .github/mergify.yml
       - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/ci-app-prod.yml
@@ -13,17 +12,12 @@ on:
       - .github/workflows/reusable-app-reg-suit.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
       - tsconfig.base.json
       - turbo.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - package.json
       - apps/app/**
       - apps/app/**
       - '!apps/app/docker/**'
       - '!apps/app/docker/**'
       - packages/**
       - packages/**
   pull_request:
   pull_request:
-    branches:
-      - master
-      - dev/7.*.x
-      - dev/6.*.x
-      - release/*
     types: [opened, reopened, synchronize]
     types: [opened, reopened, synchronize]
     paths:
     paths:
       - .github/mergify.yml
       - .github/mergify.yml
@@ -31,7 +25,7 @@ on:
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
       - tsconfig.base.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - turbo.json
       - turbo.json
       - package.json
       - package.json
       - apps/app/**
       - apps/app/**
@@ -47,6 +41,12 @@ jobs:
 
 
   test-prod-node18:
   test-prod-node18:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    if: |
+      ( github.event_name == 'push'
+        || github.base_ref == 'master'
+        || github.base_ref == 'dev/7.*.x'
+        || startsWith( github.base_ref, 'release/' )
+        || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
       node-version: 18.x
       node-version: 18.x
       skip-e2e-test: true
       skip-e2e-test: true
@@ -56,6 +56,12 @@ jobs:
 
 
   test-prod-node20:
   test-prod-node20:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    if: |
+      ( github.event_name == 'push'
+        || github.base_ref == 'master'
+        || github.base_ref == 'dev/7.*.x'
+        || startsWith( github.base_ref, 'release/' )
+        || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
       node-version: 20.x
       node-version: 20.x
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}

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

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

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

@@ -12,7 +12,7 @@ on:
       - .eslint*
       - .eslint*
       - tsconfig.base.json
       - tsconfig.base.json
       - turbo.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - package.json
       - apps/slackbot-proxy/**
       - apps/slackbot-proxy/**
       - '!apps/slackbot-proxy/docker/**'
       - '!apps/slackbot-proxy/docker/**'
@@ -35,21 +35,12 @@ jobs:
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         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
     - name: Restore dist
       uses: actions/cache/restore@v4
       uses: actions/cache/restore@v4
@@ -63,9 +54,8 @@ jobs:
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
 
     - name: Lint
     - name: Lint
       run: |
       run: |
@@ -110,21 +100,12 @@ jobs:
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         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
     - name: Restore dist
       uses: actions/cache/restore@v4
       uses: actions/cache/restore@v4
@@ -138,11 +119,10 @@ jobs:
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       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
       working-directory: ./apps/slackbot-proxy
       run: |
       run: |
         cp config/ci/.env.local.for-ci .env.development.local
         cp config/ci/.env.local.for-ci .env.development.local
@@ -198,36 +178,20 @@ jobs:
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
 
     - name: Install turbo
     - name: Install turbo
       run: |
       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
     - name: Install dependencies
       run: |
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
 
     - name: Restore dist
     - name: Restore dist
       uses: actions/cache/restore@v4
       uses: actions/cache/restore@v4
@@ -244,15 +208,17 @@ jobs:
       run: |
       run: |
         turbo run build
         turbo run build
 
 
-    - name: Install dependencies for production
+    - name: Assembling all dependencies
       run: |
       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
       working-directory: ./apps/slackbot-proxy
       run: |
       run: |
         cp config/ci/.env.local.for-ci .env.production.local
         cp config/ci/.env.local.for-ci .env.production.local
-        yarn start:prod:ci
+        pnpm run start:prod:ci
       env:
       env:
         SERVER_URI: http://localhost:8080
         SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql

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

@@ -41,7 +41,7 @@ jobs:
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
 
 
     - name: Setup gcloud
     - name: Setup gcloud
-      uses: google-github-actions/setup-gcloud@v1
+      uses: google-github-actions/setup-gcloud@v2
 
 
     - name: Configure docker for gcloud
     - name: Configure docker for gcloud
       run: |
       run: |
@@ -93,21 +93,21 @@ jobs:
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: '18'
         node-version: '18'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       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
     - name: Bump versions for next RC
       run: |
       run: |
-        turbo run version --filter=@growi/slackbot-proxy -- --prerelease
+        turbo run version:prerelease --filter=@growi/slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@2.0.1
       uses: myrotvorets/info-from-package-json-action@2.0.1

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

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

+ 14 - 16
.github/workflows/release.yml

@@ -22,22 +22,21 @@ jobs:
       with:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: '20'
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
 
     - name: Bump versions
     - name: Bump versions
       run: |
       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
         sh ./apps/app/bin/github-actions/update-readme.sh
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
@@ -64,7 +63,7 @@ jobs:
         commit_message: Release v${{ steps.package-json.outputs.packageVersion }}
         commit_message: Release v${{ steps.package-json.outputs.packageVersion }}
         tagging_message: v${{ steps.package-json.outputs.packageVersion }}
         tagging_message: v${{ steps.package-json.outputs.packageVersion }}
 
 
-    - uses: softprops/action-gh-release@v1
+    - uses: softprops/action-gh-release@v2
       with:
       with:
         body: ${{ github.event.pull_request.body }}
         body: ${{ github.event.pull_request.body }}
         tag_name: v${{ steps.package-json.outputs.packageVersion }}
         tag_name: v${{ steps.package-json.outputs.packageVersion }}
@@ -162,23 +161,22 @@ jobs:
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: '20'
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       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
     - name: Bump versions for next RC
       run: |
       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
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@2.0.1
       uses: myrotvorets/info-from-package-json-action@2.0.1

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

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

+ 3 - 0
.gitignore

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


+ 2 - 1
.vscode/launch.json

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

+ 115 - 1
CHANGELOG.md

@@ -1,9 +1,123 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.19...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.1.0...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.1.0](https://github.com/weseek/growi/compare/v7.0.23...v7.1.0) - 2024-10-31
+
+### BREAKING CHANGES
+
+* imprv: Update default value for S3\_OBJECT\_ACL (#9332) @yuki-takei
+
+### 💎 Features
+
+* feat: GROWI OpenAI Integration (#9246) @yuki-takei
+
+### 🚀 Improvement
+
+* imprv: Add GitHub Markdown alerts  (#9127) @reiji-h
+* imprv: Upgrade unified and remark-growi-directive (#9048) @reiji-h
+* imprv: ROM users can manage comments (#9101) @WNomunomu
+* imprv: Update default value for S3\_OBJECT\_ACL (#9332) @yuki-takei
+* imprv: Sandbox (#9330) @yuki-takei
+* support: JSDoc for OpenAPI document (#9311) @yuki-takei
+
+
+### 🐛 Bug Fixes
+
+* fix: Couldn't show old revision (#9296) @yuki-takei
+* fix: Replace the word ROM (#9295) @satof3
+* fix: forgot-password API (#9257) @reiji-h
+* fix: Edit button appear for the side of header (#9270) @yuki-takei
+* fix: Ensure text-only paste for mixed content from various sources (#9096) @reiji-h
+* fix: Notification count badge (#9124) @shironegi39
+* fix(ogp): Set an unknown label when the user is not found (#9232) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Migrate to pnpm from yarn v1 (#9249) @yuki-takei
+* support: Omit MongoDB 4.x compatible code (#9334) @yuki-takei
+* support: Pull LFS files with turbo (#9325) @yuki-takei
+* support: Use `pnpm deploy` instead of `turbo prune` (#9323) @yuki-takei
+* support: Maintenance API docs generation (#9302) @yuki-takei
+* support: Improve typings for PageService (#9220) @yuki-takei
+* support: Typescriptize accessTokenParser (#9320) @yuki-takei
+* support: Migrate to pnpm from yarn v1 (#9249) @yuki-takei
+* support: JSDoc for OpenAPI document (#9311) @yuki-takei
+* support: Maintenance API docs generation (#9302) @yuki-takei
+* support: Omit docs route (#9299) @yuki-takei
+
+## [v7.0.23](https://github.com/weseek/growi/compare/v7.0.22...v7.0.23) - 2024-10-24
+
+### 🐛 Bug Fixes
+
+* fix: Couln't show old revision (#9296) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Maintenance API docs generation (#9302) @yuki-takei
+* support: Omit docs route (#9299) @yuki-takei
+
+## [v7.0.22](https://github.com/weseek/growi/compare/v7.0.21...v7.0.22) - 2024-10-21
+
+### 🐛 Bug Fixes
+
+* fix: Edit button appear for the side of header (#9270) @yuki-takei
+* fix: Collaborative editing occurs unstable behavior (#9267) @yuki-takei
+
+## [v7.0.21](https://github.com/weseek/growi/compare/v7.0.20...v7.0.21) - 2024-10-15
+
+### 🚀 Improvement
+
+* imprv: Update Recent Changes when a page is created, updated, or deleted (#9092) @nHigashiWeseek
+* imprv: Documentation URL for g2gtransfer (#9183) @moekumasaka
+
+### 🐛 Bug Fixes
+
+* fix: Add validators to lsx API (#9182) @WNomunomu
+* fix: Display revisions only if they are not corrupted (#9099) @WNomunomu
+* fix: Make it impossible to overwrite grants on descendant pages when 'anyone with the link' is selected. (#9125) @WNomunomu
+* fix: Forgot password API - reject requests with invalid email format (#9179) @abichan99911111
+
+### 🧰 Maintenance
+
+* support: Upgrade codemirror and yjs packages (#9218) @yuki-takei
+
+## [v7.0.20](https://github.com/weseek/growi/compare/v7.0.19...v7.0.20) - 2024-09-25
+
+### 🚀 Improvement
+
+* imprv: The color of the dropdown list when it is activated (#9102) @WNomunomu
+* imprv: PageTitleHeader max-width (#9166) @yuki-takei
+* imprv: Documentation URL for g2gtransfer (#9157) @yuki-takei
+* imprv: Corrected wording on admin page (/admin/data-transfer) (#9106) @miya
+* imprv: Add hover-activated clipboard copy button with icon (#9095) @reiji-h
+
+### 🐛 Bug Fixes
+
+* fix: Make PageAccessoriesModal responsive (#9171) @moekumasaka
+* fix: PageControls unexpectedly move in response to opening and closing the sidebar (#9094) @WNomunomu
+* fix: Make CustomNavTab responsive (#9123) @moekumasaka
+
+### 🧰 Maintenance
+
+* ci(deps): bump rollup from 4.22.0 to 4.22.4 (#9160) @dependabot
+* ci(deps): bump google-github-actions/setup-gcloud from 1 to 2 (#9153) @dependabot
+* ci(deps): bump softprops/action-gh-release from 1 to 2 (#9152) @dependabot
+* support: Improve vitest environment (#9144) @yuki-takei
+* ci(deps): bump next from 14.1.3 to 14.2.13 (#9154) @dependabot
+* support: Upgrade @testing-library/react (#9141) @yuki-takei
+* support: Update logo image in README.md for the official docker image (#9139) @satof3
+* ci(deps-dev): bump vite from 5.2.9 to 5.2.14 (#9134) @dependabot
+* ci(deps): bump myrotvorets/info-from-package-json-action from 1.2.0 to 2.0.1 (#9129) @dependabot
+* ci(deps): bump stefanzweifel/git-auto-commit-action from 4 to 5 (#9128) @dependabot
+* ci(deps): bump nodemailer from 6.9.14 to 6.9.15 (#9075) @dependabot
+* ci(deps): bump docker/setup-buildx-action from 2 to 3 (#8207) @dependabot
+* ci(deps): bump jose from 4.11.4 to 4.15.9 (#9114) @dependabot
+* ci(deps): bump express from 4.19.2 to 4.20.0 (#9110) @dependabot
+* ci(deps): bump body-parser from 1.20.2 to 1.20.3 (#9109) @dependabot
+
 ## [v7.0.19](https://github.com/weseek/growi/compare/v7.0.18...v7.0.19) - 2024-09-12
 ## [v7.0.19](https://github.com/weseek/growi/compare/v7.0.18...v7.0.19) - 2024-09-12
 
 
 ### 🐛 Bug Fixes
 ### 🐛 Bug Fixes

+ 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
 - Node.js v18.x or v20.x
 - npm 6.x
 - npm 6.x
-- yarn
+- pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
 - [Turborepo](https://turbo.build/repo)
-- MongoDB 4.4 or above
+- MongoDB 6.0 or above
 
 
 ### Optional Dependencies
 ### Optional Dependencies
 
 
@@ -95,11 +95,11 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 
 ## Command details
 ## 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).
 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
 - Node.js v18.x or v20.x
 - npm 6.x
 - npm 6.x
-- yarn
+- pnpm 9.x
 - [Turborepo](https://turbo.build/repo)
 - [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)をご覧ください。
 詳しくは [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:StickyStretchableScroller': 'debug',
   // 'growi:cli:ItemsTree': 'debug',
   // 'growi:cli:ItemsTree': 'debug',
   'growi:searchResultList': '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',
-      },
-    },
-  },
-};

+ 28 - 76
apps/app/docker/Dockerfile

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

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

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

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

@@ -4,16 +4,15 @@ GROWI Official docker image
 
 
 [![Actions Status](https://github.com/weseek/growi/workflows/Release/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
 [![Actions Status](https://github.com/weseek/growi/workflows/Release/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
 
 
-![GROWI-x-docker](https://user-images.githubusercontent.com/1638767/38307565-105956e2-384f-11e8-8534-b1128522d68d.png)
+![GROWI-x-docker](https://github.com/user-attachments/assets/1a82236d-5a85-4a2e-842a-971b4c1625e6)
 
 
 
 
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`7.0.19`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.19/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.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?
 What is GROWI?
@@ -27,7 +26,7 @@ see: [weseek/growi](https://github.com/weseek/growi)
 Requirements
 Requirements
 -------------
 -------------
 
 
-* MongoDB (>= 4.4)
+* MongoDB (>= 6.0)
 
 
 ### Optional Dependencies
 ### Optional Dependencies
 
 

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

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

+ 1 - 1
apps/app/next-env.d.ts

@@ -2,4 +2,4 @@
 /// <reference types="next/image-types/global" />
 /// <reference types="next/image-types/global" />
 
 
 // NOTE: This file should not be edited
 // NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

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

@@ -48,6 +48,14 @@ const getTranspilePackages = () => {
     'emoticon',
     'emoticon',
     'direction', // for hast-util-select
     'direction', // for hast-util-select
     'bcp-47-match', // 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-']),
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
   ];
 
 
@@ -151,7 +159,11 @@ module.exports = async(phase, { defaultConfig }) => {
   }
   }
 
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
   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));
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 4 - 0
apps/app/nodemon.json

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

+ 106 - 74
apps/app/package.json

@@ -1,90 +1,91 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.0.20-RC.0",
+  "version": "7.1.1-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
     "build": "run-p build:*",
     "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",
     "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",
     "clean": "shx rm -rf dist transpiled",
-    "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
-    "server:ci": "yarn server --ci",
-    "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
-    "styles-prebuilt": "vite build -c vite.styles-prebuilt.config.ts",
-    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up -f config/migrate-mongo-config.js",
+    "server": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
+    "server:ci": "pnpm run server --ci",
+    "preserver": "cross-env NODE_ENV=production pnpm run migrate",
+    "pre:styles": "vite build -c vite.styles-prebuilt.config.ts",
+    "migrate": "node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js",
     "//// for development": "",
     "//// 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": "",
     "//// 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: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: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:*",
     "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": "run-p test:*",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
-    "test:vitest": "run-p vitest:run vitest:run:integ vitest:run:components",
+    "test:vitest": "vitest run --coverage",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
     "reg:run": "reg-suit run",
-    "vitest:run": "vitest run config src --coverage",
-    "vitest:run:integ": "vitest run -c vitest.config.integ.ts src --coverage",
-    "vitest:run:components": "vitest run -c vitest.config.components.ts src --coverage",
     "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "//// misc": "",
-    "console": "yarn repl",
-    "repl": "yarn cross-env NODE_ENV=development yarn ts-node src/server/repl.ts",
-    "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
-    "openapi:v3": "yarn cross-env API_VERSION=3 yarn swagger-jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
-    "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
+    "console": "npm run repl",
+    "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
+    "swagger2openapi:apiv3": "sh bin/swagger-jsdoc/generate-spec-apiv3.sh",
+    "swagger2openapi:apiv1": "sh bin/swagger-jsdoc/generate-spec-apiv1.sh",
     "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config",
     "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": {
   "// comments for dependencies": {
     "@aws-skd/*": "fix version above 3.186.0 that is required by mongodb@4.16.0",
     "@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.",
     "@keycloak/keycloak-admin-client": "19.0.0 or above exports only ESM.",
     "escape-string-regexp": "5.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",
     "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."
     "string-width": "5.0.0 or above exports only ESM."
   },
   },
   "dependencies": {
   "dependencies": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "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",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
+    "@cspell/dynamic-import": "^8.15.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@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",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
-    "@types/jest": "^29.5.2",
-    "@types/ldapjs": "^2.2.5",
     "JSONStream": "^1.3.5",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "array.prototype.flatmap": "^1.2.2",
@@ -95,12 +96,13 @@
     "browser-bunyan": "^1.8.0",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
-    "check-node-version": "^4.1.0",
+    "check-node-version": "^4.2.1",
     "compression": "^1.7.4",
     "compression": "^1.7.4",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^4.6.0",
     "connect-mongo": "^4.6.0",
     "connect-redis": "^4.0.4",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
+    "cross-env": "^7.0.0",
     "csurf": "^1.11.0",
     "csurf": "^1.11.0",
     "csv-to-markdown-table": "^1.4.1",
     "csv-to-markdown-table": "^1.4.1",
     "date-fns": "^3.6.0",
     "date-fns": "^3.6.0",
@@ -108,6 +110,7 @@
     "detect-indent": "^7.0.0",
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
     "diff_match_patch": "^0.1.1",
+    "dotenv-flow": "^3.2.0",
     "ejs": "^3.1.10",
     "ejs": "^3.1.10",
     "esa-node": "^0.2.2",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "^4.0.0",
     "escape-string-regexp": "^4.0.0",
@@ -121,21 +124,31 @@
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
     "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",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "http-errors": "^2.0.0",
     "i18next": "^23.10.1",
     "i18next": "^23.10.1",
     "i18next-resources-to-backend": "^1.2.1",
     "i18next-resources-to-backend": "^1.2.1",
     "is-absolute-url": "^4.0.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.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",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^3.0.3",
     "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
     "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",
     "method-override": "^3.0.0",
+    "micromark-extension-gfm-table": "^2.1.0",
+    "micromark-extension-wiki-link": "^0.0.4",
     "migrate-mongo": "^11.0.0",
     "migrate-mongo": "^11.0.0",
     "mkdirp": "^1.0.3",
     "mkdirp": "^1.0.3",
+    "mongodb": "^4.17.2",
     "mongoose": "^6.11.3",
     "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-paginate-v2": "^1.3.9",
@@ -143,7 +156,7 @@
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
     "mustache": "^4.2.0",
-    "next": "^14.1.3",
+    "next": "^14.2.13",
     "next-dynamic-loading-props": "^0.1.1",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.2.0",
     "next-i18next": "^15.2.0",
     "next-superjson": "^0.0.4",
     "next-superjson": "^0.0.4",
@@ -152,6 +165,7 @@
     "node-cron": "^3.0.2",
     "node-cron": "^3.0.2",
     "nodemailer": "^6.9.15",
     "nodemailer": "^6.9.15",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
+    "openai": "^4.56.0",
     "openid-client": "^5.4.0",
     "openid-client": "^5.4.0",
     "p-retry": "^4.0.0",
     "p-retry": "^4.0.0",
     "passport": "^0.6.0",
     "passport": "^0.6.0",
@@ -160,6 +174,7 @@
     "passport-ldapauth": "^3.0.1",
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
     "passport-saml": "^3.2.0",
+    "prop-types": "^15.8.1",
     "qs": "^6.11.1",
     "qs": "^6.11.1",
     "rate-limiter-flexible": "^2.3.7",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react": "^18.2.0",
@@ -171,7 +186,7 @@
     "react-error-boundary": "^3.1.4",
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^14.1.0",
     "react-i18next": "^14.1.0",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.7",
+    "react-markdown": "^9.0.1",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
     "react-stickynode": "^4.1.1",
@@ -180,27 +195,34 @@
     "reactstrap": "^9.2.2",
     "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "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",
     "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",
     "sanitize-filename": "^1.6.3",
     "socket.io": "^4.7.5",
     "socket.io": "^4.7.5",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "superjson": "^1.9.1",
-    "swagger-jsdoc": "^6.1.0",
+    "swagger-jsdoc": "^6.2.8",
     "swr": "^2.2.2",
     "swr": "^2.2.2",
     "throttle-debounce": "^5.0.0",
     "throttle-debounce": "^5.0.0",
+    "ts-deepmerge": "^6.2.0",
+    "tslib": "^2.8.0",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
+    "uid-safe": "^2.1.5",
+    "unified": "^11.0.0",
+    "unist-util-visit": "^5.0.0",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
     "unstated": "^2.1.1",
     "unzip-stream": "^0.3.2",
     "unzip-stream": "^0.3.2",
@@ -220,23 +242,34 @@
     "mongodb": "mongoose which is used requires mongo@4.16.0."
     "mongodb": "mongoose which is used requires mongo@4.16.0."
   },
   },
   "devDependencies": {
   "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",
     "@handsontable/react": "=2.1.0",
     "@next/bundle-analyzer": "^14.1.3",
     "@next/bundle-analyzer": "^14.1.3",
     "@popperjs/core": "^2.11.8",
     "@popperjs/core": "^2.11.8",
     "@swc-node/jest": "^1.8.1",
     "@swc-node/jest": "^1.8.1",
     "@swc/jest": "^0.2.36",
     "@swc/jest": "^0.2.36",
-    "@testing-library/react": "^14.1.2",
+    "@testing-library/dom": "^10.4.0",
+    "@testing-library/jest-dom": "^6.5.0",
+    "@testing-library/react": "^16.0.1",
     "@testing-library/user-event": "^14.5.2",
     "@testing-library/user-event": "^14.5.2",
     "@types/express": "^4.17.21",
     "@types/express": "^4.17.21",
+    "@types/hast": "^3.0.4",
     "@types/jest": "^29.5.2",
     "@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-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
     "@types/react-stickynode": "^4.0.3",
+    "@types/testing-library__dom": "^7.5.0",
     "@types/throttle-debounce": "^5.0.1",
     "@types/throttle-debounce": "^5.0.1",
+    "@types/unist": "^3.0.3",
     "@types/unzip-stream": "^0.3.4",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
     "@types/url-join": "^4.0.2",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
@@ -249,7 +282,7 @@
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "fslightbox-react": "^1.7.6",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
-    "happy-dom": "^13.2.0",
+    "happy-dom": "^15.7.4",
     "i18next-chained-backend": "^4.6.2",
     "i18next-chained-backend": "^4.6.2",
     "i18next-hmr": "^3.0.4",
     "i18next-hmr": "^3.0.4",
     "i18next-http-backend": "^2.5.0",
     "i18next-http-backend": "^2.5.0",
@@ -259,24 +292,23 @@
     "jest-localstorage-mock": "^2.4.14",
     "jest-localstorage-mock": "^2.4.14",
     "load-css-file": "^1.0.0",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
     "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",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
     "null-loader": "^4.0.1",
-    "plantuml-encoder": "^1.2.5",
     "pretty-bytes": "^6.1.1",
     "pretty-bytes": "^6.1.1",
-    "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dnd": "^14.0.5",
     "react-dnd": "^14.0.5",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dropzone": "^14.2.3",
     "react-dropzone": "^14.2.3",
+    "react-hook-form": "^7.45.4",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "react-input-autosize": "^3.0.0",
     "react-toastify": "^9.1.3",
     "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",
     "sass": "^1.53.0",
-    "simple-load-script": "^1.0.2",
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.7.5",
     "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",
     "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',
   reporter: process.env.CI ? 'github' : 'list',
 
 
   webServer: {
   webServer: {
-    command: 'yarn server',
+    command: 'pnpm run server',
     url: 'http://localhost:3000',
     url: 'http://localhost:3000',
     reuseExistingServer: !process.env.CI,
     reuseExistingServer: !process.env.CI,
     stdout: 'ignore',
     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');
   await page.goto('/Sandbox/Math');
 
 
   // Expect the Math-specific elements to be present
   // 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 }) => {
 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');
   await page.goto('/Sandbox/Math');
 
 
   // Check if the math elements are visible
   // 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 }) => {
 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.
   // Perform authentication steps. Replace these actions with your own.
   await page.goto('/admin');
   await page.goto('/admin');
 
 
-  const loginForm = await page.$('form#login-form');
+  const loginForm = await page.getByRole('form');
 
 
   if (loginForm != null) {
   if (loginForm != null) {
     await page.getByLabel('Username or E-mail').fill('admin');
     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",
     "scope_of_page_disclosure": "Scope of page disclosure",
     "set_point": "Set point",
     "set_point": "Set point",
     "Guest Users Access": "Guest users access",
     "Guest Users Access": "Guest users access",
+    "readonly_users_access": "Read only users' access",
     "always_hidden": "Always hidden",
     "always_hidden": "Always hidden",
     "always_displayed": "Always displayed",
     "always_displayed": "Always displayed",
     "displayed_or_hidden": "Hidden / Displayed",
     "displayed_or_hidden": "Hidden / Displayed",
@@ -37,6 +38,7 @@
     "page_delete_rights": "Delete rights",
     "page_delete_rights": "Delete rights",
     "page_delete": "Page Delete",
     "page_delete": "Page Delete",
     "page_delete_completely": "Page Delete Completely",
     "page_delete_completely": "Page Delete Completely",
+    "comment_manage_rights": "Comment management rights",
     "other_options": "Other options",
     "other_options": "Other options",
     "deletion_explanation": "Restricts users who can trash the selected single page.",
     "deletion_explanation": "Restricts users who can trash the selected single page.",
     "complete_deletion_explanation": "Restricts users who can completely delete  selected single page.",
     "complete_deletion_explanation": "Restricts users who can completely delete  selected single page.",
@@ -85,6 +87,10 @@
       "deny": "Deny (Registered users only)",
       "deny": "Deny (Registered users only)",
       "readonly": "Accept (Guests can read 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": {
     "registration_mode": {
       "open": "Open (Anyone can register)",
       "open": "Open (Anyone can register)",
       "restricted": "Restricted (Requires approval by administrators)",
       "restricted": "Restricted (Requires approval by administrators)",
@@ -1132,5 +1138,16 @@
   },
   },
   "forbidden_page": {
   "forbidden_page": {
     "do_not_have_admin_permission": "Users without administrative rights cannot access the administration screen"
     "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"
   }
   }
 }
 }

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

@@ -157,6 +157,6 @@
     "publish_transfer_key": "Publish transfer key",
     "publish_transfer_key": "Publish transfer key",
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",
     "once_transfer_key_used": "Once the transfer key is used for transfer, it cannot be used for any other transfer.",
     "once_transfer_key_used": "Once the transfer key is used for transfer, it cannot be used for any other transfer.",
-    "transfer_to_growi_cloud": "If you wish to transfer to GROWI.cloud, please click here."
+    "transfer_to_growi_cloud": "For more details, please click <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>here.</a>"
   }
   }
 }
 }

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

@@ -64,6 +64,7 @@
   "Presentation Mode": "Presentation",
   "Presentation Mode": "Presentation",
   "Not available for guest": "Not available for guest",
   "Not available for guest": "Not available for guest",
   "Not available in this version": "Not available in this version",
   "Not available in this version": "Not available in this version",
+  "Not available when \"anyone with the link\" is selected": "If \"anyone with the link\" is selected, the scope cannot be overridden.",
   "No users have liked this yet": "No users have liked this yet",
   "No users have liked this yet": "No users have liked this yet",
   "No users have liked this yet.": "No users have liked this yet.",
   "No users have liked this yet.": "No users have liked this yet.",
   "No users have bookmarked yet": "No users have bookmarked yet",
   "No users have bookmarked yet": "No users have bookmarked yet",
@@ -331,6 +332,12 @@
     "theme": "Theme",
     "theme": "Theme",
     "keymap": "Keymap",
     "keymap": "Keymap",
     "indent": "Indent",
     "indent": "Indent",
+    "paste": {
+      "title": "Paste behavior",
+      "both": "Both",
+      "text": "Text only",
+      "file": "File only"
+    },
     "editor_config": "Editor Config",
     "editor_config": "Editor Config",
     "Show active line": "Show active line",
     "Show active line": "Show active line",
     "auto_format_table": "Auto format table",
     "auto_format_table": "Auto format table",
@@ -349,7 +356,8 @@
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "No user found",
     "no_user_found": "No user found",
     "reply": "Reply",
     "reply": "Reply",
-    "delete_comment": "Delete comment?"
+    "delete_comment": "Delete comment?",
+    "comment_management_is_not_allowed": "Comment management is not allowed."
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "notfound_or_forbidden": "Original page is not found or forbidden.",
@@ -477,6 +485,19 @@
     "latest_revision": "theirs",
     "latest_revision": "theirs",
     "selected_editable_revision": "Selected Page Body (Editable)"
     "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": {
   "link_edit": {
     "edit_link": "Edit Link",
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",
     "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",
     "scope_of_page_disclosure": "Confidentialité de la page",
     "set_point": "Valeur",
     "set_point": "Valeur",
     "Guest Users Access": "Accès invité",
     "Guest Users Access": "Accès invité",
+    "readonly_users_access": "Accès des utilisateurs lecture seule",
     "always_hidden": "Toujours caché",
     "always_hidden": "Toujours caché",
     "always_displayed": "Toujours affiché",
     "always_displayed": "Toujours affiché",
     "displayed_or_hidden": "Caché / Affiché",
     "displayed_or_hidden": "Caché / Affiché",
@@ -37,6 +38,7 @@
     "page_delete_rights": "Droits de suppression",
     "page_delete_rights": "Droits de suppression",
     "page_delete": "Suppression de page",
     "page_delete": "Suppression de page",
     "page_delete_completely": "Suppression complète de page",
     "page_delete_completely": "Suppression complète de page",
+    "comment_manage_rights": "Droits de gestion des commentaires",
     "other_options": "Paramètres supplémentaires",
     "other_options": "Paramètres supplémentaires",
     "deletion_explanation": "Restreindre les utilisateurs pouvant supprimer une page.",
     "deletion_explanation": "Restreindre les utilisateurs pouvant supprimer une page.",
     "complete_deletion_explanation": "Restreindre les utilisateurs pouvant supprimer complètement une page.",
     "complete_deletion_explanation": "Restreindre les utilisateurs pouvant supprimer complètement une page.",
@@ -85,6 +87,10 @@
       "deny": "Refuser (Utilisateurs inscrits seulement)",
       "deny": "Refuser (Utilisateurs inscrits seulement)",
       "readonly": "Autoriser (Lecture seule)"
       "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": {
     "registration_mode": {
       "open": "Ouvert (Tout le monde peut s'inscrire)",
       "open": "Ouvert (Tout le monde peut s'inscrire)",
       "restricted": "Restreint (Requiert l'approbation d'administrateurs)",
       "restricted": "Restreint (Requiert l'approbation d'administrateurs)",
@@ -1131,5 +1137,16 @@
   },
   },
   "forbidden_page": {
   "forbidden_page": {
     "do_not_have_admin_permission": "Seul les administrateurs peuvent accéder à cette 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"
   }
   }
 }
 }

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

@@ -157,6 +157,6 @@
     "publish_transfer_key": "Publier la clé de transfert",
     "publish_transfer_key": "Publier la clé de transfert",
     "transfer_key_limit": "Les clés de transfert sont valides durant une heure.",
     "transfer_key_limit": "Les clés de transfert sont valides durant une heure.",
     "once_transfer_key_used": "Les clés de transfert sont à usage unique.",
     "once_transfer_key_used": "Les clés de transfert sont à usage unique.",
-    "transfer_to_growi_cloud": "Si vous souhaitez transférer depuis GROWI.cloud, cliquez ici."
+    "transfer_to_growi_cloud": "Pour plus de détails, veuillez cliquer <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>ici.</a>"
   }
   }
 }
 }

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

@@ -64,6 +64,7 @@
   "Presentation Mode": "Mode présentation",
   "Presentation Mode": "Mode présentation",
   "Not available for guest": "Indisponible pour les invités",
   "Not available for guest": "Indisponible pour les invités",
   "Not available in this version": "Indisponible dans cette version",
   "Not available in this version": "Indisponible dans cette version",
+  "Not available when \"anyone with the link\" is selected": "Si \"Tous les utilisateurs disposant du lien\" est sélectionné, la portée ne peut pas être modifiée",
   "No users have liked this yet": "Aucun utilisateur n'a aimé cette page",
   "No users have liked this yet": "Aucun utilisateur n'a aimé cette page",
   "No users have liked this yet.": "Aucun utilisateur n'a aimé cette page.",
   "No users have liked this yet.": "Aucun utilisateur n'a aimé cette page.",
   "No users have bookmarked yet": "Aucun utilisateur n'a mis en favoris cette page",
   "No users have bookmarked yet": "Aucun utilisateur n'a mis en favoris cette page",
@@ -331,6 +332,12 @@
     "theme": "Thème",
     "theme": "Thème",
     "keymap": "Touches",
     "keymap": "Touches",
     "indent": "Indentation",
     "indent": "Indentation",
+    "paste": {
+      "title": "Comportement du collage",
+      "both": "Les deux",
+      "text": "Texte seulement",
+      "file": "Fichier seulement"
+    },
     "editor_config": "Configuration de l'éditeur",
     "editor_config": "Configuration de l'éditeur",
     "Show active line": "Montrer la ligne active",
     "Show active line": "Montrer la ligne active",
     "auto_format_table": "Formattage les tables",
     "auto_format_table": "Formattage les tables",
@@ -349,7 +356,8 @@
     "display_the_page_when_posting_this_comment": "Afficher la page en postant le commentaire",
     "display_the_page_when_posting_this_comment": "Afficher la page en postant le commentaire",
     "no_user_found": "Aucun utilisateur trouvé",
     "no_user_found": "Aucun utilisateur trouvé",
     "reply": "Répondre",
     "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": {
   "page_api_error": {
     "notfound_or_forbidden": "Page originale introuvable ou accès restreint.",
     "notfound_or_forbidden": "Page originale introuvable ou accès restreint.",
@@ -471,6 +479,18 @@
     "latest_revision": "les autres",
     "latest_revision": "les autres",
     "selected_editable_revision": "Corps de page sélectionné (Modifiable)"
     "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": {
   "link_edit": {
     "edit_link": "Modifier lien",
     "edit_link": "Modifier lien",
     "set_link_and_label": "Ajouter lien et étiquette",
     "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": "ページの公開範囲",
     "scope_of_page_disclosure": "ページの公開範囲",
     "set_point": "設定値",
     "set_point": "設定値",
     "Guest Users Access":"ゲストユーザーのアクセス",
     "Guest Users Access":"ゲストユーザーのアクセス",
+    "readonly_users_access": "閲覧のみユーザーのアクセス",
     "always_hidden": "非表示 (固定)",
     "always_hidden": "非表示 (固定)",
     "always_displayed": "表示 (固定)",
     "always_displayed": "表示 (固定)",
     "displayed_or_hidden": "非表示 / 表示",
     "displayed_or_hidden": "非表示 / 表示",
@@ -46,6 +47,7 @@
     "page_delete_rights": "ページの削除権限",
     "page_delete_rights": "ページの削除権限",
     "page_delete": "ゴミ箱に入れる",
     "page_delete": "ゴミ箱に入れる",
     "page_delete_completely": "完全に削除する",
     "page_delete_completely": "完全に削除する",
+    "comment_manage_rights": "コメントの操作権限",
     "other_options": "その他のオプション",
     "other_options": "その他のオプション",
     "deletion_explanation": "ページをゴミ箱に入れることができるユーザーを制限します。",
     "deletion_explanation": "ページをゴミ箱に入れることができるユーザーを制限します。",
     "complete_deletion_explanation": "ページを完全削除することができるユーザーを制限します。",
     "complete_deletion_explanation": "ページを完全削除することができるユーザーを制限します。",
@@ -94,6 +96,10 @@
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
     },
     },
+    "read_only_users_comment": {
+      "deny": "拒否 (閲覧のみユーザーのコメント操作を禁止)",
+      "accept": "許可 (閲覧のみユーザーもコメント操作可能)"
+    },
     "registration_mode": {
     "registration_mode": {
       "open": "公開 (だれでも登録可能)",
       "open": "公開 (だれでも登録可能)",
       "restricted": "制限 (登録完了には管理者の承認が必要)",
       "restricted": "制限 (登録完了には管理者の承認が必要)",
@@ -1142,5 +1148,16 @@
   },
   },
   "forbidden_page": {
   "forbidden_page": {
     "do_not_have_admin_permission": "管理者権限のないユーザーでは管理画面にはアクセスできません"
     "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 のリビルドに失敗しました"
   }
   }
 }
 }

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

@@ -159,6 +159,6 @@
     "publish_transfer_key": "移行キーを発行する",
     "publish_transfer_key": "移行キーを発行する",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",
     "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ以降はご利用いただけなくなります。",
     "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ以降はご利用いただけなくなります。",
-    "transfer_to_growi_cloud": "※ GROWI.cloud への移行を実施する場合はこちらをご確認ください。"
+    "transfer_to_growi_cloud": "※ 詳しくは <a href='{{documentationUrl}}ja/admin-guide/management-cookbook/g2g-transfer.html'> GROWI お引越し機能</a>をご確認ください。"
   }
   }
 }
 }

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

@@ -61,6 +61,7 @@
   "Presentation Mode": "プレゼンテーション",
   "Presentation Mode": "プレゼンテーション",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
+  "Not available when \"anyone with the link\" is selected": "「リンクを知っている人のみ」を選択している場合はスコープを上書きできません。",
   "No users have liked this yet": "いいねをしているユーザーはいません",
   "No users have liked this yet": "いいねをしているユーザーはいません",
   "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "Create Archive Page": "アーカイブページの作成",
   "Create Archive Page": "アーカイブページの作成",
@@ -364,6 +365,12 @@
     "theme": "テーマ",
     "theme": "テーマ",
     "keymap": "キーマップ",
     "keymap": "キーマップ",
     "indent": "インデント",
     "indent": "インデント",
+    "paste": {
+      "title": "ペースト時の動作",
+      "both": "両方",
+      "text": "テキストのみ",
+      "file": "ファイルのみ"
+    },
     "editor_config": "エディタ設定",
     "editor_config": "エディタ設定",
     "Show active line": "アクティブ行をハイライト",
     "Show active line": "アクティブ行をハイライト",
     "auto_format_table": "表の自動整形",
     "auto_format_table": "表の自動整形",
@@ -382,7 +389,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": "コメントを削除しますか?",
+    "comment_management_is_not_allowed": "コメントの操作が許可されていません。"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
@@ -510,6 +518,18 @@
     "latest_revision": "最新の本文",
     "latest_revision": "最新の本文",
     "selected_editable_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": {
   "link_edit": {
     "edit_link": "リンク編集",
     "edit_link": "リンク編集",
     "set_link_and_label": "リンク情報",
     "set_link_and_label": "リンク情報",

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

@@ -27,6 +27,7 @@
     "always_hidden": "总是隐藏",
     "always_hidden": "总是隐藏",
     "displayed_or_hidden": "隐藏 / 显示",
     "displayed_or_hidden": "隐藏 / 显示",
     "Guest Users Access": "来宾用户访问",
     "Guest Users Access": "来宾用户访问",
+    "readonly_users_access": "只浏览用户的访问",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"register_limitation": "注册限制",
 		"register_limitation": "注册限制",
 		"register_limitation_desc": "限制新用户注册",
 		"register_limitation_desc": "限制新用户注册",
@@ -46,6 +47,7 @@
     "page_delete_rights": "删除权限",
     "page_delete_rights": "删除权限",
     "page_delete": "删除",
     "page_delete": "删除",
     "page_delete_completely": "彻底删除",
     "page_delete_completely": "彻底删除",
+    "comment_manage_rights": "评论管理权限",
     "other_options": "其他选项",
     "other_options": "其他选项",
     "deletion_explanation": "限制用户对选定的单一页面进行垃圾处理。",
     "deletion_explanation": "限制用户对选定的单一页面进行垃圾处理。",
     "complete_deletion_explanation": "限制可以完全删除所选单页的用户。",
     "complete_deletion_explanation": "限制可以完全删除所选单页的用户。",
@@ -94,6 +96,10 @@
 			"deny": "拒绝(仅限注册用户)",
 			"deny": "拒绝(仅限注册用户)",
 			"readonly": "接受(来宾可以只读)"
 			"readonly": "接受(来宾可以只读)"
 		},
 		},
+    "read_only_users_comment": {
+      "deny": "拒绝 (禁止只浏览用户操作评论)",
+      "accept": "允许 (只浏览用户可以管理评论)"
+    },
 		"registration_mode": {
 		"registration_mode": {
 			"open": "打开(任何人都可以注册)",
 			"open": "打开(任何人都可以注册)",
 			"restricted": "受限(需要管理员批准)",
 			"restricted": "受限(需要管理员批准)",
@@ -1141,5 +1147,16 @@
   },
   },
   "forbidden_page": {
   "forbidden_page": {
     "do_not_have_admin_permission": "没有管理权限的用户无法访问管理屏幕"
     "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": "向量存储区重建失败"
   }
   }
 }
 }

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

@@ -160,6 +160,6 @@
     "publish_transfer_key": "发布迁移密钥",
     "publish_transfer_key": "发布迁移密钥",
     "transfer_key_limit": "迁移密钥在签发后一小时内有效。",
     "transfer_key_limit": "迁移密钥在签发后一小时内有效。",
     "once_transfer_key_used": "一旦迁移密钥被用于迁移,它将不再可用于进一步的迁移。",
     "once_transfer_key_used": "一旦迁移密钥被用于迁移,它将不再可用于进一步的迁移。",
-    "transfer_to_growi_cloud": "如果您希望迁移到GROWI.cloud,请点击这里。"
+    "transfer_to_growi_cloud": "有关更多详情,请点击<a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>此处</a>。"
   }
   }
 }
 }

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

@@ -61,6 +61,7 @@
   "Presentation Mode": "演示文稿",
   "Presentation Mode": "演示文稿",
   "Not available for guest": "不提供给客人",
   "Not available for guest": "不提供给客人",
   "Not available in this version": "此版本中不提供",
   "Not available in this version": "此版本中不提供",
+  "Not available when \"anyone with the link\" is selected": "如果选择“任何人”,则无法覆盖范围",
   "No users have liked this yet": "还没有用户喜欢这个",
   "No users have liked this yet": "还没有用户喜欢这个",
   "No users have bookmarked yet": "还没有用户加入书签",
   "No users have bookmarked yet": "还没有用户加入书签",
   "Create Archive Page": "创建归档页",
   "Create Archive Page": "创建归档页",
@@ -321,6 +322,12 @@
     "theme": "主题",
     "theme": "主题",
     "keymap": "键表",
     "keymap": "键表",
     "indent": "缩进",
     "indent": "缩进",
+    "paste": {
+      "title": "粘贴行为",
+      "both": "两者",
+      "text": "仅文本",
+      "file": "仅文件"
+    },
     "editor_config": "编辑器配置",
     "editor_config": "编辑器配置",
 		"Show active line": "显示活动行",
 		"Show active line": "显示活动行",
 		"auto_format_table": "自动格式化表格",
 		"auto_format_table": "自动格式化表格",
@@ -339,7 +346,8 @@
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "未找到用户名",
     "no_user_found": "未找到用户名",
     "reply": "Reply",
     "reply": "Reply",
-    "delete_comment": "Delete comment?"
+    "delete_comment": "Delete comment?",
+    "comment_management_is_not_allowed": "不允许操作评论。"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "未找到或禁止原始页。",
     "notfound_or_forbidden": "未找到或禁止原始页。",
@@ -466,6 +474,18 @@
     "latest_revision": "最新页面正文",
     "latest_revision": "最新页面正文",
     "selected_editable_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": {
   "link_edit": {
     "edit_link": "Edit Link",
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",
     "set_link_and_label": "Set link and label",

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


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


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


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


+ 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)
 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/).
 See [PlantUML](http://plantuml.com/).
 
 
@@ -151,7 +151,7 @@ State3 --> [*] : Aborted
 
 
 
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 
 
 ## Pie graph
 ## 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/).
 See [KaTeX](https://katex.org/).
 
 

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

@@ -1,18 +1,17 @@
 # What is Sandbox?
 # 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
 - By inserting headings and paragraphs, you can make the text on the page easier to read
 
 
 ## Headers
 ## Headers
 - Add `#` before the heading text to create a heading 
 - 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 
     - 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
 - The number of `#` will decide the hierarchy level and help you to organize the contents
 
 
-```
+```markdown
 # First-level heading
 # First-level heading
 ## Second-level heading
 ## Second-level heading
 ### Third-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
     - 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
         - Change the line break setting in the `Markdown Settings` sector of the admin page
 
 
-#### Without line break
+#### Example: Without line break
 Paragraph 1
 Paragraph 1
 Paragraph 2
 Paragraph 2
 
 
-#### With line break
+#### Example: With line break
 Paragraph 1  
 Paragraph 1  
 Paragraph 2
 Paragraph 2
 
 
 ## Block
 ## 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
 - 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
 # :green_book: Styling Text
@@ -59,6 +75,11 @@ Paragraph 2
 - This sentence indicates emphasis with *Italic*
 - This sentence indicates emphasis with *Italic*
 - 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
 ## Bold
 - Enclose the text with two asterisks `*` or two underscores `_`
 - 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** 
 - 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
 ## Italic & Bold
 - Enclose the text with three asterisks `*` or three underscores `_`
 - 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***
 - 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
 # :orange_book: Insert Lists
 ## Bulleted List
 ## Bulleted List
@@ -88,6 +118,8 @@ Paragraph 2
 
 
 ## Numbered List
 ## Numbered List
 - `Number.` at the beginning of a line to insert a 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
 - Numbered list and bulleted list can also be combined for use
 
 
 #### Example
 #### Example
@@ -110,7 +142,46 @@ Paragraph 2
 - [x] Task 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
 ## Blockquotes
 - Use quoted expressions by putting `>` at the beginning of the paragraph
 - Use quoted expressions by putting `>` at the beginning of the paragraph
     - Multiple quotations can be expressed by using a sequence of `>` characters
     - Multiple quotations can be expressed by using a sequence of `>` characters
@@ -121,16 +192,34 @@ Paragraph 2
 > - Quotation
 > - Quotation
 >> Multiple quotations need to insert more `>`
 >> Multiple quotations need to insert more `>`
 
 
+```markdown
+> - Quotation
+> - Quotation
+>> Multiple quotations need to insert more `>`
+```
+
 ## Code
 ## Code
 - It is possible to express the code by adding it in three `` ` ``
 - It is possible to express the code by adding it in three `` ` ``
 
 
 #### Example
 #### Example
-```
+
+```markdown
 Add codes here  
 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
 ## Inline Code
@@ -139,20 +228,93 @@ Line breaks and paragraphs can be reflected in the code
 #### Example
 #### Example
 Here is the `inline code` 
 Here is the `inline code` 
 
 
-## Horizontal lines
-- Insert the horizontal line with three or more consecutive asterisks `*` or underscores `_`
+
+## Table
+
+### General syntax
 
 
 #### Example
 #### 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
 # :ledger: More Applications
-- [Bootstrap5](/Sandbox/Bootstrap5)
+- [Bootstrap](/Sandbox/Bootstrap)
 
 
 - [Diagrams](/Sandbox/Diagrams)
 - [Diagrams](/Sandbox/Diagrams)
 
 
 - [Math](/Sandbox/Math)
 - [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)
 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/).
 See [PlantUML](http://plantuml.com/).
 
 
@@ -151,7 +151,7 @@ State3 --> [*] : Aborted
 
 
 
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 
 
 ## Pie graph
 ## 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/).
 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
 ## 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 では各種機能を活用することで様々な図形の表現が可能です
 - GROWI では各種機能を活用することで様々な図形の表現が可能です
   - 各種機能の特色を活かして図形の表現をしましょう
   - 各種機能の特色を活かして図形の表現をしましょう
 
 
-# :pencil: Diagrams.net(旧 Draw.io)
+# :pencil2: Diagrams.net(旧 Draw.io)
 - 図形の挿入時に全般的にご利用いただきやすい図形の挿入方法となります
 - 図形の挿入時に全般的にご利用いただきやすい図形の挿入方法となります
   - サービスの詳細は [こちら](https://www.drawio.com/) をご確認ください
   - サービスの詳細は [こちら](https://www.drawio.com/) をご確認ください
 - Edit 画面下部のツールバーより専用の編集画面を用いて図形を編集することが可能です
 - Edit 画面下部のツールバーより専用の編集画面を用いて図形を編集することが可能です
@@ -28,7 +28,7 @@ pLzXsqNMti76NH0Pwggu8d4Jzx3eCO/R05/M+XevvTtWnIgTcapK0wCCNGN8ZmSq/oVxwy2t6dwYU1H2
 ```
 ```
 
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 - Mermaidとは、Markdownテキストでグラフを作成できるダイアグラムツールです
 - Mermaidとは、Markdownテキストでグラフを作成できるダイアグラムツールです
   - サービスの詳細は [こちら](https://mermaid.js.org/) をご確認ください
   - サービスの詳細は [こちら](https://mermaid.js.org/) をご確認ください
 
 
@@ -79,7 +79,7 @@ mindmap
 ```
 ```
 
 
 
 
-# :pencil: PlantUML
+# :pencil2: PlantUML
 - PlantUML はオープンソースの UML 描画ツールです
 - PlantUML はオープンソースの UML 描画ツールです
   - サービスの詳細は [こちら](https://plantuml.com/) をご確認ください
   - サービスの詳細は [こちら](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
 # 見出し1
 ## 見出し2
 ## 見出し2
 ### 見出し3
 ### 見出し3
@@ -23,63 +21,94 @@
 
 
 ## 改行(Br)
 ## 改行(Br)
 - 改行したい文章の行末に半角スペースを2つ挿入することで改行をすることができます
 - 改行したい文章の行末に半角スペースを2つ挿入することで改行をすることができます
-    - こちらの挙動は、設定画面から半角スペースなしで改行が反映されるように設定を変更することが可能です
-        - 「マークダウン設定_Line Break設定(/admin/markdown)」から変更が可能です
+    - 管理画面から半角スペースなしで改行が反映されるように設定を変更することも可能です
+        - 「マークダウン設定」から変更できま
 
 
-#### 改行がない場合
+#### 例: 改行なし
 文章 1 の内容が入ります
 文章 1 の内容が入ります
 文章 2 の内容が入ります
 文章 2 の内容が入ります
 
 
-#### 改行がある場合
+#### 例: 改行あり
 文章 1 の内容が入ります  
 文章 1 の内容が入ります  
 文章 2 の内容が入ります
 文章 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. この文章は番号付きリストで表現しています
     1. この文章は番号付きリストで表現しています
     1. この文章は番号付きリストで表現しています
     1. この文章は番号付きリストで表現しています
@@ -101,138 +130,83 @@
 1. この文章は箇条書きリストで表現しています
 1. この文章は箇条書きリストで表現しています
     - この文章は箇条書きリストで表現しています  
     - この文章は箇条書きリストで表現しています  
 
 
-## タスクリスト
+## タスク リスト
 - `[] ` を記述することでリストに対して未チェックのチェックボックスを挿入することができます
 - `[] ` を記述することでリストに対して未チェックのチェックボックスを挿入することができます
     - `[x] ` を記述することでチェック済みのチェックボックスを挿入することができます
     - `[x] ` を記述することでチェック済みのチェックボックスを挿入することができます
 
 
-#### 活用
+#### 例
 - [ ] タスク 1
 - [ ] タスク 1
     - [x] タスク 1-1
     - [x] タスク 1-1
     - [ ] タスク 1-2
     - [ ] タスク 1-2
 - [x] タスク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
 ```javascript:mersenne-twister.js
 function MersenneTwister(seed) {
 function MersenneTwister(seed) {
   if (arguments.length == 0) {
   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)
 - [ページの装飾方法(Bootstrap5)](/Sandbox/Bootstrap5)
 
 
 - [図形の表現方法(Diagrams)](/Sandbox/Diagrams)
 - [図形の表現方法(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)
 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/).
 See [PlantUML](http://plantuml.com/).
 
 
@@ -151,7 +151,7 @@ State3 --> [*] : Aborted
 
 
 
 
 
 
-# :pencil: Mermaid
+# :pencil2: Mermaid
 
 
 ## Pie graph
 ## 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/).
 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 段
 第 2 段
 
 
-#### 有换行符
-段落 1  
+#### 示例:有换行符
+第 1 段  
 第 2 段
 第 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: 文本样式
 # :green_book: 文本样式
-- 可以使用各种样式来丰富句子的文字表达方式
-    - 选择 "编辑 "屏幕底部的工具栏图标,也可以轻松应用这些样式
+- 可以应用各种样式来丰富句子的文本表达
+    - 也可以通过选择编辑屏幕底部的工具栏图标轻松应用这些样式
 
 
-##斜体
-- 用星号`*`或下划线`_`括住文本。
+## 斜体
+- 用星号 `*` 或下划线 `_` 括住文本。
 
 
 #### 示例
 #### 示例
-- 这句话用*斜体*表示强调
-- 这句话用 _Italic_ 表示强调 
+- 本句用 *Italic* 表示强调
+- 本句用 _Italic_ 表示强调
+
+```markdown
+- 本句用 *Italic* 表示强调
+- 本句用 _Italic_ 表示强调
+```
 
 
 ## 粗体
 ## 粗体
-- 用两个星号`*`或两个下划线`_`括住文本。
+- 用两个星号 `*` 或两个下划线 `_` 括住文本
 
 
 #### 示例
 #### 示例
-- 这句话用 ** 粗体** 表示强调 
-- 这句话用__粗体__表示强调
+- 本句用 **Bold** 表示强调
+- 本句用 __Bold__ 表示强调
 
 
-## 斜体和粗体
-- 用三个星号`*`或三个下划线`_`括起来
+```markdown
+- 本句用 **Bold** 表示强调
+- 本句用 __Bold__ 表示强调
+```
+
+## 斜体 & 粗体
+- 用三个星号 `*` 或三个下划线 `_` 括住文本
 
 
 #### 示例
 #### 示例
-- 本句用***斜体和粗体***表示强调
-- 本句用____斜体和粗体____表示强调
+- 本句用 ***Italic & 粗体*** 表示强调
+- 本句用 ___Italic & 粗体___ 表示强调
 
 
+```markdown
+-本句使用 ***斜体和粗体*** 表示强调
+- 本句使用 ___斜体和粗体___ 表示强调
+```
 
 
 # :orange_book: 插入列表
 # :orange_book: 插入列表
-## 缩略图列表
-- 用连字符 `-`、加号 `+` 或星号 `*` 开头一行,插入一个项目符号列表
+## 项目符号列表
+- 通过在行首使用连字符 `-`、加号 `+` 或星号 `*` 插入项目符号列表
 
 
 #### 示例
 #### 示例
-- 这句话出现在项目符号列表中
-    - 这句话出现在项目符号列表中
-        - 这句话出现在项目符号列表中
-        - 这句话出现在项目符号列表中
-- 此句出现在项目符号列表中
-    - 此句子出现在项目符号列表中
+- 本句在项目符号列表中
+    - 本句在项目符号列表中
+        - 本句在项目符号列表中
+        - 本句在项目符号列表中
+- 本句在项目符号列表中
+    - 本句在项目符号列表中
 
 
 ## 编号列表
 ## 编号列表
-- 在行首添加 `Number.` 以插入编号列表
-- 编号列表和项目符号列表也可合并使用
+- 在行首使用 `Number.` 插入编号列表
+- 编号自动分配
+
+- 编号列表和项目符号列表也可组合使用
 
 
 #### 示例
 #### 示例
-1. 编号列表中有这样一句话
-    1. 编号列表中包含这句话
-    1. 该句子出现在编号表中
-    1. 此句出现在编号列表中
-        - 此句出现在项目符号列表中 
-1. 此句出现在项目符号列表中
-    - 此句出现在项目符号列表中
-
-##任务列表
-- 通过写 `[] ` 插入未选中复选框列表
-    - 通过写 `[x]` 选中复选框
+1. 本句在编号列表中
+    1. 本句在编号列表中
+    1. 此句子出现在编号列表中
+    1. 此句出现在编号列表中
+        - 此句出现在项目符号列表中
+1. 此句出现在项目符号列表中
+    - 此句出现在项目符号列表中
+
+## 任务列表
+- 通过写 `[] ` 插入未选中复选框列表
+    - 通过写 `[x]` 选中复选框
 
 
 #### 示例
 #### 示例
 - [ ] 任务 1
 - [ ] 任务 1
@@ -109,51 +137,169 @@
     - [ ] 任务 1-2
     - [ ] 任务 1-2
 - [x] 任务 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: 更多应用
 # :ledger: 更多应用
-- [Bootstrap5](/Sandbox/Bootstrap5)
+- [Bootstrap](/Sandbox/Bootstrap)
 
 
 - [Diagrams](/Sandbox/Diagrams)
 - [Diagrams](/Sandbox/Diagrams)
 
 

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

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

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

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

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

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

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

@@ -8,6 +8,7 @@ import { useGenerateTransferKey } from '~/client/services/g2g-transfer';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { G2G_PROGRESS_STATUS, type G2GProgress } from '~/interfaces/g2g-transfer';
 import { G2G_PROGRESS_STATUS, type G2GProgress } from '~/interfaces/g2g-transfer';
+import { useGrowiDocumentationUrl } from '~/stores-universal/context';
 import { useAdminSocket } from '~/stores/socket-io';
 import { useAdminSocket } from '~/stores/socket-io';
 
 
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
@@ -123,6 +124,8 @@ const G2GDataTransfer = (): JSX.Element => {
     }
     }
   }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
   }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
 
 
+  const { data: documentationUrl } = useGrowiDocumentationUrl();
+
   // File upload
   // File upload
   // const onChangeFileUploadTypeHandler = useCallback((e: ChangeEvent, type: string) => {
   // const onChangeFileUploadTypeHandler = useCallback((e: ChangeEvent, type: string) => {
   //   setFileUploadType(type);
   //   setFileUploadType(type);
@@ -275,7 +278,13 @@ const G2GDataTransfer = (): JSX.Element => {
       <div className="alert alert-warning mt-4">
       <div className="alert alert-warning mt-4">
         <p className="mb-1">{t('commons:g2g_data_transfer.transfer_key_limit')}</p>
         <p className="mb-1">{t('commons:g2g_data_transfer.transfer_key_limit')}</p>
         <p className="mb-1">{t('commons:g2g_data_transfer.once_transfer_key_used')}</p>
         <p className="mb-1">{t('commons:g2g_data_transfer.once_transfer_key_used')}</p>
-        <p className="mb-0">{t('commons:g2g_data_transfer.transfer_to_growi_cloud')}</p>
+        <p
+          className="mb-0"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{
+            __html: t('commons:g2g_data_transfer.transfer_to_growi_cloud', { documentationUrl }),
+          }}
+        />
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 8 - 2
apps/app/src/client/components/Admin/Notification/NotificationSetting.jsx

@@ -14,7 +14,7 @@ import { toastError } from '~/client/util/toastr';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { CustomNavTab } from '../../CustomNavigation/CustomNav';
+import CustomNav from '../../CustomNavigation/CustomNav';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 
 
@@ -155,7 +155,13 @@ function NotificationSetting(props) {
 
 
       <h2 className="admin-setting-header mt-5">{t('notification_settings.notification_settings')}</h2>
       <h2 className="admin-setting-header mt-5">{t('notification_settings.notification_settings')}</h2>
 
 
-      <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
+      <CustomNav
+        activeTab={activeTab}
+        navTabMapping={navTabMapping}
+        onNavSelected={switchActiveTab}
+        hideBorderBottom
+        breakpointToSwitchDropdownDown="md"
+      />
 
 
       <TabContent activeTab={activeTab} className="p-5">
       <TabContent activeTab={activeTab} className="p-5">
         <TabPane tabId="user_trigger_notification">
         <TabPane tabId="user_trigger_notification">

+ 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 { t, adminGeneralSecurityContainer } = this.props;
     const {
     const {
       currentRestrictGuestMode, currentPageDeletionAuthority, currentPageCompleteDeletionAuthority,
       currentRestrictGuestMode, currentPageDeletionAuthority, currentPageCompleteDeletionAuthority,
-      currentPageRecursiveDeletionAuthority, currentPageRecursiveCompleteDeletionAuthority,
+      currentPageRecursiveDeletionAuthority, currentPageRecursiveCompleteDeletionAuthority, isRomUserAllowedToComment,
     } = adminGeneralSecurityContainer.state;
     } = adminGeneralSecurityContainer.state;
 
 
     const isButtonDisabledForDeletion = !validateDeleteConfigs(
     const isButtonDisabledForDeletion = !validateDeleteConfigs(
@@ -509,6 +509,39 @@ class SecuritySetting extends React.Component {
           </div>
           </div>
         </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>
         <h4>{t('security_settings.session')}</h4>
         <div className="row">
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.max_age')}</label>
           <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
       <select
         name="actionName"
         name="actionName"
         className="form-control"
         className="form-control"
-        placeholder="select"
         value={actionName ?? ''}
         value={actionName ?? ''}
         onChange={handleActionChange}
         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 { useDrag, useDrop } from 'react-dnd';
 
 
-import { DragItemDataType } from '~/interfaces/bookmark-info';
+import type { DragItemDataType } from '~/interfaces/bookmark-info';
 
 
 type DragAndDropWrapperProps = {
 type DragAndDropWrapperProps = {
   item?: Partial<DragItemDataType>
   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) {
     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 (
   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}
       {children}
     </div>
     </div>
   );
   );

+ 3 - 2
apps/app/src/client/components/Common/Dropdown/PageItemControl.spec.tsx

@@ -1,7 +1,8 @@
 import { type IPageInfoForOperation } from '@growi/core/dist/interfaces';
 import { type IPageInfoForOperation } from '@growi/core/dist/interfaces';
 import {
 import {
-  fireEvent, render, screen, within,
-} from '@testing-library/react';
+  fireEvent, screen, within,
+} from '@testing-library/dom';
+import { render } from '@testing-library/react';
 import { mock } from 'vitest-mock-extended';
 import { mock } from 'vitest-mock-extended';
 
 
 import { PageItemControl } from './PageItemControl';
 import { PageItemControl } from './PageItemControl';

+ 0 - 1
apps/app/src/client/components/CustomNavigation/CustomNav.module.scss

@@ -14,5 +14,4 @@
     border-bottom: 3px solid;
     border-bottom: 3px solid;
     transition: 0.3s ease-in-out;
     transition: 0.3s ease-in-out;
   }
   }
-
 }
 }

+ 19 - 3
apps/app/src/client/components/CustomNavigation/CustomNav.tsx

@@ -42,26 +42,42 @@ export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element =>
 
 
   const { Icon, i18n } = navTabMapping[activeTab];
   const { Icon, i18n } = navTabMapping[activeTab];
 
 
+  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+  const dropdownButtonRef = useRef<HTMLButtonElement>(null);
+
+  const toggleDropdown = () => {
+    setIsDropdownOpen(prev => !prev);
+  };
+
   const menuItemClickHandler = useCallback((key) => {
   const menuItemClickHandler = useCallback((key) => {
     if (onNavSelected != null) {
     if (onNavSelected != null) {
       onNavSelected(key);
       onNavSelected(key);
     }
     }
+    // Manually close the dropdown
+    setIsDropdownOpen(false);
+    if (dropdownButtonRef.current) {
+      dropdownButtonRef.current.classList.remove('show');
+    }
   }, [onNavSelected]);
   }, [onNavSelected]);
 
 
   return (
   return (
     <div className="btn-group">
     <div className="btn-group">
       <button
       <button
+        ref={dropdownButtonRef}
         className="btn btn-outline-primary btn-lg dropdown-toggle text-end"
         className="btn btn-outline-primary btn-lg dropdown-toggle text-end"
         type="button"
         type="button"
         data-bs-toggle="dropdown"
         data-bs-toggle="dropdown"
         aria-haspopup="true"
         aria-haspopup="true"
-        aria-expanded="false"
+        aria-expanded={isDropdownOpen}
+        onClick={toggleDropdown}
+        data-testid="custom-nav-dropdown"
       >
       >
         <span className="float-start">
         <span className="float-start">
           { Icon != null && <Icon /> } {i18n}
           { Icon != null && <Icon /> } {i18n}
         </span>
         </span>
       </button>
       </button>
-      <div className="dropdown-menu dropdown-menu-right">
+      <div className={`dropdown-menu dropdown-menu-right w-100 ${isDropdownOpen ? 'show' : ''} ${styles['dropdown-menu']}`}>
         {Object.entries(navTabMapping).map(([key, value]) => {
         {Object.entries(navTabMapping).map(([key, value]) => {
 
 
           const isActive = activeTab === key;
           const isActive = activeTab === key;
@@ -167,7 +183,7 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
+    <div data-testid="custom-nav-tab" className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
       <div ref={navContainerRef} className="d-flex justify-content-between">
       <div ref={navContainerRef} className="d-flex justify-content-between">
         <Nav className="nav-title">
         <Nav className="nav-title">
           {Object.entries(navTabMapping).map(([key, value]) => {
           {Object.entries(navTabMapping).map(([key, value]) => {

+ 9 - 1
apps/app/src/client/components/DataTransferForm.tsx

@@ -3,12 +3,14 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { useGenerateTransferKey } from '~/client/services/g2g-transfer';
 import { useGenerateTransferKey } from '~/client/services/g2g-transfer';
+import { useGrowiDocumentationUrl } from '~/stores-universal/context';
 
 
 import CustomCopyToClipBoard from './Common/CustomCopyToClipBoard';
 import CustomCopyToClipBoard from './Common/CustomCopyToClipBoard';
 
 
 const DataTransferForm = (): JSX.Element => {
 const DataTransferForm = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
   const { transferKey, generateTransferKey } = useGenerateTransferKey();
   const { transferKey, generateTransferKey } = useGenerateTransferKey();
+  const { data: documentationUrl } = useGrowiDocumentationUrl();
 
 
   return (
   return (
     <div data-testid="installerForm" className="py-3 px-4">
     <div data-testid="installerForm" className="py-3 px-4">
@@ -33,7 +35,13 @@ const DataTransferForm = (): JSX.Element => {
       <div className="alert alert-warning mt-4">
       <div className="alert alert-warning mt-4">
         <p className="mb-1">{t('g2g_data_transfer.transfer_key_limit')}</p>
         <p className="mb-1">{t('g2g_data_transfer.transfer_key_limit')}</p>
         <p className="mb-1">{t('g2g_data_transfer.once_transfer_key_used')}</p>
         <p className="mb-1">{t('g2g_data_transfer.once_transfer_key_used')}</p>
-        <p className="mb-0">{t('g2g_data_transfer.transfer_to_growi_cloud')}</p>
+        <p
+          className="mb-0"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{
+            __html: t('g2g_data_transfer.transfer_to_growi_cloud', { documentationUrl }),
+          }}
+        />
       </div>
       </div>
     </div>
     </div>
   );
   );

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

@@ -14,7 +14,7 @@ import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores-universal/context';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores-universal/context';
 import {
 import {
   mutatePageTree,
   mutatePageTree,
-  useSWRxPageInfoForList, useSWRxPageList,
+  useSWRxPageInfoForList, useSWRxPageList, mutateRecentlyUpdated,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 
 
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
@@ -67,7 +67,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
     else {
     else {
       toastSuccess(t('deleted_pages_completely', { path }));
       toastSuccess(t('deleted_pages_completely', { path }));
     }
     }
-
+    mutateRecentlyUpdated();
     mutatePageTree();
     mutatePageTree();
     if (onPagesDeleted != null) {
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
       onPagesDeleted(...args);
@@ -77,6 +77,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
     toastSuccess(t('page_has_been_reverted', { path }));
     toastSuccess(t('page_has_been_reverted', { path }));
 
 
+    mutateRecentlyUpdated();
     mutatePageTree();
     mutatePageTree();
     if (onPagePutBacked != null) {
     if (onPagePutBacked != null) {
       onPagePutBacked(path);
       onPagePutBacked(path);

+ 4 - 0
apps/app/src/client/components/DescendantsPageListModal.module.scss

@@ -9,6 +9,10 @@
     padding: 25px 30px;
     padding: 25px 30px;
   }
   }
 
 
+  .grw-tab-content-style-md-down {
+    padding-top: 25px;
+  }
+
   .grw-modal-body-style {
   .grw-modal-body-style {
     max-height: calc(100vh - 100px);
     max-height: calc(100vh - 100px);
   }
   }

+ 70 - 0
apps/app/src/client/components/DescendantsPageListModal.spec.tsx

@@ -0,0 +1,70 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+
+import { DescendantsPageListModal } from './DescendantsPageListModal';
+
+const mockClose = vi.hoisted(() => vi.fn());
+const useIsDeviceLargerThanLg = vi.hoisted(() => vi.fn().mockReturnValue({ data: true }));
+
+vi.mock('next/router', () => ({
+  useRouter: () => ({
+    events: {
+      on: vi.fn(),
+      off: vi.fn(),
+    },
+  }),
+}));
+
+vi.mock('~/stores/modal', () => ({
+  useDescendantsPageListModal: vi.fn().mockReturnValue({
+    data: { isOpened: true },
+    close: mockClose,
+  }),
+}));
+
+vi.mock('~/stores/ui', () => ({
+  useIsDeviceLargerThanLg,
+}));
+
+describe('DescendantsPageListModal.tsx', () => {
+
+  it('should render the modal when isOpened is true', () => {
+    render(<DescendantsPageListModal />);
+    expect(screen.getByTestId('descendants-page-list-modal')).not.toBeNull();
+  });
+
+  it('should call close function when close button is clicked', () => {
+    render(<DescendantsPageListModal />);
+    const closeButton = screen.getByLabelText('Close');
+    fireEvent.click(closeButton);
+    expect(mockClose).toHaveBeenCalled();
+  });
+
+  describe('when device is larger than lg', () => {
+
+    it('should render CustomNavTab', () => {
+      render(<DescendantsPageListModal />);
+      expect(screen.getByTestId('custom-nav-tab')).not.toBeNull();
+    });
+
+    it('should not render CustomNavDropdown', () => {
+      render(<DescendantsPageListModal />);
+      expect(screen.queryByTestId('custom-nav-dropdown')).toBeNull();
+    });
+  });
+
+  describe('when device is smaller than lg', () => {
+    beforeEach(() => {
+      useIsDeviceLargerThanLg.mockReturnValue({ data: false });
+    });
+
+    it('should render CustomNavDropdown on devices smaller than lg', () => {
+      render(<DescendantsPageListModal />);
+      expect(screen.getByTestId('custom-nav-dropdown')).not.toBeNull();
+    });
+
+    it('should not render CustomNavTab', () => {
+      render(<DescendantsPageListModal />);
+      expect(screen.queryByTestId('custom-nav-tab')).toBeNull();
+    });
+  });
+});

+ 25 - 9
apps/app/src/client/components/DescendantsPageListModal.tsx

@@ -10,8 +10,9 @@ import {
 
 
 import { useIsSharedUser } from '~/stores-universal/context';
 import { useIsSharedUser } from '~/stores-universal/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
+import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
 
-import { CustomNavTab } from './CustomNavigation/CustomNav';
+import { CustomNavDropdown, CustomNavTab } from './CustomNavigation/CustomNav';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
 import type { DescendantsPageListProps } from './DescendantsPageList';
 import type { DescendantsPageListProps } from './DescendantsPageList';
 import ExpandOrContractButton from './ExpandOrContractButton';
 import ExpandOrContractButton from './ExpandOrContractButton';
@@ -34,6 +35,8 @@ export const DescendantsPageListModal = (): JSX.Element => {
 
 
   const { events } = useRouter();
   const { events } = useRouter();
 
 
+  const { data: isDeviceLargerThanLg } = useIsDeviceLargerThanLg();
+
   useEffect(() => {
   useEffect(() => {
     events.on('routeChangeStart', close);
     events.on('routeChangeStart', close);
     return () => {
     return () => {
@@ -93,17 +96,30 @@ export const DescendantsPageListModal = (): JSX.Element => {
       data-testid="descendants-page-list-modal"
       data-testid="descendants-page-list-modal"
       className={`grw-descendants-page-list-modal ${styles['grw-descendants-page-list-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
       className={`grw-descendants-page-list-modal ${styles['grw-descendants-page-list-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
     >
-      <ModalHeader className="p-0" toggle={close} close={buttons}>
-        <CustomNavTab
+      <ModalHeader className={isDeviceLargerThanLg ? 'p-0' : ''} toggle={close} close={buttons}>
+        {isDeviceLargerThanLg && (
+          <CustomNavTab
+            activeTab={activeTab}
+            navTabMapping={navTabMapping}
+            breakpointToHideInactiveTabsDown="md"
+            onNavSelected={v => setActiveTab(v)}
+            hideBorderBottom
+          />
+        )}
+      </ModalHeader>
+      <ModalBody>
+        {!isDeviceLargerThanLg && (
+          <CustomNavDropdown
+            activeTab={activeTab}
+            navTabMapping={navTabMapping}
+            onNavSelected={v => setActiveTab(v)}
+          />
+        )}
+        <CustomTabContent
           activeTab={activeTab}
           activeTab={activeTab}
           navTabMapping={navTabMapping}
           navTabMapping={navTabMapping}
-          breakpointToHideInactiveTabsDown="md"
-          onNavSelected={v => setActiveTab(v)}
-          hideBorderBottom
+          additionalClassNames={!isDeviceLargerThanLg ? ['grw-tab-content-style-md-down'] : undefined}
         />
         />
-      </ModalHeader>
-      <ModalBody>
-        <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} />
       </ModalBody>
       </ModalBody>
     </Modal>
     </Modal>
   );
   );

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

@@ -6,19 +6,11 @@ import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastError } from '~/client/util/toastr';
 import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 import { useDefaultSocket } from '~/stores/socket-io';
 import { useDefaultSocket } from '~/stores/socket-io';
-import loggerFactory from '~/utils/logger';
 
 
 import InAppNotificationList from './InAppNotificationList';
 import InAppNotificationList from './InAppNotificationList';
 
 
-
-const logger = loggerFactory('growi:InAppNotificationDropdown');
-
-
 export const InAppNotificationDropdown = (): JSX.Element => {
 export const InAppNotificationDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
 
 
@@ -36,16 +28,6 @@ export const InAppNotificationDropdown = (): JSX.Element => {
   const buttonRef = useRef(null);
   const buttonRef = useRef(null);
   useRipple(buttonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
   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(() => {
   useEffect(() => {
     if (socket != null) {
     if (socket != null) {
       socket.on('notificationUpdated', () => {
       socket.on('notificationUpdated', () => {
@@ -62,7 +44,6 @@ export const InAppNotificationDropdown = (): JSX.Element => {
 
 
   const toggleDropdownHandler = async() => {
   const toggleDropdownHandler = async() => {
     if (!isOpen && inAppNotificationUnreadStatusCount != null && inAppNotificationUnreadStatusCount > 0) {
     if (!isOpen && inAppNotificationUnreadStatusCount != null && inAppNotificationUnreadStatusCount > 0) {
-      await updateNotificationStatus();
       mutateInAppNotificationUnreadStatusCount();
       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 type { HasObjectId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 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';
 import { useModelNotification } from './PageNotification';
 
 
@@ -21,6 +24,7 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
 
 
   const Notification = modelNotificationUtils?.Notification;
   const Notification = modelNotificationUtils?.Notification;
   const publishOpen = modelNotificationUtils?.publishOpen;
   const publishOpen = modelNotificationUtils?.publishOpen;
+  const { mutate: mutateNotificationCount } = useSWRxInAppNotificationStatus();
 
 
   if (Notification == null || publishOpen == null) {
   if (Notification == null || publishOpen == null) {
     return <></>;
     return <></>;
@@ -31,6 +35,7 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       // set notification status "OPEND"
       // set notification status "OPEND"
       await apiv3Post('/in-app-notification/open', { id: notification._id });
       await apiv3Post('/in-app-notification/open', { id: notification._id });
       onUnopenedNotificationOpend?.();
       onUnopenedNotificationOpend?.();
+      mutateNotificationCount();
     }
     }
 
 
     publishOpen();
     publishOpen();

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

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

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

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

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

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

+ 8 - 2
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -34,7 +34,7 @@ import {
 import {
 import {
   useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo,
   useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo,
 } from '~/stores/page';
 } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import {
 import {
   useIsAbleToShowPageManagement,
   useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
   useIsAbleToChangeEditorMode,
@@ -271,6 +271,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       mutateCurrentPage();
       mutateCurrentPage();
       mutatePageInfo();
       mutatePageInfo();
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
     };
     };
     openRenameModal(page, { onRenamed: renamedHandler });
     openRenameModal(page, { onRenamed: renamedHandler });
   }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
   }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
@@ -294,6 +295,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       mutateCurrentPage();
       mutateCurrentPage();
       mutatePageInfo();
       mutatePageInfo();
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
     };
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);
@@ -348,7 +350,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     <>
     <>
       <GroundGlassBar className="py-4 d-block d-md-none d-print-none border-bottom" />
       <GroundGlassBar className="py-4 d-block d-md-none d-print-none border-bottom" />
 
 
-      <Sticky className="z-1" onStateChange={status => setStickyActive(status.status === Sticky.STATUS_FIXED)}>
+      <Sticky
+        className="z-1"
+        onStateChange={status => setStickyActive(status.status === Sticky.STATUS_FIXED)}
+        innerActiveClass="w-100 end-0"
+      >
         <GroundGlassBar>
         <GroundGlassBar>
 
 
           <nav
           <nav

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

@@ -1,7 +1,8 @@
 import React from 'react';
 import React from 'react';
 
 
 import { Disable } from 'react-disable';
 import { Disable } from 'react-disable';
-import { UncontrolledTooltip, UncontrolledTooltipProps } from 'reactstrap';
+import type { UncontrolledTooltipProps } from 'reactstrap';
+import { UncontrolledTooltip } from 'reactstrap';
 
 
 type NotAvailableProps = {
 type NotAvailableProps = {
   children: JSX.Element
   children: JSX.Element

+ 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 { useTranslation } from 'next-i18next';
 
 
-import { useIsReadOnlyUser } from '~/stores-universal/context';
+import { useIsReadOnlyUser, useIsRomUserAllowedToComment } from '~/stores-universal/context';
 
 
 import { NotAvailable } from './NotAvailable';
 import { NotAvailable } from './NotAvailable';
 
 
@@ -26,3 +26,26 @@ export const NotAvailableForReadOnlyUser: React.FC<{
   );
   );
 });
 });
 NotAvailableForReadOnlyUser.displayName = 'NotAvailableForReadOnlyUser';
 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';
 import { usePresentationViewOptions } from '~/stores/renderer';
 
 

+ 4 - 0
apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.module.scss

@@ -9,6 +9,10 @@
     padding: 25px 30px;
     padding: 25px 30px;
   }
   }
 
 
+  .grw-tab-content-style-md-down {
+    padding-top: 25px;
+  }
+
   .grw-modal-body-style {
   .grw-modal-body-style {
     max-height: calc(100vh - 100px);
     max-height: calc(100vh - 100px);
   }
   }

+ 24 - 9
apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -10,8 +10,9 @@ import {
   useDisableLinkSharing, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser,
   useDisableLinkSharing, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
+import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
 
-import { CustomNavTab } from '../CustomNavigation/CustomNav';
+import { CustomNavDropdown, CustomNavTab } from '../CustomNavigation/CustomNav';
 import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import ExpandOrContractButton from '../ExpandOrContractButton';
 import ExpandOrContractButton from '../ExpandOrContractButton';
 
 
@@ -35,6 +36,7 @@ export const PageAccessoriesModal = (): JSX.Element => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
+  const { data: isDeviceLargerThanLg } = useIsDeviceLargerThanLg();
 
 
   const { data: status, close, selectContents } = usePageAccessoriesModal();
   const { data: status, close, selectContents } = usePageAccessoriesModal();
 
 
@@ -93,17 +95,30 @@ export const PageAccessoriesModal = (): JSX.Element => {
       data-testid="page-accessories-modal"
       data-testid="page-accessories-modal"
       className={`grw-page-accessories-modal ${styles['grw-page-accessories-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
       className={`grw-page-accessories-modal ${styles['grw-page-accessories-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
     >
-      <ModalHeader className="p-0" toggle={close} close={buttons}>
-        <CustomNavTab
+      <ModalHeader className={isDeviceLargerThanLg ? 'p-0' : ''} toggle={close} close={buttons}>
+        {isDeviceLargerThanLg && (
+          <CustomNavTab
+            activeTab={status.activatedContents}
+            navTabMapping={navTabMapping}
+            breakpointToHideInactiveTabsDown="md"
+            onNavSelected={selectContents}
+            hideBorderBottom
+          />
+        )}
+      </ModalHeader>
+      <ModalBody className="overflow-auto grw-modal-body-style">
+        {!isDeviceLargerThanLg && (
+          <CustomNavDropdown
+            activeTab={status.activatedContents}
+            navTabMapping={navTabMapping}
+            onNavSelected={selectContents}
+          />
+        )}
+        <CustomTabContent
           activeTab={status.activatedContents}
           activeTab={status.activatedContents}
           navTabMapping={navTabMapping}
           navTabMapping={navTabMapping}
-          breakpointToHideInactiveTabsDown="md"
-          onNavSelected={selectContents}
-          hideBorderBottom
+          additionalClassNames={!isDeviceLargerThanLg ? ['grw-tab-content-style-md-down'] : undefined}
         />
         />
-      </ModalHeader>
-      <ModalBody className="overflow-auto grw-modal-body-style">
-        <CustomTabContent activeTab={status.activatedContents} navTabMapping={navTabMapping} />
       </ModalBody>
       </ModalBody>
     </Modal>
     </Modal>
   );
   );

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

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