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

Merge commit 'ffcb885af03134f55dd22a687eab4b7c87d5516f' (7.0.x) into feat/opentelemetry

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

+ 26 - 0
.changeset/config.json

@@ -0,0 +1,26 @@
+{
+  "$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/markdown-splitter",
+    "@growi/editor",
+    "@growi/presentation",
+    "@growi/preset-*",
+    "@growi/remark-*",
+    "@growi/slack",
+    "@growi/ui"
+  ]
+}

+ 3 - 2
.devcontainer/devcontainer.json

@@ -19,14 +19,15 @@
     "eamodio.gitlens",
     "eamodio.gitlens",
     "github.vscode-pull-request-github",
     "github.vscode-pull-request-github",
     "cschleiden.vscode-github-actions",
     "cschleiden.vscode-github-actions",
+    "cweijan.vscode-database-client2",
     "mongodb.mongodb-vscode",
     "mongodb.mongodb-vscode",
     "msjsdiag.debugger-for-chrome",
     "msjsdiag.debugger-for-chrome",
     "firefox-devtools.vscode-firefox-debug",
     "firefox-devtools.vscode-firefox-debug",
     "editorconfig.editorconfig",
     "editorconfig.editorconfig",
-    "esbenp.prettier-vscode",
     "shinnn.stylelint",
     "shinnn.stylelint",
     "stylelint.vscode-stylelint",
     "stylelint.vscode-stylelint",
-    "vitest.explorer"
+    "vitest.explorer",
+    "ms-playwright.playwright"
   ],
   ],
 
 
   // Uncomment the next line if you want start specific services in your Docker Compose config.
   // Uncomment the next line if you want start specific services in your Docker Compose config.

+ 1 - 1
.eslintrc.js

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

+ 5 - 1
.github/dependabot.yml

@@ -4,7 +4,8 @@ updates:
     directory: '/'
     directory: '/'
     open-pull-requests-limit: 3
     open-pull-requests-limit: 3
     schedule:
     schedule:
-      interval: monthly
+      interval: weekly
+      day: saturday
     labels:
     labels:
       - "type/dependencies"
       - "type/dependencies"
     commit-message:
     commit-message:
@@ -16,6 +17,7 @@ updates:
     open-pull-requests-limit: 3
     open-pull-requests-limit: 3
     schedule:
     schedule:
       interval: weekly
       interval: weekly
+      day: saturday
     labels:
     labels:
       - "type/dependencies"
       - "type/dependencies"
     commit-message:
     commit-message:
@@ -27,4 +29,6 @@ updates:
       - dependency-name: "@handsontable/react"
       - dependency-name: "@handsontable/react"
       - dependency-name: handsontable
       - dependency-name: handsontable
       - dependency-name: typeorm
       - 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
     runs-on: ubuntu-latest
 
 
     if: |
     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:
     steps:
       - uses: release-drafter/release-drafter@v5
       - uses: release-drafter/release-drafter@v5
@@ -33,8 +35,9 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     if: |
     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:
     steps:
       - uses: amannn/action-semantic-pull-request@v5
       - uses: amannn/action-semantic-pull-request@v5

+ 28 - 28
.github/workflows/ci-app-prod.yml

@@ -7,6 +7,7 @@ on:
       - dev/7.*.x
       - dev/7.*.x
       - dev/6.*.x
       - dev/6.*.x
     paths:
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - .github/workflows/reusable-app-reg-suit.yml
@@ -18,12 +19,9 @@ on:
       - '!apps/app/docker/**'
       - '!apps/app/docker/**'
       - packages/**
       - packages/**
   pull_request:
   pull_request:
-    branches:
-      - master
-      - dev/7.*.x
-      - dev/6.*.x
     types: [opened, reopened, synchronize]
     types: [opened, reopened, synchronize]
     paths:
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
       - .github/workflows/reusable-app-reg-suit.yml
       - .github/workflows/reusable-app-reg-suit.yml
@@ -34,12 +32,6 @@ on:
       - apps/app/**
       - apps/app/**
       - '!apps/app/docker/**'
       - '!apps/app/docker/**'
       - packages/**
       - packages/**
-  workflow_call:
-    inputs:
-      cypress-config-video:
-        description: 'Enable video when running Cypress test'
-        type: boolean
-        default: false
 
 
 concurrency:
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -50,37 +42,45 @@ jobs:
 
 
   test-prod-node18:
   test-prod-node18:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    if: |
+      ( github.event_name == 'push'
+        || github.base_ref == 'master'
+        || github.base_ref == 'dev/7.*.x'
+        || startsWith( github.base_ref, 'release/' )
+        || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
       node-version: 18.x
       node-version: 18.x
-      skip-cypress: true
+      skip-e2e-test: true
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
 
 
   test-prod-node20:
   test-prod-node20:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    if: |
+      ( github.event_name == 'push'
+        || github.base_ref == 'master'
+        || github.base_ref == 'dev/7.*.x'
+        || startsWith( github.base_ref, 'release/' )
+        || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
       node-version: 20.x
       node-version: 20.x
-      skip-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:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       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 }}

+ 30 - 52
.github/workflows/ci-app.yml

@@ -5,7 +5,9 @@ on:
     branches-ignore:
     branches-ignore:
       - release/**
       - release/**
       - rc/**
       - rc/**
+      - changeset-release/**
     paths:
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-app.yml
       - .github/workflows/ci-app.yml
       - .eslint*
       - .eslint*
       - tsconfig.base.json
       - tsconfig.base.json
@@ -43,20 +45,21 @@ jobs:
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
-          key: node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/app/package.json') }}
+            !**/node_modules/.cache/turbo
+          key: node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
           restore-keys: |
           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 }}-
