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

Merge branch 'feat/opentelemetry' into imprv/switch-otel-enabled

Yuki Takei 1 год назад
Родитель
Сommit
f869361dfb
100 измененных файлов с 3042 добавлено и 1573 удалено
  1. 28 0
      .changeset/config.json
  2. 0 59
      .devcontainer/Dockerfile
  3. 3 22
      .devcontainer/compose.yml
  4. 34 32
      .devcontainer/devcontainer.json
  5. 17 0
      .devcontainer/postCreateCommand.sh
  6. 1 1
      .eslintrc.js
  7. 0 2
      .gitattributes
  8. 5 1
      .github/dependabot.yml
  9. 37 0
      .github/mergify.yml
  10. 6 3
      .github/workflows/auto-labeling.yml
  11. 30 31
      .github/workflows/ci-app-prod.yml
  12. 33 86
      .github/workflows/ci-app.yml
  13. 28 58
      .github/workflows/ci-slackbot-proxy.yml
  14. 1 1
      .github/workflows/draft-release.yml
  15. 1 14
      .github/workflows/release-rc-scheduled.yml
  16. 1 1
      .github/workflows/release-rc.yml
  17. 10 10
      .github/workflows/release-slackbot-proxy.yml
  18. 95 0
      .github/workflows/release-subpackages.yml
  19. 23 25
      .github/workflows/release.yml
  20. 119 135
      .github/workflows/reusable-app-prod.yml
  21. 5 25
      .github/workflows/reusable-app-reg-suit.yml
  22. 3 0
      .gitignore
  23. 0 24
      .mergify.yml
  24. 0 0
      .npmrc
  25. 14 5
      .stylelintrc.json
  26. 2 1
      .vscode/launch.json
  27. 8 1
      .vscode/settings.json
  28. 451 1
      CHANGELOG.md
  29. 7 7
      README.md
  30. 7 7
      README_JP.md
  31. 0 1
      apps/app/.eslintrc.js
  32. 0 2
      apps/app/.gitignore
  33. 0 2
      apps/app/.prettierignore
  34. 3 17
      apps/app/.stylelintrc.json
  35. 0 710
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  36. 28 0
      apps/app/bin/swagger-jsdoc/definition-apiv1.js
  37. 93 0
      apps/app/bin/swagger-jsdoc/definition-apiv3.js
  38. 15 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv1.sh
  39. 14 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh
  40. 4 6
      apps/app/config/i18next.config.js
  41. 3 1
      apps/app/config/logger/config.dev.js
  42. 0 37
      apps/app/config/swagger-definition.js
  43. 0 30
      apps/app/cypress.config.ts
  44. 28 76
      apps/app/docker/Dockerfile
  45. 1 0
      apps/app/docker/Dockerfile.dockerignore
  46. 4 5
      apps/app/docker/README.md
  47. 1 8
      apps/app/docker/codebuild/buildspec.yml
  48. 1 1
      apps/app/next-env.d.ts
  49. 29 1
      apps/app/next.config.js
  50. 19 0
      apps/app/nodemon.json
  51. 146 113
      apps/app/package.json
  52. 115 0
      apps/app/playwright.config.ts
  53. 0 0
      apps/app/playwright/.auth/.gitkeep
  54. 16 0
      apps/app/playwright/.eslintrc.mjs
  55. 2 0
      apps/app/playwright/.gitignore
  56. 47 0
      apps/app/playwright/10-installer/install.spec.ts
  57. 150 0
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  58. 30 0
      apps/app/playwright/20-basic-features/access-to-pagelist.spec.ts
  59. 50 0
      apps/app/playwright/20-basic-features/click-page-icons.spec.ts
  60. 49 0
      apps/app/playwright/20-basic-features/comments.spec.ts
  61. 27 0
      apps/app/playwright/20-basic-features/create-page-button.spec.ts
  62. 28 0
      apps/app/playwright/20-basic-features/presentation.spec.ts
  63. 47 0
      apps/app/playwright/20-basic-features/sticky-features.spec.ts
  64. 86 0
      apps/app/playwright/20-basic-features/use-tools.spec.ts
  65. 46 0
      apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts
  66. 14 0
      apps/app/playwright/21-basic-features-for-guest/sticky-for-guest.spec.ts
  67. 37 0
      apps/app/playwright/22-sharelink/access-to-sharelink.spec.ts
  68. 0 0
      apps/app/playwright/23-editor/assets/example.txt
  69. 50 0
      apps/app/playwright/23-editor/saving.spec.ts
  70. 27 0
      apps/app/playwright/23-editor/template-modal.spec.ts
  71. 113 0
      apps/app/playwright/23-editor/with-navigation.spec.ts
  72. 227 0
      apps/app/playwright/30-search/search.spect.ts
  73. 103 0
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  74. 170 0
      apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts
  75. 43 0
      apps/app/playwright/50-sidebar/switching-sidebar-mode.spec.ts
  76. 122 0
      apps/app/playwright/60-home/home.spec.ts
  77. 9 0
      apps/app/playwright/auth.setup.ts
  78. 19 0
      apps/app/playwright/utils/CollapseSidebar.ts
  79. 24 0
      apps/app/playwright/utils/Login.ts
  80. 2 0
      apps/app/playwright/utils/index.ts
  81. BIN
      apps/app/public/favicon.ico
  82. 24 0
      apps/app/public/favicon.svg
  83. 7 0
      apps/app/public/images/growi-brand-logo-login.svg
  84. 0 1
      apps/app/public/images/icons/editor/bold.svg
  85. 0 1
      apps/app/public/images/icons/editor/check.svg
  86. 0 1
      apps/app/public/images/icons/editor/code.svg
  87. 0 1
      apps/app/public/images/icons/editor/header.svg
  88. 0 1
      apps/app/public/images/icons/editor/italic.svg
  89. 0 1
      apps/app/public/images/icons/editor/list-ol.svg
  90. 0 1
      apps/app/public/images/icons/editor/list-ul.svg
  91. 0 1
      apps/app/public/images/icons/editor/picture.svg
  92. 0 1
      apps/app/public/images/icons/editor/quote.svg
  93. 0 1
      apps/app/public/images/icons/editor/strikethrough.svg
  94. 0 1
      apps/app/public/images/icons/editor/table.svg
  95. BIN
      apps/app/public/images/icons/favicon/android-icon-144x144.png
  96. BIN
      apps/app/public/images/icons/favicon/android-icon-192x192.png
  97. BIN
      apps/app/public/images/icons/favicon/android-icon-36x36.png
  98. BIN
      apps/app/public/images/icons/favicon/android-icon-48x48.png
  99. BIN
      apps/app/public/images/icons/favicon/android-icon-72x72.png
  100. BIN
      apps/app/public/images/icons/favicon/android-icon-96x96.png

+ 28 - 0
.changeset/config.json

@@ -0,0 +1,28 @@
+{
+  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
+  "changelog": ["@changesets/changelog-github", { "repo": "weseek/growi" }],
+  "commit": false,
+  "fixed": [],
+  "linked": [],
+  "access": "public",
+  "baseBranch": "master",
+  "updateInternalDependencies": "patch",
+  "snapshot": {
+    "useCalculatedVersion": true,
+    "prereleaseTemplate": "{tag}.{commit}"
+  },
+  "ignore": [
+    "@growi/app",
+    "@growi/slackbot-proxy",
+    "@growi/custom-icons",
+    "@growi/editor",
+    "@growi/presentation",
+    "@growi/preset-templates",
+    "@growi/preset-themes",
+    "@growi/remark-attachment-refs",
+    "@growi/remark-drawio",
+    "@growi/remark-lsx",
+    "@growi/slack",
+    "@growi/ui"
+  ]
+}

+ 0 - 59
.devcontainer/Dockerfile

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

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

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

+ 34 - 32
.devcontainer/devcontainer.json

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

+ 17 - 0
.devcontainer/postCreateCommand.sh

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

+ 1 - 1
.eslintrc.js

@@ -48,7 +48,7 @@ module.exports = {
         'newlines-between': 'always',
       },
     ],
-    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/consistent-type-imports': 'warn',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     indent: [
       'error',

+ 0 - 2
.gitattributes

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

+ 5 - 1
.github/dependabot.yml

@@ -4,7 +4,8 @@ updates:
     directory: '/'
     open-pull-requests-limit: 3
     schedule:
-      interval: monthly
+      interval: weekly
+      day: saturday
     labels:
       - "type/dependencies"
     commit-message:
@@ -16,6 +17,7 @@ updates:
     open-pull-requests-limit: 3
     schedule:
       interval: weekly
+      day: saturday
     labels:
       - "type/dependencies"
     commit-message:
@@ -27,4 +29,6 @@ updates:
       - dependency-name: "@handsontable/react"
       - dependency-name: handsontable
       - dependency-name: typeorm
+      - dependency-name: mysql2
+      - dependency-name: "@codemirror/*"
 

+ 37 - 0
.github/mergify.yml

@@ -0,0 +1,37 @@
+queue_rules:
+  - name: default
+    allow_inplace_checks: false
+    queue_conditions:
+      - check-success ~= ci-app-lint
+      - check-success ~= ci-app-test
+      - check-success ~= ci-app-launch-dev
+      - -check-failure ~= ci-app-
+      - -check-failure ~= ci-slackbot-
+      - -check-failure ~= test-prod-node20 /
+    merge_conditions:
+      - check-success ~= ci-app-lint
+      - check-success ~= ci-app-test
+      - check-success ~= ci-app-launch-dev
+      - check-success = test-prod-node20 / build-prod
+      - check-success = test-prod-node20 / launch-prod
+      - check-success ~= test-prod-node20 / run-playwright
+      - -check-failure ~= ci-app-
+      - -check-failure ~= ci-slackbot-
+      - -check-failure ~= test-prod-node20 /
+
+pull_request_rules:
+  - name: Automatic queue to merge
+    conditions:
+      - '#approved-reviews-by >= 1'
+      - '#changes-requested-reviews-by = 0'
+      - '#review-requested = 0'
+    actions:
+      queue:
+
+  - name: Automatic merge for Preparing next version
+    conditions:
+      - author = github-actions[bot]
+      - label = type/prepare-next-version
+    actions:
+      merge:
+        method: merge

+ 6 - 3
.github/workflows/auto-labeling.yml

@@ -20,7 +20,9 @@ jobs:
     runs-on: ubuntu-latest
 
     if: |
-      !contains(github.event.pull_request.labels.*.name, 'flag/exclude-from-changelog')
+      (!contains( github.event.pull_request.labels.*.name, 'flag/exclude-from-changelog' )
+        && !startsWith( github.head_ref, 'changeset-release/' )
+        && !startsWith( github.head_ref, 'mergify/merge-queue/' ))
 
     steps:
       - uses: release-drafter/release-drafter@v5
@@ -33,8 +35,9 @@ jobs:
     runs-on: ubuntu-latest
 
     if: |
-      (!contains( github.event.pull_request.labels.*.name, 'flag/exclude-from-changelog' ) &&
-        !startsWith( github.head_ref, 'dependabot/' ))
+      (!contains( github.event.pull_request.labels.*.name, 'flag/exclude-from-changelog' )
+        && !startsWith( github.head_ref, 'changeset-release/' )
+        && !startsWith( github.head_ref, 'mergify/merge-queue/' ))
 
     steps:
       - uses: amannn/action-semantic-pull-request@v5

+ 30 - 31
.github/workflows/ci-app-prod.yml

@@ -5,41 +5,32 @@ on:
     branches:
       - master
       - dev/7.*.x
-      - dev/6.*.x
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - apps/app/**
       - '!apps/app/docker/**'
       - packages/**
   pull_request:
-    branches:
-      - master
-      - dev/7.*.x
-      - dev/6.*.x
     types: [opened, reopened, synchronize]
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - tsconfig.base.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - turbo.json
       - package.json
       - apps/app/**
       - '!apps/app/docker/**'
       - packages/**
-  workflow_call:
-    inputs:
-      cypress-config-video:
-        description: 'Enable video when running Cypress test'
-        type: boolean
-        default: false
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -50,37 +41,45 @@ jobs:
 
   test-prod-node18:
     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:
       node-version: 18.x
-      skip-cypress: true
+      skip-e2e-test: true
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
   test-prod-node20:
     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:
       node-version: 20.x
-      skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name-prefix: cypress-report-
-      cypress-config-video: ${{ inputs.cypress-config-video || false }}
+      skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
+  # run-reg-suit-node20:
+  #   needs: [test-prod-node20]
 
-  run-reg-suit-node20:
-    needs: [test-prod-node20]
-
-    uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
+  #   uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
 
-    if: always()
+  #   if: always()
 
-    with:
-      node-version: 20.x
-      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name-pattern: cypress-report-*
-    secrets:
-      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
-      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+  #   with:
+  #     node-version: 20.x
+  #     skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
+  #   secrets:
+  #     REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
+  #     AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+  #     AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+  #     SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 33 - 86
.github/workflows/ci-app.yml

@@ -5,12 +5,14 @@ on:
     branches-ignore:
       - release/**
       - rc/**
+      - changeset-release/**
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app.yml
       - .eslint*
       - tsconfig.base.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - apps/app/**
       - '!apps/app/docker/**'
@@ -32,37 +34,28 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - uses: pnpm/action-setup@v4
+
       - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
+          cache: 'pnpm'
 
-      - name: Cache/Restore node_modules
+      - name: Cache/Restore dist
         uses: actions/cache@v4
-        with:
-          path: |
-            **/node_modules
-          key: node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/app/package.json') }}
-          restore-keys: |
-            node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-            node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
-
-      - name: Restore dist
-        uses: actions/cache/restore@v4
         with:
           path: |
             **/.turbo
             **/dist
-          key: dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
+            **/node_modules/.cache/turbo
+          key: dist-ci-app-${{ runner.OS }}-node${{ matrix.node-version }}-${{ github.sha }}
           restore-keys: |
-            dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-
+            dist-ci-app-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
-          yarn global add node-gyp
-          yarn --frozen-lockfile
+          pnpm add turbo --global
+          pnpm install --frozen-lockfile
 
       - name: Lint
         run: |
@@ -78,14 +71,6 @@ jobs:
           isCompactMode: true
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
-      - name: Cache dist
-        uses: actions/cache/save@v4
-        with:
-          path: |
-            **/.turbo
-            **/dist
-          key: dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
-
 
   ci-app-test:
     runs-on: ubuntu-latest
@@ -103,42 +88,32 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - uses: pnpm/action-setup@v4
+
       - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
+          cache: 'pnpm'
 
-      - name: Cache/Restore node_modules
-        id: cache-dependencies
+      - name: Cache/Restore dist
         uses: actions/cache@v4
-        with:
-          path: |
-            **/node_modules
-          key: node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/app/package.json') }}
-          restore-keys: |
-            node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-            node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
-
-      - name: Restore dist
-        uses: actions/cache/restore@v4
         with:
           path: |
             **/.turbo
             **/dist
-          key: dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
+            **/node_modules/.cache/turbo
+          key: dist-ci-app-${{ runner.OS }}-node${{ matrix.node-version }}-${{ github.sha }}
           restore-keys: |
-            dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-
+            dist-ci-app-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
-          yarn global add node-gyp
-          yarn --frozen-lockfile
+          pnpm add turbo --global
+          pnpm install --frozen-lockfile
 
       - name: Test
         run: |
-          turbo run test --filter=!@growi/slackbot-proxy
+          turbo run test --filter=!@growi/slackbot-proxy --env-mode=loose
         env:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
@@ -160,14 +135,6 @@ jobs:
           isCompactMode: true
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
-      - name: Cache dist
-        uses: actions/cache/save@v4
-        with:
-          path: |
-            **/.turbo
-            **/dist
-          key: dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
-
 
   ci-app-launch-dev:
     runs-on: ubuntu-latest
@@ -185,45 +152,34 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
+      - uses: pnpm/action-setup@v4
+
       - uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
+          cache: 'pnpm'
 
-      - name: Cache/Restore node_modules
-        id: cache-dependencies
+      - name: Cache/Restore dist
         uses: actions/cache@v4
-        with:
-          path: |
-            **/node_modules
-          key: node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/app/package.json') }}
-          restore-keys: |
-            node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-            node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-
-
-      - name: Restore dist
-        uses: actions/cache/restore@v4
         with:
           path: |
             **/.turbo
             **/dist
-            ${{ github.workspace }}/apps/app/.next
-          key: dist-app-7.x-dev-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
+            **/node_modules/.cache/turbo
+          key: dist-ci-app-${{ runner.OS }}-node${{ matrix.node-version }}-${{ github.sha }}
           restore-keys: |
-            dist-app-7.x-dev-${{ runner.OS }}-node${{ matrix.node-version }}-
+            dist-ci-app-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
-          yarn global add node-gyp
-          yarn --frozen-lockfile
+          pnpm add turbo --global
+          pnpm install --frozen-lockfile
 
-      - name: turbo run dev:ci
+      - name: turbo run launch-dev:ci
         working-directory: ./apps/app
         run: |
           cp config/ci/.env.local.for-ci .env.development.local
-          turbo run dev:ci
+          turbo run launch-dev:ci --env-mode=loose
         env:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
 
@@ -236,12 +192,3 @@ jobs:
           channel: '#ci'
           isCompactMode: true
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-      - name: Cache dist
-        uses: actions/cache/save@v4
-        with:
-          path: |
-            **/.turbo
-            **/dist
-            ${{ github.workspace }}/apps/app/.next
-          key: dist-app-7.x-dev-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}

+ 28 - 58
.github/workflows/ci-slackbot-proxy.yml

@@ -7,11 +7,12 @@ on:
       - rc/**
       - support/prepare-v**
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-slackbot-proxy.yml
       - .eslint*
       - tsconfig.base.json
       - turbo.json
-      - yarn.lock
+      - pnpm-lock.yaml
       - package.json
       - apps/slackbot-proxy/**
       - '!apps/slackbot-proxy/docker/**'
@@ -34,21 +35,12 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Cache/Restore node_modules
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/slackbot-proxy/package.json') }}
-        restore-keys: |
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+        cache: 'pnpm'
 
     - name: Restore dist
       uses: actions/cache/restore@v4
@@ -62,9 +54,8 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Lint
       run: |
@@ -109,21 +100,12 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Cache/Restore node_modules
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/slackbot-proxy/package.json') }}
-        restore-keys: |
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+        cache: 'pnpm'
 
     - name: Restore dist
       uses: actions/cache/restore@v4
@@ -137,15 +119,14 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
-    - name: yarn dev:ci
+    - name: turbo run dev:ci
       working-directory: ./apps/slackbot-proxy
       run: |
         cp config/ci/.env.local.for-ci .env.development.local
-        turbo run dev:ci
+        turbo run dev:ci --env-mode=loose
       env:
         SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql
@@ -175,6 +156,9 @@ jobs:
 
 
   ci-slackbot-proxy-launch-prod:
+
+    if: startsWith(github.head_ref, 'mergify/merge-queue/')
+
     runs-on: ubuntu-latest
 
     strategy:
@@ -194,36 +178,20 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install turbo
       run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune --scope=@growi/slackbot-proxy
-        rm -rf apps packages
-        mv out/* .
-
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v4
-      with:
-        path: |
-          **/node_modules
-        key: node_modules-slackbot-prxy-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-slackbot-proxy-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+        pnpm add turbo --global
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
     - name: Restore dist
       uses: actions/cache/restore@v4
@@ -240,15 +208,17 @@ jobs:
       run: |
         turbo run build
 
-    - name: Install dependencies for production
+    - name: Assembling all dependencies
       run: |
-        yarn --production
+        rm -rf out
+        pnpm deploy out --prod --filter @growi/slackbot-proxy
+        rm -rf apps/slackbot-proxy/node_modules && mv out/node_modules apps/slackbot-proxy/node_modules
 
-    - name: yarn start:prod:ci
+    - name: pnpm run start:prod:ci
       working-directory: ./apps/slackbot-proxy
       run: |
         cp config/ci/.env.local.for-ci .env.production.local
-        yarn start:prod:ci
+        pnpm run start:prod:ci
       env:
         SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql

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

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

+ 1 - 14
.github/workflows/release-rc-scheduled.yml

@@ -23,7 +23,7 @@ jobs:
     - uses: actions/checkout@v4
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
 
     - name: Docker meta for docker.io
@@ -65,16 +65,3 @@ jobs:
       tag-temporary: latest-rc
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
-
-  publish-image-rc-ghcr:
-    needs: [determine-tags, build-image-rc]
-
-    uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
-    with:
-      tags: ${{ needs.determine-tags.outputs.TAGS_GHCR }}
-      registry: ghcr.io
-      image-name: weseek/growi
-      tag-temporary: latest-rc
-    secrets:
-      DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
-

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