+            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
-      - name: Restore dist
-        uses: actions/cache/restore@v4
+      - name: Cache/Restore dist
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
             **/dist
             **/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: |
           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
       - name: Install dependencies
         run: |
         run: |
@@ -78,14 +81,6 @@ jobs:
           isCompactMode: true
           isCompactMode: true
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           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:
   ci-app-test:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -110,25 +105,25 @@ jobs:
           cache-dependency-path: '**/yarn.lock'
           cache-dependency-path: '**/yarn.lock'
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
-        id: cache-dependencies
         uses: actions/cache@v4
         uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
-          key: node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/app/package.json') }}
+            !**/node_modules/.cache/turbo
+          key: node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
           restore-keys: |
           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 }}-
+            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
-      - name: Restore dist
-        uses: actions/cache/restore@v4
+      - name: Cache/Restore dist
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
             **/dist
             **/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: |
           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
       - name: Install dependencies
         run: |
         run: |
@@ -138,7 +133,7 @@ jobs:
 
 
       - name: Test
       - name: Test
         run: |
         run: |
-          turbo run test --filter=!@growi/slackbot-proxy
+          turbo run test --filter=!@growi/slackbot-proxy --env-mode=loose
         env:
         env:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
 
@@ -160,14 +155,6 @@ jobs:
           isCompactMode: true
           isCompactMode: true
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           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:
   ci-app-launch-dev:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -192,26 +179,26 @@ jobs:
           cache-dependency-path: '**/yarn.lock'
           cache-dependency-path: '**/yarn.lock'
 
 
       - name: Cache/Restore node_modules
       - name: Cache/Restore node_modules
-        id: cache-dependencies
         uses: actions/cache@v4
         uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/node_modules
             **/node_modules
-          key: node_modules-7.x-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('apps/app/package.json') }}
+            !**/node_modules/.cache/turbo
+          key: node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
           restore-keys: |
           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 }}-
+            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
+            node_modules-app-devdependencies-${{ runner.OS }}-node${{ matrix.node-version }}-
 
 
-      - name: Restore dist
-        uses: actions/cache/restore@v4
+      - name: Cache/Restore dist
+        uses: actions/cache@v4
         with:
         with:
           path: |
           path: |
             **/.turbo
             **/.turbo
             **/dist
             **/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: |
           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
       - name: Install dependencies
         run: |
         run: |
@@ -219,11 +206,11 @@ jobs:
           yarn global add node-gyp
           yarn global add node-gyp
           yarn --frozen-lockfile
           yarn --frozen-lockfile
 
 
-      - name: turbo run dev:ci
+      - name: turbo run launch-dev:ci
         working-directory: ./apps/app
         working-directory: ./apps/app
         run: |
         run: |
           cp config/ci/.env.local.for-ci .env.development.local
           cp config/ci/.env.local.for-ci .env.development.local
-          turbo run dev:ci
+          turbo run launch-dev:ci --env-mode=loose
         env:
         env:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
 
 
@@ -236,12 +223,3 @@ jobs:
           channel: '#ci'
           channel: '#ci'
           isCompactMode: true
           isCompactMode: true
           url: ${{ secrets.SLACK_WEBHOOK_URL }}
           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') }}

+ 6 - 2
.github/workflows/ci-slackbot-proxy.yml

@@ -7,6 +7,7 @@ on:
       - rc/**
       - rc/**
       - support/prepare-v**
       - support/prepare-v**
     paths:
     paths:
+      - .github/mergify.yml
       - .github/workflows/ci-slackbot-proxy.yml
       - .github/workflows/ci-slackbot-proxy.yml
       - .eslint*
       - .eslint*
       - tsconfig.base.json
       - tsconfig.base.json
@@ -145,7 +146,7 @@ jobs:
       working-directory: ./apps/slackbot-proxy
       working-directory: ./apps/slackbot-proxy
       run: |
       run: |
         cp config/ci/.env.local.for-ci .env.development.local
         cp config/ci/.env.local.for-ci .env.development.local
-        turbo run dev:ci
+        turbo run dev:ci --env-mode=loose
       env:
       env:
         SERVER_URI: http://localhost:8080
         SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql
@@ -175,6 +176,9 @@ jobs:
 
 
 
 
   ci-slackbot-proxy-launch-prod:
   ci-slackbot-proxy-launch-prod:
+
+    if: startsWith(github.head_ref, 'mergify/merge-queue/')
+
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     strategy:
     strategy:
@@ -206,7 +210,7 @@ jobs:
 
 
     - name: Prune repositories
     - name: Prune repositories
       run: |
       run: |
-        turbo prune --scope=@growi/slackbot-proxy
+        turbo prune @growi/slackbot-proxy
         rm -rf apps packages
         rm -rf apps packages
         mv out/* .
         mv out/* .
 
 

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

@@ -26,7 +26,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
 
 
       - name: Retrieve information from package.json
       - 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
         id: package-json
 
 
       - uses: release-drafter/release-drafter@v5
       - uses: release-drafter/release-drafter@v5

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

@@ -23,7 +23,7 @@ jobs:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
 
 
     - name: Docker meta for docker.io
     - name: Docker meta for docker.io
@@ -65,16 +65,3 @@ jobs:
       tag-temporary: latest-rc
       tag-temporary: latest-rc
     secrets:
     secrets:
       DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
       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
     - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
 
 
     - name: Docker meta for docker.io
     - name: Docker meta for docker.io

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

@@ -17,7 +17,7 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
       with:
       with:
         workingDir: apps/slackbot-proxy
         workingDir: apps/slackbot-proxy
@@ -41,14 +41,14 @@ jobs:
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
 
 
     - name: Setup gcloud
     - name: Setup gcloud
-      uses: google-github-actions/setup-gcloud@v1
+      uses: google-github-actions/setup-gcloud@v2
 
 
     - name: Configure docker for gcloud
     - name: Configure docker for gcloud
       run: |
       run: |
         gcloud auth configure-docker --quiet
         gcloud auth configure-docker --quiet
 
 
     - name: Set up Docker Buildx
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v2
+      uses: docker/setup-buildx-action@v3
 
 
     - name: Build and push
     - name: Build and push
       uses: docker/build-push-action@v4
       uses: docker/build-push-action@v4
@@ -110,7 +110,7 @@ jobs:
         turbo run version --filter=@growi/slackbot-proxy -- --prerelease
         turbo run version --filter=@growi/slackbot-proxy -- --prerelease
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
       with:
       with:
         workingDir: apps/slackbot-proxy
         workingDir: apps/slackbot-proxy

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

@@ -0,0 +1,115 @@
+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: actions/setup-node@v4
+      with:
+        node-version: '20'
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v4
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-
+
+    - name: Install dependencies
+      run: |
+        yarn global add turbo
+        yarn global add node-gyp
+        yarn --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: |
+        yarn changeset status --output status.json
+        echo "CHANGESETS_LENGTH=$(jq -r '.changesets | length' status.json)" >> $GITHUB_OUTPUT
+        rm status.json
+
+    - name: Snapshot release to npm
+      if: steps.changesets-status.outputs.CHANGESETS_LENGTH > 0
+      run: |
+        yarn release-subpackages:snapshot
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+
+  release-subpackages:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v4
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-release-subpackages-${{ runner.OS }}-node${{ inputs.node-version }}-
+
+    - name: Install dependencies
+      run: |
+        yarn global add turbo
+        yarn global add node-gyp
+        yarn --frozen-lockfile
+
+    - name: Create Release Pull Request or Publish to npm
+      id: changesets
+      uses: changesets/action@v1
+      with:
+        title: Release Subpackages
+        version: yarn version-subpackages
+        publish: yarn release-subpackages
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

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

@@ -41,7 +41,7 @@ jobs:
         sh ./apps/app/bin/github-actions/update-readme.sh
         sh ./apps/app/bin/github-actions/update-readme.sh
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.2.0
+      uses: myrotvorets/info-from-package-json-action@2.0.1
       id: package-json
       id: package-json
 
 
     - name: Update Changelog
     - name: Update Changelog
@@ -58,13 +58,13 @@ jobs:
         RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
         RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
 
 
     - name: Commit, Tag and Push
     - name: Commit, Tag and Push
-      uses: stefanzweifel/git-auto-commit-action@v4
+      uses: stefanzweifel/git-auto-commit-action@v5
       with:
       with:
         branch: ${{ github.event.pull_request.base.ref }}
         branch: ${{ github.event.pull_request.base.ref }}
         commit_message: Release v${{ steps.package-json.outputs.packageVersion }}
         commit_message: Release v${{ steps.package-json.outputs.packageVersion }}
         tagging_message: v${{ steps.package-json.outputs.packageVersion }}
         tagging_message: v${{ steps.package-json.outputs.packageVersion }}
 
 
-    - uses: softprops/action-gh-release@v1
+    - uses: softprops/action-gh-release@v2
       with:
       with:
         body: ${{ github.event.pull_request.body }}
         body: ${{ github.event.pull_request.body }}
         tag_name: v${{ steps.package-json.outputs.packageVersion }}
         tag_name: v${{ steps.package-json.outputs.packageVersion }}
@@ -87,7 +87,7 @@ jobs:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
 
 
     - name: Docker meta for docker.io
     - name: Docker meta for docker.io
@@ -103,7 +103,7 @@ jobs:
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
 
 
 
 
-  build-image:
+  build-app-image:
     needs: create-github-release
     needs: create-github-release
 
 
     uses: weseek/growi/.github/workflows/reusable-app-build-image.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-build-image.yml@master
@@ -115,8 +115,8 @@ jobs:
       AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
       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
     uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
     with:
     with:
@@ -129,7 +129,7 @@ jobs:
 
 
 
 
   post-publish:
   post-publish:
-    needs: [create-github-release, publish-image]
+    needs: [create-github-release, publish-app-image]
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
@@ -154,7 +154,7 @@ jobs:
 
 
 
 
   create-pr-for-next-rc:
   create-pr-for-next-rc:
-    needs: [create-github-release, publish-image]
+    needs: [create-github-release, publish-app-image]
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
@@ -181,7 +181,7 @@ jobs:
         yarn upgrade --scope=@growi
         yarn upgrade --scope=@growi
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
 
 
     - name: Commit
     - name: Commit

+ 60 - 85
.github/workflows/reusable-app-prod.yml

@@ -6,13 +6,8 @@ on:
       node-version:
       node-version:
         required: true
         required: true
         type: string
         type: string
-      skip-cypress:
+      skip-e2e-test:
         type: boolean
         type: boolean
-      cypress-report-artifact-name-prefix:
-        type: string
-      cypress-config-video:
-        type: boolean
-        default: false
     secrets:
     secrets:
       SLACK_WEBHOOK_URL:
       SLACK_WEBHOOK_URL:
         required: true
         required: true
@@ -43,44 +38,43 @@ jobs:
 
 
     - name: Prune repositories
     - name: Prune repositories
       run: |
       run: |
-        turbo prune --scope=@growi/app
+        turbo prune @growi/app
         rm -rf apps packages
         rm -rf apps packages
         mv out/* .
         mv out/* .
 
 
     - name: Cache/Restore node_modules
     - name: Cache/Restore node_modules
-      id: cache-dependencies
       uses: actions/cache@v4
       uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
-        key: node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+          !**/node_modules/.cache/turbo
+        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
         restore-keys: |