@@ -23,7 +23,7 @@ jobs:
     - uses: actions/checkout@v4
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
 
     - name: Docker meta for docker.io

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

@@ -17,7 +17,7 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
       with:
         workingDir: apps/slackbot-proxy
@@ -41,14 +41,14 @@ jobs:
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
 
     - name: Setup gcloud
-      uses: google-github-actions/setup-gcloud@v1
+      uses: google-github-actions/setup-gcloud@v2
 
     - name: Configure docker for gcloud
       run: |
         gcloud auth configure-docker --quiet
 
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v2
+      uses: docker/setup-buildx-action@v3
 
     - name: Build and push
       uses: docker/build-push-action@v4
@@ -93,24 +93,24 @@ jobs:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '18'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Bump versions for next RC
       run: |
-        turbo run version --filter=@growi/slackbot-proxy -- --prerelease
+        turbo run version:prerelease --filter=@growi/slackbot-proxy
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
       with:
         workingDir: apps/slackbot-proxy

+ 95 - 0
.github/workflows/release-subpackages.yml

@@ -0,0 +1,95 @@
+name: Release Subpackages
+
+on:
+  push:
+    branches:
+      - master
+    paths:
+      - .changeset/**
+      - .github/workflows/release-subpackages.yml
+  workflow_run:
+    workflows: ["Node CI for app development"]
+    types:
+      - completed
+    branches:
+      - master
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  release-subpackages-snapshot:
+
+    if: "!startsWith(github.head_ref, 'changeset-release/')"
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+        cache: 'pnpm'
+
+    - name: Install dependencies
+      run: |
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
+
+    - name: Setup .npmrc
+      run: |
+        cat << EOF > "$HOME/.npmrc"
+          //registry.npmjs.org/:_authToken=$NPM_TOKEN
+        EOF
+      env:
+        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+    - name: Retrieve changesets information
+      id: changesets-status
+      run: |
+        pnpm changeset status --output status.json
+        echo "CHANGESETS_LENGTH=$(jq -r '.changesets | length' status.json)" >> $GITHUB_OUTPUT
+        rm status.json
+
+    - name: Snapshot release to npm
+      if: steps.changesets-status.outputs.CHANGESETS_LENGTH > 0
+      run: |
+        pnpm run release-subpackages:snapshot
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+
+  release-subpackages:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+        cache: 'pnpm'
+
+    - name: Install dependencies
+      run: |
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
+
+    - name: Create Release Pull Request or Publish to npm
+      id: changesets
+      uses: changesets/action@v1
+      with:
+        title: Release Subpackages
+        version: pnpm run version-subpackages
+        publish: pnpm run release-subpackages
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

+ 23 - 25
.github/workflows/release.yml

@@ -22,26 +22,25 @@ jobs:
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Bump versions
       run: |
-        turbo run version --filter=@growi/app -- --patch
-        yarn upgrade --scope=@growi
+        turbo run version:patch --filter=@growi/app
         sh ./apps/app/bin/github-actions/update-readme.sh
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
 
     - name: Update Changelog
@@ -58,13 +57,13 @@ jobs:
         RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
 
     - name: Commit, Tag and Push
-      uses: stefanzweifel/git-auto-commit-action@v4
+      uses: stefanzweifel/git-auto-commit-action@v5
       with:
         branch: ${{ github.event.pull_request.base.ref }}
         commit_message: Release 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:
         body: ${{ github.event.pull_request.body }}
         tag_name: v${{ steps.package-json.outputs.packageVersion }}
@@ -87,7 +86,7 @@ jobs:
     - uses: actions/checkout@v4
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
 
     - name: Docker meta for docker.io
@@ -103,7 +102,7 @@ jobs:
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
 
 
-  build-image:
+  build-app-image:
     needs: create-github-release
 
     uses: weseek/growi/.github/workflows/reusable-app-build-image.yml@master
@@ -115,8 +114,8 @@ jobs:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
 
 
-  publish-image:
-    needs: [determine-tags, build-image]
+  publish-app-image:
+    needs: [determine-tags, build-app-image]
 
     uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
     with:
@@ -129,7 +128,7 @@ jobs:
 
 
   post-publish:
-    needs: [create-github-release, publish-image]
+    needs: [create-github-release, publish-app-image]
     runs-on: ubuntu-latest
 
     steps:
@@ -154,7 +153,7 @@ jobs:
 
 
   create-pr-for-next-rc:
-    needs: [create-github-release, publish-image]
+    needs: [create-github-release, publish-app-image]
     runs-on: ubuntu-latest
 
     steps:
@@ -162,26 +161,25 @@ jobs:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: '20'
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm add turbo --global
+        pnpm install --frozen-lockfile
 
     - name: Bump versions for next RC
       run: |
-        turbo run version --filter=@growi/app -- --prepatch
-        turbo run version --filter=@growi/slackbot-proxy -- --prepatch
-        yarn upgrade --scope=@growi
+        turbo run version:prepatch --filter=@growi/app
+        turbo run version:prepatch --filter=@growi/slackbot-proxy
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
 
     - name: Commit

+ 119 - 135
.github/workflows/reusable-app-prod.yml

@@ -6,11 +6,18 @@ on:
       node-version:
         required: true
         type: string
-      skip-cypress:
+      skip-e2e-test:
         type: boolean
-      cypress-report-artifact-name-prefix:
+    secrets:
+      SLACK_WEBHOOK_URL:
+        required: true
+  workflow_dispatch:
+    inputs:
+      node-version:
+        required: true
         type: string
-      cypress-config-video:
+        default: 20.x
+      skip-e2e-test:
         type: boolean
         default: false
     secrets:
@@ -27,65 +34,51 @@ jobs:
 
     steps:
     - uses: actions/checkout@v4
-      with:
-        # retrieve local font files
-        lfs: true
+
+    - uses: pnpm/action-setup@v4
 
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
     - name: Install turbo
       run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune --scope=@growi/app
-        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-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+        pnpm add turbo --global
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
-    - name: Restore dist
+    - name: Cache/Restore dist
       uses: actions/cache@v4
       with:
         path: |
-          node_modules/.cache/turbo
           **/.turbo
           **/dist
+          **/node_modules/.cache/turbo
           ${{ github.workspace }}/apps/app/.next
-        key: dist-app-7.x-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ github.ref_name }}-${{ github.sha }}
+        key: dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ github.sha }}
         restore-keys: |
-          dist-app-7.x-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ github.ref_name }}-
-          dist-app-7.x-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+          dist-app-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
     - name: Build
       working-directory: ./apps/app
       run: |
-        turbo run build
+        turbo run build --env-mode=loose
       env:
-        ANALYZE_BUNDLE_SIZE: 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
       id: archive-prod-files
       run: |
-        tar -zcf production.tar.gz \
+        tar -zcf production.tar.gz --exclude ./apps/app/.next/cache \
           package.json \
           apps/app/.next \
           apps/app/config \
@@ -94,9 +87,8 @@ jobs:
           apps/app/resource \
           apps/app/tmp \
           apps/app/.env.production* \
-          apps/app/package.json \
-          packages/*/dist \
-          packages/*/package.json
+          apps/app/node_modules \
+          apps/app/package.json
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
     - name: Upload production files as artifact
@@ -110,8 +102,7 @@ jobs:
       with:
         name: Bundle Analyzing Report (node${{ inputs.node-version }})
         path: |
-          apps/app/.next/analyze/client.html
-          apps/app/.next/analyze/server.html
+          apps/app/.next/analyze
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
@@ -143,50 +134,27 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Install turbo
-      run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune --scope=@growi/app
-        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-app-7.x-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-7.x-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
-
-    - name: Install dependencies
-      run: |
-        yarn --production
+        cache: 'pnpm'
 
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       with:
         name: Production Files (node${{ inputs.node-version }})
 
-    - name: Extract procution files artifact
+    - name: Extract procution files
       run: |
         tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
 
-    - name: yarn server:ci
+    - name: pnpm run server:ci
       working-directory: ./apps/app
       run: |
         cp config/ci/.env.local.for-ci .env.production.local
-        yarn server:ci
+        pnpm run server:ci
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
@@ -202,19 +170,24 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
-
-  run-cypress:
+  run-playwright:
     needs: [build-prod]
 
-    if: ${{ !inputs.skip-cypress }}
+    if: |
+      github.event_name == 'workflow_dispatch' ||
+      (!inputs.skip-e2e-test && startsWith(github.head_ref, 'mergify/merge-queue/'))
 
     runs-on: ubuntu-latest
+    container:
+      # Match the Playwright version
+      # https://github.com/microsoft/playwright/issues/20010
+      image: mcr.microsoft.com/playwright:v1.46.0-jammy
 
     strategy:
       fail-fast: false
       matrix:
-        # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['10', '20', '21', '22', '23', '30', '40', '50', '60']
+        browser: [chromium, firefox, webkit]
+        shard: [1/2, 2/2]
 
     services:
       mongodb:
@@ -231,111 +204,122 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
-    - name: Install fonts
-      run: sudo apt install fonts-noto
+    - uses: pnpm/action-setup@v4
 
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
+        cache: 'pnpm'
 
-    - name: Install turbo
+    - name: Install dependencies
       run: |
-        yarn global add turbo
+        pnpm install --frozen-lockfile
 
-    - name: Prune repositories
+    - name: Install Playwright browsers
       run: |
-        turbo prune --scope=@growi/app
-        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-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
-
-    - name: Cache/Restore Cypress files
-      uses: actions/cache@v4
-      with:
-        path: |
-          ~/.cache/Cypress
-        key: deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-
-
-    - name: Install dependencies
-      run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
-        yarn cypress install
+        pnpm playwright install --with-deps ${{ matrix.browser }}
 
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       with:
         name: Production Files (node${{ inputs.node-version }})
 
-    - name: Extract procution files artifact
+    - name: Extract procution files
       run: |
         tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
 
-    - name: Determine spec expression
-      id: determine-spec-exp
-      run: |
-        SPEC=`node bin/github-actions/generate-cypress-spec-arg.mjs --prefix="test/cypress/e2e/" --suffix="-*/*.cy.{ts,tsx}" "${{ matrix.spec-group }}"`
-        echo "value=$SPEC" >> $GITHUB_OUTPUT
-
     - name: Copy dotenv file for ci
       working-directory: ./apps/app
       run: |
         cat config/ci/.env.local.for-ci >> .env.production.local
 
+    - name: Playwright Run (--project=chromium/installer)
+      if: ${{ matrix.browser == 'chromium' }}
+      working-directory: ./apps/app
+      run: |
+        pnpm playwright test --project=chromium/installer
+      env:
+        DEBUG: pw:api
+        HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
+        MONGO_URI: mongodb://mongodb:27017/growi-playwright-installer
+        ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
+
     - name: Copy dotenv file for automatic installation
-      if: ${{ matrix.spec-group != '10' }}
       working-directory: ./apps/app
       run: |
         cat config/ci/.env.local.for-auto-install >> .env.production.local
 
+    - name: Playwright Run
+      working-directory: ./apps/app
+      run: |
+        pnpm playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
+      env:
+        DEBUG: pw:api
+        HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
+        MONGO_URI: mongodb://mongodb:27017/growi-playwright
+        ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
+
     - name: Copy dotenv file for automatic installation with allowing guest mode
-      if: ${{ matrix.spec-group == '21' }}
       working-directory: ./apps/app
       run: |
         cat config/ci/.env.local.for-auto-install-with-allowing-guest >> .env.production.local
 
-    - name: Cypress Run
-      uses: cypress-io/github-action@v6
-      with:
-        browser: chromium
-        working-directory: ./apps/app
-        spec: '${{ steps.determine-spec-exp.outputs.value }}'
-        install: false
-        start: yarn server
-        wait-on: 'http://localhost:3000'
-        config: video=${{ inputs.cypress-config-video }}
+    - name: Playwright Run (--project=${browser}/guest-mode)
+      working-directory: ./apps/app
+      run: |
+        pnpm playwright test --project=${{ matrix.browser }}/guest-mode --shard=${{ matrix.shard }}
       env:
-        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
+        DEBUG: pw:api
+        HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
+        MONGO_URI: mongodb://mongodb:27017/growi-playwright-guest-mode
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
 
-    - name: Upload results
+    - name: Upload test results
       if: always()
       uses: actions/upload-artifact@v4
       with:
-        name: ${{ inputs.cypress-report-artifact-name-prefix }}${{ matrix.spec-group }}
-        path: |
-          apps/app/test/cypress/screenshots
-          apps/app/test/cypress/videos
+        name: blob-report-${{ matrix.shard }}
+        path: blob-report
+        retention-days: 30
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*Node CI for growi - run-cypress (${{ inputs.node-version }})*'
+        job_name: '*Node CI for growi - run-playwright*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  report-playwright:
+    needs: [run-playwright]
+
+    if: always() && needs.run-playwright.result != 'skipped'
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: pnpm/action-setup@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: ${{ inputs.node-version }}
+        cache: 'pnpm'
+
+    - name: Install dependencies
+      run: |
+        pnpm install --frozen-lockfile
+
+    - name: Merge into HTML Report
+      run: pnpm playwright merge-reports --reporter html ./all-blob-reports
+
+    - name: Upload HTML report
+      uses: actions/upload-artifact@v4
+      with:
+        name: html-report
+        path: playwright-report
+        retention-days: 30

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

@@ -54,36 +54,16 @@ jobs:
         ref: ${{ inputs.checkout-ref }}
         fetch-depth: 0
 
+    - uses: pnpm/action-setup@v4
+
     - uses: actions/setup-node@v4
       with:
         node-version: ${{ inputs.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Install turbo
-      run: |
-        yarn global add turbo
-
-    - name: Prune repositories
-      run: |
-        turbo prune --scope=@growi/app
-        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-7.x-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          node_modules-7.x-${{ runner.OS }}-node${{ inputs.node-version }}-
+        cache: 'pnpm'
 
     - name: Install dependencies
       run: |
-        yarn global add node-gyp
-        yarn --frozen-lockfile
+        pnpm install --frozen-lockfile
 
     - name: Download screenshots taken by cypress
       uses: actions/download-artifact@v4
@@ -95,7 +75,7 @@ jobs:
     - name: Run reg-suit
       working-directory: ./apps/app
       run: |
-        yarn reg:run
+        pnpm run reg:run
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master

+ 3 - 0
.gitignore

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

+ 0 - 24
.mergify.yml

@@ -1,24 +0,0 @@
-pull_request_rules:
-  - name: Automatic merge for Dependabot pull requests
-    conditions:
-      - author = dependabot[bot]
-      - '#approved-reviews-by >= 1'
-      - check-success = "ci-slackbot-proxy-lint (20.x)"
-      - check-success = "ci-slackbot-proxy-launch-dev (20.x)"
-      - check-success = "ci-slackbot-proxy-launch-prod (20.x)"
-      - check-success = "ci-app-lint (20.x)"
-      - check-success = "ci-app-test (20.x)"
-      - check-success = "ci-app-launch-dev (20.x)"
-      - check-success = "test-prod-node18 / launch-prod"
-      - check-success = "test-prod-node20 / launch-prod"
-    actions:
-      merge:
-        method: merge
-
-  - name: Automatic merge for Preparing next version
-    conditions:
-      - author = github-actions[bot]
-      - label = "type/prepare-next-version"
-    actions:
-      merge:
-        method: merge

+ 0 - 0
apps/app/src/components/ItemsTree/ItemsTree.module.scss → .npmrc


+ 14 - 5
.stylelintrc.json

@@ -1,16 +1,25 @@
 {
   "extends": [
+    "stylelint-config-recommended-scss",
     "stylelint-config-recess-order"
   ],
-  "customSyntax": "postcss-scss",
   "rules": {
-    "indentation": 2,
-    "string-quotes": "single",
+    "no-duplicate-selectors": null,
+    "scss/comment-no-empty": null,
+    "scss/at-extend-no-missing-placeholder": null,
     "rule-empty-line-before": [ "always-multi-line", {
       "except": ["after-single-line-comment", "first-nested"],
       "ignore": ["after-comment", "inside-block"]
     } ],
-    "selector-combinator-space-before": "always",
-    "selector-combinator-space-after": "always"
+    "color-function-notation": "legacy",
+    "selector-pseudo-class-no-unknown": [
+      true,
+      {
+        "ignorePseudoClasses": [
+          "global",
+          "local"
+        ]
+      }
+    ]
   }
 }

+ 2 - 1
.vscode/launch.json

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

+ 8 - 1
.vscode/settings.json

@@ -19,5 +19,12 @@
 
   "githubPullRequests.ignoredPullRequestBranches": [
     "master"
-  ]
+  ],
+
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "typescript.enablePromptUseWorkspaceTsdk": true,
+  "typescript.preferences.autoImportFileExcludePatterns": ["node_modules/*"],
+  "typescript.validate.enable": true,
+  "typescript.surveys.enabled": false
+
 }

+ 451 - 1
CHANGELOG.md