-          node_modules-app-7.x-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
         yarn global add node-gyp
         yarn global add node-gyp
         yarn --frozen-lockfile
         yarn --frozen-lockfile
 
 
-    - name: Restore dist
+    - name: Cache/Restore dist
       uses: actions/cache@v4
       uses: actions/cache@v4
       with:
       with:
         path: |
         path: |
-          node_modules/.cache/turbo
           **/.turbo
           **/.turbo
           **/dist
           **/dist
+          **/node_modules/.cache/turbo
           ${{ github.workspace }}/apps/app/.next
           ${{ 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: |
         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
     - name: Build
       working-directory: ./apps/app
       working-directory: ./apps/app
       run: |
       run: |
-        turbo run build
+        turbo run build --env-mode=loose
       env:
       env:
-        ANALYZE_BUNDLE_SIZE: 1
+        ANALYZE: 1
 
 
     - name: Archive production files
     - name: Archive production files
       id: archive-prod-files
       id: archive-prod-files
@@ -155,19 +149,19 @@ jobs:
 
 
     - name: Prune repositories
     - name: Prune repositories
       run: |
       run: |
-        turbo prune --scope=@growi/app
+        turbo prune @growi/app
         rm -rf apps packages
         rm -rf apps packages
         mv out/* .
         mv out/* .
 
 
-    - name: Cache/Restore node_modules
-      id: cache-dependencies
-      uses: actions/cache@v4
+    - name: Restore node_modules
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
-        key: node_modules-app-7.x-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        # shared key with build-prod
+        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
         restore-keys: |
-          node_modules-app-7.x-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
@@ -202,19 +196,22 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
 
 
-
-  run-cypress:
+  run-playwright:
     needs: [build-prod]
     needs: [build-prod]
 
 
-    if: ${{ !inputs.skip-cypress }}
+    if: ${{ !inputs.skip-e2e-test && startsWith(github.head_ref, 'mergify/merge-queue/') }}
 
 
     runs-on: ubuntu-latest
     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:
     strategy:
       fail-fast: false
       fail-fast: false
       matrix:
       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:
     services:
       mongodb:
       mongodb:
@@ -231,49 +228,30 @@ jobs:
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
 
 
-    - name: Install fonts
-      run: sudo apt install fonts-noto
-
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         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
+    - name: Restore node_modules
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/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') }}
+        # saved key by build-prod
+        key: node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
         restore-keys: |
-          deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-app-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
         yarn global add node-gyp
         yarn global add node-gyp
         yarn --frozen-lockfile
         yarn --frozen-lockfile
-        yarn cypress install
+
+    - name: Install Playwright browsers
+      run: |
+        yarn playwright install --with-deps ${{ matrix.browser }}
 
 
     - name: Download production files artifact
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       uses: actions/download-artifact@v4
@@ -284,58 +262,55 @@ jobs:
       run: |
       run: |
         tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
         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
     - name: Copy dotenv file for ci
       working-directory: ./apps/app
       working-directory: ./apps/app
       run: |
       run: |
         cat config/ci/.env.local.for-ci >> .env.production.local
         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: |
+        yarn playwright test --project=chromium/installer
+      env:
+        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
     - name: Copy dotenv file for automatic installation
-      if: ${{ matrix.spec-group != '10' }}
       working-directory: ./apps/app
       working-directory: ./apps/app
       run: |
       run: |
         cat config/ci/.env.local.for-auto-install >> .env.production.local
         cat config/ci/.env.local.for-auto-install >> .env.production.local
 
 
+    - name: Playwright Run
+      working-directory: ./apps/app
+      run: |
+        yarn playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
+      env:
+        HOME: /root # ref: https://github.com/microsoft/playwright/issues/6500
+        MONGO_URI: mongodb://mongodb:27017/growi-playwright
+        ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
+
     - name: Copy dotenv file for automatic installation with allowing guest mode
     - name: Copy dotenv file for automatic installation with allowing guest mode
-      if: ${{ matrix.spec-group == '21' }}
       working-directory: ./apps/app
       working-directory: ./apps/app
       run: |
       run: |
         cat config/ci/.env.local.for-auto-install-with-allowing-guest >> .env.production.local
         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: |
+        yarn playwright test --project=${{ matrix.browser }}/guest-mode --shard=${{ matrix.shard }}
       env:
       env:
-        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
+        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
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
 
 
-    - name: Upload 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: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
       if: failure()
       if: failure()
       with:
       with:
         type: ${{ job.status }}
         type: ${{ job.status }}
-        job_name: '*Node CI for growi - run-cypress (${{ inputs.node-version }})*'
+        job_name: '*Node CI for growi - run-playwright*'
         channel: '#ci'
         channel: '#ci'
         isCompactMode: true
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
         url: ${{ secrets.SLACK_WEBHOOK_URL }}

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

@@ -60,25 +60,15 @@ jobs:
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         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
+    - name: Restore node_modules
+      uses: actions/cache/restore@v4
       with:
       with:
         path: |
         path: |
           **/node_modules
           **/node_modules
-        key: node_modules-7.x-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        # saved key by launch-prod
+        key: node_modules-app-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
         restore-keys: |
-          node_modules-7.x-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-app-launch-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
 
 
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |

+ 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

+ 14 - 5
.stylelintrc.json

@@ -1,16 +1,25 @@
 {
 {
   "extends": [
   "extends": [
+    "stylelint-config-recommended-scss",
     "stylelint-config-recess-order"
     "stylelint-config-recess-order"
   ],
   ],
-  "customSyntax": "postcss-scss",
   "rules": {
   "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", {
     "rule-empty-line-before": [ "always-multi-line", {
       "except": ["after-single-line-comment", "first-nested"],
       "except": ["after-single-line-comment", "first-nested"],
       "ignore": ["after-comment", "inside-block"]
       "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"
+        ]
+      }
+    ]
   }
   }
 }
 }

+ 357 - 1
CHANGELOG.md

@@ -1,9 +1,365 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.4...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.22...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.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
 ## [v7.0.4](https://github.com/weseek/growi/compare/v7.0.3...v7.0.4) - 2024-05-13
 
 
 ### 💎 Features
 ### 💎 Features

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

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

+ 0 - 2
apps/app/.gitignore

@@ -3,8 +3,6 @@
 /out/
 /out/
 
 
 # test
 # test
-test/cypress/screenshots
-test/cypress/videos
 .reg
 .reg
 
 
 # dist
 # 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": [
   "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',
+      },
+    },
+  },
+};

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

@@ -0,0 +1,28 @@
+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',
+      },
+    },
+  },
+};

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

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

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

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

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

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

+ 1 - 1
apps/app/docker/Dockerfile

@@ -12,7 +12,7 @@ WORKDIR ${optDir}
 
 
 RUN yarn global add turbo
 RUN yarn global add turbo
 COPY . .
 COPY . .
-RUN turbo prune --scope=@growi/app --docker
+RUN turbo prune @growi/app --docker
 
 
 
 
 ##
 ##

+ 2 - 2
apps/app/docker/README.md

@@ -4,13 +4,13 @@ GROWI Official docker image
 
 
 [![Actions Status](https://github.com/weseek/growi/workflows/Release/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
 [![Actions Status](https://github.com/weseek/growi/workflows/Release/badge.svg)](https://github.com/weseek/growi/actions) [![docker-pulls](https://img.shields.io/docker/pulls/weseek/growi.svg)](https://hub.docker.com/r/weseek/growi/) [![](https://images.microbadger.com/badges/image/weseek/growi.svg)](https://microbadger.com/images/weseek/growi)
 
 
-![GROWI-x-docker](https://user-images.githubusercontent.com/1638767/38307565-105956e2-384f-11e8-8534-b1128522d68d.png)
+![GROWI-x-docker](https://github.com/user-attachments/assets/1a82236d-5a85-4a2e-842a-971b4c1625e6)
 
 
 
 
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`7.0.4`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.4/apps/app/docker/Dockerfile)
+* [`7.0.22`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.22/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.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)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

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

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

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

@@ -48,6 +48,14 @@ const getTranspilePackages = () => {
     'emoticon',
     'emoticon',
     'direction', // for hast-util-select
     'direction', // for hast-util-select
     'bcp-47-match', // for hast-util-select
     'bcp-47-match', // for hast-util-select
+    'parse-entities',
+    'character-reference-invalid',
+    'is-hexadecimal',
+    'is-alphabetical',
+    'is-alphanumerical',
+    'github-slugger',
+    'html-url-attributes',
+    'estree-util-is-identifier-name',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
   ];
 
 
@@ -62,6 +70,20 @@ const getTranspilePackages = () => {
   return packages;
   return packages;
 };
 };
 
 
+const optimizePackageImports = [
+  '@growi/core',
+  '@growi/editor',
+  '@growi/markdown-splitter',
+  '@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 }) => {
 module.exports = async(phase, { defaultConfig }) => {
 
 
@@ -85,6 +107,9 @@ module.exports = async(phase, { defaultConfig }) => {
     transpilePackages: phase !== PHASE_PRODUCTION_SERVER
     transpilePackages: phase !== PHASE_PRODUCTION_SERVER
       ? getTranspilePackages()
       ? getTranspilePackages()
       : undefined,
       : undefined,
+    experimental: {
+      optimizePackageImports,
+    },
 
 
     /** @param config {import('next').NextConfig} */
     /** @param config {import('next').NextConfig} */
     webpack(config, options) {
     webpack(config, options) {
@@ -135,7 +160,7 @@ module.exports = async(phase, { defaultConfig }) => {
   }
   }
 
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
-    enabled: phase === PHASE_PRODUCTION_BUILD || process.env.ANALYZE === 'true',
+    enabled: phase === PHASE_PRODUCTION_BUILD && process.env.ANALYZE === 'true',
   });
   });
 
 
   return withBundleAnalyzer(withSuperjson()(nextConfig));
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 15 - 0
apps/app/nodemon.json

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

+ 79 - 75
apps/app/package.json

@@ -1,13 +1,14 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.0.5-RC.0",
+  "version": "7.1.0-RC.0",
   "license": "MIT",
   "license": "MIT",
+  "private": "true",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
     "build": "run-p build:*",
     "build": "run-p build:*",
     "start": "yarn next start",
     "start": "yarn next start",
     "build:client": "yarn next build",
     "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",
+    "build:server": "yarn cross-env NODE_ENV=production tspc -p tsconfig.build.server.json",
     "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
     "postbuild:server": "shx echo \"Listing files under transpiled\" && shx ls transpiled && shx rm -rf dist && shx mv transpiled/src dist && shx rm -rf transpiled",
     "clean": "shx rm -rf dist transpiled",
     "clean": "shx rm -rf dist transpiled",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
@@ -16,7 +17,7 @@
     "styles-prebuilt": "vite build -c vite.styles-prebuilt.config.ts",
     "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",
     "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up -f config/migrate-mongo-config.js",
     "//// for development": "",
     "//// for development": "",
-    "dev": "yarn cross-env NODE_ENV=development yarn ts-node-dev --inspect --transpile-only src/server/app.ts",
+    "dev": "yarn cross-env NODE_ENV=development nodemon --exec yarn ts-node --inspect src/server/app.ts",
     "dev:styles-prebuilt": "yarn styles-prebuilt --mode dev",
     "dev: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-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": "yarn dev:migrate:status > tmp/cache/migration-status.out && yarn dev:migrate:up",
@@ -24,32 +25,29 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status -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:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
-    "cy:run": "cypress run --browser chromium",
     "//// for CI": "",
     "//// for CI": "",
-    "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
-    "lint:typecheck": "npx -y tsc",
+    "launch-dev:ci": "yarn cross-env NODE_ENV=development yarn dev:migrate && yarn ts-node src/server/app.ts --ci",
+    "lint:typecheck": "npx -y tspc",
     "lint:eslint": "yarn eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
     "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",
+    "lint:styles": "stylelint \"src/**/*.scss\"",
+    "lint:swagger2openapi:apiv3": "node node_modules/.bin/oas-validate tmp/openapi-spec-apiv3.json",
+    "lint:swagger2openapi:apiv1": "node node_modules/.bin/oas-validate tmp/openapi-spec-apiv1.json",
     "lint": "run-p lint:*",
     "lint": "run-p lint:*",
-    "prelint:swagger2openapi": "yarn openapi:v3",
+    "prelint:swagger2openapi:apiv3": "yarn swagger2openapi:apiv3",
+    "prelint:swagger2openapi:apiv1": "yarn swagger2openapi:apiv1",
     "test": "run-p test:*",
     "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",
     "reg:run": "reg-suit run",
-    "vitest:run": "vitest run config src --coverage",
-    "vitest:run:integ": "vitest run -c vitest.config.integ.ts src --coverage",
-    "vitest:run:components": "vitest run -c vitest.config.components.ts src --coverage",
     "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "//// misc": "",
-    "console": "yarn 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": "yarn repl",
+    "repl": "yarn cross-env NODE_ENV=development yarn ts-node src/server/repl.ts",
+    "swagger2openapi:apiv3": "sh bin/swagger-jsdoc/generate-spec-apiv3.sh",
+    "swagger2openapi:apiv1": "sh bin/swagger-jsdoc/generate-spec-apiv1.sh",
+    "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config",
+    "version": "yarn version --no-git-tag-version --non-interactive --preid=RC"
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
     "@aws-skd/*": "fix version above 3.186.0 that is required by mongodb@4.16.0",
     "@aws-skd/*": "fix version above 3.186.0 that is required by mongodb@4.16.0",
@@ -63,7 +61,8 @@
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
-    "@azure/identity": "^4.0.1",
+    "@azure/identity": "^4.4.1",
+    "@azure/openai": "^2.0.0-beta.2",
     "@azure/storage-blob": "^12.16.0",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
@@ -71,8 +70,8 @@
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "link:../../packages/core",
     "@growi/core": "link:../../packages/core",
-    "@growi/custom-icons": "link:../../packages/custom-icons",
     "@growi/pluginkit": "link:../../packages/pluginkit",
     "@growi/pluginkit": "link:../../packages/pluginkit",
+    "@growi/presentation": "link:../../packages/presentation",
     "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-themes": "link:../../packages/preset-themes",
     "@growi/preset-themes": "link:../../packages/preset-themes",
     "@growi/remark-attachment-refs": "link:../../packages/remark-attachment-refs",
     "@growi/remark-attachment-refs": "link:../../packages/remark-attachment-refs",
@@ -98,7 +97,7 @@
     "async-canvas-to-blob": "^1.0.3",
     "async-canvas-to-blob": "^1.0.3",
     "axios": "^0.24.0",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
     "axios-retry": "^3.2.4",
-    "body-parser": "^1.18.2",
+    "body-parser": "^1.20.3",
     "browser-bunyan": "^1.8.0",
     "browser-bunyan": "^1.8.0",
     "bson-objectid": "^2.0.4",
     "bson-objectid": "^2.0.4",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
@@ -120,7 +119,7 @@
     "escape-string-regexp": "^4.0.0",
     "escape-string-regexp": "^4.0.0",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "expose-gc": "^1.0.0",
     "expose-gc": "^1.0.0",
-    "express": "^4.19.2",
+    "express": "^4.20.0",
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
     "express-mongo-sanitize": "^2.1.0",
     "express-mongo-sanitize": "^2.1.0",
     "express-session": "^1.16.1",
     "express-session": "^1.16.1",
@@ -128,7 +127,7 @@
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
-    "hast-util-select": "^5.0.5",
+    "hast-util-select": "^6.0.2",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "http-errors": "^2.0.0",
     "i18next": "^23.10.1",
     "i18next": "^23.10.1",
@@ -139,9 +138,9 @@
     "lucene-query-parser": "^1.2.0",
     "lucene-query-parser": "^1.2.0",
     "markdown-table": "^3.0.3",
     "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
-    "mermaid": "^10.1.0",
+    "mermaid": "^11.2.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
-    "migrate-mongo": "^8.2.3",
+    "migrate-mongo": "^11.0.0",
     "mkdirp": "^1.0.3",
     "mkdirp": "^1.0.3",
     "mongoose": "^6.11.3",
     "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-gridfs": "^1.2.42",
@@ -150,14 +149,16 @@
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
     "mustache": "^4.2.0",
-    "next": "^14.1.3",
+    "next": "^14.2.13",
+    "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.2.0",
     "next-i18next": "^15.2.0",
     "next-superjson": "^0.0.4",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.1",
     "next-themes": "^0.2.1",
-    "nocache": "^3.0.1",
+    "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
     "node-cron": "^3.0.2",
-    "nodemailer": "^6.6.2",
+    "nodemailer": "^6.9.15",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
+    "openai": "^4.56.0",
     "openid-client": "^5.4.0",
     "openid-client": "^5.4.0",
     "p-retry": "^4.0.0",
     "p-retry": "^4.0.0",
     "passport": "^0.6.0",
     "passport": "^0.6.0",
@@ -173,93 +174,94 @@
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-datepicker": "^4.7.0",
     "react-disable": "^0.1.1",
     "react-disable": "^0.1.1",
-    "react-dnd": "^14.0.5",
-    "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^3.1.4",
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^14.1.0",
     "react-i18next": "^14.1.0",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.7",
+    "react-markdown": "^9.0.1",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
     "react-stickynode": "^4.1.1",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
-    "react-toastify": "^9.1.3",
     "react-use-ripple": "^1.5.2",
     "react-use-ripple": "^1.5.2",
-    "reactstrap": "^9.2.0",
+    "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
-    "rehype-katex": "^6.0.2",
-    "rehype-raw": "^6.1.1",
-    "rehype-sanitize": "^5.0.1",
-    "rehype-slug": "^5.0.1",
+    "rehype-katex": "^7.0.0",
+    "rehype-raw": "^7.0.0",
+    "rehype-sanitize": "^6.0.0",
+    "rehype-slug": "^6.0.0",
     "rehype-toc": "^3.0.2",
     "rehype-toc": "^3.0.2",
-    "remark-breaks": "^3.0.2",
-    "remark-emoji": "^3.0.2",
-    "remark-frontmatter": "^4.0.1",
-    "remark-gfm": "^3.0.1",
-    "remark-math": "^5.1.1",
-    "remark-toc": "^8.0.1",
-    "remark-wiki-link": "^1.0.4",
+    "remark-breaks": "^4.0.0",
+    "remark-emoji": "^5.0.0",
+    "remark-frontmatter": "^5.0.0",
+    "remark-gfm": "^4.0.0",
+    "remark-math": "^6.0.0",
+    "remark-toc": "^9.0.0",
+    "remark-wiki-link": "^2.0.1",
     "sanitize-filename": "^1.6.3",
     "sanitize-filename": "^1.6.3",
-    "socket.io": "^4.7.2",
+    "socket.io": "^4.7.5",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "superjson": "^1.9.1",
-    "swagger-jsdoc": "^6.1.0",
+    "swagger-jsdoc": "^6.2.8",
     "swr": "^2.2.2",
     "swr": "^2.2.2",
     "throttle-debounce": "^5.0.0",
     "throttle-debounce": "^5.0.0",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
     "unstated": "^2.1.1",
-    "unzip-stream": "^0.3.1",
+    "unzip-stream": "^0.3.2",
     "url-join": "^4.0.0",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",
     "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": {
   "// 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/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": {
   "devDependencies": {
+    "@growi/core-styles": "link:../../packages/core-styles",
+    "@growi/custom-icons": "link:../../packages/custom-icons",
     "@growi/editor": "link:../../packages/editor",
     "@growi/editor": "link:../../packages/editor",
-    "@growi/presentation": "link:../../packages/presentation",
+    "@growi/markdown-splitter": "link:../../packages/markdown-splitter",
     "@growi/ui": "link:../../packages/ui",
     "@growi/ui": "link:../../packages/ui",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@next/bundle-analyzer": "^14.1.3",
     "@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",
     "@testing-library/user-event": "^14.5.2",
-    "@types/express": "^4.17.11",
+    "@types/express": "^4.17.21",
     "@types/jest": "^29.5.2",
     "@types/jest": "^29.5.2",
+    "@types/node-cron": "^3.0.11",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-scroll": "^1.8.4",
     "@types/react-stickynode": "^4.0.3",
     "@types/react-stickynode": "^4.0.3",
+    "@types/testing-library__dom": "^7.5.0",
     "@types/throttle-debounce": "^5.0.1",
     "@types/throttle-debounce": "^5.0.1",
     "@types/unzip-stream": "^0.3.4",
     "@types/unzip-stream": "^0.3.4",
     "@types/url-join": "^4.0.2",
     "@types/url-join": "^4.0.2",
-    "@vitejs/plugin-react": "^4.2.1",
-    "@vitest/coverage-v8": "^0.34.6",
-    "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
-    "bootstrap": "^5.3.1",
+    "bootstrap": "=5.3.2",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
-    "cypress-real-events": "^1.12.0",
     "diff2html": "^3.4.47",
     "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
-    "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "fslightbox-react": "^1.7.6",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
-    "happy-dom": "^13.2.0",
+    "happy-dom": "^15.7.4",
     "i18next-chained-backend": "^4.6.2",
     "i18next-chained-backend": "^4.6.2",
     "i18next-hmr": "^3.0.4",
     "i18next-hmr": "^3.0.4",
     "i18next-http-backend": "^2.5.0",
     "i18next-http-backend": "^2.5.0",
@@ -269,25 +271,27 @@
     "jest-localstorage-mock": "^2.4.14",
     "jest-localstorage-mock": "^2.4.14",
     "load-css-file": "^1.0.0",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
     "material-icons": "^1.11.3",
+    "mongodb": "4.16.0",
     "mongodb-memory-server-core": "^9.1.1",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
     "null-loader": "^4.0.1",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
-    "prettier": "^1.19.1",
     "pretty-bytes": "^6.1.1",
     "pretty-bytes": "^6.1.1",
-    "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
+    "react-dnd": "^14.0.5",
+    "react-dnd-html5-backend": "^14.1.0",
     "react-dropzone": "^14.2.3",
     "react-dropzone": "^14.2.3",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "react-input-autosize": "^3.0.0",
-    "rehype-rewrite": "^3.0.6",
+    "react-toastify": "^9.1.3",
+    "rehype-rewrite": "^4.0.2",
+    "remark-github-admonitions-to-directives": "^2.0.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",
     "sass": "^1.53.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
-    "socket.io-client": "^4.2.0",
+    "socket.io-client": "^4.7.5",
     "source-map-loader": "^4.0.1",
     "source-map-loader": "^4.0.1",
-    "swagger2openapi": "^7.0.8",
-    "tsc-alias": "^1.2.9"
+    "swagger2openapi": "^7.0.8"
   }
   }
 }
 }

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

@@ -0,0 +1,110 @@
+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' : 'list',
+
+  webServer: {
+    command: 'yarn 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/ItemsTree/ItemsTree.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.$('form#login-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


BIN
apps/app/public/images/icons/favicon/apple-icon-114x114.png


BIN
apps/app/public/images/icons/favicon/apple-icon-120x120.png


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


BIN
apps/app/public/images/icons/favicon/apple-icon-152x152.png


BIN
apps/app/public/images/icons/favicon/apple-icon-180x180.png


BIN
apps/app/public/images/icons/favicon/apple-icon-57x57.png


BIN
apps/app/public/images/icons/favicon/apple-icon-60x60.png


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


BIN
apps/app/public/images/icons/favicon/apple-icon-76x76.png


BIN
apps/app/public/images/icons/favicon/apple-icon-precomposed.png


BIN
apps/app/public/images/icons/favicon/apple-icon.png


+ 0 - 2
apps/app/public/images/icons/favicon/browserconfig.xml

@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

BIN
apps/app/public/images/icons/favicon/favicon-16x16.png


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