@@ -1,9 +1,459 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.4...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.1.1...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.1.1](https://github.com/weseek/growi/compare/v7.1.0...v7.1.1) - 2024-11-12
+
+### 💎 Features
+
+* feat(ai): Swtch summary mode (#9377) @yuki-takei
+* feat: Return sources when generating responses (Knowledge assistant) (#9362) @miya
+* feat: Set the maximum number of minutes until the request in an environment variable (#9347) @miya
+
+### 🚀 Improvement
+
+* imprv: GitHub Alert with directive syntax (#9392) @yuki-takei
+* imprv: Sidebar button displays tooltip (#9371) @reiji-h
+* imprv: Open the link of PageTreeItem in a new tab when the user middle click (#9365) @yuki-takei
+* support: Avoid using req.t() (#9149) @shironegi39
+* imprv: Tidy up /Sandbox (#9355) @yuki-takei
+* imprv: Reduce sanitizing (#9350) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Output TextDirective and LeafDirective HTML (#9388) @yuki-takei
+* fix: NextLink isCreatablePage always returns false (#9356) @yuki-takei
+* fix: Duplicate page names alert should not occur on a single page (#9348) @reiji-h
+* fix: i18n for security settings (#9379) @yuki-takei
+* fix: Output TextDirective and LeafDirective HTML (#9388) @yuki-takei
+* fix(i18n): i18n for server side (#9372) @yuki-takei
+* fix: Duplicate page names alert should not occur on a single page (#9348) @reiji-h
+* fix: NextLink isCreatablePage always returns false (#9356) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Welcome back new Christmas theme (#9374) @satof3
+* support: Type checking (#9393) @yuki-takei
+* support: Welcome back new Christmas theme (#9374) @satof3
+* support: Omit remark-toc (#9383) @yuki-takei
+* support: Stop managing font files with Git LFS (#9351) @yuki-takei
+* support: Avoid using req.t() (#9149) @shironegi39
+* support: Improve playwright report (#9363) @yuki-takei
+* support: Avoid using req.t() (#9149) @shironegi39
+* support: Stop managing font files with Git LFS (#9351) @yuki-takei
+
+## [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
+
+### 🐛 Bug Fixes
+
+* fix: Shared page is not displayed when skipping SSR (#9089) @miya
+* fix: The grant of pages can be changed via api even if restricted (#9087) @WNomunomu
+* fix: Updated content is not reflected on the View screen even after refreshing the page (#9086) @miya
+* fix: Removing comment doesn't work (#9083) @yuki-takei
+
+## [v7.0.18](https://github.com/weseek/growi/compare/v7.0.17...v7.0.18) - 2024-09-09
+
+### 🚀 Improvement
+
+* imprv: Prevent looping to update a hook for TrashPageAlert (#9066) @yuki-takei
+* imprv: Display page tree in page select modal with scrollbar (#9023) @kazutoweseek
+
+### 🐛 Bug Fixes
+
+* fix: issue that material symbols icons are not displayed in ReplyComments component (#9076) @WNomunomu
+* fix: Unable to navigate to the data transfer page (#9071) @miya
+* fix: Page content does not update when switching revisions (#9072) @miya
+* fix: Supress rendering too many invisible DropdownMenu components (#9073) @yuki-takei
+* fix: Return error when grant is string for PUT /_api/v3/page (#9069) @arafubeatbox
+* fix: Scrolling may not occurs when clicking on the edit button next to the header (#9043) @reiji-h
+* fix: API v3 Page update (#9053) @maeshinshin
+* fix: Input text becomes empty when opening the ReadOnlyEditor (#9059) @miya
+* fix: Show pages with grants that are set to be visible in security settings on RecentChanges and PageTree as well (#9044) @miya
+
+### 🧰 Maintenance
+
+* support: Omit Cypress (#9065) @miya
+* ci(deps): bump unzip-stream from 0.3.1 to 0.3.2 (#9049) @dependabot
+
+## [v7.0.17](https://github.com/weseek/growi/compare/v7.0.16...v7.0.17) - 2024-08-26
+
+### 🚀 Improvement
+
+* imprv: Serializers for User model and Attachment model (#9019) @yuki-takei
+* imprv: translation modification (#9035) @maeshinshin
+* imprv: Add UI and logic for disabled user registration (#9034) @maeshinshin
+* imprv: lang attribute in Html element (#8960) @maeshinshin
+
+### 🐛 Bug Fixes
+
+* fix: Serializer for accessing to an empty page (#9042) @yuki-takei
+* fix: Import data (#8994) @yuki-takei
+* fix: Comment operation by API (#9026) @yuki-takei
+* fix: Tests fail due to docker image and Playwright  version mismatch on CI (#9022) @miya
+* fix: Use the scrollbar to prevent the toolbar from being hidden (#8976) @maeshinshin
+* fix: Revision pageId schema type (#9008) @yuki-takei
+* fix: Revision pageId schema type (add a changeset) (#9010) @yuki-takei
+* fix: Hide WideViewMenuItem in search result (#9009) @yuki-takei
+* fix: Wrongly autofocus to PageHeader even after updating (#9011) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Dark mode support for CountBadge (#9036) @satof3
+* support: Update import lines (#9018) @yuki-takei
+* support: Typescriptize REPL launcher (#9013) @yuki-takei
+
+## [v7.0.16](https://github.com/weseek/growi/compare/v7.0.15...v7.0.16) - 2024-07-31
+
+### 💎 Features
+
+* feat: Automatically repair corrupted data, at least for the latest revision (#9002) @yuki-takei
+
+### 🚀 Improvement
+
+* imprv: User group link in admin page (#8855) @kazutoweseek
+* imprv: Sidebar header text size (#8986) @satof3
+* imprv: Replace possition usericon (#8991) @satof3
+
+### 🐛 Bug Fixes
+
+* fix: Undo in the comment editor (#9005) @yuki-takei
+* fix: Some OIDC authentication settings not being applied (#9000) @WNomunomu
+* fix: font-family for monospace (#9004) @yuki-takei
+* fix: Pointer cursor for the create button in the installer (#9003) @yuki-takei
+* fix: Migration script (20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js) (#8998) @miya
+* fix: Non-admin user gets 500 error when opening history modal (#9001) @miya
+* fix: Enable page creation under GRANT_RESTRICTED pages (#8996) @arafubeatbox
+
+## [v7.0.15](https://github.com/weseek/growi/compare/v7.0.14...v7.0.15) - 2024-07-23
+
+### 🐛 Bug Fixes
+
+* fix: The $size query when aggregation to rebuild the index (#8987) @yuki-takei
+* fix: Regaining lost backward compatibility for MongoDB 4.4 (#8985) @yuki-takei
+* fix: Activate express-session middlewares for all sockets in SocketIoService (#8981) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Chage text size in sidebar (#8965) @satof3
+
+## [v7.0.14](https://github.com/weseek/growi/compare/v7.0.13...v7.0.14) - 2024-07-19
+
+### 🐛 Bug Fixes
+
+### 💎 Features
+
+* feat: Alerts when trying to sync with latest revision when yjs data is corrupt (#8971) @miya
+
+### 🚀 Improvement
+
+* imprv: Restrict use of the editing UI from View if there is at least one user currently editing (#8966) @miya
+
+### 🐛 Bug Fixes
+
+* fix: Handle error when folding drawio blocks (#8977) @yuki-takei
+* fix: Sync the editor text with the latest revision menu (1) (#8975) @yuki-takei
+* fix: Sync the editor text with the latest revision menu (2) (#8978) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Normalize Revision.pageId (for #8954) (#8973) @miya
+
+## [v7.0.13](https://github.com/weseek/growi/compare/v7.0.12...v7.0.13) - 2024-07-16
+
+### 💎 Features
+
+* feat: Sync latest revision body to Yjs draft (#8939) @miya
+
+### 🚀 Improvement
+
+* imprv: Better synchronizing between YDoc and the latest revision (#8959) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Revision model (#8967) @yuki-takei
+* fix: Healthcheck with checkServices=mongo (#8961) @yuki-takei
+* fix: Enable  # next to headline in view (#8826) @reiji-h
+
+### 🧰 Maintenance
+
+* ci(deps): bump nodemailer from 6.6.2 to 6.9.14 (#8928) @dependabot
+* support: Update favicon (#8957) @satof3
+
+## [v7.0.12](https://github.com/weseek/growi/compare/v7.0.11...v7.0.12) - 2024-07-10
+
+### 🚀 Improvement
+
+* imprv: lang attribute in Html element to correctly reflect locale (#8940) @maeshinshin
+* imprv: Archive importing and exporting (#8943) @yuki-takei
+* imprv: Restrict indexing for full text search when the body length exceeds the threshold (#8937) @yuki-takei
+* imprv: Dark theme support for emoji mart (#8936) @reiji-h
+* imprv: Add env var for set Elasticsearch reindex bulk size (#8933) @yuki-takei
+* imprv: Size for skeleton for tags (#8923) @yuki-takei
+* imprv: Button opacity of TableWithEditButton and DrawioViewerWithEditButton (#8924) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Initialize sanitize option (#8946) @yuki-takei
+* fix: PageTitleHeader rename input status (#8944) @yuki-takei
+* fix: Presentation section tag (#8941) @yuki-takei
+* fix: Page history colorscheme is broken (#8938) @reiji-h
+* imprv: Rename label for bookmark item (#8925) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Refactor Yjs service (#8949) @yuki-takei
+* support: Upgrade y-mongodb-provider (#8953) @yuki-takei
+* support: Typescriptize Revision model (#8954) @yuki-takei
+* support: Typescriptize SocketIoService (#8948) @yuki-takei
+* support: Update GROWI logo type in NoLogin (#8942) @satof3
+* support: Update logo design (#8934) @satof3
+* ci(deps): bump @azure/identity from 4.0.1 to 4.3.0 (#8927) @dependabot
+* support: Upgrade vitest (#8920) @yuki-takei
+* support: Upgrade playwright (#8921) @yuki-takei
+
+## [v7.0.11](https://github.com/weseek/growi/compare/v7.0.10...v7.0.11) - 2024-06-25
+
+### 💎 Features
+
+### 🚀 Improvement
+
+* imprv: New marker color (#8891) @satof3
+* imprv: SSR performance (#8916) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Vim keymap works correctly (#8901) @reiji-h
+* fix: Readonly editor prevents ctrl+v and paste. (#8902) @reiji-h
+* fix: Missing HTTP Response in SAML Login With ABLC Callback (#8879) @maeshinshin
+* fix: Set `z-0` to correct navbar and header overlap when the anchor is specified (#8905) @yuki-takei
+* fix: Minimum number of characters in password cannot be changed (#8896) @miya
+
+### 🧰 Maintenance
+
+* support: Replace tests with playwright (20-basic-features/20-basic-features--click-page-icons) (#8903) @miya
+* support: Relocate components dir (#8917) @yuki-takei
+* ci(deps): bump ws from 8.11.0 to 8.17.1 (#8906) @dependabot
+* support: Update module resolution settings (#8898) @yuki-takei
+* support: Decrease max SSR body length (#8895) @yuki-takei
+* support: Use typescript-transform-paths instead of tsconfig-paths (#8892) @yuki-takei
+
+## [v7.0.10](https://github.com/weseek/growi/compare/v7.0.9...v7.0.10) - 2024-06-13
+
+### 💎 Features
+
+* imprv: Autofocus on PageTitleHeader when edigin untitled page (#8813) @WNomunomu
+* imprv: Autofocus on PageTitleHeader when creating untitled page (#8813) @WNomunomu
+
+### 🚀 Improvement
+
+* imprv: DrawioViewerScript should respect the base path in DRAWIO_URI 2 (#8889) @yuki-takei
+* imprv: Styling icon on the side of header (#8833) @reiji-h
+* imprv: DrawioViewerScript should respect the base path in DRAWIO_URI (#8878) @yuki-takei
+* imprv: Behavior when clicking on a label in the dropdown (#8857) @maeshinshin
+* imprv(plugin): Support github tag in githuburl.ts (#8868) @reiji-h
+* imprv: Display selected tree item in page select modal as active (#8802) @WNomunomu
+
+### 🐛 Bug Fixes
+
+* fix: Match width of video tag to img tag (#8836) @TatsuyaIse
+* fix: Behaviour of input during Japanese input (#8880) @miya
+* fix: Supress warning of checkbox 2 (#8871) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Watch with nodemon (#8877) @yuki-takei
+* support: Add playwright test for installer (#8874) @yuki-takei
+* support: Upgrade turbo to v2 (#8875) @yuki-takei
+
+## [v7.0.9](https://github.com/weseek/growi/compare/v7.0.8...v7.0.9) - 2024-05-30
+
+### 🐛 Bug Fixes
+
+* fix: Supress warning of checkbox (#8865) @yuki-takei
+* fix: Editor package import/export (#8864) @yuki-takei
+
+## [v7.0.8](https://github.com/weseek/growi/compare/v7.0.7...v7.0.8) - 2024-05-30
+
+### 💎 Features
+
+* feat: Select unrelated group inheritance on child page create (#8812) @arafubeatbox
+
+### 🚀 Improvement
+
+* imprv: Design coding of search result page (#8828) @miya
+
+### 🐛 Bug Fixes
+
+* fix: Page body sometimes appears doubled up when the editor is opened (#8858) @miya
+* fix: Brackets appearance when Nord editor theme (#8852) @satof3
+* fix: Slack notification not sent on page update (#8841) @miya
+* fix: Table icon is not displayed when hovering over the table (#8830) @WNomunomu
+
+### 🧰 Maintenance
+
+* support: Reorganize editor module exports (#8846) @yuki-takei
+
+## [v7.0.7](https://github.com/weseek/growi/compare/v7.0.6...v7.0.7) - 2024-05-27
+
+### 🚀 Improvement
+
+* imprv: Behavior of dropdown toggle in groundglassbar (#8832) @maeshinshin
+* imprv: toastr location (#8831) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Do not insert initial value when input is empty in editor (#8773) @miya
+
+### 🧰 Maintenance
+
+* support: Apply changesets (#8840) @yuki-takei
+* support: Upgrade yjs packages (#8839) @yuki-takei
+* support: Upgrade stylelint (#8835) @yuki-takei
+
+## [v7.0.6](https://github.com/weseek/growi/compare/v7.0.5...v7.0.6) - 2024-05-20
+
+### 🐛 Bug Fixes
+
+* fix: S3 configurations are considered invalid wrongly (#8823) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Publish @growi/core-styles package (#8819) @yuki-takei
+
+## [v7.0.5](https://github.com/weseek/growi/compare/v7.0.4...v7.0.5) - 2024-05-20
+
+### 🚀 Improvement
+
+* imprv: Behavior of dropdown toggle for WIP in 'Page Tree' sidebar (#8796) @maeshinshin
+* imprv: Hide the page creation button for users without editing permissions (#8808) @miya
+* imprv: Add config to toggle ACL between public_read and private on PutObject when using S3 with FileUploader (#8778) @ToshihitoKon
+
+### 🐛 Bug Fixes
+
+* fix: BookmarkItem occures an error when the related page has been deleted 2 (#8818) @yuki-takei
+* fix: BookmarkItem occures an error when the related page has been deleted (#8817) @yuki-takei
+* fix: Display share page date (#8809) @TatsuyaIse
+* fix: Admin layout (#8806) @satof3
+
+### 🧰 Maintenance
+
+* support: Modify env var for S3 Object-ACL (#8805) @yuki-takei
+
 ## [v7.0.4](https://github.com/weseek/growi/compare/v7.0.3...v7.0.4) - 2024-05-13
 
 ### 💎 Features

+ 7 - 7
README.md

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

+ 7 - 7
README_JP.md

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

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

@@ -27,7 +27,6 @@ module.exports = {
       },
     ]],
     '@typescript-eslint/no-var-requires': 'off',
-    '@typescript-eslint/consistent-type-imports': 'warn',
 
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     '@typescript-eslint/no-use-before-define': ['warn'],

+ 0 - 2
apps/app/.gitignore

@@ -3,8 +3,6 @@
 /out/
 
 # test
-test/cypress/screenshots
-test/cypress/videos
 .reg
 
 # dist

+ 0 - 2
apps/app/.prettierignore

@@ -1,2 +0,0 @@
-src/client/styles/bootstrap4/
-src/client/styles/scss/_override-bootstrap-variables.scss

+ 3 - 17
apps/app/.stylelintrc.json

@@ -1,20 +1,6 @@
 {
-  "extends": [
-    "stylelint-config-recess-order"
-  ],
-  "customSyntax": "postcss-scss",
+  "extends": "../../.stylelintrc.json",
   "ignoreFiles": [
-    "src/styles/prebuilt/*.css",
-    "src/linter-checker/test.scss"
-  ],
-  "rules": {
-    "indentation": 2,
-    "string-quotes": "single",
-    "rule-empty-line-before": [ "always-multi-line", {
-      "except": ["after-single-line-comment", "first-nested"],
-      "ignore": ["after-comment", "inside-block"]
-    } ],
-    "selector-combinator-space-before": "always",
-    "selector-combinator-space-after": "always"
-  }
+    "src/styles/prebuilt/*.css"
+  ]
 }

+ 0 - 710
apps/app/_obsolete/src/styles/theme/apply-colors.scss

@@ -1,710 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as *;
-
-@use '../variables' as var;
-@use '../mixins';
-@use '../atoms/mixins/code';
-@use './mixins/hsl-button';
-@use './hsl-functions' as hsl;
-
-@import 'apply-colors-dark';
-@import 'apply-colors-light';
-
-//
-//== Apply to Bootstrap
-//
-
-// determine optional variables
-$bgcolor-search-top-dropdown: var(--bgcolor-search-top-dropdown,var(--secondary));
-$bgcolor-sidebar-nav-item-active: var(--bgcolor-sidebar-nav-item-active,#{hsl.darken(var(--primary),10%)});
-$text-shadow-sidebar-nav-item-active: var(--text-shadow-sidebar-nav-item-active,1px 1px 2px var(--primary));
-$bgcolor-inline-code: var(--bgcolor-inline-code, #{$gray-100});
-$color-inline-code: var(--color-inline-code, #{darken($red, 15%)});
-$bordercolor-inline-code: var(--bordercolor-inline-code, #{$gray-400});
-$bordercolor-nav-tabs: var(--bordercolor-nav-tabs, #{$gray-300});
-$bordercolor-nav-tabs-hover: var(--bordercolor-nav-tabs-hover,#{$gray-200} #{$gray-200} #{$bordercolor-nav-tabs});
-$border-nav-tabs-link-active: var(--border-nav-tabs-link-active, #{$gray-600});
-$bordercolor-nav-tabs-active: var(--bordercolor-nav-tabs-active,$bordercolor-nav-tabs $bordercolor-nav-tabs var(--bgcolor-global));
-$color-btn-reload-in-sidebar: var(--color-btn-reload-in-sidebar,#{$gray-500});
-$bgcolor-keyword-highlighted: var(--bgcolor-keyword-highlighted,#{var.$grw-marker-yellow});
-$color-page-list-group-item-meta: var(--color-page-list-group-item-meta,#{$gray-500});
-$color-search-page-list-title: var(--color-search-page-list-title,var(--color-global));
-
-// override bootstrap variables
-$body-bg: var(--bgcolor-global);
-$body-color: var(--color-global);
-$link-color: var(--color-link);
-$link-hover-color: var(--color-link-hover);
-$input-focus-color: var(--color-global);
-$nav-tabs-border-color: $bordercolor-nav-tabs;
-$nav-tabs-link-hover-border-color: $bordercolor-nav-tabs-hover;
-$nav-tabs-link-active-color: var(--color-nav-tabs-link-active);
-$nav-tabs-link-active-bg: var(--bgcolor-global);
-$nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
-$theme-colors: map-merge($theme-colors, ( primary: $primary ));
-
-// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-// @import 'reboot-bootstrap-buttons';
-// @import 'reboot-bootstrap-colors';
-// @import 'reboot-bootstrap-theme-colors';
-// @import 'hsl-reboot-bootstrap-theme-colors';
-// @import 'reboot-bootstrap-nav';
-// @import 'reboot-toastr-colors';
-
-// determine variables with bootstrap function (These variables can be used after importing bootstrap above)
-$color-modal-header: var(--color-modal-header,#{hsl.contrast(var(--primary))});
-
-code:not([class^='language-']) {
-  @include code.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
-}
-
-.code-highlighted {
-  border-color: $bordercolor-inline-code;
-}
-
-//
-//== Apply to Bootstrap Elements
-//
-
-// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-// theme-color-level() dropped in bootstrap v5
-// Alert link
-// @each $color, $value in $theme-colors {
-//   .alert.alert-#{$color} {
-//     a,
-//     a:hover {
-//       color: theme-color-level($color, $alert-color-level - 2);
-//     }
-//   }
-// }
-
-// Dropdown
-.grw-apperance-mode-dropdown {
-  .grw-sidebar-mode-icon svg {
-    fill: var(--secondary);
-  }
-  .grw-color-mode-icon svg {
-    fill: var(--color-global);
-  }
-  .grw-color-mode-icon-muted svg {
-    fill: var(--secondary);
-  }
-}
-
-// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-// form-control-focus() dropped in bootstrap v5
-// Form
-// .form-control {
-//   @include form-control-focus();
-// }
-
-// Tabs
-.nav.nav-tabs .nav-link.active {
-  color: var(--color-link);
-  background: transparent;
-
-  &:hover,
-  &:focus {
-    color: var(--color-link-hover);
-  }
-}
-
-// Pagination
-ul.pagination {
-  li.page-item.disabled {
-    button.page-link {
-      color: $gray-400;
-    }
-  }
-  li.page-item.active {
-    button.page-link {
-      color: hsl.contrast(var(--primary));
-      background-color: var(--primary);
-      &:hover,
-      &:focus {
-        color: hsl.contrast(var(--primary));
-        background-color: var(--primary);
-      }
-    }
-  }
-  li.page-item {
-    button.page-link {
-      color: var(--primary);
-      border-color: var(--secondary) !important;
-      &:hover,
-      &:active,
-      &:focus {
-        color: var(--primary);
-      }
-    }
-  }
-}
-
-//
-//== Apply to Handsontable
-//
-.handsontable {
-  color: initial;
-}
-
-//
-//== Apply to GROWI Elements
-//
-
-.grw-logo {
-  // set transition for fill
-  svg, svg * {
-    transition: fill 0.8s ease-out;
-  }
-
-  svg {
-    fill: var(--fillcolor-logo-mark);
-  }
-
-  &:hover {
-    svg {
-      .group1 {
-        fill: var.$growi-green;
-      }
-
-      .group2 {
-        fill: var.$growi-blue;
-      }
-    }
-  }
-}
-
-.grw-navbar {
-  background: var(--bgcolor-navbar);
-  .nav-item .nav-link {
-    color: var(--color-link-nabvar);
-  }
-
-  border-image: var(--border-image-navbar) !important;
-  border-image-slice: 1 !important;
-
-  .grw-app-title {
-    color: var(--fillcolor-logo-mark);
-  }
-}
-
-.grw-global-search {
-  .btn-secondary.dropdown-toggle {
-    @include hsl-button.button-variant(var(--bgcolor-search-top-dropdown), var(--bgcolor-search-top-dropdown));
-  }
-
-  // for https://youtrack.weseek.co.jp/issue/GW-2603
-  .search-typeahead {
-    background-color: hsl.alpha(var(--bgcolor-global),10%);
-  }
-  input.form-control {
-    border: none;
-  }
-}
-
-.grw-sidebar {
-  $color-resize-button: var(--color-resize-button,var(--color-global));
-  $bgcolor-resize-button: var(--bgcolor-resize-button,white);
-  $color-resize-button-hover: var(--color-resize-button-hover,var(--color-reversal));
-  $bgcolor-resize-button-hover: var(--bgcolor-resize-button-hover,#{hsl.lighten(var(--bgcolor-resize-button), 5%)});
-  // .grw-navigation-resize-button {
-  //   .hexagon-container svg {
-  //     .background {
-  //       fill: var(--bgcolor-resize-button);
-  //     }
-  //     .icon {
-  //       fill: var(--color-resize-button);
-  //     }
-  //   }
-  //   &:hover .hexagon-container svg {
-  //     .background {
-  //       fill: var(--bgcolor-resize-button-hover);
-  //     }
-  //     .icon {
-  //       fill: var(--color-resize-button-hover);
-  //     }
-  //   }
-  // }
-  div.grw-contextual-navigation {
-    > div {
-      color: var(--color-sidebar-context);
-      background-color: var(--bgcolor-sidebar-context);
-    }
-  }
-
-  .grw-sidebar-nav {
-    .btn {
-      @include hsl-button.button-variant(
-        var(--bgcolor-sidebar),
-        var(--bgcolor-sidebar),
-      );
-    }
-  }
-  .grw-sidebar-nav-primary-container {
-    .btn.active {
-      i {
-        text-shadow: $text-shadow-sidebar-nav-item-active;
-      }
-      // fukidashi
-      &:after {
-        border-right-color: var(--bgcolor-sidebar-context) !important;
-      }
-    }
-  }
-
-  .grw-sidebar-content-header {
-    .grw-btn-reload {
-      color: $color-btn-reload-in-sidebar;
-    }
-
-    .grw-recent-changes-resize-button {
-      .form-check-label::before {
-        background-color: var(--primary);
-      }
-
-      .form-check-label::after {
-        background-color: var(--bgcolor-global);
-      }
-
-      .form-check-input:not(:checked) + .form-check-label::before {
-        color: var(--bgcolor-global);
-      }
-
-      .form-check-input:checked + .form-check-label::before {
-        color: var(--bgcolor-global);
-        background-color: var(--primary);
-        border-color: var(--primary);
-      }
-      .form-check-input:checked + .form-check-label::after {
-        color: var(--bgcolor-global);
-      }
-    }
-  }
-
-  .grw-pagetree, .grw-foldertree {
-    .list-group-item {
-      .grw-pagetree-title-anchor, .grw-foldertree-title-anchor {
-        color: inherit;
-      }
-    }
-  }
-
-  .grw-pagetree-footer {
-    .h5.grw-private-legacy-pages-anchor {
-      color: inherit;
-    }
-  }
-
-  .grw-recent-changes {
-    .list-group {
-      .list-group-item {
-        background-color: transparent !important;
-
-        .icon-lock {
-          color: var(--color-link);
-        }
-
-        .grw-recent-changes-item-lower {
-          color: $gray-500;
-
-          svg {
-            fill: $gray-500;
-          }
-        }
-      }
-    }
-  }
-
-}
-
-/*
- * Icon
- */
-.editor-container .navbar-editor svg {
-  fill: var(--color-editor-icons);
-}
-
-// page preview button in link form
-.btn-page-preview svg {
-  fill: white;
-}
-
-/*
- * Modal
- */
-.modal {
-  .modal-header {
-    border-bottom-color: var(--border-color-theme);
-    .modal-title {
-      color: $color-modal-header;
-    }
-    .btn-close {
-      color: $color-modal-header;
-      opacity: 0.5;
-
-      &:hover {
-        opacity: 0.9;
-      }
-    }
-  }
-
-  .modal-content {
-    background-color: var(--bgcolor-global);
-  }
-
-  .modal-footer {
-    border-top-color: var(--border-color-theme);
-  }
-}
-
-.grw-page-accessories-modal,.grw-descendants-page-list-modal {
-  .modal-header {
-    .btn-close {
-      color: #{hsl.contrast(var(--bgcolor-global))};
-    }
-  }
-}
-
-.grw-custom-nav-tab {
-  .nav-item {
-    &:hover,
-    &:focus {
-      background-color: hsl.alpha(var(--color-link),10%);
-    }
-    .nav-link {
-      -webkit-appearance: none;
-      color: var(--color-link);
-      svg {
-        fill: var(--color-link);
-      }
-
-      // Disabled state lightens text
-      &.disabled {
-        color: $nav-link-disabled-color;
-        svg {
-          fill: $nav-link-disabled-color;
-        }
-      }
-    }
-  }
-
-  .grw-nav-slide-hr {
-    border-color: var(--color-link) !important;
-  }
-}
-
-/*
- * cards
- */
-.card.custom-card {
-  color: var(--color-global);
-  background-color: var(--bgcolor-card);
-  border-color: var(--light);
-  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-/*
- * Form Slider
- */
-.admin-page {
-  span.slider {
-    background-color: $gray-300;
-
-    &:before {
-      background-color: white;
-    }
-  }
-
-  input:checked + .slider {
-    background-color: #007bff;
-  }
-
-  input:focus + .slider {
-    box-shadow: 0 0 1px #007bff;
-  }
-}
-
-/*
- * GROWI wiki
- */
-.wiki {
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6,
-  h7 {
-    &.blink {
-      @include mixins.blink-bgcolor(var(--bgcolor-blinked-section));
-    }
-  }
-
-  .highlighted-keyword {
-    background: linear-gradient(transparent 60%, $bgcolor-keyword-highlighted 60%);
-  }
-
-  a {
-    color: var(--color-link-wiki);
-
-    &:hover {
-      color: var(--color-link-wiki-hover);
-    }
-  }
-
-  // table with handsontable modal button
-  .editable-with-handsontable {
-    button {
-      color: var(--color-link-wiki);
-    }
-
-    button:hover {
-      color: var(--color-link-wiki-hover);
-    }
-  }
-}
-
-/*
- * GROWI page-list
- */
-.page-list {
-  // List group
-  .list-group {
-    .list-group-item {
-      background-color: var(--bgcolor-global) !important;
-      a {
-        svg {
-          fill: var(--color-global);
-        }
-
-        &:hover {
-          svg {
-            fill: var(--color-global);
-          }
-        }
-      }
-
-      .page-list-meta {
-        color: $color-page-list-group-item-meta;
-        svg {
-          fill: $color-page-list-group-item-meta;
-        }
-      }
-
-      &.list-group-item-action {
-        background-color: var(--bgcolor-list);
-        &.active {
-          border-left-color: var(--primary);
-        }
-      }
-    }
-  }
-}
-
-/*
- * GROWI Editor
- */
-.layout-root.editing {
-  background-color: hsl.darken(var(--bgcolor-global),2%);
-
-  &.builtin-editor {
-    .page-editor-editor-container {
-      border-right-color: var(--border-color-theme);
-    }
-  }
-
-  .navbar-editor {
-    background-color: var(--bgcolor-global); // same color with active tab
-    border-bottom-color: var(--border-color-theme);
-  }
-
-  .page-editor-preview-container {
-    background-color: var(--bgcolor-global);
-  }
-}
-
-
-/*
- * Preview for editing /Sidebar
- */
-.page-editor-preview-body.preview-sidebar {
-  color: var(--color-sidebar-context);
-  background-color: var(--bgcolor-sidebar-context);
-}
-
-/*
- * GROWI Grid Edit Modal
- */
-.grw-grid-edit-preview {
-  .desktop-preview,
-  .tablet-preview,
-  .mobile-preview {
-    background: var(--bgcolor-global);
-  }
-  .grid-edit-border-for-each-cols {
-    border: 2px solid var(--bgcolor-global);
-  }
-}
-
-.grid-preview-col-0 {
-  background: var.$growi-blue;
-}
-
-.grid-preview-col-1 {
-  background: var(--info);
-}
-
-.grid-preview-col-2 {
-  background: var(--success);
-}
-
-.grid-preview-col-3 {
-  background: var.$growi-green;
-}
-
-/*
- * GROWI comment form
- */
-.page-comments-row {
-  background: var(--bgcolor-subnav);
-  .page-comment .page-comment-main,
-  .page-comment-form .comment-form-main {
-    background-color: var(--bgcolor-global);
-
-    .nav.nav-tabs {
-      > li > a.active {
-        background: transparent;
-        border-bottom: solid 1px hsl.darken(var(--bgcolor-global),4%);
-        border-bottom-color: hsl.darken(var(--bgcolor-global),4%);
-      }
-    }
-  }
-}
-
-/*
- * GROWI search result
- */
-.search-result-base {
-  .grw-search-page-nav {
-    background-color: var(--bgcolor-subnav);
-  }
-  .search-control {
-    background-color: var(--bgcolor-global);
-  }
-  .page-list {
-    .highlighted-keyword {
-      background: linear-gradient(transparent 60%, $bgcolor-keyword-highlighted 60%);
-    }
-  }
-}
-
-/*
- * react bootstrap typeahead
- */
-mark.rbt-highlight-text {
-  // Temporarily the highlight color is black
-  color: black;
-}
-
-/*
- * GROWI page content footer
- */
-.page-content-footer {
-  background-color: hsl.darken(var(--bgcolor-global),2%);
-  border-top-color: var(--border-color-theme);
-}
-
-/*
- * GROWI admin page #layoutOptions #themeOptions
- */
-.admin-page {
-  #layoutOptions {
-    .customize-layout-card {
-      &.border-active {
-        border-color: var(--color-theme-color-box);
-      }
-    }
-  }
-
-  #themeOptions {
-    .theme-option-container.active {
-      .theme-option-name {
-        color: var(--color-global);
-      }
-      a {
-        background-color: var(--color-theme-color-box);
-        border-color: var(--color-theme-color-box);
-      }
-    }
-  }
-}
-
-/*
- * HackMd
- */
-.bg-box {
-  background-color: var(--bgcolor-global);
-}
-
-/*
-  Slack Integration
-*/
-.selecting-bot-type {
-  .bot-type-disc {
-    width: 20px;
-  }
-}
-
-/*
-  In App Notification
-*/
-.grw-unopend-notification {
-  width: 7px;
-  height: 7px;
-  background-color: var(--primary);
-}
-
-/*
-Emoji picker modal
-*/
-.emoji-picker-modal {
-  background-color: transparent !important;
-}
-
-/*
-Expand / compress button bookmark list on users page
-*/
-.grw-user-page-list-m {
-  .grw-expand-compress-btn {
-    color: $body-color;
-    background-color: $body-bg;
-    &.active {
-      background-color: hsl.darken($body-bg, 12%),
-    }
-  }
-}
-
-/*
- * Questionnaire modal
- */
-.grw-questionnaire-btn-group {
-  .btn-outline-primary {
-    @include hsl-button.button-outline-variant(
-      #{hsl.lighten(var(--primary), 30%)} !important,
-      #{hsl.contrast(var(--primary))} !important,
-      var(--primary) !important,
-      #{hsl.lighten(var(--primary), 30%)} !important,
-    );
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: #{hsl.contrast(var(--primary))} !important;
-      background-color: var(--primary) !important;
-    }
-  }
-}
-
-/*
- * revision-history-diff
- */
-.revision-history-diff {
-  background-color: white;
-}

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

+ 4 - 6
apps/app/config/i18next.config.js

@@ -1,6 +1,6 @@
-const { Lang, AllLang } = require('@growi/core');
+const { Lang, AllLang } = require('@growi/core/dist/interfaces');
 
-/** @type {Lang} */
+/** @type {import('@growi/core/dist/interfaces').Lang} */
 const defaultLang = Lang.en_US;
 
 /** @type {import('i18next').InitOptions} */
@@ -10,7 +10,5 @@ const initOptions = {
   defaultNS: 'translation',
 };
 
-module.exports = {
-  defaultLang,
-  initOptions,
-};
+exports.defaultLang = defaultLang;
+exports.initOptions = initOptions;

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

@@ -17,6 +17,8 @@ module.exports = {
   'growi:middleware:safe-redirect': 'debug',
   'growi:service:PassportService': 'debug',
   'growi:service:s2s-messaging:*': 'debug',
+  'growi:service:yjs': 'debug',
+  'growi:service:yjs:*': 'debug',
   // 'growi:service:socket-io': 'debug',
   // 'growi:service:ConfigManager': 'debug',
   // 'growi:service:mail': 'debug',
@@ -41,5 +43,5 @@ module.exports = {
   // 'growi:cli:StickyStretchableScroller': 'debug',
   // 'growi:cli:ItemsTree': 'debug',
   'growi:searchResultList': 'debug',
-
+  'growi:service:openai': 'debug',
 };

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

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

+ 0 - 30
apps/app/cypress.config.ts

@@ -1,30 +0,0 @@
-import { defineConfig } from 'cypress';
-
-export default defineConfig({
-  e2e: {
-    baseUrl: 'http://localhost:3000',
-    specPattern: 'test/cypress/e2e/**/*.cy.{ts,tsx}',
-    supportFile: 'test/cypress/support/index.ts',
-    setupNodeEvents: (on) => {
-      // change screen size
-      // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
-      on('before:browser:launch', (browser, launchOptions) => {
-        if (browser.name === 'chromium' && browser.isHeadless) {
-          launchOptions.args.push('--window-size=1400,1024');
-          launchOptions.args.push('--force-device-scale-factor=1');
-        }
-        return launchOptions;
-      });
-    },
-    defaultCommandTimeout: 7000,
-  },
-  fileServerFolder: 'test/cypress',
-  fixturesFolder: 'test/cypress/fixtures',
-  screenshotsFolder: 'test/cypress/screenshots',
-  videosFolder: 'test/cypress/videos',
-  video: false,
-
-  viewportWidth: 1400,
-  viewportHeight: 1024,
-
-});

+ 28 - 76
apps/app/docker/Dockerfile

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

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

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

+ 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)
 
-![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
 ------------------------------------------------
 
-* [`7.0.4`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.4/apps/app/docker/Dockerfile)
+* [`7.1.0`, `7.1`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.1.0/apps/app/docker/Dockerfile)
+* [`7.0.23`, `7.0` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.23/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
-* [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
-* [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 
 
 What is GROWI?
@@ -27,7 +26,7 @@ see: [weseek/growi](https://github.com/weseek/growi)
 Requirements
 -------------
 
-* MongoDB (>= 4.4)
+* MongoDB (>= 6.0)
 
 ### Optional Dependencies
 

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

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

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

@@ -2,4 +2,4 @@
 /// <reference types="next/image-types/global" />
 
 // 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.

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

@@ -48,6 +48,14 @@ const getTranspilePackages = () => {
     'emoticon',
     'direction', // for hast-util-select
     'bcp-47-match', // for hast-util-select
+    'parse-entities',
+    'character-reference-invalid',
+    'is-hexadecimal',
+    'is-alphabetical',
+    'is-alphanumerical',
+    'github-slugger',
+    'html-url-attributes',
+    'estree-util-is-identifier-name',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
 
@@ -62,6 +70,19 @@ const getTranspilePackages = () => {
   return packages;
 };
 
+const optimizePackageImports = [
+  '@growi/core',
+  '@growi/editor',
+  '@growi/pluginkit',
+  '@growi/presentation',
+  '@growi/preset-themes',
+  '@growi/remark-attachment-refs',
+  '@growi/remark-drawio',
+  '@growi/remark-growi-directive',
+  '@growi/remark-lsx',
+  '@growi/slack',
+  '@growi/ui',
+];
 
 module.exports = async(phase, { defaultConfig }) => {
 
@@ -85,6 +106,9 @@ module.exports = async(phase, { defaultConfig }) => {
     transpilePackages: phase !== PHASE_PRODUCTION_SERVER
       ? getTranspilePackages()
       : undefined,
+    experimental: {
+      optimizePackageImports,
+    },
 
     /** @param config {import('next').NextConfig} */
     webpack(config, options) {
@@ -135,7 +159,11 @@ module.exports = async(phase, { defaultConfig }) => {
   }
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
-    enabled: phase === PHASE_PRODUCTION_BUILD || process.env.ANALYZE === 'true',
+    enabled: phase === PHASE_PRODUCTION_BUILD
+      && (
+        process.env.ANALYZE === 'true'
+          || process.env.ANALYZE === '1'
+      ),
   });
 
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 19 - 0
apps/app/nodemon.json

@@ -0,0 +1,19 @@
+{
+  "ext": "js,ts,json",
+  "watch": [
+    ".",
+    "../../packages/**/dist"
+  ],
+  "ignore": [
+    ".next",
+    "public/static",
+    "package.json",
+    "playwright",
+    "src/client",
+    "src/**/client",
+    "test",
+    "test-with-vite",
+    "tmp",
+    "*.mongodb.js"
+  ]
+}

+ 146 - 113
apps/app/package.json

@@ -1,85 +1,88 @@
 {
   "name": "@growi/app",
-  "version": "7.0.5-RC.0",
+  "version": "7.1.2-RC.0",
   "license": "MIT",
+  "private": "true",
   "scripts": {
     "//// for production": "",
     "build": "run-p build:*",
-    "start": "yarn next start",
-    "build:client": "yarn next build",
-    "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server-tsc-alias.json",
+    "start": "next start",
+    "build:client": "next build",
+    "build:server": "cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
     "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
     "clean": "shx rm -rf dist transpiled",
-    "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
-    "server:ci": "yarn server --ci",
-    "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
-    "styles-prebuilt": "vite build -c vite.styles-prebuilt.config.ts",
-    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up -f config/migrate-mongo-config.js",
+    "server": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
+    "server:ci": "pnpm run server --ci",
+    "preserver": "cross-env NODE_ENV=production pnpm run migrate",
+    "pre:styles": "vite build -c vite.styles-prebuilt.config.ts",
+    "migrate": "node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js",
     "//// for development": "",
-    "dev": "yarn cross-env NODE_ENV=development yarn ts-node-dev --inspect --transpile-only 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",
-    "cy:run": "cypress run --browser chromium",
+    "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": "",
-    "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
-    "lint:typecheck": "npx -y tsc",
-    "lint:eslint": "yarn eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
-    "lint:styles": "stylelint src/**/*.scss",
-    "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
+    "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
+    "lint:typecheck": "vue-tsc --noEmit",
+    "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
+    "lint:styles": "stylelint \"src/**/*.scss\"",
+    "lint:swagger2openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
+    "lint:swagger2openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
     "lint": "run-p lint:*",
-    "prelint:swagger2openapi": "yarn openapi:v3",
+    "prelint:swagger2openapi:apiv3": "pnpm run swagger2openapi:apiv3",
+    "prelint:swagger2openapi:apiv1": "pnpm run swagger2openapi:apiv1",
     "test": "run-p test:*",
-    "test:jest": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --logHeapUsage",
-    "test:vitest": "run-p vitest:run vitest:run:integ vitest:run:components",
-    "jest:run": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
+    "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
+    "test:vitest": "vitest run --coverage",
+    "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "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",
     "//// misc": "",
-    "console": "yarn cross-env NODE_ENV=development yarn ts-node --experimental-repl-await src/server/console.js",
-    "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\"",
-    "ts-node": "node -r ts-node/register -r tsconfig-paths/register -r dotenv-flow/config",
-    "ts-node-dev": "ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config",
-    "version": "yarn version --no-git-tag-version --preid=RC"
+    "console": "npm run repl",
+    "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
+    "swagger2openapi:apiv3": "sh bin/swagger-jsdoc/generate-spec-apiv3.sh",
+    "swagger2openapi:apiv1": "sh bin/swagger-jsdoc/generate-spec-apiv1.sh",
+    "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config",
+    "version:patch": "pnpm version patch",
+    "version:prerelease": "pnpm version prerelease --preid=RC",
+    "version:prepatch": "pnpm version prepatch --preid=RC",
+    "version:preminor": "pnpm version preminor --preid=RC",
+    "version:premajor": "pnpm version premajor --preid=RC"
   },
   "// comments for dependencies": {
     "@aws-skd/*": "fix version above 3.186.0 that is required by mongodb@4.16.0",
     "@keycloak/keycloak-admin-client": "19.0.0 or above exports only ESM.",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "next-themes": "0.3.0 causes a type error: https://github.com/pacocoursey/next-themes/issues/122",
-    "remark-wiki-link": "!!DO NOT REMOVE!! including 'mdast-util-wiki-link' and 'micromark-extension-wiki-link' required by pukiwiki-like-linker",
     "string-width": "5.0.0 or above exports only ESM."
   },
   "dependencies": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
-    "@azure/identity": "^4.0.1",
+    "@azure/identity": "^4.4.1",
+    "@azure/openai": "^2.0.0-beta.2",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
+    "@cspell/dynamic-import": "^8.15.4",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/core": "link:../../packages/core",
-    "@growi/custom-icons": "link:../../packages/custom-icons",
-    "@growi/pluginkit": "link:../../packages/pluginkit",
-    "@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",
     "@opentelemetry/api": "^1.8.0",
     "@opentelemetry/auto-instrumentations-node": "^0.44.0",
@@ -90,24 +93,23 @@
     "@opentelemetry/sdk-trace-node": "^1.23.0",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
-    "@types/jest": "^29.5.2",
-    "@types/ldapjs": "^2.2.5",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
-    "body-parser": "^1.18.2",
+    "body-parser": "^1.20.3",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
-    "check-node-version": "^4.1.0",
+    "check-node-version": "^4.2.1",
     "compression": "^1.7.4",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^4.6.0",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
+    "cross-env": "^7.0.0",
     "csurf": "^1.11.0",
     "csv-to-markdown-table": "^1.4.1",
     "date-fns": "^3.6.0",
@@ -115,12 +117,13 @@
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
+    "dotenv-flow": "^3.2.0",
     "ejs": "^3.1.10",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "^4.0.0",
     "eslint-plugin-regex": "^1.8.0",
     "expose-gc": "^1.0.0",
-    "express": "^4.19.2",
+    "express": "^4.20.0",
     "express-bunyan-logger": "^1.3.3",
     "express-mongo-sanitize": "^2.1.0",
     "express-session": "^1.16.1",
@@ -128,21 +131,32 @@
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
-    "hast-util-select": "^5.0.5",
+    "hast-util-sanitize": "^5.0.1",
+    "hast-util-select": "^6.0.2",
+    "hastscript": "^8.0.0",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
-    "i18next": "^23.10.1",
+    "i18next": "^23.16.5",
     "i18next-resources-to-backend": "^1.2.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
+    "js-tiktoken": "^1.0.15",
+    "js-yaml": "^4.1.0",
+    "katex": "^0.16.11",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
-    "mermaid": "^10.1.0",
+    "mdast-util-from-markdown": "^2.0.1",
+    "mdast-util-gfm-table": "^2.0.0",
+    "mdast-util-wiki-link": "^0.1.2",
+    "mermaid": "^11.2.0",
     "method-override": "^3.0.0",
-    "migrate-mongo": "^8.2.3",
+    "micromark-extension-gfm-table": "^2.1.0",
+    "micromark-extension-wiki-link": "^0.0.4",
+    "migrate-mongo": "^11.0.0",
     "mkdirp": "^1.0.3",
+    "mongodb": "^4.17.2",
     "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
@@ -150,14 +164,16 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
-    "next": "^14.1.3",
-    "next-i18next": "^15.2.0",
+    "next": "^14.2.13",
+    "next-dynamic-loading-props": "^0.1.1",
+    "next-i18next": "^15.3.1",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.1",
-    "nocache": "^3.0.1",
+    "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
-    "nodemailer": "^6.6.2",
+    "nodemailer": "^6.9.15",
     "nodemailer-ses-transport": "~1.5.0",
+    "openai": "^4.56.0",
     "openid-client": "^5.4.0",
     "p-retry": "^4.0.0",
     "passport": "^0.6.0",
@@ -166,6 +182,7 @@
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
+    "prop-types": "^15.8.1",
     "qs": "^6.11.1",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
@@ -173,121 +190,137 @@
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-disable": "^0.1.1",
-    "react-dnd": "^14.0.5",
-    "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^3.1.4",
-    "react-i18next": "^14.1.0",
+    "react-i18next": "^15.1.1",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.7",
+    "react-markdown": "^9.0.1",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
     "react-syntax-highlighter": "^15.5.0",
-    "react-toastify": "^9.1.3",
     "react-use-ripple": "^1.5.2",
-    "reactstrap": "^9.2.0",
+    "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
-    "rehype-katex": "^6.0.2",
-    "rehype-raw": "^6.1.1",
-    "rehype-sanitize": "^5.0.1",
-    "rehype-slug": "^5.0.1",
+    "rehype-katex": "^7.0.1",
+    "rehype-raw": "^7.0.0",
+    "rehype-sanitize": "^6.0.0",
+    "rehype-slug": "^6.0.0",
     "rehype-toc": "^3.0.2",
-    "remark-breaks": "^3.0.2",
-    "remark-emoji": "^3.0.2",
-    "remark-frontmatter": "^4.0.1",
-    "remark-gfm": "^3.0.1",
-    "remark-math": "^5.1.1",
-    "remark-toc": "^8.0.1",
-    "remark-wiki-link": "^1.0.4",
+    "remark-breaks": "^4.0.0",
+    "remark-directive": "^3.0.0",
+    "remark-frontmatter": "^5.0.0",
+    "remark-gfm": "^4.0.0",
+    "remark-math": "^6.0.0",
+    "remark-parse": "^11.0.0",
+    "remark-rehype": "^11.1.1",
+    "remark-stringify": "^11.0.0",
     "sanitize-filename": "^1.6.3",
-    "socket.io": "^4.7.2",
+    "socket.io": "^4.7.5",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
-    "swagger-jsdoc": "^6.1.0",
+    "swagger-jsdoc": "^6.2.8",
     "swr": "^2.2.2",
     "throttle-debounce": "^5.0.0",
+    "ts-deepmerge": "^6.2.0",
+    "tslib": "^2.8.0",
     "uglifycss": "^0.0.29",
+    "uid-safe": "^2.1.5",
+    "unified": "^11.0.0",
+    "unist-util-visit": "^5.0.0",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
-    "unzip-stream": "^0.3.1",
+    "unzip-stream": "^0.3.2",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
+    "uuid": "^11.0.3",
     "validator": "^13.7.0",
-    "ws": "^8.3.0",
-    "xss": "^1.0.14",
-    "y-mongodb-provider": "^0.1.7",
-    "y-socket.io": "^1.1.0",
-    "yjs": "^13.6.12"
+    "ws": "^8.17.1",
+    "xss": "^1.0.15",
+    "y-mongodb-provider": "^0.2.0",
+    "y-socket.io": "^1.1.3",
+    "yjs": "^13.6.18"
   },
   "// comments for defDependencies": {
+    "bootstrap": "v5.3.3 has a bug. refs: https://github.com/twbs/bootstrap/issues/39798",
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
-    "handsontable": "v7.0.0 or above is no loger MIT lisence."
+    "handsontable": "v7.0.0 or above is no loger MIT lisence.",
+    "mongodb": "mongoose which is used requires mongo@4.16.0."
   },
   "devDependencies": {
-    "@growi/editor": "link:../../packages/editor",
-    "@growi/presentation": "link:../../packages/presentation",
-    "@growi/ui": "link:../../packages/ui",
+    "@emoji-mart/data": "^1.2.1",
+    "@growi/core-styles": "workspace:^",
+    "@growi/custom-icons": "workspace:^",
+    "@growi/editor": "workspace:^",
+    "@growi/ui": "workspace:^",
     "@handsontable/react": "=2.1.0",
     "@next/bundle-analyzer": "^14.1.3",
-    "@swc-node/jest": "^1.6.2",
-    "@swc/jest": "^0.2.24",
-    "@testing-library/react": "^14.1.2",
+    "@popperjs/core": "^2.11.8",
+    "@swc-node/jest": "^1.8.1",
+    "@swc/jest": "^0.2.36",
+    "@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",
-    "@types/express": "^4.17.11",
+    "@types/express": "^4.17.21",
+    "@types/hast": "^3.0.4",
     "@types/jest": "^29.5.2",
+    "@types/ldapjs": "^2.2.5",
+    "@types/mdast": "^4.0.4",
+    "@types/node-cron": "^3.0.11",
+    "@types/react": "^18.2.14",
+    "@types/react-dom": "^18.2.6",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
+    "@types/testing-library__dom": "^7.5.0",
     "@types/throttle-debounce": "^5.0.1",
+    "@types/unist": "^3.0.3",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
-    "@vitejs/plugin-react": "^4.2.1",
-    "@vitest/coverage-v8": "^0.34.6",
-    "autoprefixer": "^9.0.0",
+    "@types/uuid": "^10.0.0",
     "babel-loader": "^8.2.5",
-    "bootstrap": "^5.3.1",
+    "bootstrap": "=5.3.2",
     "connect-browser-sync": "^2.1.0",
-    "cypress-real-events": "^1.12.0",
     "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
-    "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-regex": "^1.8.0",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
-    "happy-dom": "^13.2.0",
+    "happy-dom": "^15.7.4",
     "i18next-chained-backend": "^4.6.2",
-    "i18next-hmr": "^3.0.4",
-    "i18next-http-backend": "^2.5.0",
+    "i18next-hmr": "^3.1.3",
+    "i18next-http-backend": "^2.6.2",
     "i18next-localstorage-backend": "^4.2.0",
     "jest": "^29.5.0",
     "jest-date-mock": "^1.0.8",
     "jest-localstorage-mock": "^2.4.14",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
+    "mdast-util-directive": "^3.0.0",
+    "mdast-util-find-and-replace": "^3.0.1",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
-    "plantuml-encoder": "^1.2.5",
-    "prettier": "^1.19.1",
     "pretty-bytes": "^6.1.1",
-    "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
+    "react-dnd": "^14.0.5",
+    "react-dnd-html5-backend": "^14.1.0",
     "react-dropzone": "^14.2.3",
+    "react-hook-form": "^7.45.4",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
-    "rehype-rewrite": "^3.0.6",
-    "replacestream": "^4.0.3",
+    "react-toastify": "^9.1.3",
+    "rehype-rewrite": "^4.0.2",
+    "remark-github-admonitions-to-directives": "^2.0.0",
     "sass": "^1.53.0",
-    "simple-load-script": "^1.0.2",
     "simplebar-react": "^2.3.6",
-    "socket.io-client": "^4.2.0",
+    "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",
-    "swagger2openapi": "^7.0.8",
-    "tsc-alias": "^1.2.9"
+    "swagger2openapi": "^7.0.8"
   }
 }

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

@@ -0,0 +1,115 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { defineConfig, devices, type Project } from '@playwright/test';
+
+const authFile = path.resolve(__dirname, './playwright/.auth/admin.json');
+
+// Use prepared auth state.
+const storageState = fs.existsSync(authFile) ? authFile : undefined;
+
+const supportedBrowsers = ['chromium', 'firefox', 'webkit'] as const;
+
+const projects: Array<Project> = supportedBrowsers.map(browser => ({
+  name: browser,
+  use: { ...devices[`Desktop ${browser}`], storageState },
+  testIgnore: /(10-installer|21-basic-features-for-guest)\/.*\.spec\.ts/,
+  dependencies: ['setup', 'auth'],
+}));
+
+const projectsForGuestMode: Array<Project> = supportedBrowsers.map(browser => ({
+  name: `${browser}/guest-mode`,
+  use: { ...devices[`Desktop ${browser}`] }, // Do not use storageState
+  testMatch: /21-basic-features-for-guest\/.*\.spec\.ts/,
+}));
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  expect: {
+    timeout: 7 * 1000,
+  },
+
+  testDir: './playwright',
+  outputDir: './playwright/output',
+  /* Run tests in files in parallel */
+  fullyParallel: true,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Retry on CI only */
+  retries: process.env.CI ? 2 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: process.env.CI
+    ? [
+      ['github'],
+      ['blob'],
+    ]
+    : 'list',
+
+  webServer: {
+    command: 'pnpm run server',
+    url: 'http://localhost:3000',
+    reuseExistingServer: !process.env.CI,
+    stdout: 'ignore',
+    stderr: 'pipe',
+  },
+
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: 'http://localhost:3000',
+
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: 'on-first-retry',
+
+    viewport: { width: 1400, height: 1024 },
+  },
+
+  /* Configure projects for major browsers */
+  projects: [
+    // Setup project
+    { name: 'setup', testMatch: /.*\.setup\.ts/, testIgnore: /auth\.setup\.ts/ },
+    { name: 'auth', testMatch: /auth\.setup\.ts/ },
+
+    {
+      name: 'chromium/installer',
+      use: { ...devices['Desktop Chrome'], storageState },
+      testMatch: /10-installer\/.*\.spec\.ts/,
+      dependencies: ['setup'],
+    },
+
+    ...projects,
+
+    ...projectsForGuestMode,
+
+    /* Test against mobile viewports. */
+    // {
+    //   name: 'Mobile Chrome',
+    //   use: { ...devices['Pixel 5'] },
+    // },
+    // {
+    //   name: 'Mobile Safari',
+    //   use: { ...devices['iPhone 12'] },
+    // },
+
+    /* Test against branded browsers. */
+    // {
+    //   name: 'Microsoft Edge',
+    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
+    // },
+    // {
+    //   name: 'Google Chrome',
+    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+    // },
+  ],
+
+});

+ 0 - 0
apps/app/src/components/Me/ColorModeSettings.module.scss → apps/app/playwright/.auth/.gitkeep


+ 16 - 0
apps/app/playwright/.eslintrc.mjs

@@ -0,0 +1,16 @@
+import playwright from 'eslint-plugin-playwright';
+
+// eslint-disable-next-line import/no-anonymous-default-export
+export default [
+  {
+    ...playwright.configs['flat/recommended'],
+    files: ['./**'],
+  },
+  {
+    files: ['./**'],
+    rules: {
+      // Customize Playwright rules
+      // ...
+    },
+  },
+];

+ 2 - 0
apps/app/playwright/.gitignore

@@ -0,0 +1,2 @@
+.auth
+output

+ 47 - 0
apps/app/playwright/10-installer/install.spec.ts

@@ -0,0 +1,47 @@
+import { test, expect } from '@playwright/test';
+
+test('Installer', async({ page }) => {
+  await page.goto('/');
+  await page.waitForURL('/installer');
+
+  // show installer form
+  await expect(page.getByTestId('installerForm')).toBeVisible();
+
+  // choose Japanese
+  await page.getByTestId('dropdownLanguage').click();
+  await page.getByTestId('dropdownLanguageMenu-ja_JP').click();
+  await expect(page.getByRole('textbox', { name: 'ユーザーID' })).toBeVisible();
+  await expect(page.getByRole('textbox', { name: 'ユーザーID' })).toHaveAttribute('placeholder', 'ユーザーID');
+
+  // choose Chinese
+  await page.getByTestId('dropdownLanguage').click();
+  await page.getByTestId('dropdownLanguageMenu-zh_CN').click();
+  await expect(page.getByRole('textbox', { name: '用户ID' })).toBeVisible();
+  await expect(page.getByRole('textbox', { name: '用户ID' })).toHaveAttribute('placeholder', '用户ID');
+  // // choose English
+  await page.getByTestId('dropdownLanguage').click();
+  await page.getByTestId('dropdownLanguageMenu-en_US').click();
+  await expect(page.getByRole('textbox', { name: 'User ID' })).toBeVisible();
+  await expect(page.getByRole('textbox', { name: 'User ID' })).toHaveAttribute('placeholder', 'User ID');
+
+  await page.getByRole('textbox', { name: 'User ID' }).focus();
+
+  // fill form
+  await page.getByLabel('User ID').fill('admin');
+  await page.getByLabel('User ID').press('Tab');
+  await expect(page.getByRole('textbox', { name: 'Name' })).toBeFocused();
+
+  await page.getByLabel('Name').fill('Admin');
+  await page.getByLabel('Name').press('Tab');
+  await expect(page.getByRole('textbox', { name: 'Email' })).toBeFocused();
+
+  await page.getByLabel('Email').fill('admin@example.com');
+  await page.getByLabel('Email').press('Tab');
+  await expect(page.getByRole('textbox', { name: 'Password' })).toBeFocused();
+
+  await page.getByLabel('Password').fill('adminadmin');
+  await page.getByLabel('Password').press('Enter');
+
+  await page.waitForURL('/', { timeout: 20000 });
+  await expect(page).toHaveTitle(/\/ - GROWI/);
+});

+ 150 - 0
apps/app/playwright/20-basic-features/access-to-page.spec.ts

@@ -0,0 +1,150 @@
+import { test, expect, type Page } from '@playwright/test';
+
+const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+};
+
+test('has title', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  // Expect a title "to contain" a substring.
+  await expect(page).toHaveTitle(/Sandbox/);
+});
+
+test('get h1', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  // Expects page to have a heading with the name of Installation.
+  await expect(page.getByRole('heading').filter({ hasText: /\/Sandbox/ })).toBeVisible();
+});
+
+test('/Sandbox/Math is successfully loaded', async({ page }) => {
+  await page.goto('/Sandbox/Math');
+
+  // Expect the Math-specific elements to be present
+  await expect(page.locator('.katex').first()).toBeVisible();
+});
+
+test('Sandbox with edit is successfully loaded', async({ page }) => {
+  await page.goto('/Sandbox#edit');
+
+  // Expect the Editor-specific elements to be present
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await expect(page.getByTestId('save-page-btn')).toBeVisible();
+  await expect(page.getByTestId('grw-grant-selector')).toBeVisible();
+});
+
+test.describe.serial('PageEditor', () => {
+  const body1 = 'hello';
+  const body2 = ' world!';
+  const targetPath = '/Sandbox/testForUseEditingMarkdown';
+
+  test('Edit and save with save-page-btn', async({ page }) => {
+    await page.goto(targetPath);
+
+    await page.getByTestId('editor-button').click();
+    await appendTextToEditorUntilContains(page, body1);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(body1);
+  });
+
+  test('Edit and save with shortcut key', async({ page }) => {
+    const savePageShortcutKey = 'Control+s';
+
+    await page.goto(targetPath);
+
+    await page.getByTestId('editor-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(body1);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(body1);
+
+    await appendTextToEditorUntilContains(page, body1 + body2);
+    await page.keyboard.press(savePageShortcutKey);
+    await page.getByTestId('view-button').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(body1 + body2);
+  });
+});
+
+test('Access to /me page', async({ page }) => {
+  await page.goto('/me');
+
+  // Expect the UserSettgins-specific elements to be present when accessing /me (UserSettgins)
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+});
+
+test('All In-App Notification list is successfully loaded', async({ page }) => {
+  await page.goto('/me/all-in-app-notifications');
+
+  // Expect the In-App Notification-specific elements to be present when accessing /me/all-in-app-notifications
+  await expect(page.getByTestId('grw-in-app-notification-page')).toBeVisible();
+});
+
+test('/trash is successfully loaded', async({ page }) => {
+  await page.goto('/trash');
+
+  await expect(page.getByTestId('trash-page-list')).toContainText('There are no pages under this page.');
+});
+
+test('/tags is successfully loaded', async({ page }) => {
+  await page.goto('/tags');
+
+  await expect(page.getByTestId('grw-tags-list')).toContainText('You have no tag, You can set tags on pages');
+});
+
+test.describe.serial('Access to Template Editing Mode', () => {
+  const templateBody1 = 'Template for children';
+  const templateBody2 = 'Template for descendants';
+
+  test('Successfully created template for children', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-template-modal-btn').click();
+    expect(page.getByTestId('page-template-modal')).toBeVisible();
+
+    await page.getByTestId('template-button-children').click();
+
+    await appendTextToEditorUntilContains(page, templateBody1);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(templateBody1);
+  });
+
+  test('Template is applied to pages created (template for children)', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await page.getByTestId('grw-page-create-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(templateBody1);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody1);
+  });
+
+  test('Successfully created template for descendants', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-template-modal-btn').click();
+    expect(page.getByTestId('page-template-modal')).toBeVisible();
+
+    await page.getByTestId('template-button-descendants').click();
+
+    await appendTextToEditorUntilContains(page, templateBody2);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(templateBody2);
+  });
+
+  test('Template is applied to pages created (template for descendants)', async({ page }) => {
+    await page.goto('/Sandbox/Bootstrap5');
+
+    await page.getByTestId('grw-page-create-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(templateBody2);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody2);
+  });
+});

+ 30 - 0
apps/app/playwright/20-basic-features/access-to-pagelist.spec.ts

@@ -0,0 +1,30 @@
+import { test, expect, type Page } from '@playwright/test';
+
+const openPageAccessoriesModal = async(page: Page): Promise<void> => {
+  await page.goto('/');
+  await page.getByTestId('pageListButton').click();
+  await expect(page.getByTestId('descendants-page-list-modal')).toBeVisible();
+};
+
+test('Page list modal is successfully opened', async({ page }) => {
+  await openPageAccessoriesModal(page);
+  await expect(page.getByTestId('page-list-item-L').first()).not.toContainText('You cannot see this page');
+});
+
+test('Successfully open PageItemControl', async({ page }) => {
+  await openPageAccessoriesModal(page);
+  await page.getByTestId('page-list-item-L').first().getByTestId('open-page-item-control-btn').click();
+  await expect(page.locator('.dropdown-menu.show')).toBeVisible();
+});
+
+test('Successfully close modal', async({ page }) => {
+  await openPageAccessoriesModal(page);
+  await page.locator('.btn-close').click();
+  await expect(page.getByTestId('descendants-page-list-modal')).not.toBeVisible();
+});
+
+test('Timeline list successfully openend', async({ page }) => {
+  await openPageAccessoriesModal(page);
+  await page.getByTestId('timeline-tab-button').click();
+  await expect(page.locator('.card-timeline').first()).toBeVisible();
+});

+ 50 - 0
apps/app/playwright/20-basic-features/click-page-icons.spec.ts

@@ -0,0 +1,50 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Click page icons', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/Sandbox');
+  });
+
+  test('Successfully Subscribe/Unsubscribe a page', async({ page }) => {
+    const subscribeButton = page.locator('.btn-subscribe');
+
+    // Subscribe
+    await subscribeButton.click();
+    await expect(subscribeButton).toHaveClass(/active/);
+
+    // Unsubscribe
+    await subscribeButton.click();
+    await expect(subscribeButton).not.toHaveClass(/active/);
+  });
+
+  test('Successfully Like/Unlike a page', async({ page }) => {
+    const likeButton = page.locator('.btn-like').first();
+
+    // Like
+    await likeButton.click();
+    await expect(likeButton).toHaveClass(/active/);
+
+    // Unlike
+    await likeButton.click();
+    await expect(likeButton).not.toHaveClass(/active/);
+  });
+
+  test('Successfully Bookmark / Unbookmark a page', async({ page }) => {
+    const bookmarkButton = page.locator('.btn-bookmark').first();
+
+    // Bookmark
+    await bookmarkButton.click();
+    await expect(bookmarkButton).toHaveClass(/active/);
+
+    // Unbookmark
+    await page.locator('.grw-bookmark-folder-menu-item').click();
+    await expect(bookmarkButton).not.toHaveClass(/active/);
+  });
+
+  test('Successfully display list of "seen by user"', async({ page }) => {
+    await page.locator('.btn-seen-user').click();
+
+    const imgCount = await page.locator('.user-list-content').locator('img').count();
+    expect(imgCount).toBe(1);
+  });
+});

+ 49 - 0
apps/app/playwright/20-basic-features/comments.spec.ts

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

+ 27 - 0
apps/app/playwright/20-basic-features/create-page-button.spec.ts

@@ -0,0 +1,27 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Create page button', () => {
+  test('click and autofocus to title text input', async({ page }) => {
+    await page.goto('/');
+
+    await page.getByTestId('grw-page-create-button').getByRole('button', { name: 'Create' }).click();
+
+    // should be focused
+    await expect(page.getByPlaceholder('Input page name')).toBeFocused();
+  });
+});
+
+test.describe('Create page button dropdown menu', () => {
+  test('open and create today page', async({ page }) => {
+    await page.goto('/');
+
+    // open dropdown menu
+    await page.getByTestId('grw-page-create-button').hover();
+    await expect(page.getByTestId('grw-page-create-button').getByLabel('Open create page menu')).toBeVisible();
+    await page.getByTestId('grw-page-create-button').getByLabel('Open create page menu').dispatchEvent('click'); // simulate the click
+    await page.getByRole('menuitem', { name: 'Create today page' }).click();
+
+    // should not be visible
+    await expect(page.getByPlaceholder('Input page name')).not.toBeVisible();
+  });
+});

+ 28 - 0
apps/app/playwright/20-basic-features/presentation.spec.ts

@@ -0,0 +1,28 @@
+import { test, expect } from '@playwright/test';
+
+test('Presentation', async({ page }) => {
+  await page.goto('/');
+
+  // show presentation modal
+  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  await page.getByTestId('open-presentation-modal-btn').click();
+
+  // check the content of the h1
+  await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
+    .toHaveText(/Welcome to GROWI/);
+
+  // forward the slide with keyboard
+  await page.keyboard.press('ArrowRight');
+
+  // check the content of the h1
+  await expect(page.getByRole('application').getByRole('heading', { level: 1 }))
+    .toHaveText(/What can you do with GROWI?/);
+
+  // forward the slide with button
+  await page.getByRole('application').getByLabel('next slide').click();
+
+  // check the content of the h2
+  await expect(page.getByRole('application').getByRole('heading', { level: 2 }))
+    .toHaveText(/1. Knowledge Management: Create pages to store information and knowledge/);
+
+});

+ 47 - 0
apps/app/playwright/20-basic-features/sticky-features.spec.ts

@@ -0,0 +1,47 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Sticky features', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/');
+  });
+
+  test('Subnavigation displays changes on scroll down and up', async({ page }) => {
+    // Scroll down to trigger sticky effect
+    await page.evaluate(() => window.scrollTo(0, 250));
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+
+    // Scroll back to top
+    await page.evaluate(() => window.scrollTo(0, 0));
+    await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(/active/);
+  });
+
+  test('Subnavigation is not displayed when move to other pages', async({ page }) => {
+    // Scroll down to trigger sticky effect
+    await page.evaluate(() => window.scrollTo(0, 250));
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+
+    // Move to /Sandbox page
+    await page.goto('/Sandbox');
+    await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(/active/);
+  });
+
+  test('Able to click buttons on subnavigation switcher when sticky', async({ page }) => {
+    // Scroll down to trigger sticky effect
+    await page.evaluate(() => window.scrollTo(0, 250));
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+
+    // Click editor button
+    await page.getByTestId('editor-button').click();
+    await expect(page.locator('.layout-root')).toHaveClass(/editing/);
+  });
+
+  test('Subnavigation is sticky when on small window', async({ page }) => {
+    // Scroll down to trigger sticky effect
+    await page.evaluate(() => window.scrollTo(0, 500));
+    await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+
+    // Set viewport to small size
+    await page.setViewportSize({ width: 600, height: 1024 });
+    await expect(page.getByTestId('grw-contextual-sub-nav').getByTestId('grw-page-editor-mode-manager')).toBeVisible();
+  });
+});

+ 86 - 0
apps/app/playwright/20-basic-features/use-tools.spec.ts

@@ -0,0 +1,86 @@
+import { test, expect, type Page } from '@playwright/test';
+
+const openPageItemControl = async(page: Page): Promise<void> => {
+  await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+};
+
+test('Page Deletion and PutBack is executed successfully', async({ page }) => {
+  await page.goto('/Sandbox/Bootstrap5');
+
+  // Delete
+  await openPageItemControl(page);
+  await page.getByTestId('open-page-delete-modal-btn').click();
+  await expect(page.getByTestId('page-delete-modal')).toBeVisible();
+  await page.getByTestId('delete-page-button').click();
+
+  // PutBack
+  await expect(page.getByTestId('trash-page-alert')).toBeVisible();
+  await page.getByTestId('put-back-button').click();
+  await expect(page.getByTestId('put-back-page-modal')).toBeVisible();
+  await page.getByTestId('put-back-execution-button').click();
+  await expect(page.getByTestId('trash-page-alert')).not.toBeVisible();
+});
+
+test('PageDuplicateModal is shown successfully', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await openPageItemControl(page);
+  await page.getByTestId('open-page-duplicate-modal-btn').click();
+
+  await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
+});
+
+test('PageMoveRenameModal is shown successfully', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  await openPageItemControl(page);
+  await page.getByTestId('rename-page-btn').click();
+
+  await expect(page.getByTestId('page-rename-modal')).toBeVisible();
+});
+
+// TODO: Uncomment after https://redmine.weseek.co.jp/issues/149786
+// test('PresentationModal for "/" is shown successfully', async({ page }) => {
+//   await page.goto('/');
+
+//   await openPageItemControl(page);
+//   await page.getByTestId('open-presentation-modal-btn').click();
+
+//   expect(page.getByTestId('page-presentation-modal')).toBeVisible();
+// });
+
+test.describe('Page Accessories Modal', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/');
+    await openPageItemControl(page);
+  });
+
+  test('Page History is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-history-tab').click();
+    await expect(page.getByTestId(('page-history'))).toBeVisible();
+  });
+
+  test('Page Attachment Data is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-attachment-data-tab').click();
+    await expect(page.getByTestId('page-attachment')).toBeVisible();
+  });
+
+  test('Share Link Management is shown successfully', async({ page }) => {
+    await page.getByTestId('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
+    await expect(page.getByTestId('share-link-management')).toBeVisible();
+  });
+});
+
+test('Successfully add new tag', async({ page }) => {
+  const tag = 'we';
+  await page.goto('/Sandbox/Bootstrap5');
+
+  await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
+  await expect(page.locator('#edit-tag-modal')).toBeVisible();
+  await page.locator('.rbt-input-main').fill(tag);
+  await expect(page.locator('#tag-typeahead-asynctypeahead-item-0')).toBeVisible();
+  await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
+  await page.getByTestId('tag-edit-done-btn').click();
+  await expect(page.getByTestId('grw-tag-labels')).toContainText(tag);
+});

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

@@ -0,0 +1,46 @@
+import { test, expect } from '@playwright/test';
+
+import { collapseSidebar } from '../utils';
+
+test('/Sandbox is successfully loaded', async({ page }) => {
+
+  await page.goto('/Sandbox');
+
+  // Expect a title "to contain" a substring.
+  await expect(page).toHaveTitle(/Sandbox/);
+});
+
+test('/Sandbox/math is successfully loaded', async({ page }) => {
+
+  await page.goto('/Sandbox/Math');
+
+  // Check if the math elements are visible
+  await expect(page.locator('.katex').first()).toBeVisible();
+});
+
+test('Access to /me page', async({ page }) => {
+  await page.goto('/me');
+
+  // Expect to be redirected to /login when accessing /me
+  await expect(page.getByTestId('login-form')).toBeVisible();
+});
+
+test('Access to /trash page', async({ page }) => {
+  await page.goto('/trash');
+
+  // Expect the trash page specific elements to be present when accessing /trash
+  await expect(page.getByTestId('trash-page-list')).toBeVisible();
+});
+
+test('Access to /tags page', async({ page }) => {
+  await page.goto('/');
+
+  await collapseSidebar(page, false);
+  await page.getByTestId('grw-sidebar-nav-primary-tags').click();
+  await expect(page.getByTestId('grw-sidebar-content-tags')).toBeVisible();
+  await expect(page.getByTestId('grw-tags-list').first()).toBeVisible();
+  await expect(page.getByTestId('grw-tags-list').first()).toContainText('You have no tag, You can set tags on pages');
+
+  await page.getByTestId('check-all-tags-button').click();
+  await expect(page.getByTestId('tags-page')).toBeVisible();
+});

+ 14 - 0
apps/app/playwright/21-basic-features-for-guest/sticky-for-guest.spec.ts

@@ -0,0 +1,14 @@
+import { test, expect } from '@playwright/test';
+
+
+test('Sub navigation sticky changes when scrolling down and up', async({ page }) => {
+  await page.goto('/Sandbox');
+
+  // Sticky
+  await page.evaluate(() => window.scrollTo(0, 250));
+  await expect(page.locator('.sticky-outer-wrapper').first()).toHaveClass(/active/);
+
+  // Not sticky
+  await page.evaluate(() => window.scrollTo(0, 0));
+  await expect(page.locator('.sticky-outer-wrapper').first()).not.toHaveClass(/active/);
+});

+ 37 - 0
apps/app/playwright/22-sharelink/access-to-sharelink.spec.ts

@@ -0,0 +1,37 @@
+import { test, expect } from '@playwright/test';
+
+import { login } from '../utils/Login';
+
+test.describe.serial('Access to sharelink by guest', () => {
+  let createdSharelink: string | null;
+
+  test('Prepare sharelink', async({ page }) => {
+    await page.goto('/Sandbox/Bootstrap5');
+
+    // Create Sharelink
+    await page.getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
+    await page.getByTestId('btn-sharelink-toggleform').click();
+    await page.getByTestId('btn-sharelink-issue').click();
+
+    // Get ShareLink
+    createdSharelink = await page.getByTestId('share-link').textContent();
+    expect(createdSharelink).toHaveLength(24);
+  });
+
+  test('The sharelink page is successfully loaded', async({ page }) => {
+    await page.goto('/');
+
+    // Logout
+    await page.getByTestId('personal-dropdown-button').click();
+    await expect(page.getByTestId('logout-button')).toBeVisible();
+    await page.getByTestId('logout-button').click();
+    await page.waitForURL('http://localhost:3000/login');
+
+    // Access sharelink
+    await page.goto(`/share/${createdSharelink}`);
+    await expect(page.locator('.page-meta')).toBeVisible();
+
+    await login(page);
+  });
+});

+ 0 - 0
apps/app/test/cypress/e2e/23-editor/assets/example.txt → apps/app/playwright/23-editor/assets/example.txt


+ 50 - 0
apps/app/playwright/23-editor/saving.spec.ts

@@ -0,0 +1,50 @@
+import path from 'path';
+
+import { test, expect, type Page } from '@playwright/test';
+
+const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+};
+
+
+test('Successfully create page under specific path', async({ page }) => {
+  const newPagePath = '/child';
+  const openPageCreateModalShortcutKey = 'c';
+
+  await page.goto('/Sandbox');
+
+  await page.keyboard.press(openPageCreateModalShortcutKey);
+  await expect(page.getByTestId('page-create-modal')).toBeVisible();
+  page.getByTestId('page-create-modal').locator('.rbt-input-main').fill(newPagePath);
+  page.getByTestId('btn-create-page-under-below').click();
+  await page.getByTestId('view-button').click();
+
+  const createdPageId = path.basename(page.url());
+  expect(createdPageId.length).toBe(24);
+});
+
+
+test('Successfully updating a page using a shortcut on a previously created page', async({ page }) => {
+  const body1 = 'hello';
+  const body2 = ' world!';
+  const savePageShortcutKey = 'Control+s';
+
+  await page.goto('/Sandbox/child');
+
+  // 1st
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await appendTextToEditorUntilContains(page, body1);
+  await page.keyboard.press(savePageShortcutKey);
+  await page.getByTestId('view-button').click();
+  await expect(page.locator('.main')).toContainText(body1);
+
+  // 2nd
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await appendTextToEditorUntilContains(page, body1 + body2);
+  await page.keyboard.press(savePageShortcutKey);
+  await page.getByTestId('view-button').click();
+  await expect(page.locator('.main')).toContainText(body1 + body2);
+});

+ 27 - 0
apps/app/playwright/23-editor/template-modal.spec.ts

@@ -0,0 +1,27 @@
+import { test, expect } from '@playwright/test';
+
+test('Successfully select template and template locale', async({ page }) => {
+  const jaText = '今日の目標';
+  const enText = "TODAY'S GOALS";
+  await page.goto('/Sandbox/TemplateModal');
+
+  // move to edit mode
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+
+  // open TemplateModal
+  const templateModal = page.getByTestId('template-modal');
+  await page.getByTestId('open-template-button').click();
+  await expect(templateModal).toBeVisible();
+
+  // select template and template locale
+  await templateModal.locator('.list-group-item').nth(0).click();
+  await expect(templateModal.locator('.card-body').locator('.has-data-line').nth(1)).toHaveText(enText);
+  await templateModal.getByTestId('select-locale-dropdown-toggle').click();
+  await templateModal.getByTestId('select-locale-dropdown-item').nth(1).click();
+  await expect(templateModal.locator('.card-body').locator('.has-data-line').nth(1)).toHaveText(jaText);
+
+  // insert
+  await templateModal.locator('.btn-primary').click();
+  await expect(page.locator('.has-data-line').nth(1)).toHaveText(jaText);
+});

+ 113 - 0
apps/app/playwright/23-editor/with-navigation.spec.ts

@@ -0,0 +1,113 @@
+import { readFileSync } from 'fs';
+import path from 'path';
+
+import { test, expect, type Page } from '@playwright/test';
+
+/**
+ * for the issues:
+ * @see https://redmine.weseek.co.jp/issues/122040
+ * @see https://redmine.weseek.co.jp/issues/124281
+ */
+test('should not be cleared and should prevent GrantSelector from modified', async({ page }) => {
+  await page.goto('/Sandbox/for-122040');
+
+  // Open Editor
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+
+  // Open GrantSelector and select "only me"
+  await page.getByTestId('grw-grant-selector').click();
+  const dropdownMenu = page.getByTestId('grw-grant-selector-dropdown-menu');
+  await expect(dropdownMenu).toBeVisible();
+  await dropdownMenu.locator('.dropdown-item').nth(2).click();
+  await expect(page.getByTestId('grw-grant-selector')).toContainText('Only me');
+
+  // Upload attachment
+  const filePath = path.resolve(__dirname, '../23-editor/assets/example.txt');
+  const buffer = readFileSync(filePath).toString('base64');
+  const dataTransfer = await page.evaluateHandle(
+    async({ bufferData, localFileName, localFileType }) => {
+      const dt = new DataTransfer();
+
+      const blobData = await fetch(bufferData).then(res => res.blob());
+
+      const file = new File([blobData], localFileName, {
+        type: localFileType,
+      });
+      dt.items.add(file);
+      return dt;
+    },
+    {
+      bufferData: `data:application/octet-stream;base64,${buffer}`,
+      localFileName: 'sample.tst',
+      localFileType: 'application/octet-stream',
+    },
+  );
+  await page.locator('.dropzone').first().dispatchEvent('drop', { dataTransfer });
+  await expect(page.getByTestId('page-editor-preview-body').getByTestId('rich-attachment')).toBeVisible();
+
+  // Save page
+  await page.getByTestId('save-page-btn').click();
+
+  // Expect grant not to be reset after uploading an attachment
+  await expect(page.getByTestId('page-grant-alert')).toContainText('Browsing of this page is restricted');
+});
+
+const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+};
+
+/**
+ * for the issue:
+ * @see https://redmine.weseek.co.jp/issues/115285
+ */
+test('Successfully updating the page body', async({ page }) => {
+  const page1Path = '/Sandbox/for-115285/page1';
+  const page2Path = '/Sandbox/for-115285/page2';
+
+  const page1Body = 'Hello';
+  const page2Body = 'World';
+
+
+  await page.goto(page1Path);
+
+  // Open Editor (page1)
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+
+  // Append text
+  await appendTextToEditorUntilContains(page, page1Body);
+
+  // Save page
+  await page.getByTestId('save-page-btn').click();
+
+  await expect(page.locator('.main')).toContainText(page1Body);
+
+  // Duplicate page1
+  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  await page.getByTestId('open-page-duplicate-modal-btn').click();
+  await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
+  await page.locator('.form-control').fill(page2Path);
+  await page.getByTestId('btn-duplicate').click();
+
+  // Open Editor (page2)
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+
+  // Expect to see the text from which you are duplicating
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(page1Body);
+
+  // Append text
+  await appendTextToEditorUntilContains(page, page1Body + page2Body);
+
+
+  await page.goto(page1Path);
+
+  // Open Editor (page1)
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(page1Body);
+
+});

+ 227 - 0
apps/app/playwright/30-search/search.spect.ts

@@ -0,0 +1,227 @@
+import { test, expect } from '@playwright/test';
+
+test('Search page with "q" param is successfully loaded', async({ page }) => {
+  // Navigate to the search page with query parameters
+  await page.goto('/_search?q=alerts');
+
+  // Confirm search result elements are visible
+  await expect(page.getByTestId('search-result-base')).toBeVisible();
+  await expect(page.getByTestId('search-result-list')).toBeVisible();
+  await expect(page.getByTestId('search-result-content')).toBeVisible();
+  await expect(page.locator('.wiki')).toBeVisible();
+});
+
+test('checkboxes behaviors', async({ page }) => {
+  // Navigate to the search page with query parameters
+  await page.goto('/_search?q=alerts');
+
+  // Confirm search result elements are visible
+  await expect(page.getByTestId('search-result-base')).toBeVisible();
+  await expect(page.getByTestId('search-result-list')).toBeVisible();
+  await expect(page.getByTestId('search-result-content')).toBeVisible();
+  await expect(page.locator('.wiki')).toBeVisible();
+
+  // Click the first checkbox
+  await page.getByTestId('cb-select').first().click({ force: true });
+
+  // Unclick the first checkbox
+  await page.getByTestId('cb-select').first().click({ force: true });
+
+  // Click the select all checkbox
+  await page.getByTestId('delete-control-button').first().click({ force: true });
+  await page.getByTestId('cb-select-all').click({ force: true });
+
+  // Unclick the first checkbox after selecting all
+  await page.getByTestId('cb-select').first().click({ force: true });
+
+  // Click the first checkbox again
+  await page.getByTestId('cb-select').first().click({ force: true });
+
+  // Unclick the select all checkbox
+  await page.getByTestId('cb-select').first().click({ force: true });
+});
+
+
+test('successfully loads /_private-legacy-pages', async({ page }) => {
+  await page.goto('/_private-legacy-pages');
+
+  // Confirm search result elements are visible
+  await expect(page.locator('[data-testid="search-result-base"]')).toBeVisible();
+  await expect(page.locator('[data-testid="search-result-private-legacy-pages"]')).toBeVisible();
+});
+
+test('Search all pages by word', async({ page }) => {
+  await page.goto('/');
+  await page.getByTestId('open-search-modal-button').click();
+  await expect(page.getByTestId('search-modal')).toBeVisible();
+  await page.locator('.form-control').fill('sand');
+  await expect(page.locator('.search-menu-item').first()).toBeVisible();
+});
+
+test.describe.serial('Search all pages', () => {
+  const tag = 'help';
+  const searchText = `tag:${tag}`;
+
+  test('Successfully created tags', async({ page }) => {
+    await page.goto('/');
+
+    // open Edit Tags Modal to add tag
+    await page.locator('.grw-side-contents-sticky-container').isVisible();
+    await page.locator('#edit-tags-btn-wrapper-for-tooltip').click();
+    await expect(page.locator('#edit-tag-modal')).toBeVisible();
+    await page.locator('.rbt-input-main').fill(tag);
+    await page.locator('#tag-typeahead-asynctypeahead-item-0').click();
+    await page.getByTestId('tag-edit-done-btn').click();
+
+  });
+
+  test('Search all pages by tag is successfully loaded', async({ page }) => {
+    await page.goto('/');
+
+    // Search
+    await page.getByTestId('open-search-modal-button').click();
+    await expect(page.getByTestId('search-modal')).toBeVisible();
+    await page.locator('.form-control').fill(searchText);
+    await page.getByTestId('search-all-menu-item').click();
+
+    // Confirm search result elements are visible
+    const searchResultList = page.getByTestId('search-result-list');
+    await expect(searchResultList).toBeVisible();
+    await expect(searchResultList.locator('li')).toHaveCount(1);
+  });
+
+  test('Successfully order page search results by tag', async({ page }) => {
+    await page.goto('/');
+
+    await page.locator('.grw-tag-simple-bar').locator('a').click();
+
+    expect(page.getByTestId('search-result-base')).toBeVisible();
+    expect(page.getByTestId('search-result-list')).toBeVisible();
+    expect(page.getByTestId('search-result-content')).toBeVisible();
+  });
+});
+
+test.describe('Sort with dropdown', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/_search?q=sand');
+
+    await expect(page.getByTestId('search-result-base')).toBeVisible();
+    await expect(page.getByTestId('search-result-list')).toBeVisible();
+    await expect(page.getByTestId('search-result-content')).toBeVisible();
+
+    // open sort dropdown
+    await page.locator('.search-control').locator('button').first().click();
+  });
+
+  test('Open sort dropdown', async({ page }) => {
+    await expect(page.locator('.search-control .dropdown-menu.show')).toBeVisible();
+  });
+
+  test('Sort by relevance', async({ page }) => {
+    const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
+
+    await expect(dropdownMenu).toBeVisible();
+    await dropdownMenu.locator('.dropdown-item').nth(0).click();
+
+
+    await expect(page.getByTestId('search-result-base')).toBeVisible();
+    await expect(page.getByTestId('search-result-list')).toBeVisible();
+    await expect(page.getByTestId('search-result-content')).toBeVisible();
+  });
+
+  test('Sort by creation date', async({ page }) => {
+    const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
+
+    await expect(dropdownMenu).toBeVisible();
+    await dropdownMenu.locator('.dropdown-item').nth(1).click();
+
+
+    await expect(page.getByTestId('search-result-base')).toBeVisible();
+    await expect(page.getByTestId('search-result-list')).toBeVisible();
+    await expect(page.getByTestId('search-result-content')).toBeVisible();
+  });
+
+  test('Sort by last update date', async({ page }) => {
+    const dropdownMenu = page.locator('.search-control .dropdown-menu.show');
+
+    await expect(dropdownMenu).toBeVisible();
+    await dropdownMenu.locator('.dropdown-item').nth(2).click();
+
+
+    await expect(page.getByTestId('search-result-base')).toBeVisible();
+    await expect(page.getByTestId('search-result-list')).toBeVisible();
+    await expect(page.getByTestId('search-result-content')).toBeVisible();
+  });
+});
+
+test.describe('Search and use', () => {
+  test.beforeEach(async({ page }) => {
+    await page.goto('/_search?q=alerts');
+
+    await expect(page.getByTestId('search-result-base')).toBeVisible();
+    await expect(page.getByTestId('search-result-list')).toBeVisible();
+    await expect(page.getByTestId('search-result-content')).toBeVisible();
+
+    await page.getByTestId('page-list-item-L').first().getByTestId('open-page-item-control-btn').click();
+    await expect(page.locator('.dropdown-menu.show')).toBeVisible();
+  });
+
+  test('Successfully the dropdown is opened', async({ page }) => {
+    await expect(page.locator('.dropdown-menu.show')).toBeVisible();
+  });
+
+  test('Successfully add bookmark', async({ page }) => {
+    const dropdonwMenu = page.locator('.dropdown-menu.show');
+
+    await expect(dropdonwMenu).toBeVisible();
+
+    // Add bookmark
+    await dropdonwMenu.getByTestId('add-bookmark-btn').click();
+
+    await expect(page.getByTestId('search-result-content').locator('.btn-bookmark.active').first()).toBeVisible();
+  });
+
+  test('Successfully open duplicate modal', async({ page }) => {
+    const dropdonwMenu = page.locator('.dropdown-menu.show');
+
+    await expect(dropdonwMenu).toBeVisible();
+
+    await dropdonwMenu.getByTestId('open-page-duplicate-modal-btn').click();
+
+    await expect(page.getByTestId('page-duplicate-modal')).toBeVisible();
+  });
+
+  test('Successfully open move/rename modal', async({ page }) => {
+    const dropdonwMenu = page.locator('.dropdown-menu.show');
+
+    await expect(dropdonwMenu).toBeVisible();
+
+    await dropdonwMenu.getByTestId('rename-page-btn').click();
+
+    await expect(page.getByTestId('page-rename-modal')).toBeVisible();
+  });
+
+  test('Successfully open delete modal', async({ page }) => {
+    const dropdonwMenu = page.locator('.dropdown-menu.show');
+
+    await expect(dropdonwMenu).toBeVisible();
+
+    await dropdonwMenu.getByTestId('open-page-delete-modal-btn').click();
+
+    await expect(page.getByTestId('page-delete-modal')).toBeVisible();
+  });
+});
+
+test('Search current tree by word is successfully loaded', async({ page }) => {
+  await page.goto('/');
+  const searchText = 'GROWI';
+
+  await page.getByTestId('open-search-modal-button').click();
+  await expect(page.getByTestId('search-modal')).toBeVisible();
+  await page.locator('.form-control').fill(searchText);
+  await page.getByTestId('search-prefix-menu-item').click();
+
+  await expect(page.getByTestId('search-result-base')).toBeVisible();
+  await expect(page.getByTestId('search-result-list')).toBeVisible();
+  await expect(page.getByTestId('search-result-content')).toBeVisible();
+});

+ 103 - 0
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -0,0 +1,103 @@
+import { test, expect } from '@playwright/test';
+
+test('admin is successfully loaded', async({ page }) => {
+  await page.goto('/admin');
+
+  await expect(page.getByTestId('admin-home')).toBeVisible();
+  await expect(page.getByTestId('admin-system-information-table')).toBeVisible();
+});
+
+test('admin/app is successfully loaded', async({ page }) => {
+  await page.goto('/admin/app');
+
+  await expect(page.getByTestId('admin-app-settings')).toBeVisible();
+  // await expect(page.getByTestId('v5-page-migration')).toBeVisible();
+  await expect(page.locator('#cbFileUpload')).toBeChecked();
+  await expect(page.locator('#isQuestionnaireEnabled')).toBeChecked();
+  await expect(page.locator('#isAppSiteUrlHashed')).not.toBeChecked();
+});
+
+test('admin/security is successfully loaded', async({ page }) => {
+  await page.goto('/admin/security');
+
+  await expect(page.getByTestId('admin-security')).toBeVisible();
+  await expect(page.locator('#isShowRestrictedByOwner')).not.toBeChecked();
+  await expect(page.locator('#isShowRestrictedByGroup')).not.toBeChecked();
+});
+
+test('admin/markdown is successfully loaded', async({ page }) => {
+  await page.goto('/admin/markdown');
+
+  await expect(page.getByTestId('admin-markdown')).toBeVisible();
+  await expect(page.locator('#isEnabledLinebreaksInComments')).toBeChecked();
+});
+
+test('admin/customize is successfully loaded', async({ page }) => {
+  await page.goto('/admin/customize');
+
+  await expect(page.getByTestId('admin-customize')).toBeVisible();
+});
+
+test('admin/importer is successfully loaded', async({ page }) => {
+  await page.goto('/admin/importer');
+
+  await expect(page.getByTestId('admin-import-data')).toBeVisible();
+});
+
+test('admin/export is successfully loaded', async({ page }) => {
+  await page.goto('/admin/export');
+
+  await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
+});
+
+test('admin/data-transfer is successfully loaded', async({ page }) => {
+  await page.goto('/admin/data-transfer');
+
+  await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
+});
+
+test('admin/notification is successfully loaded', async({ page }) => {
+  await page.goto('/admin/notification');
+
+  await expect(page.getByTestId('admin-notification')).toBeVisible();
+  // wait for retrieving slack integration status
+  await expect(page.getByTestId('slack-integration-list-item')).toBeVisible();
+});
+
+test('admin/slack-integration is successfully loaded', async({ page }) => {
+  await page.goto('/admin/slack-integration');
+
+  await expect(page.getByTestId('admin-slack-integration')).toBeVisible();
+  await expect(page.locator('img.bot-difficulty-icon')).toHaveCount(3);
+  await expect(page.locator('img.bot-difficulty-icon').first()).toBeVisible();
+});
+
+test('admin/slack-integration-legacy is successfully loaded', async({ page }) => {
+  await page.goto('/admin/slack-integration-legacy');
+
+  await expect(page.getByTestId('admin-slack-integration-legacy')).toBeVisible();
+});
+
+test('admin/users is successfully loaded', async({ page }) => {
+  await page.goto('/admin/users');
+
+  await expect(page.getByTestId('admin-users')).toBeVisible();
+  await expect(page.getByTestId('user-table-tr').first()).toBeVisible();
+});
+
+test('admin/user-groups is successfully loaded', async({ page }) => {
+  await page.goto('/admin/user-groups');
+
+  await expect(page.getByTestId('admin-user-groups')).toBeVisible();
+  await expect(page.getByTestId('grw-user-group-table').first()).toBeVisible();
+});
+
+test('admin/search is successfully loaded', async({ page }) => {
+  await page.goto('/admin/search');
+
+  await expect(page.getByTestId('admin-full-text-search')).toBeVisible();
+
+  // Only successful in the local environment.
+  // wait for connected
+  // await expect(page.getByTestId('connection-status-badge-connected')).toBeVisible();
+});

+ 170 - 0
apps/app/playwright/50-sidebar/access-to-sidebar.spec.ts

@@ -0,0 +1,170 @@
+import { test, expect } from '@playwright/test';
+
+import { collapseSidebar } from '../utils';
+
+
+test.describe('Access to sidebar', () => {
+
+  test.beforeEach(async({ page }) => {
+    await page.goto('/');
+    await collapseSidebar(page, false);
+  });
+
+  test('Successfully show sidebar', async({ page }) => {
+    await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
+  });
+
+  test('Successfully access to page tree', async({ page }) => {
+    await page.getByTestId('grw-sidebar-nav-primary-page-tree').click();
+    await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
+    await expect(page.getByTestId('grw-pagetree-item-container').first()).toBeVisible();
+  });
+
+  test('Successfully access to recent changes', async({ page }) => {
+    await page.getByTestId('grw-sidebar-nav-primary-recent-changes').click();
+    await expect(page.getByTestId('grw-recent-changes')).toBeVisible();
+    await expect(page.locator('.list-group-item').first()).toBeVisible();
+  });
+
+  test('Successfully access to custom sidebar', async({ page }) => {
+    await page.getByTestId('grw-sidebar-nav-primary-custom-sidebar').click();
+    await expect(page.getByTestId('grw-sidebar-contents')).toBeVisible();
+    await expect(page.locator('.grw-sidebar-content-header > h3').locator('a')).toBeVisible();
+  });
+
+  test('Successfully access to GROWI Docs page', async({ page }) => {
+    const linkElement = page.locator('.grw-sidebar-nav-secondary-container a[href*="https://docs.growi.org"]');
+    const docsUrl = await linkElement.getAttribute('href');
+    if (docsUrl == null) {
+      throw new Error('url is null');
+    }
+    const response = await page.request.get(docsUrl);
+    const body = await response.text();
+    expect(body).toContain('</html>');
+  });
+
+  test('Successfully access to trash page', async({ page }) => {
+    await page.locator('.grw-sidebar-nav-secondary-container a[href*="/trash"]').click();
+    await expect(page.getByTestId('trash-page-list')).toBeVisible();
+  });
+
+
+  //
+  // Deactivate: An error occurs that cannot be reproduced in the development environment. -- Yuki Takei 2024.05.10
+  //
+
+  // it('Successfully click Add to Bookmarks button', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     // take a screenshot for dropdown menu
+  //     cy.screenshot(`${ssPrefix}page-tree-2-before-adding-bookmark`)
+  //     // click add remove bookmark btn
+  //     cy.getByTestid('add-bookmark-btn').click();
+  //   })
+
+  //   // show dropdown again
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     // expect to be visible
+  //     cy.getByTestid('remove-bookmark-btn').should('be.visible');
+  //     // take a screenshot for dropdown menu
+  //     cy.screenshot(`${ssPrefix}page-tree-2-after-adding-bookmark`);
+  //   });
+  // });
+
+  // it('Successfully show duplicate page modal', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     cy.getByTestid('open-page-duplicate-modal-btn').click();
+  //   })
+
+  //   cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
+  //     cy.get('.form-control').type('_test');
+
+  //     cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`, { blackout: blackoutOverride });
+
+  //     cy.get('.modal-header > button').click();
+  //   });
+  // });
+
+  // it('Successfully rename page', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     cy.getByTestid('rename-page-btn').click();
+  //   })
+
+  //   cy.getByTestid('grw-sidebar-contents').within(() => {
+  //     cy.getByTestid('autosize-submittable-input').type('_newname');
+  //   })
+
+  //   cy.screenshot(`${ssPrefix}page-tree-6-rename-page`, { blackout: blackoutOverride });
+  // });
+
+  // it('Successfully show delete page modal', () => {
+  //   cy.waitUntil(() => {
+  //     // do
+  //     cy.getByTestid('grw-sidebar-contents').within(() => {
+  //       cy.getByTestid('grw-pagetree-item-container').eq(1).within(() => { // against the second element
+  //         cy.get('li').realHover();
+  //         cy.getByTestid('open-page-item-control-btn').find('button').first().realClick();
+  //       });
+  //     });
+  //     // wait until
+  //     return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
+  //   });
+
+  //   cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+  //     cy.getByTestid('open-page-delete-modal-btn').click();
+  //   })
+
+  //   cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+  //     cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`, { blackout: blackoutOverride });
+  //     cy.get('.modal-header > button').click();
+  //   });
+  // });
+
+});

+ 43 - 0
apps/app/playwright/50-sidebar/switching-sidebar-mode.spec.ts

@@ -0,0 +1,43 @@
+import { test } from '@playwright/test';
+
+import { collapseSidebar } from '../utils';
+
+
+test('Switch sidebar mode', async({ page }) => {
+  await page.goto('/');
+  await collapseSidebar(page, false);
+  await collapseSidebar(page, true);
+});
+
+// Write tests using VRT
+// context('Switch viewport size', () => {
+//   const ssPrefix = 'switch-viewport-size-';
+
+//   const sizes = {
+//     'xl': [1200, 1024],
+//     'lg': [992, 1024],
+//     'md': [768, 1024],
+//     'sm': [576, 1024],
+//     'xs': [575, 1024],
+//     'iphone-x': [375, 812],
+//   };
+
+//   Object.entries(sizes).forEach(([screenLabel, size]) => {
+//     it(`on ${screenLabel} screen`, () => {
+//       cy.viewport(size[0], size[1]);
+
+//       // login
+//       cy.fixture("user-admin.json").then(user => {
+//         cy.login(user.username, user.password);
+//       });
+//       cy.visit('/');
+
+//       cy.get('.layout-root').should('be.visible');
+
+//       cy.screenshot(`${ssPrefix}-${screenLabel}`, {
+//         blackout: blackoutOverride,
+//       });
+//     });
+//   });
+
+// });

+ 122 - 0
apps/app/playwright/60-home/home.spec.ts

@@ -0,0 +1,122 @@
+import { test, expect } from '@playwright/test';
+
+
+test('Visit User home', async({ page }) => {
+  await page.goto('dummy');
+
+  // Open PersonalDropdown
+  await page.getByTestId('personal-dropdown-button').click();
+  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
+
+  // Click UserHomeMenu
+  await page.getByTestId('grw-personal-dropdown-menu-user-home').click();
+  await expect(page.getByTestId('grw-users-info')).toBeVisible();
+});
+
+test('Vist User settings', async({ page }) => {
+  await page.goto('dummy');
+
+  // Open PersonalDropdown
+  await page.getByTestId('personal-dropdown-button').click();
+  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
+
+  // Click UserSettingsMenu
+  page.getByTestId('grw-personal-dropdown-menu-user-settings').click();
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+});
+
+test('Open questionnaire modal', async({ page }) => {
+  await page.goto('/dummy');
+
+  // Open PersonalDropdown
+  await page.getByTestId('personal-dropdown-button').click();
+  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
+
+  // Expect the questionnaire modal to be displayed when the QuestionnaireModalToggleButton is clicked
+  await page.getByTestId('grw-proactive-questionnaire-modal-toggle-btn').click();
+  await expect(page.getByTestId('grw-proactive-questionnaire-modal')).toBeVisible();
+});
+
+test('Access User information', async({ page }) => {
+  await page.goto('/me');
+
+  // Click BasicInfoSettingUpdateButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+
+  // Expect a success toaster to be displayed when the BasicInfoSettingUpdateButton is pressed
+  await page.getByTestId('grw-besic-info-settings-update-button').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+});
+
+test('Access External account', async({ page }) => {
+  await page.goto('/me');
+
+  // Click ExternalAccountsTabButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+  await page.getByTestId('external-accounts-tab-button').first().click();
+
+  // Expect an error toaster to be displayed when the AddExternalAccountsButton is pressed
+  await page.getByTestId('grw-external-account-add-button').click();
+  await expect(page.getByTestId('grw-associate-modal')).toBeVisible();
+  await page.getByTestId('add-external-account-button').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+  await page.locator('.Toastify__close-button').click();
+  await expect(page.locator('.Toastify__toast')).not.toBeVisible();
+});
+
+test('Access Password setting', async({ page }) => {
+  await page.goto('/me');
+
+  // Click PasswordSettingTabButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+  await page.getByTestId('password-settings-tab-button').first().click();
+
+  // Expect three error toasters to be displayed when the PasswordUpdateButton is pressed
+  await page.getByTestId('grw-password-settings-update-button').click();
+  const toastElements = page.locator('.Toastify__toast');
+
+  const toastElementsCount = await toastElements.count();
+  for (let i = 0; i < toastElementsCount; i++) {
+    // eslint-disable-next-line no-await-in-loop
+    await toastElements.nth(i).click();
+  }
+
+  await expect(page.getByTestId('.Toastify__toast')).not.toBeVisible();
+});
+
+
+test('Access API setting', async({ page }) => {
+  await page.goto('/me');
+
+  // Click ApiSettingTabButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+  await page.getByTestId('api-settings-tab-button').first().click();
+
+  // Expect a success toaster to be displayed when the UpdateApiTokenButton is clicked
+  await page.getByTestId('grw-api-settings-update-button').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+});
+
+test('Access In-App Notification setting', async({ page }) => {
+  await page.goto('/me');
+
+  // Click InAppNotificationSettingTabButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+  await page.getByTestId('in-app-notification-settings-tab-button').first().click();
+
+  // Expect a success toaster to be displayed when the InAppNotificationSettingsUpdateButton is clicked
+  await page.getByTestId('grw-in-app-notification-settings-update-button').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+});
+
+test('Acccess Other setting', async({ page }) => {
+  await page.goto('/me');
+
+  // Click OtherSettingTabButton
+  await expect(page.getByTestId('grw-user-settings')).toBeVisible();
+  await page.getByTestId('other-settings-tab-button').first().click();
+
+  // Expect a success toaster to be displayed when the QuestionnaireSettingsUpdateButton is clicked
+  await page.getByTestId('grw-questionnaire-settings-update-btn').click();
+  await expect(page.locator('.Toastify__toast')).toBeVisible();
+});

+ 9 - 0
apps/app/playwright/auth.setup.ts

@@ -0,0 +1,9 @@
+import { test as setup } from '@playwright/test';
+
+import { login } from './utils/Login';
+
+// Commonised login process for use elsewhere
+// see: https://github.com/microsoft/playwright/issues/22114
+setup('Authenticate as the "admin" user', async({ page }) => {
+  await login(page);
+});

+ 19 - 0
apps/app/playwright/utils/CollapseSidebar.ts

@@ -0,0 +1,19 @@
+import { expect, type Page } from '@playwright/test';
+
+export const collapseSidebar = async(page: Page, isCollapsed: boolean): Promise<void> => {
+  const isSidebarContentsHidden = !(await page.getByTestId('grw-sidebar-contents').isVisible());
+  if (isSidebarContentsHidden === isCollapsed) {
+    return;
+  }
+
+  const collapseSidebarToggle = page.getByTestId('btn-toggle-collapse');
+  await expect(collapseSidebarToggle).toBeVisible();
+  await collapseSidebarToggle.click();
+
+  if (isCollapsed) {
+    await expect(page.locator('.grw-sidebar-dock')).not.toBeVisible();
+  }
+  else {
+    await expect(page.locator('.grw-sidebar-dock')).toBeVisible();
+  }
+};

+ 24 - 0
apps/app/playwright/utils/Login.ts

@@ -0,0 +1,24 @@
+import path from 'node:path';
+
+import { expect, type Page } from '@playwright/test';
+
+const authFile = path.resolve(__dirname, '../.auth/admin.json');
+
+export const login = async(page: Page): Promise<void> => {
+  // Perform authentication steps. Replace these actions with your own.
+  await page.goto('/admin');
+
+  const loginForm = await page.getByRole('form');
+
+  if (loginForm != null) {
+    await page.getByLabel('Username or E-mail').fill('admin');
+    await page.getByLabel('Password').fill('adminadmin');
+    await page.locator('[type=submit]').filter({ hasText: 'Login' }).click();
+  }
+
+  await page.waitForURL('/admin');
+  await expect(page).toHaveTitle(/Wiki Management Homepage/);
+
+  // End of authentication steps.
+  await page.context().storageState({ path: authFile });
+};

+ 2 - 0
apps/app/playwright/utils/index.ts

@@ -0,0 +1,2 @@
+export * from './CollapseSidebar';
+export * from './Login';

BIN
apps/app/public/favicon.ico


+ 24 - 0
apps/app/public/favicon.svg

@@ -0,0 +1,24 @@
+
+<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .logo {
+      fill: #777570;
+    }
+    .bg {
+      fill: white;
+    }
+    @media (prefers-color-scheme: dark) {
+      .logo {
+        fill: #E6E5E3;
+      }
+      .bg {
+        fill: black;
+      }
+    }
+  </style>
+<path class="bg" d="M47.134 22.5C47.6699 23.4282 47.6699 24.5718 47.134 25.5L36.866 43.2846C36.3301 44.2128 35.3397 44.7846 34.2679 44.7846L13.7321 44.7846C12.6603 44.7846 11.6699 44.2128 11.134 43.2846L0.866028 25.5C0.33013 24.5718 0.33013 23.4282 0.866028 22.5L11.134 4.71539C11.6699 3.78719 12.6603 3.21539 13.7321 3.21539L34.268 3.21539C35.3397 3.21539 36.3301 3.78719 36.866 4.71539L47.134 22.5Z"/>
+<path class="logo" d="M16.0962 27.332L12.5626 33.4861C12.4547 33.6759 12.4547 33.9097 12.5626 34.0961L15.2836 38.8337C15.3847 39.0065 15.5904 39.1251 15.7995 39.1251H16.0962L19.4848 33.2286L16.0962 27.332Z"/>
+<path class="logo" d="M33.9938 24.8076L29.3307 32.9272C29.243 33.0763 29.0845 33.2322 28.8148 33.2322H19.4819L16.0933 39.1254H32.2135C32.4226 39.1254 32.5979 39.0203 32.7058 38.8339L40.7642 24.8042H33.9938V24.8076Z"/>
+<path class="logo" d="M40.9127 24.5569C41.024 24.3671 41.0307 24.1536 40.9228 23.9639L38.1985 19.2229C38.0906 19.0331 37.9051 18.9111 37.686 18.9111H21.2892C21.0701 18.9111 20.8712 19.0297 20.7599 19.2127L18.1873 23.686L21.5758 29.5893L24.0676 25.2516C24.226 24.9805 24.516 24.8111 24.8262 24.8111H40.7677L40.9127 24.5603V24.5569Z"/>
+<path class="logo" d="M19.1953 15.2715H35.9292L32.7193 9.68338C32.6114 9.49361 32.426 9.375 32.2068 9.375H15.8101C15.5909 9.375 15.392 9.48344 15.2807 9.67322L7.08068 23.9435C6.97278 24.1333 6.97278 24.3638 7.08068 24.5535L10.2973 30.1519L18.6659 15.5698C18.7738 15.38 18.9761 15.2682 19.1953 15.2682V15.2715Z"/>
+</svg>

+ 7 - 0
apps/app/public/images/growi-brand-logo-login.svg

@@ -0,0 +1,7 @@
+<svg viewBox="0 0 353 78" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M352.62 2.32007H338V75.4701H352.62V2.32007Z" fill="white"/>
+<path d="M293.71 75.4701L279.67 28.4501L265.56 75.4701H251.85L230.46 2.32007H245.76L258.94 50.6901L272.85 5.33007H286.81L300.39 50.4801L313.26 2.32007H328.81L308.26 75.4701H293.71Z" fill="white"/>
+<path d="M186.671 77.28C181.341 77.28 176.351 76.33 171.831 74.45C167.301 72.57 163.331 69.89 160.041 66.5C156.741 63.11 154.131 59.01 152.271 54.31C150.411 49.62 149.471 44.37 149.471 38.7C149.471 33.03 150.411 27.58 152.271 22.89C154.131 18.19 156.751 14.14 160.051 10.84C163.351 7.55001 167.321 4.97001 171.851 3.19001C176.371 1.41001 181.361 0.51001 186.681 0.51001C192.001 0.51001 197.101 1.41001 201.651 3.19001C206.221 4.97001 210.221 7.55001 213.541 10.84C216.881 14.14 219.511 18.19 221.371 22.89C223.231 27.58 224.171 32.9 224.171 38.7C224.171 44.5 223.231 49.62 221.371 54.31C219.511 59 216.881 63.1 213.551 66.5C210.221 69.89 206.221 72.57 201.661 74.45C197.101 76.33 192.061 77.28 186.681 77.28H186.671ZM186.761 13.62C183.401 13.62 180.321 14.27 177.621 15.54C174.911 16.82 172.541 18.61 170.601 20.84C168.651 23.09 167.121 25.75 166.051 28.77C164.981 31.8 164.431 35.13 164.431 38.69C164.431 42.25 164.971 45.8 166.051 48.86C167.121 51.91 168.661 54.59 170.651 56.83C172.631 59.07 175.001 60.83 177.711 62.08C180.421 63.33 183.461 63.96 186.761 63.96C190.061 63.96 193.131 63.33 195.871 62.08C198.611 60.83 201.001 59.07 202.981 56.83C204.961 54.59 206.511 51.91 207.571 48.86C208.641 45.79 209.191 42.37 209.191 38.69C209.191 35.01 208.641 31.81 207.571 28.77C206.501 25.75 204.971 23.09 203.021 20.84C201.081 18.61 198.701 16.82 195.961 15.54C193.221 14.26 190.131 13.61 186.771 13.61L186.761 13.62Z" fill="white"/>
+<path d="M123 75.4701L107.33 48.3101H98.8104V75.4701H84.1904V2.32007H109.9C113.31 2.32007 116.66 2.68007 119.87 3.40007C123.12 4.13007 126.05 5.34007 128.59 7.01007C131.16 8.70007 133.24 10.9401 134.79 13.6801C136.34 16.4301 137.13 21.5601 137.13 25.4701C137.13 32.6801 132.83 41.7601 122.21 46.3201L139.04 75.4801H123.01L123 75.4701ZM108.2 36.0101C109.96 36.0101 111.74 35.8801 113.49 35.6201C115.18 35.3701 116.75 34.9401 118.07 34.2101C120.83 32.6801 122.52 28.4301 122.52 25.4801C122.52 22.5301 121.06 19.0801 118.76 17.2001C117.09 15.8301 114.48 14.9401 109.31 14.9401H98.8204V36.0101H108.2Z" fill="white"/>
+<path d="M40.0404 77.28C34.1804 77.28 28.7504 76.33 23.9004 74.45C19.0404 72.57 14.8004 69.89 11.3104 66.5C7.81039 63.11 5.05039 59 3.09039 54.31C1.14039 49.62 0.150391 44.37 0.150391 38.7C0.150391 33.03 1.17039 27.57 3.20039 22.88C5.22039 18.18 8.04039 14.13 11.5704 10.84C15.1004 7.55002 19.3204 4.98002 24.1104 3.20002C28.8904 1.42002 34.1504 0.52002 39.7304 0.52002C45.3104 0.52002 50.8804 1.37002 55.6904 3.04002C60.5204 4.72002 64.4804 6.97002 67.4504 9.73002L68.1204 10.35L60.4204 23.7C58.7404 21 53.1704 17.01 50.0604 15.66C46.9504 14.31 43.5104 13.63 39.8404 13.63C36.1704 13.63 32.9404 14.28 30.0304 15.56C27.1204 16.84 24.6004 18.62 22.5604 20.86C20.5104 23.1 18.9004 25.77 17.7804 28.79C16.6504 31.82 16.0804 35.15 16.0804 38.71C16.0804 42.27 16.6504 45.82 17.7804 48.88C18.9004 51.93 20.5304 54.61 22.6104 56.85C24.6804 59.08 27.2404 60.85 30.2204 62.09C33.2004 63.34 36.6404 63.98 40.4504 63.98C45.4604 63.98 49.7604 63.29 53.5104 61.83V48.33H46.1004L38.9304 36.02H68.1204V71.4C64.4504 73.17 59.5604 74.83 55.1804 75.81C50.8004 76.79 45.7004 77.29 40.0304 77.29L40.0404 77.28Z" fill="white"/>
+</svg>

+ 0 - 1
apps/app/public/images/icons/editor/bold.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="109" height="140" viewBox="0 0 10.9 14"><path d="M0 0h5.6c3 0 4.7 1.1 4.7 3.4a3.1 3.1 0 0 1-2.5 3.1 3.7 3.7 0 0 1 3.1 3.5c0 2.9-1.4 4-4.2 4H0zm5.2 6.5c2.7 0 2.6-1.4 2.6-3.1S7.9.7 5.6.7H2.3v5.8zm-2.9 6.6h3.4c2.1 0 2.7-1.1 2.7-3.1s0-2.8-3.2-2.8H2.3z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/check.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="144" height="160" viewBox="0 0 14.4 16"><path d="M13.9 5.5a.5.5 0 0 1 .5.5v9a1.1 1.1 0 0 1-1.1 1H1a1.1 1.1 0 0 1-1-1V2.6a1.1 1.1 0 0 1 1-1h7.1a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5H1V15h12.3V6a.6.6 0 0 1 .6-.5zM3.6 8.3a.5.5 0 0 0 0 .7l2.5 2.5a.8.8 0 0 0 1.1 0h.1l7-10.7c.1-.2.1-.6-.2-.7a.5.5 0 0 0-.7.1L6.6 10.6 4.3 8.3a.5.5 0 0 0-.7 0z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/code.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="181" height="140" viewBox="0 0 18.1 14"><path d="M17.8 7.9l-4 3.8a.5.5 0 0 1-.8 0 .5.5 0 0 1 0-.8L16.8 7 13 3.2a.6.6 0 0 1 0-.9.5.5 0 0 1 .8 0l4 3.8a1.3 1.3 0 0 1 0 1.8zM5.2 2.3a.7.7 0 0 1 0 .9L1.3 7l3.9 3.9a.6.6 0 0 1 0 .8.6.6 0 0 1-.9 0L.4 7.9a1.3 1.3 0 0 1 0-1.8l3.9-3.8a.6.6 0 0 1 .9 0zM11.5.8L7.8 13.6a.6.6 0 0 1-.7.4.6.6 0 0 1-.5-.8L10.3.4a.7.7 0 0 1 .8-.4.6.6 0 0 1 .4.8z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/header.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="137" height="140" viewBox="0 0 13.7 14"><path d="M10.2 0h2.9a.6.6 0 0 1 .6.6.6.6 0 0 1-.6.6h-.8v11.6h.8a.6.6 0 0 1 .6.6.6.6 0 0 1-.6.6h-2.9a.6.6 0 0 1-.6-.6.6.6 0 0 1 .6-.6h.8V7.2H2.7v5.6h.8a.6.6 0 0 1 .6.6.6.6 0 0 1-.6.6H.6a.6.6 0 0 1-.6-.6.6.6 0 0 1 .6-.6h.7V1.2H.6A.6.6 0 0 1 0 .6.6.6 0 0 1 .6 0h2.9a.6.6 0 0 1 .6.6.6.6 0 0 1-.6.6h-.8v4.9H11V1.2h-.8a.6.6 0 0 1-.6-.6.6.6 0 0 1 .6-.6z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/italic.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="86" height="139" viewBox="0 0 8.6 13.9"><path d="M8.1 0a.6.6 0 0 1 .5.6c0 .3-.2.6-.7.6H6.2L3.8 12.8h1.8c.2 0 .4.3.4.5a.7.7 0 0 1-.7.6H.5c-.3 0-.5-.4-.5-.6s.4-.6.7-.6h1.7L4.9 1.2H3.1a.5.5 0 0 1-.5-.5c0-.3.1-.7.8-.7z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/list-ol.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="237" height="160" viewBox="0 0 23.7 16"><path d="M23.7 2a.8.8 0 0 1-.8.8H6.6a.8.8 0 0 1-.7-.8.7.7 0 0 1 .7-.7h16.3a.7.7 0 0 1 .8.7zM6.6 8.7h16.3a.7.7 0 0 0 .8-.7.8.8 0 0 0-.8-.8H6.6a.8.8 0 0 0-.7.8.7.7 0 0 0 .7.7zm0 5.9h16.3a.7.7 0 0 0 .8-.7.7.7 0 0 0-.8-.7H6.6a.7.7 0 0 0-.7.7.7.7 0 0 0 .7.7zM1.5.5V4h.6V0h-.5L.7.5v.4l.8-.4zM.9 9.6l.3-.3c.9-.9 1.4-1.5 1.4-2.2a1.2 1.2 0 0 0-1.3-1.2h-.1a1.4 1.4 0 0 0-1.2.6l.3.4a1.2 1.2 0 0 1 .9-.5.6.6 0 0 1 .8.6v.2c0 .6-.4 1.1-1.5 2.1l-.4.4v.3h2.6v-.4zm.9 4.1a1 1 0 0 0 .7-.9 1 1 0 0 0-1.1-1 2 2 0 0 0-1.1.3v.4l.8-.2c.5 0 .8.2.8.6s-.5.7-.9.7H.7v.4H1c.6 0 1.1.2 1.1.8a.8.8 0 0 1-.9.8l-.9-.3-.2.4a2 2 0 0 0 1.1.3c1 0 1.5-.6 1.5-1.2a1.2 1.2 0 0 0-.9-1.1z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/list-ul.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="216" height="135" viewBox="0 0 21.6 13.5"><path d="M6.4 1.5h14.5a.7.7 0 0 0 .7-.7.7.7 0 0 0-.7-.7H6.4a.8.8 0 0 0-.8.7.8.8 0 0 0 .8.7zm0 6h14.5a.7.7 0 0 0 .7-.7.7.7 0 0 0-.7-.7H6.4a.8.8 0 0 0-.8.7.8.8 0 0 0 .8.7zm0 6h14.5a.7.7 0 0 0 .7-.7.7.7 0 0 0-.7-.7H6.4a.8.8 0 0 0-.8.7.8.8 0 0 0 .8.7zM.9 1.5h1a.8.8 0 0 0 .9-.7.8.8 0 0 0-.9-.8h-1a.8.8 0 0 0-.9.7.8.8 0 0 0 .9.8zm0 6h1a.8.8 0 0 0 .9-.7.8.8 0 0 0-.9-.8h-1a.8.8 0 0 0-.9.7.8.8 0 0 0 .9.8zm0 6h1a.8.8 0 0 0 .9-.7.8.8 0 0 0-.9-.7h-1a.8.8 0 0 0-.9.7.8.8 0 0 0 .9.7z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/picture.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="190" height="160" viewBox="0 0 19 16"><path d="M17.8 0H1.2A1.2 1.2 0 0 0 0 1.2v13.6A1.2 1.2 0 0 0 1.2 16h16.6a1.2 1.2 0 0 0 1.2-1.2V1.2a1.4 1.4 0 0 0-.2-.6.8.8 0 0 0-.4-.4zm0 14.8H1.2v-3.5l4.7-4.6 5 4.9.3.2.5-.2 2.1-1.9 3.9 4h.1v1.1zm0-2.8l-3.5-3.5-.4-.2h-.4l-2.2 2-4.9-4.8-.4-.2c-.2 0-.4 0-.5.2L1.2 9.7V1.2h16.6V12zm-4.2-6.1h.6a1.1 1.1 0 0 0 .6-1.1 1.2 1.2 0 0 0-1.2-1.1 1.3 1.3 0 0 0-1.2 1.2 1.2 1.2 0 0 0 .4.8 1.1 1.1 0 0 0 .8.3z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/quote.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="170" height="120" viewBox="0 0 17 12"><path d="M5 0H2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h3a1.7 1.7 0 0 0 1-.3V10a.9.9 0 0 1-1 1H3v1h2a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 6H2a.9.9 0 0 1-1-1V2a.9.9 0 0 1 1-1h3a.9.9 0 0 1 1 1v3a.9.9 0 0 1-1 1zm10-6h-3a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h3a1.7 1.7 0 0 0 1-.3V10a.9.9 0 0 1-1 1h-2v1h2a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 6h-3a.9.9 0 0 1-1-1V2a.9.9 0 0 1 1-1h3a.9.9 0 0 1 1 1v3a.9.9 0 0 1-1 1z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/strikethrough.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="195" height="140" viewBox="0 0 19.5 14"><path d="M5.8 6.2H9C7.2 5.7 6.3 5 6.3 3.8a2.2 2.2 0 0 1 .9-1.9 4.3 4.3 0 0 1 2.5-.7 4.3 4.3 0 0 1 2.5.7 3.1 3.1 0 0 1 1.1 1.6.7.7 0 0 0 .6.4h.3a.7.7 0 0 0 .4-.8A3.6 3.6 0 0 0 13.1 1a6.7 6.7 0 0 0-6-.5 3.1 3.1 0 0 0-1.7 1.3 3.6 3.6 0 0 0-.6 2 2.9 2.9 0 0 0 1 2.3zm7 2.5a2 2 0 0 1 .6 1.4 2.4 2.4 0 0 1-1 1.9 3.7 3.7 0 0 1-2.5.7 4.6 4.6 0 0 1-3-.8 3.7 3.7 0 0 1-1.2-2 .6.6 0 0 0-.6-.5h-.2a.7.7 0 0 0-.5.8 4.1 4.1 0 0 0 1.5 2.5A6 6 0 0 0 9.8 14a7.5 7.5 0 0 0 2.6-.5 4.9 4.9 0 0 0 1.8-1.4 4.3 4.3 0 0 0 .6-2.2 5 5 0 0 0-.2-1.2zM.4 7.9a.7.7 0 0 1-.4-.5.4.4 0 0 1 .4-.4h18.8a.4.4 0 0 1 .3.6c0 .1-.1.2-.2.3z"/></svg>

+ 0 - 1
apps/app/public/images/icons/editor/table.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="203" height="160" viewBox="0 0 20.3 16"><path d="M19.1 16H1.2A1.2 1.2 0 0 1 0 14.8V1.2A1.2 1.2 0 0 1 1.2 0h17.9a1.2 1.2 0 0 1 1.2 1.2v13.6a1.2 1.2 0 0 1-1.2 1.2zm-5.2-4.3v3.2h5.3v-3.2zm-6.4 0v3.2h5.3v-3.2zm-6.4 0v3.2h5.3v-3.2zm12.8-4.2v3.2h5.3V7.5zm-6.4 0v3.2h5.3V7.5zm-6.4 0v3.2h5.3V7.5zm12.8-4.3v3.2h5.3V3.2zm-6.4 0v3.2h5.3V3.2zm-6.4 0v3.2h5.3V3.2z"/></svg>

BIN
apps/app/public/images/icons/favicon/android-icon-144x144.png


BIN
apps/app/public/images/icons/favicon/android-icon-192x192.png


BIN
apps/app/public/images/icons/favicon/android-icon-36x36.png


BIN
apps/app/public/images/icons/favicon/android-icon-48x48.png


BIN
apps/app/public/images/icons/favicon/android-icon-72x72.png


BIN
apps/app/public/images/icons/favicon/android-icon-96x96.png


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