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

Merge branch 'master' into feat/g2g-nextjs

Taichi Masuyama 3 лет назад
Родитель
Сommit
ca85878998
100 измененных файлов с 713 добавлено и 2718 удалено
  1. 3 2
      .github/dependabot.yml
  2. 6 2
      .github/workflows/ci-app-prod.yml
  3. 5 0
      .github/workflows/ci-app.yml
  4. 5 0
      .github/workflows/ci-slackbot-proxy.yml
  5. 3 3
      .github/workflows/codeql-analysis.yml
  6. 26 0
      .github/workflows/dependabot-auto-approve.yml
  7. 5 0
      .github/workflows/draft-release.yml
  8. 5 0
      .github/workflows/pr-to-master.yml
  9. 10 2
      .github/workflows/release-rc.yml
  10. 4 4
      .github/workflows/release-slackbot-proxy.yml
  11. 10 15
      .github/workflows/release.yml
  12. 24 0
      .mergify.yml
  13. 48 1
      CHANGELOG.md
  14. 1 1
      lerna.json
  15. 1 1
      package.json
  16. 0 191
      packages/app/_obsolete/config/webpack.common.js
  17. 0 49
      packages/app/_obsolete/config/webpack.dev.dll.js
  18. 0 81
      packages/app/_obsolete/config/webpack.dev.js
  19. 0 77
      packages/app/_obsolete/config/webpack.prod.js
  20. 0 167
      packages/app/_obsolete/src/client/app.jsx
  21. 0 69
      packages/app/_obsolete/src/client/base.jsx
  22. 0 5
      packages/app/_obsolete/src/client/boot.js
  23. 0 60
      packages/app/_obsolete/src/client/installer.jsx
  24. 0 52
      packages/app/_obsolete/src/client/legacy/crowi.js
  25. 0 142
      packages/app/_obsolete/src/client/nologin.jsx
  26. 0 55
      packages/app/_obsolete/src/client/plugin.js
  27. 0 133
      packages/app/_obsolete/src/client/services/AppContainer.js
  28. 0 214
      packages/app/_obsolete/src/client/services/ContextExtractor.tsx
  29. 0 358
      packages/app/_obsolete/src/client/services/PageContainer.js
  30. 0 144
      packages/app/_obsolete/src/components/MyDraftList/Draft.tsx
  31. 0 192
      packages/app/_obsolete/src/components/MyDraftList/MyDraftList.jsx
  32. 0 76
      packages/app/_obsolete/src/util/i18n.js
  33. 0 17
      packages/app/_obsolete/src/util/old-ios.js
  34. 10 16
      packages/app/docker/Dockerfile
  35. 1 1
      packages/app/docker/README.md
  36. 16 17
      packages/app/package.json
  37. 15 4
      packages/app/public/static/locales/en_US/admin.json
  38. 14 1
      packages/app/public/static/locales/en_US/translation.json
  39. 15 4
      packages/app/public/static/locales/ja_JP/admin.json
  40. 15 1
      packages/app/public/static/locales/ja_JP/translation.json
  41. 15 4
      packages/app/public/static/locales/zh_CN/admin.json
  42. 15 2
      packages/app/public/static/locales/zh_CN/translation.json
  43. 6 6
      packages/app/resource/locales/en_US/admin/userInvitation.txt
  44. 8 8
      packages/app/resource/locales/en_US/admin/userWaitingActivation.txt
  45. 3 3
      packages/app/resource/locales/en_US/notifications/comment.txt
  46. 4 4
      packages/app/resource/locales/en_US/notifications/notActiveUser.txt
  47. 2 2
      packages/app/resource/locales/en_US/notifications/pageCreate.txt
  48. 2 2
      packages/app/resource/locales/en_US/notifications/pageDelete.txt
  49. 2 2
      packages/app/resource/locales/en_US/notifications/pageEdit.txt
  50. 2 2
      packages/app/resource/locales/en_US/notifications/pageLike.txt
  51. 2 2
      packages/app/resource/locales/en_US/notifications/pageMove.txt
  52. 4 4
      packages/app/resource/locales/en_US/notifications/passwordReset.txt
  53. 1 1
      packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt
  54. 4 4
      packages/app/resource/locales/en_US/notifications/userActivation.txt
  55. 1 11
      packages/app/resource/locales/en_US/sandbox.md
  56. 6 6
      packages/app/resource/locales/ja_JP/admin/userInvitation.txt
  57. 8 8
      packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt
  58. 4 4
      packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt
  59. 4 4
      packages/app/resource/locales/ja_JP/notifications/passwordReset.txt
  60. 1 1
      packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt
  61. 4 4
      packages/app/resource/locales/ja_JP/notifications/userActivation.txt
  62. 0 10
      packages/app/resource/locales/ja_JP/sandbox.md
  63. 6 6
      packages/app/resource/locales/zh_CN/admin/userInvitation.txt
  64. 8 8
      packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt
  65. 3 3
      packages/app/resource/locales/zh_CN/notifications/comment.txt
  66. 4 4
      packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt
  67. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageCreate.txt
  68. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageDelete.txt
  69. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageEdit.txt
  70. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageLike.txt
  71. 2 2
      packages/app/resource/locales/zh_CN/notifications/pageMove.txt
  72. 3 3
      packages/app/resource/locales/zh_CN/notifications/passwordReset.txt
  73. 1 1
      packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt
  74. 4 4
      packages/app/resource/locales/zh_CN/notifications/userActivation.txt
  75. 1 11
      packages/app/resource/locales/zh_CN/sandbox.md
  76. 0 20
      packages/app/src/client/services/AdminAppContainer.js
  77. 2 3
      packages/app/src/client/services/layout.ts
  78. 12 8
      packages/app/src/client/services/page-operation.ts
  79. 1 1
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  80. 0 8
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  81. 1 1
      packages/app/src/components/Admin/App/MailSetting.tsx
  82. 0 66
      packages/app/src/components/Admin/App/PluginSetting.tsx
  83. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  84. 31 42
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  85. 12 7
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  86. 4 1
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  87. 0 13
      packages/app/src/components/Admin/PluginsExtension/Loading.js
  88. 92 49
      packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx
  89. 17 36
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  90. 47 32
      packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx
  91. 3 3
      packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  92. 1 1
      packages/app/src/components/CompleteUserRegistration.tsx
  93. 1 1
      packages/app/src/components/CompleteUserRegistrationForm.tsx
  94. 21 48
      packages/app/src/components/IdenticalPathPage.tsx
  95. 4 1
      packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  96. 1 1
      packages/app/src/components/InstallerForm.tsx
  97. 1 1
      packages/app/src/components/InvitedForm.tsx
  98. 0 27
      packages/app/src/components/Layout/Login.module.scss
  99. 49 0
      packages/app/src/components/Layout/MainPane.tsx
  100. 32 46
      packages/app/src/components/Layout/NoLoginLayout.module.scss

+ 3 - 2
.github/dependabot.yml.org → .github/dependabot.yml

@@ -2,7 +2,7 @@ version: 2
 updates:
   - package-ecosystem: github-actions
     directory: '/'
-    open-pull-requests-limit: 0
+    open-pull-requests-limit: 3
     schedule:
       interval: monthly
     commit-message:
@@ -11,7 +11,7 @@ updates:
 
   - package-ecosystem: npm
     directory: '/'
-    open-pull-requests-limit: 0
+    open-pull-requests-limit: 3
     schedule:
       interval: weekly
     commit-message:
@@ -22,4 +22,5 @@ updates:
       - dependency-name: string-width
       - dependency-name: "@handsontable/react"
       - dependency-name: handsontable
+      - dependency-name: reveal.js
 

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

@@ -41,6 +41,10 @@ on:
         type: boolean
         default: false
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 
 jobs:
 
@@ -57,7 +61,7 @@ jobs:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     with:
       node-version: 16.x
-      skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
+      skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       cypress-report-artifact-name: Cypress report
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
     secrets:
@@ -73,7 +77,7 @@ jobs:
 
     with:
       node-version: 16.x
-      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
+      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       cypress-report-artifact-name: Cypress report
     secrets:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}

+ 5 - 0
.github/workflows/ci-app.yml

@@ -20,6 +20,11 @@ on:
       - packages/slack/**
       - packages/ui/**
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+
 jobs:
   lint:
     runs-on: ubuntu-latest

+ 5 - 0
.github/workflows/ci-slackbot-proxy.yml

@@ -16,6 +16,11 @@ on:
       - '!packages/slackbot-proxy/docker/**'
       - packages/slack/**
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+
 jobs:
 
   test:

+ 3 - 3
.github/workflows/codeql-analysis.yml

@@ -45,7 +45,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
+      uses: github/codeql-action/init@v2
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
+      uses: github/codeql-action/autobuild@v2
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@ jobs:
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
+      uses: github/codeql-action/analyze@v2

+ 26 - 0
.github/workflows/dependabot-auto-approve.yml

@@ -0,0 +1,26 @@
+# by https://zenn.dev/nemuki/articles/dependabot-auto-merge
+name: Auto approve on dependabot PR at patch update
+
+on:
+  pull_request_target:
+    types: [opened, reopened, synchronize]
+
+permissions:
+  pull-requests: write
+
+jobs:
+  dependabot-auto-approve:
+    runs-on: ubuntu-latest
+    if: ${{ github.actor == 'dependabot[bot]' }}
+    steps:
+      - name: Dependabot metadata
+        id: dependabot-metadata
+        uses: dependabot/fetch-metadata@v1
+        with:
+          github-token: '${{ secrets.GITHUB_TOKEN }}'
+      - name: Approve a PR
+        if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' }}
+        run: gh pr review --approve "$PR_URL"
+        env:
+          PR_URL: ${{ github.event.pull_request.html_url }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 5 - 0
.github/workflows/draft-release.yml

@@ -5,6 +5,11 @@ on:
     branches:
       - master
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+
 jobs:
 
   # Refs: https://github.com/release-drafter/release-drafter

+ 5 - 0
.github/workflows/pr-to-master.yml

@@ -7,6 +7,11 @@ on:
     # Only following types are handled by the action, but one can default to all as well
     types: [opened, reopened, edited, synchronize]
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+
 jobs:
 
   # Refs: https://github.com/release-drafter/release-drafter

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

@@ -11,6 +11,10 @@ jobs:
 
     runs-on: ubuntu-latest
 
+    strategy:
+      matrix:
+        platform: [linux/amd64, linux/arm64]
+
     steps:
     - uses: actions/checkout@v3
       with:
@@ -22,7 +26,7 @@ jobs:
 
     - name: Docker meta
       id: meta
-      uses: docker/metadata-action@v3
+      uses: docker/metadata-action@v4
       with:
         images: weseek/growi,ghcr.io/weseek/growi
         tags: |
@@ -40,6 +44,10 @@ jobs:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
+    - name: Set up QEMU
+      if: ${{ matrix.platform == 'linux/arm64' }}
+      uses: docker/setup-qemu-action@v1
+
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v2
 
@@ -48,7 +56,7 @@ jobs:
       with:
         context: .
         file: ./packages/app/docker/Dockerfile
-        platforms: linux/amd64
+        platforms: ${{ matrix.platform }}
         push: true
         builder: ${{ steps.buildx.outputs.name }}
         cache-from: type=gha

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

@@ -24,7 +24,7 @@ jobs:
 
     - name: Docker meta
       id: meta
-      uses: docker/metadata-action@v3
+      uses: docker/metadata-action@v4
       with:
         images: weseek/growi-slackbot-proxy,ghcr.io/weseek/growi-slackbot-proxy,asia.gcr.io/${{ secrets.GCP_PRJ_ID_SLACKBOT_PROXY }}/growi-slackbot-proxy
         tags: |
@@ -43,12 +43,12 @@ jobs:
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
     - name: Authenticate to Google Cloud for GROWI.cloud
-      uses: google-github-actions/auth@v0
+      uses: google-github-actions/auth@v1
       with:
         credentials_json: '${{ secrets.GCP_SA_KEY_SLACKBOT_PROXY }}'
 
     - name: Setup gcloud
-      uses: google-github-actions/setup-gcloud@v0
+      uses: google-github-actions/setup-gcloud@v1
 
     - name: Configure docker for gcloud
       run: |
@@ -75,7 +75,7 @@ jobs:
         mv /tmp/.buildx-cache-new /tmp/.buildx-cache
 
     - name: Add tag
-      uses: anothrNick/github-tag-action@1.38.0
+      uses: anothrNick/github-tag-action@v1
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         CUSTOM_TAG: v${{ steps.package-json.outputs.packageVersion }}

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

@@ -116,8 +116,8 @@ jobs:
         source_branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
         destination_branch: master
         pr_title: Prepare v${{ steps.package-json.outputs.packageVersion }}
-        pr_label: exclude from changelog
-        pr_body: "An automated PR generated by ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+        pr_label: exclude from changelog,prepare next version
+        pr_body: "An automated PR generated by create-pr-for-next-rc"
         github_token: ${{ secrets.GITHUB_TOKEN }}
 
 
@@ -128,7 +128,7 @@ jobs:
 
     strategy:
       matrix:
-        flavor: [default, nocdn]
+        platform: [linux/amd64, linux/arm64]
 
     steps:
     - uses: actions/checkout@v3
@@ -136,19 +136,11 @@ jobs:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         lfs: true
 
-    - name: Setup suffix
-      id: suffix
-      run: |
-        [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
-        echo "SUFFIX=$suffix" >> $GITHUB_OUTPUT
-
     - name: Docker meta
       id: meta
       uses: docker/metadata-action@v4
       with:
         images: weseek/growi,ghcr.io/weseek/growi
-        flavor: |
-          suffix=${{ steps.suffix.outputs.SUFFIX }}
         tags: |
           type=raw,value=latest
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}
@@ -168,6 +160,10 @@ jobs:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
+    - name: Set up QEMU
+      if: ${{ matrix.platform == 'linux/arm64' }}
+      uses: docker/setup-qemu-action@v1
+
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v2
 
@@ -176,10 +172,8 @@ jobs:
       with:
         context: .
         file: ./packages/app/docker/Dockerfile
-        platforms: linux/amd64
+        platforms: ${{ matrix.platform }}
         push: true
-        build-args: |
-          flavor=${{ matrix.flavor }}
         builder: ${{ steps.buildx.outputs.name }}
         cache-from: type=gha
         cache-to: type=gha,mode=max
@@ -198,7 +192,8 @@ jobs:
       with:
         channel: '#release'
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
-        created_tag: 'v${{ needs.create-github-release.outputs.RELEASED_VERSION }}${{ steps.suffix.outputs.SUFFIX }}'
+        created_tag: 'v${{ needs.create-github-release.outputs.RELEASED_VERSION }}'
+        message: '*Release v${{ needs.create-github-release.outputs.RELEASED_VERSION }} (${{ matrix.platform }})* Succeeded'
 
     - name: Check whether workspace is clean
       run: |

+ 24 - 0
.mergify.yml

@@ -0,0 +1,24 @@
+pull_request_rules:
+  - name: Automatic merge for Dependabot pull requests
+    conditions:
+      - author = dependabot[bot]
+      - '#approved-reviews-by >= 1'
+      - check-success = "lint (16.x)"
+      - check-success = "test (16.x)"
+      - check-success = "launch-dev (16.x)"
+      - check-success = "test-prod-node14 / launch-prod"
+      - check-success = "test-prod-node16 / launch-prod"
+    actions:
+      merge:
+        method: merge
+
+  - name: Automatic merge for Preparing next version
+    conditions:
+      - author = github-actions[bot]
+      - '#approved-reviews-by >= 1'
+      - check-skipped = "test (16.x)"
+      - check-skipped = "test-prod-node14 / launch-prod"
+      - check-skipped = "test-prod-node16 / launch-prod"
+    actions:
+      merge:
+        method: merge

+ 48 - 1
CHANGELOG.md

@@ -1,9 +1,56 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.1.7...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.1...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.0.1](https://github.com/weseek/growi/compare/v6.0.0...v6.0.1) - 2023-01-07
+
+### 🚀 Improvement
+
+- imprv: Reduce frequent API calling by SWR (#7218) @yuki-takei
+- imprv: Do not use api for fetching pages when using shared pages (#7213) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Custom logo not displayed on shared page (#7205) @miya
+- fix: Attach i18n User Setting and TagEditModal (#7216) @jam411
+- fix: Make PLANTUML_URI v5.x compatible (#7215) @yuki-takei
+- fix: Launch with PROMSTER_ENABLED=true failed (#7210) @yuki-takei
+- fix: Lsx performs with strange behavior (#7209) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Arm architecture (#7212) @yuki-takei
+- ci(deps): bump anothrNick/github-tag-action from 1.38.0 to 1.56.0 (#7195) @dependabot
+- ci(deps): bump google-github-actions/setup-gcloud from 0 to 1 (#7193) @dependabot
+- ci(deps): bump github/codeql-action from 1 to 2 (#7194) @dependabot
+- ci(deps): bump flat from 5.0.0 to 5.0.2 (#7200) @dependabot
+- ci(deps): bump json5 from 1.0.1 to 1.0.2 (#7201) @dependabot
+- ci(Mergify): configuration update (#7202) @yuki-takei
+- support: Uninstall swig-template (#7192) @yuki-takei
+
+## [v6.0.0](https://github.com/weseek/growi/compare/v5.1.8...v6.0.0) - 2022-12-27
+
+### 💎 Features
+
+- feat: Apply Next.js
+- feat: New plugin (#7034) @yuki-takei
+- feat: Save prevent xss custom settings in new format (#7124) @miya
+
+### 🧰 Maintenance
+
+- support: Request scoped SWR (#6742) @yuki-takei
+- support: Build preset themes within external package (#7057) @yuki-takei
+
+## [v5.1.8](https://github.com/weseek/growi/compare/v5.1.7...v5.1.8) - 2022-11-17
+
+### 🐛 Bug Fixes
+
+- fix: Put back page from trash (#6835) @yukendev
+- fix: Updating page content width is not working (#6914) @yukendev
+- fix: Create page at installer (#6930) @hakumizuki @yuki-takei
+
 ## [v5.1.7](https://github.com/weseek/growi/compare/v5.1.6...v5.1.7) - 2022-10-26
 
 ### 🐛 Bug Fixes

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "6.0.0-RC.13",
+  "version": "6.0.2-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.2-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 0 - 191
packages/app/_obsolete/config/webpack.common.js

@@ -1,191 +0,0 @@
-/* eslint-disable */
-/**
- * @author: Yuki Takei <yuki@weseek.co.jp>
- */
-const path = require('path');
-
-const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
-const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
-const webpack = require('webpack');
-
-/*
-  * Webpack Plugins
-  */
-const WebpackAssetsManifest = require('webpack-assets-manifest');
-
-/*
-  * Webpack configuration
-  *
-  * See: http://webpack.github.io/docs/configuration.html#cli
-  */
-module.exports = (options) => {
-  return {
-    mode: options.mode,
-    entry: Object.assign({
-      'js/boot':                      './src/client/boot',
-      'js/app':                       './src/client/app',
-      'js/admin':                     './src/client/admin',
-      'js/nologin':                   './src/client/nologin',
-      'js/installer':                   './src/client/installer',
-      'js/legacy':                    './src/client/legacy/crowi',
-      'js/legacy-presentation':       './src/client/legacy/crowi-presentation',
-      'js/plugin':                    './src/client/plugin',
-      'js/hackmd-agent':              './src/client/hackmd-agent',
-      'js/hackmd-styles':             './src/client/hackmd-styles',
-      // styles
-      'styles/style-app':             './src/styles/style-app.scss',
-      'styles/style-presentation':    './src/styles/style-presentation.scss',
-      // themes
-      'styles/theme-default':         './src/styles/theme/default.scss',
-      'styles/theme-nature':          './src/styles/theme/nature.scss',
-      'styles/theme-mono-blue':       './src/styles/theme/mono-blue.scss',
-      'styles/theme-future':          './src/styles/theme/future.scss',
-      'styles/theme-kibela':          './src/styles/theme/kibela.scss',
-      'styles/theme-halloween':       './src/styles/theme/halloween.scss',
-      'styles/theme-christmas':       './src/styles/theme/christmas.scss',
-      'styles/theme-wood':            './src/styles/theme/wood.scss',
-      'styles/theme-island':          './src/styles/theme/island.scss',
-      'styles/theme-antarctic':       './src/styles/theme/antarctic.scss',
-      'styles/theme-spring':          './src/styles/theme/spring.scss',
-      'styles/theme-hufflepuff':      './src/styles/theme/hufflepuff.scss',
-      'styles/theme-fire-red':      './src/styles/theme/fire-red.scss',
-      'styles/theme-jade-green':      './src/styles/theme/jade-green.scss',
-      'styles/theme-blackboard':      './src/styles/theme/blackboard.scss',
-      // styles for external services
-      'styles/style-hackmd':          './src/styles-hackmd/style.scss',
-    }, options.entry || {}), // Merge with env dependent settings
-    output: Object.assign({
-      path: path.resolve(__dirname, '../public'),
-      publicPath: '/',
-      filename: '[name].bundle.js',
-    }, options.output || {}), // Merge with env dependent settings
-    externals: {
-      // require("jquery") is external and available
-      //  on the global var jQuery
-      jquery: 'jQuery',
-      hljs: 'hljs',
-      'dtrace-provider': 'dtrace-provider',
-    },
-    resolve: {
-      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
-      plugins: [
-        new TsconfigPathsPlugin({
-          configFile: path.resolve(__dirname, '../tsconfig.build.client.json'),
-          extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
-        }),
-      ],
-    },
-    node: {
-      fs: 'empty',
-      module: 'empty',
-    },
-    module: {
-      rules: options.module.rules.concat([
-        {
-          test: /.(jsx?|tsx?)$/,
-          exclude: {
-            test: /node_modules/,
-            exclude: [ // include as a result
-              /node_modules\/codemirror/,
-            ],
-          },
-          use: [{
-            loader: 'ts-loader',
-            options: {
-              transpileOnly: true,
-              configFile: path.resolve(__dirname, '../tsconfig.build.client.json'),
-            },
-          }],
-        },
-        {
-          test: /locales/,
-          loader: '@alienfast/i18next-loader',
-          options: {
-            basenameAsNamespace: true,
-          },
-        },
-        /*
-          * File loader for supporting images, for example, in CSS files.
-          */
-        {
-          test: /\.(jpg|png|gif)$/,
-          use: 'file-loader',
-        },
-        /* File loader for supporting fonts, for example, in CSS files.
-         */
-        {
-          test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/,
-          use: 'null-loader',
-        },
-      ]),
-    },
-    plugins: options.plugins.concat([
-
-      new WebpackAssetsManifest({ publicPath: true }),
-
-      new webpack.DefinePlugin({
-        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
-      }),
-
-      // ignore
-      new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
-      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
-
-      new LodashModuleReplacementPlugin({
-        flattening: true,
-      }),
-
-      new webpack.ProvidePlugin({ // refs externals
-        jQuery: 'jquery',
-        $: 'jquery',
-      }),
-
-    ]),
-
-    devtool: options.devtool,
-    target: 'web', // Make web variables accessible to webpack, e.g. window
-    optimization: {
-      namedModules: true,
-      splitChunks: {
-        cacheGroups: {
-          style_commons: {
-            test: /\.(sc|sa|c)ss$/,
-            chunks: (chunk) => {
-              // ignore patterns
-              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/);
-            },
-            name: 'styles/style-commons',
-            minSize: 1,
-            priority: 30,
-            enforce: true,
-          },
-          commons: {
-            test: /(src|resource)[\\/].*\.(js|jsx|json)$/,
-            chunks: (chunk) => {
-              // ignore patterns
-              return chunk.name != null && !chunk.name.match(/boot/);
-            },
-            name: 'js/commons',
-            minChunks: 2,
-            minSize: 1,
-            priority: 20,
-          },
-          vendors: {
-            test: /node_modules[\\/].*\.(js|jsx|json)$/,
-            chunks: (chunk) => {
-              // ignore patterns
-              return chunk.name != null && !chunk.name.match(/boot|legacy-presentation|hackmd-/);
-            },
-            name: 'js/vendors',
-            minSize: 1,
-            priority: 10,
-            enforce: true,
-          },
-        },
-      },
-      minimizer: options.optimization.minimizer || [],
-    },
-    performance: options.performance || {},
-    stats: options.stats || {},
-  };
-};

+ 0 - 49
packages/app/_obsolete/config/webpack.dev.dll.js

@@ -1,49 +0,0 @@
-/* eslint-disable */
-/**
- * @author: Yuki Takei <yuki@weseek.co.jp>
- */
-const path = require('path');
-const webpack = require('webpack');
-
-
-module.exports = {
-  mode: 'development',
-  entry: {
-    dlls: [
-      // Libraries
-      'axios',
-      'browser-bunyan', 'bunyan-format',
-      'codemirror', 'react-codemirror2',
-      'date-fns',
-      'diff2html',
-      'debug',
-      'entities',
-      'i18next', 'i18next-browser-languagedetector',
-      'jquery-slimscroll',
-      'lodash', 'pako',
-      'markdown-it', 'csv-to-markdown-table',
-      'react', 'react-dom',
-      'reactstrap', 'react-bootstrap-typeahead',
-      'react-i18next', 'react-dropzone', 'react-hotkeys', 'react-copy-to-clipboard', 'react-waypoint',
-      'socket.io-client',
-      'toastr',
-      'unstated',
-      'xss',
-    ],
-  },
-  output: {
-    path: path.resolve(__dirname, '../public/dll'),
-    filename: 'dll.js',
-    library: 'growi_dlls',
-  },
-  resolve: {
-    extensions: ['.js', '.json'],
-    modules: [path.resolve(__dirname, '../src'), path.resolve(__dirname, '../node_modules')],
-  },
-  plugins: [
-    new webpack.DllPlugin({
-      path: path.resolve(__dirname, '../public/dll/manifest.json'),
-      name: 'growi_dlls',
-    }),
-  ],
-};

+ 0 - 81
packages/app/_obsolete/config/webpack.dev.js

@@ -1,81 +0,0 @@
-/* eslint-disable */
-/**
- * @author: Yuki Takei <yuki@weseek.co.jp>
- */
-
-const path = require('path');
-
-/*
- * Webpack Plugins
- */
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
-const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
-
-/**
- * Webpack Constants
- */
-const { ANALYZE } = process.env;
-
-module.exports = require('./webpack.common')({
-  mode: 'development',
-  devtool: 'cheap-module-eval-source-map',
-  entry: {
-    'js/dev': './src/client/dev',
-  },
-  resolve: {
-    modules: ['../node_modules'],
-  },
-  module: {
-    rules: [
-      {
-        test: /\.(css|scss)$/,
-        use: [
-          'style-loader',
-          { loader: 'css-loader', options: { sourceMap: true } },
-          { loader: 'sass-loader', options: { sourceMap: true } },
-        ],
-        exclude: [
-          path.resolve(__dirname, '../src/styles-hackmd'),
-          path.resolve(__dirname, '../src/styles/style-presentation.scss'),
-        ],
-      },
-      { // Dump CSS for HackMD
-        test: /\.(css|scss)$/,
-        use: [
-          MiniCssExtractPlugin.loader,
-          'css-loader',
-          'sass-loader',
-        ],
-        include: [
-          path.resolve(__dirname, '../src/styles-hackmd'),
-          path.resolve(__dirname, '../src/styles/style-presentation.scss'),
-        ],
-      },
-    ],
-  },
-  plugins: [
-
-    new MiniCssExtractPlugin({
-      filename: '[name].bundle.css',
-    }),
-
-    new BundleAnalyzerPlugin({
-      analyzerMode: ANALYZE ? 'server' : 'disabled',
-    }),
-
-    new HardSourceWebpackPlugin(),
-    new HardSourceWebpackPlugin.ExcludeModulePlugin([
-      {
-        // see https://github.com/mzgoddard/hard-source-webpack-plugin/blob/master/README.md#excludemoduleplugin
-        test: /mini-css-extract-plugin[\\/]dist[\\/]loader/,
-      },
-    ]),
-
-  ],
-  optimization: {},
-  performance: {
-    hints: false,
-  },
-
-});

+ 0 - 77
packages/app/_obsolete/config/webpack.prod.js

@@ -1,77 +0,0 @@
-/* eslint-disable */
-/**
- * @author: Yuki Takei <yuki@weseek.co.jp>
- */
-
-const path = require('path');
-
-/**
-  * Webpack Plugins
-  */
-const TerserPlugin = require('terser-webpack-plugin');
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
-const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
-
-/**
-  * Webpack Constants
-  */
-const { ANALYZE_BUNDLE_SIZE } = process.env;
-
-module.exports = require('./webpack.common')({
-  mode: 'production',
-  devtool: undefined,
-  output: {
-    filename: '[name].[chunkhash].bundle.js',
-    chunkFilename: '[name].[chunkhash].chunk.js',
-  },
-  module: {
-    rules: [
-      {
-        test: /\.(css|scss)$/,
-        use: [
-          MiniCssExtractPlugin.loader,
-          'css-loader',
-          {
-            loader: 'postcss-loader',
-            options: {
-              sourceMap: false,
-              postcssOptions: {
-                plugins: [
-                  require('autoprefixer')(),
-                ],
-              },
-            },
-          },
-          'sass-loader',
-        ],
-        exclude: [path.resolve(__dirname, '../src/client/legacy')],
-      },
-      {
-        test: /\.(css|scss)$/,
-        use: ['style-loader', 'css-loader', 'sass-loader'],
-        include: [path.resolve(__dirname, '../src/client/legacy')],
-      },
-    ],
-  },
-  plugins: [
-
-    new MiniCssExtractPlugin({
-      filename: '[name].[hash].css',
-    }),
-
-    new BundleAnalyzerPlugin({
-      analyzerMode: ANALYZE_BUNDLE_SIZE ? 'static' : 'disabled',
-      reportFilename: path.resolve(__dirname, '../report/bundle-analyzer.html'),
-      openAnalyzer: false,
-    }),
-
-  ],
-  optimization: {
-    minimize: true,
-    minimizer: [
-      new TerserPlugin({}),
-      new OptimizeCSSAssetsPlugin({}),
-    ],
-  },
-});

+ 0 - 167
packages/app/_obsolete/src/client/app.jsx

@@ -1,167 +0,0 @@
-import React from 'react';
-
-import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
-import ReactDOM from 'react-dom';
-import { I18nextProvider } from 'react-i18next';
-import { SWRConfig } from 'swr';
-import { Provider } from 'unstated';
-
-import ContextExtractor from '~/client/services/ContextExtractor';
-import EditorContainer from '~/client/services/EditorContainer';
-import PageContainer from '~/client/services/PageContainer';
-import { IdenticalPathPage } from '~/components/IdenticalPathPage';
-import PrivateLegacyPages from '~/components/PrivateLegacyPages';
-import loggerFactory from '~/utils/logger';
-import { swrGlobalConfiguration } from '~/utils/swr-utils';
-
-import ErrorBoundary from '../components/ErrorBoudary';
-import Fab from '../components/Fab';
-import ForbiddenPage from '../components/ForbiddenPage';
-import RecentlyCreatedIcon from '../components/Icons/RecentlyCreatedIcon';
-import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
-import PersonalSettings from '../components/Me/PersonalSettings';
-import MyDraftList from '../components/MyDraftList/MyDraftList';
-import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
-import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
-import NotFoundPage from '../components/NotFoundPage';
-import { Page } from '../components/Page';
-import DisplaySwitcher from '../components/Page/DisplaySwitcher';
-import RedirectedAlert from '../components/Page/RedirectedAlert';
-import ShareLinkAlert from '../components/Page/ShareLinkAlert';
-import { PageComment } from '../components/PageComment';
-import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
-import PageContentFooter from '../components/PageContentFooter';
-import BookmarkList from '../components/PageList/BookmarkList';
-import PageStatusAlert from '../components/PageStatusAlert';
-import { PageTimeline } from '../components/PageTimeline';
-import RecentCreated from '../components/RecentCreated/RecentCreated';
-import { SearchPage } from '../components/SearchPage';
-import Sidebar from '../components/Sidebar';
-import TrashPageList from '../components/TrashPageList';
-
-import { appContainer, componentMappings } from './base';
-
-const logger = loggerFactory('growi:cli:app');
-
-appContainer.initContents();
-
-const { i18n } = appContainer;
-const socketIoContainer = appContainer.getContainer('SocketIoContainer');
-
-// create unstated container instance
-const pageContainer = new PageContainer(appContainer);
-const editorContainer = new EditorContainer(appContainer);
-const injectableContainers = [
-  appContainer, socketIoContainer, pageContainer, editorContainer,
-];
-
-logger.info('unstated containers have been initialized');
-
-/**
- * define components
- *  key: id of element
- *  value: React Element
- */
-Object.assign(componentMappings, {
-  'grw-sidebar-wrapper': <Sidebar />,
-
-  'search-page': <SearchPage appContainer={appContainer} />,
-  'private-regacy-pages': <PrivateLegacyPages appContainer={appContainer} />,
-
-  'all-in-app-notifications': <InAppNotificationPage />,
-  'identical-path-page': <IdenticalPathPage />,
-
-  // 'revision-history': <PageHistory pageId={pageId} />,
-
-  'grw-page-status-alert-container': <PageStatusAlert />,
-
-  'maintenance-mode-content': <MaintenanceModeContent />,
-
-  'trash-page-list-container': <TrashPageList />,
-
-  'not-found-page': <NotFoundPage />,
-
-  'forbidden-page': <ForbiddenPage isLinkSharingDisabled={appContainer.config.disableLinkSharing} />,
-
-  'page-timeline': <PageTimeline />,
-
-  'personal-setting': <PersonalSettings />,
-  'my-drafts': <MyDraftList />,
-
-  'grw-fab-container': <Fab />,
-
-  'share-link-alert': <ShareLinkAlert />,
-  'redirected-alert': <RedirectedAlert />,
-});
-
-// additional definitions if data exists
-if (pageContainer.state.pageId != null) {
-  Object.assign(componentMappings, {
-    'page-comments-list': <PageComment appContainer={appContainer} pageId={pageContainer.state.pageId} isReadOnly={false} titleAlign="left" />,
-    'page-comment-write': <CommentEditorLazyRenderer appContainer={appContainer} pageId={pageContainer.state.pageId} />,
-    'page-content-footer': <PageContentFooter
-      createdAt={new Date(pageContainer.state.createdAt)}
-      updatedAt={new Date(pageContainer.state.updatedAt)}
-      creator={pageContainer.state.creator}
-      revisionAuthor={pageContainer.state.revisionAuthor}
-    />,
-
-    'recent-created-icon': <RecentlyCreatedIcon />,
-  });
-}
-if (pageContainer.state.creator != null) {
-  Object.assign(componentMappings, {
-    'user-created-list': <RecentCreated userId={pageContainer.state.creator._id} />,
-    'user-bookmark-list': <BookmarkList userId={pageContainer.state.creator._id} />,
-  });
-}
-if (pageContainer.state.path != null) {
-  Object.assign(componentMappings, {
-    // eslint-disable-next-line quote-props
-    'page': <Page />,
-    'grw-subnav-container': <GrowiContextualSubNavigation isLinkSharingDisabled={appContainer.config.disableLinkSharing} />,
-    'grw-subnav-switcher-container': <GrowiSubNavigationSwitcher isLinkSharingDisabled={appContainer.config.disableLinkSharing} />,
-    'display-switcher': <DisplaySwitcher />,
-  });
-}
-
-const renderMainComponents = () => {
-  Object.keys(componentMappings).forEach((key) => {
-    const elem = document.getElementById(key);
-    if (elem) {
-      ReactDOM.render(
-        <I18nextProvider i18n={i18n}>
-          <ErrorBoundary>
-            <SWRConfig value={swrGlobalConfiguration}>
-              <Provider inject={injectableContainers}>
-                <DndProvider backend={HTML5Backend}>
-                  {componentMappings[key]}
-                </DndProvider>
-              </Provider>
-            </SWRConfig>
-          </ErrorBoundary>
-        </I18nextProvider>,
-        elem,
-      );
-    }
-  });
-};
-
-// extract context before rendering main components
-const elem = document.getElementById('growi-context-extractor');
-if (elem != null) {
-  ReactDOM.render(
-    <SWRConfig value={swrGlobalConfiguration}>
-      <ContextExtractor></ContextExtractor>
-    </SWRConfig>,
-    elem,
-    renderMainComponents,
-  );
-}
-else {
-  renderMainComponents();
-}
-
-// initialize scrollpos-styler
-ScrollPosStyler.init();

+ 0 - 69
packages/app/_obsolete/src/client/base.jsx

@@ -1,69 +0,0 @@
-import React from 'react';
-
-import EventEmitter from 'events';
-
-import AppContainer from '~/client/services/AppContainer';
-import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
-import PutbackPageModal from '~/components/PutbackPageModal';
-import ShortcutsModal from '~/components/ShortcutsModal';
-import SystemVersion from '~/components/SystemVersion';
-import InterceptorManager from '~/services/interceptor-manager';
-import loggerFactory from '~/utils/logger';
-
-import EmptyTrashModal from '../components/EmptyTrashModal';
-import HotkeysManager from '../components/Hotkeys/HotkeysManager';
-import { GrowiNavbar } from '../components/Navbar/GrowiNavbar';
-import { GrowiNavbarBottom } from '../components/Navbar/GrowiNavbarBottom';
-import PageAccessoriesModal from '../components/PageAccessoriesModal';
-import PageCreateModal from '../components/PageCreateModal';
-import PageDeleteModal from '../components/PageDeleteModal';
-import PageDuplicateModal from '../components/PageDuplicateModal';
-import PagePresentationModal from '../components/PagePresentationModal';
-import PageRenameModal from '../components/PageRenameModal';
-
-import ShowPageAccessoriesModal from './services/ShowPageAccessoriesModal';
-
-const logger = loggerFactory('growi:cli:app');
-
-if (!window) {
-  window = {};
-}
-
-window.globalEmitter = new EventEmitter();
-window.interceptorManager = new InterceptorManager();
-
-// create unstated container instance
-const appContainer = new AppContainer();
-
-appContainer.initApp();
-
-logger.info('AppContainer has been initialized');
-
-/**
- * define components
- *  key: id of element
- *  value: React Element
- */
-const componentMappings = {
-  'grw-navbar': <GrowiNavbar />,
-  'grw-navbar-bottom-container': <GrowiNavbarBottom />,
-
-  'page-create-modal': <PageCreateModal />,
-  'page-delete-modal': <PageDeleteModal />,
-  'empty-trash-modal': <EmptyTrashModal />,
-  'page-duplicate-modal': <PageDuplicateModal />,
-  'page-rename-modal': <PageRenameModal />,
-  'page-presentation-modal': <PagePresentationModal />,
-  'page-accessories-modal': <PageAccessoriesModal />,
-  'descendants-page-list-modal': <DescendantsPageListModal />,
-  'page-put-back-modal': <PutbackPageModal />,
-  'shortcuts-modal': <ShortcutsModal />,
-
-  'grw-hotkeys-manager': <HotkeysManager />,
-  'system-version': <SystemVersion />,
-
-
-  'show-page-accessories-modal': <ShowPageAccessoriesModal />,
-};
-
-export { appContainer, componentMappings };

+ 0 - 5
packages/app/_obsolete/src/client/boot.js

@@ -1,5 +0,0 @@
-import {
-  applyOldIos,
-} from './util/old-ios';
-
-applyOldIos();

+ 0 - 60
packages/app/_obsolete/src/client/installer.jsx

@@ -1,60 +0,0 @@
-import React from 'react';
-
-import ReactDOM from 'react-dom';
-import { I18nextProvider } from 'react-i18next';
-import { SWRConfig } from 'swr';
-
-
-import { swrGlobalConfiguration } from '~/utils/swr-utils';
-
-import InstallerForm from '../components/InstallerForm';
-
-import ContextExtractor from './services/ContextExtractor';
-import { i18nFactory } from './util/i18n';
-
-const i18n = i18nFactory();
-
-const componentMappings = {};
-
-// render InstallerForm
-const installerFormContainerElem = document.getElementById('installer-form-container');
-if (installerFormContainerElem) {
-  const userName = installerFormContainerElem.dataset.userName;
-  const name = installerFormContainerElem.dataset.name;
-  const email = installerFormContainerElem.dataset.email;
-
-  Object.assign(componentMappings, {
-    'installer-form-container': <InstallerForm userName={userName} name={name} email={email} />,
-  });
-}
-
-const renderMainComponents = () => {
-  Object.keys(componentMappings).forEach((key) => {
-    const elem = document.getElementById(key);
-    if (elem) {
-      ReactDOM.render(
-        <I18nextProvider i18n={i18n}>
-          <SWRConfig value={swrGlobalConfiguration}>
-            {componentMappings[key]}
-          </SWRConfig>
-        </I18nextProvider>,
-        elem,
-      );
-    }
-  });
-};
-
-// extract context before rendering main components
-const elem = document.getElementById('growi-context-extractor');
-if (elem != null) {
-  ReactDOM.render(
-    <SWRConfig value={swrGlobalConfiguration}>
-      <ContextExtractor></ContextExtractor>
-    </SWRConfig>,
-    elem,
-    renderMainComponents,
-  );
-}
-else {
-  renderMainComponents();
-}

+ 0 - 52
packages/app/_obsolete/src/client/legacy/crowi.js

@@ -1,52 +0,0 @@
-/* eslint-disable react/jsx-filename-extension */
-require('jquery.cookie');
-
-const Crowi = {};
-
-if (!window) {
-  window = {};
-}
-window.Crowi = Crowi;
-
-Crowi.setCaretLine = function(line) {
-  // eslint-disable-next-line no-undef
-  globalEmitter.emit('setCaretLine', line);
-};
-
-// original: middleware.swigFilter
-Crowi.userPicture = function(user) {
-  if (!user) {
-    return '/images/icons/user.svg';
-  }
-
-  return user.image || '/images/icons/user.svg';
-};
-
-Crowi.initClassesByOS = function() {
-  // add classes to cmd-key by OS
-  const platform = navigator.platform.toLowerCase();
-  const isMac = (platform.indexOf('mac') > -1);
-
-  document.querySelectorAll('.system-version .cmd-key').forEach((element) => {
-    if (isMac) {
-      element.classList.add('mac');
-    }
-    else {
-      element.classList.add('win');
-    }
-  });
-
-  document.querySelectorAll('#shortcuts-modal .cmd-key').forEach((element) => {
-    if (isMac) {
-      element.classList.add('mac');
-    }
-    else {
-      element.classList.add('win', 'key-longer');
-    }
-  });
-};
-
-// adjust min-height of page for print temporarily
-window.onbeforeprint = function() {
-  $('#page-wrapper').css('min-height', '0px');
-};

+ 0 - 142
packages/app/_obsolete/src/client/nologin.jsx

@@ -1,142 +0,0 @@
-import React from 'react';
-
-import ReactDOM from 'react-dom';
-import { I18nextProvider } from 'react-i18next';
-import { SWRConfig } from 'swr';
-import { Provider } from 'unstated';
-
-
-import AppContainer from '~/client/services/AppContainer';
-import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
-import { swrGlobalConfiguration } from '~/utils/swr-utils';
-
-import LoginForm from '../components/LoginForm';
-import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
-import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
-
-import ContextExtractor from './services/ContextExtractor';
-import { i18nFactory } from './util/i18n';
-
-const i18n = i18nFactory();
-
-
-const componentMappings = {};
-
-const appContainer = new AppContainer();
-appContainer.initApp();
-
-// render loginForm
-const loginFormElem = document.getElementById('login-form');
-if (loginFormElem) {
-  const username = loginFormElem.dataset.username;
-  const name = loginFormElem.dataset.name;
-  const email = loginFormElem.dataset.email;
-  const isRegistrationEnabled = loginFormElem.dataset.isRegistrationEnabled === 'true';
-  const isEmailAuthenticationEnabled = loginFormElem.dataset.isEmailAuthenticationEnabled === 'true';
-  const registrationMode = loginFormElem.dataset.registrationMode;
-  const isPasswordResetEnabled = loginFormElem.dataset.isPasswordResetEnabled === 'true';
-
-
-  let registrationWhiteList = loginFormElem.dataset.registrationWhiteList;
-  registrationWhiteList = registrationWhiteList.length > 0
-    ? registrationWhiteList = loginFormElem.dataset.registrationWhiteList.split(',')
-    : registrationWhiteList = [];
-
-
-  const isLocalStrategySetup = loginFormElem.dataset.isLocalStrategySetup === 'true';
-  const isLdapStrategySetup = loginFormElem.dataset.isLdapStrategySetup === 'true';
-  const objOfIsExternalAuthEnableds = {
-    google: loginFormElem.dataset.isGoogleAuthEnabled === 'true',
-    github: loginFormElem.dataset.isGithubAuthEnabled === 'true',
-    facebook: loginFormElem.dataset.isFacebookAuthEnabled === 'true',
-    saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
-    oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
-  };
-
-  Object.assign(componentMappings, {
-    [loginFormElem.id]: (
-      <LoginForm
-        username={username}
-        name={name}
-        email={email}
-        isRegistrationEnabled={isRegistrationEnabled}
-        isEmailAuthenticationEnabled={isEmailAuthenticationEnabled}
-        registrationMode={registrationMode}
-        registrationWhiteList={registrationWhiteList}
-        isPasswordResetEnabled={isPasswordResetEnabled}
-        isLocalStrategySetup={isLocalStrategySetup}
-        isLdapStrategySetup={isLdapStrategySetup}
-        objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
-      />
-    ),
-  });
-}
-
-// render PasswordResetRequestForm
-const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
-if (passwordResetRequestFormElem) {
-  Object.assign(componentMappings, {
-    [passwordResetRequestFormElem.id]: <PasswordResetRequestForm />,
-  });
-}
-
-// render PasswordResetExecutionForm
-const passwordResetExecutionFormElem = document.getElementById('password-reset-execution-form');
-if (passwordResetExecutionFormElem) {
-  Object.assign(componentMappings, {
-    [passwordResetExecutionFormElem.id]: <PasswordResetExecutionForm />,
-  });
-}
-
-// render UserActivationForm
-const UserActivationForm = document.getElementById('user-activation-form');
-if (UserActivationForm) {
-  const messageErrors = UserActivationForm.dataset.messageErrors;
-  const inputs = UserActivationForm.dataset.inputs;
-  const email = UserActivationForm.dataset.email;
-  const token = UserActivationForm.dataset.token;
-
-  Object.assign(componentMappings, {
-    [UserActivationForm.id]: (
-      <CompleteUserRegistrationForm
-        messageErrors={messageErrors}
-        inputs={inputs}
-        email={email}
-        token={token}
-      />
-    ),
-  });
-}
-
-const renderMainComponents = () => {
-  Object.keys(componentMappings).forEach((key) => {
-    const elem = document.getElementById(key);
-    if (elem) {
-      ReactDOM.render(
-        <I18nextProvider i18n={i18n}>
-          <SWRConfig value={swrGlobalConfiguration}>
-            <Provider inject={[appContainer]}>
-              {componentMappings[key]}
-            </Provider>
-          </SWRConfig>
-        </I18nextProvider>,
-        elem,
-      );
-    }
-  });
-};
-
-// extract context before rendering main components
-const elem = document.getElementById('growi-context-extractor');
-if (elem != null) {
-  ReactDOM.render(
-    <SWRConfig value={swrGlobalConfiguration}>
-      <ContextExtractor></ContextExtractor>
-    </SWRConfig>,
-    elem,
-    renderMainComponents,
-  );
-}
-else {
-  renderMainComponents();
-}

+ 0 - 55
packages/app/_obsolete/src/client/plugin.js

@@ -1,55 +0,0 @@
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:plugin');
-
-export default class GrowiPlugin {
-
-  /**
-   * process plugin entry
-   *
-   * @param {AppContainer} appContainer
-   *
-   * @memberof CrowiPlugin
-   */
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  installAll(appContainer) {
-    // import plugin definitions
-    let definitions = [];
-    try {
-      definitions = require('^/tmp/plugins/plugin-definitions');
-    }
-    catch (e) {
-      logger.error('failed to load definitions');
-      logger.error(e);
-      return;
-    }
-
-    definitions.forEach((definition) => {
-      const meta = definition.meta;
-
-      switch (meta.pluginSchemaVersion) {
-        // v1, v2 and v3 is deprecated
-        case 1:
-          logger.warn('pluginSchemaVersion 1 is deprecated', definition);
-          break;
-        case 2:
-          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
-          break;
-        case 3:
-          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
-          break;
-        case 4:
-          definition.entries.forEach((entry) => {
-            entry(appContainer);
-          });
-          break;
-        default:
-          logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
-      }
-    });
-
-  }
-
-}
-
-window.growiPlugin = new GrowiPlugin();

+ 0 - 133
packages/app/_obsolete/src/client/services/AppContainer.js

@@ -1,133 +0,0 @@
-import { Container } from 'unstated';
-
-// import { i18nFactory } from '../util/i18n';
-
-/**
- * Service container related to options for Application
- * @extends {Container} unstated Container
- */
-export default class AppContainer extends Container {
-
-  constructor() {
-    super();
-
-    this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
-
-    // init i18n
-    const currentUserElem = document.getElementById('growi-current-user');
-    let userLocaleId;
-    if (currentUserElem != null) {
-      const currentUser = JSON.parse(currentUserElem.textContent);
-      userLocaleId = currentUser?.lang;
-    }
-    // this.i18n = i18nFactory(userLocaleId);
-
-    this.containerInstances = {};
-    this.componentInstances = {};
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AppContainer';
-  }
-
-  initApp() {
-    this.injectToWindow();
-  }
-
-  initContents() {
-    const body = document.querySelector('body');
-
-    this.isDocSaved = true;
-
-    const isPluginEnabled = body.dataset.pluginEnabled === 'true';
-    if (isPluginEnabled) {
-      this.initPlugins();
-    }
-
-    this.injectToWindow();
-  }
-
-  initPlugins() {
-    const growiPlugin = window.growiPlugin;
-    growiPlugin.installAll(this);
-  }
-
-  injectToWindow() {
-    // for fix lint error
-
-    // window.appContainer = this;
-
-    // const growiRenderer = new GrowiRenderer(this.getConfig());
-    // growiRenderer.init();
-
-    // window.growiRenderer = growiRenderer;
-
-    // // backward compatibility
-    // window.crowi = this;
-    // window.crowiRenderer = window.growiRenderer;
-    // window.crowiPlugin = window.growiPlugin;
-  }
-
-  getConfig() {
-    return this.config;
-  }
-
-  /**
-   * Register unstated container instance
-   * @param {object} instance unstated container instance
-   */
-  registerContainer(instance) {
-    if (instance == null) {
-      throw new Error('The specified instance must not be null');
-    }
-
-    const className = instance.constructor.getClassName();
-
-    if (this.containerInstances[className] != null) {
-      throw new Error('The specified instance couldn\'t register because the same type object has already been registered');
-    }
-
-    this.containerInstances[className] = instance;
-  }
-
-  /**
-   * Get registered unstated container instance
-   * !! THIS METHOD SHOULD ONLY BE USED FROM unstated CONTAINERS !!
-   * !! From component instances, inject containers with `import { Subscribe } from 'unstated'` !!
-   *
-   * @param {string} className
-   */
-  getContainer(className) {
-    return this.containerInstances[className];
-  }
-
-
-  /*
-  * Note: Use globalEmitter instaead of registerComponentInstance and getComponentInstance
-  */
-
-  /**
-   * Register React component instance
-   * @param {string} id
-   * @param {object} instance React component instance
-   */
-  // registerComponentInstance(id, instance) {
-  //   if (instance == null) {
-  //     throw new Error('The specified instance must not be null');
-  //   }
-
-  //   this.componentInstances[id] = instance;
-  // }
-
-  /**
-   * Get registered React component instance
-   * @param {string} id
-   */
-  // getComponentInstance(id) {
-  //   return this.componentInstances[id];
-  // }
-
-}

+ 0 - 214
packages/app/_obsolete/src/client/services/ContextExtractor.tsx

@@ -1,214 +0,0 @@
-/* eslint-disable */
-import React, { FC, useEffect, useState } from 'react';
-
-import { pagePathUtils } from '@growi/core';
-
-import { CustomWindow } from '~/interfaces/global';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
-// import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
-import {
-  useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-  useSelectedGrant,
-} from '~/stores/ui';
-import { useSetupGlobalSocket, useSetupGlobalAdminSocket } from '~/stores/websocket';
-
-import {
-  useSiteUrl,
-  useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd,
-  useIsTrashPage, useIsUserPage, useLastUpdateUsername,
-  useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
-  useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUser, useTargetAndAncestors,
-  useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
-  useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader,
-  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useGrowiVersion, useAuditLogEnabled,
-  useActivityExpirationSeconds, useAuditLogAvailableActions, useRendererConfig,
-} from '../../stores/context';
-
-const { isTrashPage: _isTrashPage } = pagePathUtils;
-
-const jsonNull = 'null';
-
-const ContextExtractorOnce: FC = () => {
-
-  const mainContent = document.querySelector('#content-main');
-  const notFoundContentForPt = document.getElementById('growi-pagetree-not-found-context');
-  const notFoundContext = document.getElementById('growi-not-found-context');
-  const forbiddenContent = document.getElementById('forbidden-page');
-
-  // get csrf token from body element
-  // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
-  const body = document.querySelector('body');
-  const csrfToken = body?.dataset.csrftoken;
-
-  /*
-   * App Context from DOM
-   */
-  const currentUser = JSON.parse(document.getElementById('growi-current-user')?.textContent || jsonNull);
-
-  /*
-   * Settings from context-hydrate DOM
-   */
-  const configByContextHydrate = JSON.parse(document.getElementById('growi-context-hydrate')?.textContent || jsonNull);
-
-  /*
-   * UserUISettings from DOM
-   */
-  const userUISettings: Partial<IUserUISettings> = JSON.parse(document.getElementById('growi-user-ui-settings')?.textContent || jsonNull);
-
-  /*
-   * Page Context from DOM
-   */
-  const revisionId = mainContent?.getAttribute('data-page-revision-id');
-  const path = decodeURI(mainContent?.getAttribute('data-path') || '');
-  // assign `null` to avoid returning empty string
-  const pageId = mainContent?.getAttribute('data-page-id') || null;
-
-  const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
-
-  const deletedAt = mainContent?.getAttribute('data-page-deleted-at') || null;
-  const isIdenticalPath = JSON.parse(mainContent?.getAttribute('data-identical-path') || jsonNull) ?? false;
-  const isUserPage = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull) != null;
-  const isTrashPage = _isTrashPage(path);
-  const isNotCreatable = JSON.parse(mainContent?.getAttribute('data-page-is-not-creatable') || jsonNull) ?? false;
-  const isForbidden = forbiddenContent != null;
-  const pageUser = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull);
-  const hasChildren = JSON.parse(mainContent?.getAttribute('data-page-has-children') || jsonNull);
-  const hasParent = JSON.parse(mainContent?.getAttribute('data-has-parent') || jsonNull);
-  const templateTagData = mainContent?.getAttribute('data-template-tags') || null;
-  const shareLinksNumber = mainContent?.getAttribute('data-share-links-number');
-  const shareLinkId = JSON.parse(mainContent?.getAttribute('data-share-link-id') || jsonNull);
-  const revisionIdHackmdSynced = mainContent?.getAttribute('data-page-revision-id-hackmd-synced') || null;
-  const lastUpdateUsername = mainContent?.getAttribute('data-page-last-update-username') || null;
-  const deleteUsername = mainContent?.getAttribute('data-page-delete-username') || null;
-  const pageIdOnHackmd = mainContent?.getAttribute('data-page-id-on-hackmd') || null;
-  const hasDraftOnHackmd = !!mainContent?.getAttribute('data-page-has-draft-on-hackmd');
-  const targetAndAncestors = JSON.parse(document.getElementById('growi-pagetree-target-and-ancestors')?.textContent || jsonNull);
-  const notFoundTargetPathOrId = JSON.parse(notFoundContentForPt?.getAttribute('data-not-found-target-path-or-id') || jsonNull);
-  const isSearchPage = document.getElementById('search-page') != null;
-  const isEmptyPage = JSON.parse(mainContent?.getAttribute('data-page-is-empty') || jsonNull) ?? false;
-
-  const grant = +(mainContent?.getAttribute('data-page-grant') || 1);
-  const grantGroupId = mainContent?.getAttribute('data-page-grant-group') || null;
-  const grantGroupName = mainContent?.getAttribute('data-page-grant-group-name') || null;
-
-  /*
-   * use static swr
-   */
-  useCsrfToken(csrfToken);
-
-  // App
-  useCurrentUser(currentUser);
-
-  // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? configByContextHydrate.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? configByContextHydrate.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
-
-  // hydrated config
-  useSiteUrl(configByContextHydrate.crowi.url);
-  useIsAclEnabled(configByContextHydrate.isAclEnabled);
-  useIsSearchServiceConfigured(configByContextHydrate.isSearchServiceConfigured);
-  useIsSearchServiceReachable(configByContextHydrate.isSearchServiceReachable);
-  useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
-  useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
-  useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
-  useAuditLogEnabled(configByContextHydrate.auditLogEnabled);
-  useActivityExpirationSeconds(configByContextHydrate.activityExpirationSeconds);
-  useAuditLogAvailableActions(configByContextHydrate.auditLogAvailableActions);
-  useGrowiVersion(configByContextHydrate.crowi.version);
-  useRendererConfig({
-    isEnabledLinebreaks: configByContextHydrate.isEnabledLinebreaks,
-    isEnabledLinebreaksInComments: configByContextHydrate.isEnabledLinebreaksInComments,
-    adminPreferredIndentSize: configByContextHydrate.adminPreferredIndentSize,
-    isIndentSizeForced: configByContextHydrate.isIndentSizeForced,
-
-    isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
-    attrWhiteList: configByContextHydrate.attrWhiteList,
-    tagWhiteList: configByContextHydrate.tagWhiteList,
-    highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
-
-    plantumlUri: configByContextHydrate.env.PLANTUML_URI,
-    blockdiagUri: configByContextHydrate.env.BLOCKDIAG_URI,
-  });
-  // useNoCdn(configByContextHydrate.env.NO_CDN);
-  // useUploadableImage(configByContextHydrate.upload.image);
-  // useUploadableFile(configByContextHydrate.upload.file);
-
-  // Page
-  useDeleteUsername(deleteUsername);
-  useDeletedAt(deletedAt);
-  useHasChildren(hasChildren);
-  useHasDraftOnHackmd(hasDraftOnHackmd);
-  useIsIdenticalPath(isIdenticalPath);
-  // useIsNotCreatable(isNotCreatable);
-  useIsForbidden(isForbidden);
-  // useIsTrashPage(isTrashPage);
-  useIsUserPage(isUserPage);
-  useLastUpdateUsername(lastUpdateUsername);
-  useCurrentPageId(pageId);
-  usePageIdOnHackmd(pageIdOnHackmd);
-  usePageUser(pageUser);
-  useCurrentPagePath(path);
-  useRevisionCreatedAt(revisionCreatedAt);
-  useRevisionId(revisionId);
-  useRevisionIdHackmdSynced(revisionIdHackmdSynced);
-  useShareLinkId(shareLinkId);
-  useShareLinksNumber(shareLinksNumber);
-  useTemplateTagData(templateTagData);
-  useTargetAndAncestors(targetAndAncestors);
-  useIsSearchPage(isSearchPage);
-  useHasParent(hasParent);
-
-  // Navigation
-  usePreferDrawerModeByUser();
-  usePreferDrawerModeOnEditByUser();
-  useIsDeviceSmallerThanMd();
-
-  // Editor
-  // useSelectedGrant(grant);
-  // useSelectedGrantGroupId(grantGroupId);
-  // useSelectedGrantGroupName(grantGroupName);
-
-  // SearchResult
-  useIsDeviceSmallerThanLg();
-
-  // Global Socket
-  useSetupGlobalSocket();
-  const shouldInitAdminSock = !!currentUser?.isAdmin;
-  useSetupGlobalAdminSocket(shouldInitAdminSock);
-
-  // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223
-  // Do not access this property from other than reveal.js plugins.
-  // (window as CustomWindow).previewRenderer = generatePreviewRenderer(
-  //   {
-  //     isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
-  //     attrWhiteList: configByContextHydrate.attrWhiteList,
-  //     tagWhiteList: configByContextHydrate.tagWhiteList,
-  //     highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
-  //     env: {
-  //       MATHJAX: configByContextHydrate.env.MATHJAX,
-  //       PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
-  //       BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
-  //     },
-  //   },
-  //   null,
-  //   path,
-  // );
-
-  return null;
-};
-
-const ContextExtractor: FC = React.memo(() => {
-  const [isRunOnce, setRunOnce] = useState(false);
-
-  useEffect(() => {
-    setRunOnce(true);
-  }, []);
-
-  return isRunOnce ? null : <ContextExtractorOnce></ContextExtractorOnce>;
-});
-
-export default ContextExtractor;

+ 0 - 358
packages/app/_obsolete/src/client/services/PageContainer.js

@@ -1,358 +0,0 @@
-import { pagePathUtils } from '@growi/core';
-import * as entities from 'entities';
-import * as toastr from 'toastr';
-import { Container } from 'unstated';
-
-
-import { EditorMode } from '~/stores/ui';
-import loggerFactory from '~/utils/logger';
-
-import {
-  DetachCodeBlockInterceptor,
-  RestoreCodeBlockInterceptor,
-} from '../../services/renderer/interceptor/detach-code-blocks';
-import {
-  DrawioInterceptor,
-} from '../../services/renderer/interceptor/drawio-interceptor';
-
-const { isTrashPage } = pagePathUtils;
-
-const logger = loggerFactory('growi:services:PageContainer');
-
-/**
- * Service container related to Page
- * @extends {Container} unstated Container
- */
-export default class PageContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    this.appContainer = appContainer;
-    this.appContainer.registerContainer(this);
-
-    this.state = {};
-
-    const mainContent = document.querySelector('#content-main');
-    if (mainContent == null) {
-      logger.debug('#content-main element is not exists');
-      return;
-    }
-
-    const revisionId = mainContent.getAttribute('data-page-revision-id');
-    const path = decodeURI(mainContent.getAttribute('data-path'));
-
-    this.state = {
-      // local page data
-      markdown: null, // will be initialized after initStateMarkdown()
-      pageId: mainContent.getAttribute('data-page-id'),
-      revisionId,
-      revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
-      path,
-      isEmpty: mainContent.getAttribute('data-page-is-empty'),
-
-      deletedAt: mainContent.getAttribute('data-page-deleted-at') || null,
-
-      isUserPage: JSON.parse(mainContent.getAttribute('data-page-user')) != null,
-      isTrashPage: isTrashPage(path),
-      isNotCreatable: JSON.parse(mainContent.getAttribute('data-page-is-not-creatable')),
-      isPageExist: mainContent.getAttribute('data-page-id') != null,
-
-      pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
-      tags: null,
-      hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
-      templateTagData: mainContent.getAttribute('data-template-tags') || null,
-      shareLinksNumber: mainContent.getAttribute('data-share-links-number'),
-      shareLinkId: JSON.parse(mainContent.getAttribute('data-share-link-id') || null),
-
-      // latest(on remote) information
-      remoteRevisionId: revisionId,
-      remoteRevisionBody: null,
-      remoteRevisionUpdateAt: null,
-      revisionIdHackmdSynced: mainContent.getAttribute('data-page-revision-id-hackmd-synced') || null,
-      lastUpdateUsername: mainContent.getAttribute('data-page-last-update-username') || null,
-      deleteUsername: mainContent.getAttribute('data-page-delete-username') || null,
-      pageIdOnHackmd: mainContent.getAttribute('data-page-id-on-hackmd') || null,
-      hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
-      isHackmdDraftUpdatingInRealtime: false,
-      isConflictDiffModalOpen: false,
-    };
-
-    // parse creator, lastUpdateUser and revisionAuthor
-    try {
-      this.state.creator = JSON.parse(mainContent.getAttribute('data-page-creator'));
-    }
-    catch (e) {
-      logger.warn('The data of \'data-page-creator\' is invalid', e);
-    }
-    try {
-      this.state.revisionAuthor = JSON.parse(mainContent.getAttribute('data-page-revision-author'));
-      this.state.lastUpdateUser = JSON.parse(mainContent.getAttribute('data-page-revision-author'));
-    }
-    catch (e) {
-      logger.warn('The data of \'data-page-revision-author\' is invalid', e);
-    }
-
-    const { interceptorManager } = window;
-    interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(), 10); // process as soon as possible
-    interceptorManager.addInterceptor(new DrawioInterceptor(), 20);
-    interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(), 900); // process as late as possible
-
-    this.initStateMarkdown();
-
-    this.save = this.save.bind(this);
-
-    this.emitJoinPageRoomRequest = this.emitJoinPageRoomRequest.bind(this);
-    this.emitJoinPageRoomRequest();
-
-    this.addWebSocketEventHandlers = this.addWebSocketEventHandlers.bind(this);
-    this.addWebSocketEventHandlers();
-
-    const unlinkPageButton = document.getElementById('unlink-page-button');
-    if (unlinkPageButton != null) {
-      unlinkPageButton.addEventListener('click', async() => {
-        try {
-          const res = await apiPost('/pages.unlink', { path });
-          window.location.href = encodeURI(`${res.path}?unlinked=true`);
-        }
-        catch (err) {
-          toastError(err);
-        }
-      });
-    }
-
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'PageContainer';
-  }
-
-  /**
-   * initialize state for markdown data
-   * [Already SWRized]
-   */
-  initStateMarkdown() {
-    let pageContent = '';
-
-    const rawText = document.getElementById('raw-text-original');
-    if (rawText) {
-      pageContent = rawText.innerHTML;
-    }
-    const markdown = entities.decodeHTML(pageContent);
-
-    this.state.markdown = markdown;
-  }
-
-  setLatestRemotePageData(s2cMessagePageUpdated) {
-    const newState = {
-      remoteRevisionId: s2cMessagePageUpdated.revisionId,
-      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
-      remoteRevisionUpdateAt: s2cMessagePageUpdated.revisionUpdateAt,
-      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
-      // TODO // TODO remove lastUpdateUsername and refactor parts that lastUpdateUsername is used
-      lastUpdateUsername: s2cMessagePageUpdated.lastUpdateUsername,
-      lastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
-    };
-
-    if (s2cMessagePageUpdated.hasDraftOnHackmd != null) {
-      newState.hasDraftOnHackmd = s2cMessagePageUpdated.hasDraftOnHackmd;
-    }
-
-    this.setState(newState);
-  }
-
-  /**
-   * save success handler
-   * @param {object} page Page instance
-   * @param {Array[Tag]} tags Array of Tag
-   * @param {object} revision Revision instance
-   */
-  updateStateAfterSave(page, tags, revision, editorMode) {
-    // update state of PageContainer
-    // const newState = {
-    //   pageId: page._id,
-    //   revisionId: revision._id,
-    //   revisionCreatedAt: new Date(revision.createdAt).getTime() / 1000,
-    //   remoteRevisionId: revision._id,
-    //   revisionAuthor: revision.author,
-    //   revisionIdHackmdSynced: page.revisionHackmdSynced,
-    //   hasDraftOnHackmd: page.hasDraftOnHackmd,
-    //   markdown: revision.body,
-    //   createdAt: page.createdAt,
-    //   updatedAt: page.updatedAt,
-    // };
-    // if (tags != null) {
-    //   newState.tags = tags;
-    // }
-    // this.setState(newState);
-
-    // PageEditorByHackmd component
-    // const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
-    // if (pageEditorByHackmd != null) {
-    //   // reset
-    //   if (editorMode !== EditorMode.HackMD) {
-    //     pageEditorByHackmd.reset();
-    //   }
-    // }
-  }
-
-  /**
-   * update page meta data
-   * @param {object} page Page instance
-   * @param {object} revision Revision instance
-   * @param {String[]} tags Array of Tag
-   */
-  updatePageMetaData(page, revision, tags) {
-
-    const newState = {
-      revisionId: revision._id,
-      revisionCreatedAt: new Date(revision.createdAt).getTime() / 1000,
-      remoteRevisionId: revision._id,
-      revisionAuthor: revision.author,
-      revisionIdHackmdSynced: page.revisionHackmdSynced,
-      hasDraftOnHackmd: page.hasDraftOnHackmd,
-      updatedAt: page.updatedAt,
-    };
-    if (tags != null) {
-      newState.tags = tags;
-    }
-
-    this.setState(newState);
-
-  }
-
-  /**
-   * Save page
-   * @param {string} markdown
-   * @param {object} optionsToSave
-   * @return {object} { page: Page, tags: Tag[] }
-   */
-  async save(markdown, editorMode, optionsToSave = {}) {
-    const { pageId, path } = this.state;
-    let { revisionId } = this.state;
-    const options = Object.assign({}, optionsToSave);
-
-    if (editorMode === EditorMode.HackMD) {
-      // set option to sync
-      options.isSyncRevisionToHackmd = true;
-      revisionId = this.state.revisionIdHackmdSynced;
-    }
-
-    let res;
-    if (pageId == null) {
-      res = await this.createPage(path, markdown, options);
-    }
-    else {
-      res = await this.updatePage(pageId, revisionId, markdown, options);
-    }
-
-    this.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
-    return res;
-  }
-
-
-  showSuccessToastr() {
-    toastr.success(undefined, 'Saved successfully', {
-      closeButton: true,
-      progressBar: true,
-      newestOnTop: false,
-      showDuration: '100',
-      hideDuration: '100',
-      timeOut: '1200',
-      extendedTimeOut: '150',
-    });
-  }
-
-  showErrorToastr(error) {
-    toastr.error(error.message, 'Error occured', {
-      closeButton: true,
-      progressBar: true,
-      newestOnTop: false,
-      showDuration: '100',
-      hideDuration: '100',
-      timeOut: '3000',
-    });
-  }
-
-  // replaced accompanied by omiting container: https://github.com/weseek/growi/pull/6968
-  // // request to server so the client to join a room for each page
-  // emitJoinPageRoomRequest() {
-  //   const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-  //   const socket = socketIoContainer.getSocket();
-  //   socket.emit('join:page', { socketId: socket.id, pageId: this.state.pageId });
-  // }
-
-  addWebSocketEventHandlers() {
-    // eslint-disable-next-line @typescript-eslint/no-this-alias
-    const pageContainer = this;
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const socket = socketIoContainer.getSocket();
-
-    socket.on('page:create', (data) => {
-      logger.debug({ obj: data }, `websocket on 'page:create'`); // eslint-disable-line quotes
-
-      // update remote page data
-      const { s2cMessagePageUpdated } = data;
-      if (s2cMessagePageUpdated.pageId === pageContainer.state.pageId) {
-        pageContainer.setLatestRemotePageData(s2cMessagePageUpdated);
-      }
-    });
-
-    // replaced accompanied by omiting container: https://github.com/weseek/growi/pull/6968
-    // socket.on('page:update', (data) => {
-    //   logger.debug({ obj: data }, `websocket on 'page:update'`); // eslint-disable-line quotes
-
-    //   // update remote page data
-    //   const { s2cMessagePageUpdated } = data;
-    //   if (s2cMessagePageUpdated.pageId === pageContainer.state.pageId) {
-    //     pageContainer.setLatestRemotePageData(s2cMessagePageUpdated);
-    //   }
-    // });
-
-    socket.on('page:delete', (data) => {
-      logger.debug({ obj: data }, `websocket on 'page:delete'`); // eslint-disable-line quotes
-
-      // update remote page data
-      const { s2cMessagePageUpdated } = data;
-      if (s2cMessagePageUpdated.pageId === pageContainer.state.pageId) {
-        pageContainer.setLatestRemotePageData(s2cMessagePageUpdated);
-      }
-    });
-
-    socket.on('page:editingWithHackmd', (data) => {
-      logger.debug({ obj: data }, `websocket on 'page:editingWithHackmd'`); // eslint-disable-line quotes
-
-      // update isHackmdDraftUpdatingInRealtime
-      const { s2cMessagePageUpdated } = data;
-      if (s2cMessagePageUpdated.pageId === pageContainer.state.pageId) {
-        pageContainer.setState({ isHackmdDraftUpdatingInRealtime: true });
-      }
-    });
-
-  }
-
-  /* TODO GW-325 */
-  retrieveMyBookmarkList() {
-  }
-
-  async resolveConflict(markdown, editorMode, optionsToSave) {
-
-    const { pageId, remoteRevisionId, path } = this.state;
-    const editorContainer = this.appContainer.getContainer('EditorContainer');
-
-    const res = await this.updatePage(pageId, remoteRevisionId, markdown, optionsToSave);
-
-    editorContainer.clearDraft(path);
-    this.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
-
-    window.globalEmitter.emit('updateEditorValue', markdown);
-
-    editorContainer.setState({ tags: res.tags });
-
-    return res;
-  }
-
-}

+ 0 - 144
packages/app/_obsolete/src/components/MyDraftList/Draft.tsx

@@ -1,144 +0,0 @@
-import React, { useState } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-import ReactMarkdown from 'react-markdown';
-import {
-  Collapse,
-  UncontrolledTooltip,
-} from 'reactstrap';
-
-import { useDraftOptions } from '~/stores/renderer';
-
-type DraftProps = {
-  path: string,
-  isExist: boolean,
-  index: number,
-  markdown: string,
-  clearDraft: (path: string) => void,
-}
-
-export const Draft = (props: DraftProps): JSX.Element => {
-
-  const {
-    path, isExist, index, markdown, clearDraft,
-  } = props;
-  const { t } = useTranslation();
-  const { data: rendererOptions } = useDraftOptions();
-  const [isPanelExpanded, setIsPanelExpanded] = useState(false);
-  const [showCopiedMessage, setShowCopiedMessage] = useState(false);
-
-  const changeToolTipLabel = () => {
-    setShowCopiedMessage(true);
-    setTimeout(() => {
-      setShowCopiedMessage(false);
-    }, 1000);
-  };
-
-  const collapsePanelHandler = () => {
-    setIsPanelExpanded(false);
-  };
-
-  const expandPanelHandler = () => {
-    setIsPanelExpanded(true);
-  };
-
-  const Controls = () => {
-
-    const tooltipTargetId = `draft-copied-tooltip_${index}`;
-
-    return (
-      <div className="icon-container">
-        {isExist
-          ? null
-          : (
-            <a
-              href={`${path}#edit`}
-              target="_blank"
-              rel="noopener noreferrer"
-              data-toggle="tooltip"
-              title={t('Edit')}
-            >
-              <i className="mx-2 icon-note" />
-            </a>
-          )
-        }
-        <span id={tooltipTargetId}>
-          <CopyToClipboard text={markdown} onCopy={changeToolTipLabel}>
-            <a
-              className="text-center draft-copy"
-            >
-              <i className="mx-2 ti-clipboard" />
-            </a>
-          </CopyToClipboard>
-        </span>
-        <UncontrolledTooltip placement="top" target={tooltipTargetId} fade={false} trigger="hover">
-          { showCopiedMessage && (
-            <strong>copied!</strong>
-          ) }
-          { !showCopiedMessage && (
-            <span>{t('Copy')}</span>
-          ) }
-        </UncontrolledTooltip>
-        <a
-          className="text-danger text-center"
-          data-toggle="tooltip"
-          data-placement="top"
-          title={t('Delete')}
-          onClick={() => { return clearDraft(path) }}
-        >
-          <i className="mx-2 icon-trash" />
-        </a>
-      </div>
-    );
-  };
-
-  const AccordionTitle = () => {
-    const iconClass = isPanelExpanded ? 'fa-rotate-90' : '';
-
-    return (
-      <span>
-
-        <span className="mr-2 draft-path" onClick={() => setIsPanelExpanded(!isPanelExpanded)}>
-          <i className={`fa fa-fw fa-angle-right mr-2 ${iconClass}`}></i>
-          {path}
-        </span>
-        { isExist && (
-          <span className="badge badge-warning">{t('page exists')}</span>
-        ) }
-        { !isExist && (
-          <span className="badge badge-info">draft</span>
-        ) }
-
-        <a className="ml-2" href={path}><i className="icon icon-login"></i></a>
-      </span>
-    );
-  };
-
-
-  return (
-    <div className="accordion draft-list-item" role="tablist">
-      <div className="card">
-
-        <div className="card-header d-flex" role="tab">
-          <AccordionTitle/>
-
-          <div className="flex-grow-1"></div>
-
-          <Controls/>
-        </div>
-
-        <Collapse isOpen={isPanelExpanded} onEntering={expandPanelHandler} onExiting={collapsePanelHandler}>
-          <div className="card-body">
-            { isPanelExpanded && (
-              <ReactMarkdown {...rendererOptions} className='wiki'>
-                {markdown}
-              </ReactMarkdown>
-            ) }
-          </div>
-        </Collapse>
-
-      </div>
-    </div>
-  );
-};

+ 0 - 192
packages/app/_obsolete/src/components/MyDraftList/MyDraftList.jsx

@@ -1,192 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import PageContainer from '~/client/services/PageContainer';
-import { apiGet } from '~/client/util/apiv1-client';
-
-import PaginationWrapper from '../PaginationWrapper';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import { Draft } from './Draft';
-
-class MyDraftList extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      drafts: [],
-      currentDrafts: [],
-      activePage: 1,
-      totalDrafts: 0,
-      pagingLimit: Infinity,
-    };
-
-    this.handlePage = this.handlePage.bind(this);
-    this.getDraftsFromLocalStorage = this.getDraftsFromLocalStorage.bind(this);
-    this.getCurrentDrafts = this.getCurrentDrafts.bind(this);
-    this.clearDraft = this.clearDraft.bind(this);
-    this.clearAllDrafts = this.clearAllDrafts.bind(this);
-  }
-
-  async UNSAFE_componentWillMount() {
-    await this.getDraftsFromLocalStorage();
-    this.getCurrentDrafts(1);
-  }
-
-  async handlePage(selectedPage) {
-    await this.getDraftsFromLocalStorage();
-    await this.getCurrentDrafts(selectedPage);
-  }
-
-  async getDraftsFromLocalStorage() {
-    const draftsAsObj = this.props.editorContainer.drafts;
-
-    if (draftsAsObj == null) {
-      return;
-    }
-
-    const res = await apiGet('/pages.exist', {
-      pagePaths: JSON.stringify(Object.keys(draftsAsObj)),
-    });
-
-    // {'/a': '#a', '/b': '#b'} => [{path: '/a', markdown: '#a'}, {path: '/b', markdown: '#b'}]
-    const drafts = Object.entries(draftsAsObj).map((d) => {
-      const path = d[0];
-      return {
-        path,
-        markdown: d[1],
-        isExist: res.pages[path],
-      };
-    });
-
-    this.setState({ drafts, totalDrafts: drafts.length });
-  }
-
-  getCurrentDrafts(selectPageNumber) {
-
-    const limit = 50; // implement only this component.(this default value is 50 (pageLimitationL))
-
-    const totalDrafts = this.state.drafts.length;
-    const activePage = selectPageNumber;
-
-    const currentDrafts = this.state.drafts.slice((activePage - 1) * limit, activePage * limit);
-
-    this.setState({
-      currentDrafts,
-      activePage,
-      totalDrafts,
-      pagingLimit: limit,
-    });
-  }
-
-  /**
-   * generate Elements of Draft
-   *
-   * @param {any} drafts Array of pages Model Obj
-   *
-   */
-  generateDraftList(drafts) {
-    return drafts.map((draft, index) => {
-      return (
-        <Draft
-          index={index}
-          key={draft.path}
-          path={draft.path}
-          markdown={draft.markdown}
-          isExist={draft.isExist}
-          clearDraft={this.clearDraft}
-        />
-      );
-    });
-  }
-
-  clearDraft(path) {
-    // this.props.editorContainer.clearDraft(path);
-
-    this.setState((prevState) => {
-      return {
-        drafts: prevState.drafts.filter((draft) => { return draft.path !== path }),
-        currentDrafts: prevState.drafts.filter((draft) => { return draft.path !== path }),
-      };
-    });
-  }
-
-  clearAllDrafts() {
-    // this.props.editorContainer.clearAllDrafts();
-
-    this.setState({
-      drafts: [],
-      currentDrafts: [],
-      activePage: 1,
-      totalDrafts: 0,
-      pagingLimit: Infinity,
-    });
-  }
-
-  render() {
-    const { t } = this.props;
-
-    const draftList = this.generateDraftList(this.state.currentDrafts);
-    const totalCount = this.state.totalDrafts;
-
-    return (
-      <div className="page-list-container-create ">
-        { totalCount === 0
-          && <span className="mt-2">No drafts yet.</span>
-        }
-
-        { totalCount > 0 && (
-          <React.Fragment>
-            <div className="d-flex justify-content-between mt-2">
-              <h4>Total: {totalCount} drafts</h4>
-              <div className="align-self-center">
-                <button type="button" className="btn btn-sm btn-outline-danger" onClick={this.clearAllDrafts}>
-                  <i className="icon-fw icon-fire"></i>
-                  {t('delete_all')}
-                </button>
-              </div>
-            </div>
-
-            <div className="tab-pane mt-2 accordion" id="draft-list">
-              {draftList}
-            </div>
-            <PaginationWrapper
-              activePage={this.state.activePage}
-              changePage={this.handlePage}
-              totalItemsCount={this.state.totalDrafts}
-              pagingLimit={this.state.pagingLimit}
-              align="center"
-              size="sm"
-            />
-          </React.Fragment>
-        ) }
-
-      </div>
-    );
-  }
-
-}
-
-MyDraftList.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  // editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-};
-
-const MyDraftListWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <MyDraftList t={t} {...props} />;
-};
-
-export default MyDraftListWrapperFC;
-
-/**
- * Wrapper component for using unstated
- */
-// const MyDraftListWrapper = withUnstatedContainers(MyDraftListWrapperFC, [PageContainer, EditorContainer]);
-
-// export default MyDraftListWrapper;

+ 0 - 76
packages/app/_obsolete/src/util/i18n.js

@@ -1,76 +0,0 @@
-
-/* eslint-disable */
-import i18n from 'i18next';
-import LanguageDetector from 'i18next-browser-languagedetector';
-import { initReactI18next } from 'react-i18next';
-
-import locales from '^/public/static/locales';
-
-const aliasesMapping = {};
-Object.values(locales).forEach((locale) => {
-  if (locale.meta.aliases == null) {
-    return;
-  }
-  locale.meta.aliases.forEach((alias) => {
-    aliasesMapping[alias] = locale.meta.id;
-  });
-});
-
-/*
-* Note: This file will be deleted. use "^/config/next-i18next.config" instead
-*/
-// extract metadata list from 'public/static/locales/${locale}/meta.json'
-export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
-
-export const i18nFactory = (userLocaleId) => {
-  // setup LanguageDetector
-  const langDetector = new LanguageDetector();
-  langDetector.addDetector({
-    name: 'userSettingDetector',
-    lookup(options) {
-      return userLocaleId;
-    },
-  });
-  // Wrapper to convert lang after detected from browser
-  langDetector.addDetector({
-    name: 'navigatorWrapperToConvertByAlias',
-    lookup(options) {
-      const results = langDetector.detectors.navigator.lookup(options);
-      const lang = results[0];
-      if (lang == null) {
-        return;
-      }
-
-      return aliasesMapping[lang] || lang;
-    },
-  });
-
-  i18n
-    .use(langDetector)
-    .use(initReactI18next) // if not using I18nextProvider
-    .init({
-      debug: (process.env.NODE_ENV !== 'production'),
-      resources: locales,
-      load: 'currentOnly',
-
-      fallbackLng: 'en_US',
-      detection: {
-        order: ['userSettingDetector', 'navigatorWrapperToConvertByAlias', 'querystring'],
-      },
-
-      interpolation: {
-        escapeValue: false, // not needed for react!!
-      },
-
-      // react i18next special options (optional)
-      react: {
-        wait: false,
-        withRef: true,
-        bindI18n: 'languageChanged loaded',
-        bindStore: 'added removed',
-        nsMode: 'default',
-      },
-    });
-
-  return i18n;
-};

+ 0 - 17
packages/app/_obsolete/src/util/old-ios.js

@@ -1,17 +0,0 @@
-const userAgent = window.navigator.userAgent.toLowerCase();
-// https://youtrack.weseek.co.jp/issue/GW-4826
-const isOldIos = /(iphone|ipad|ipod) os (9|10|11|12)/.test(userAgent);
-
-/**
- * Apply 'oldIos' attribute to <html></html>
- */
-function applyOldIos() {
-  if (isOldIos) {
-    document.documentElement.setAttribute('old-ios', 'true');
-  }
-}
-
-export {
-  // eslint-disable-next-line import/prefer-default-export
-  applyOldIos,
-};

+ 10 - 16
packages/app/docker/Dockerfile

@@ -1,7 +1,5 @@
 # syntax = docker/dockerfile:1.4
 
-ARG flavor=default
-
 
 ##
 ## packages-json-picker
@@ -30,11 +28,15 @@ ENV nodeModulesGrowiPackagesDir ${optDir}/node_modules/@growi
 # expect a string seperated by commas (e.g. "A,B")
 ENV removeNodeModulesSymlinkPaths ${nodeModulesGrowiPackagesDir}/slackbot-proxy
 
+RUN set -eux; \
+	apt-get update; \
+	apt-get install -y python3 build-essential;
+
 # copy files
 COPY --from=packages-json-picker ${optDir} .
 
-# setup
-RUN yarn config set network-timeout 300000
+# setup (with network-timeout = 1 hour)
+RUN yarn config set network-timeout 3600000
 RUN npx -y lerna bootstrap -- --frozen-lockfile
 
 # remove unnecessary symlinks
@@ -63,9 +65,9 @@ RUN tar -cf node_modules.tar \
 
 
 ##
-## prebuilder-default
+## prebuilder
 ##
-FROM node:16-slim AS prebuilder-default
+FROM node:16-slim AS prebuilder
 
 ENV optDir /opt
 
@@ -80,20 +82,11 @@ RUN tar -xf node_modules.tar
 RUN rm node_modules.tar
 
 
-##
-## prebuilder-nocdn
-##
-FROM prebuilder-default AS prebuilder-nocdn
-
-# add dotenv file for NO_CDN
-COPY packages/app/docker/nocdn/.env.production.local ${optDir}/packages/app/
-
-
 
 ##
 ## builder
 ##
-FROM prebuilder-${flavor} AS builder
+FROM prebuilder AS builder
 
 ENV optDir /opt
 
@@ -125,6 +118,7 @@ RUN tar -cf packages.tar \
   packages/app/resource \
   packages/app/tmp \
   packages/app/.env.production* \
+  packages/app/next.config.js \
   packages/*/package.json \
   packages/*/dist
 

+ 1 - 1
packages/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.0.0`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.0/packages/app/docker/Dockerfile)
+* [`6.0.1`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.1/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 16 - 17
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.0-RC.13",
+  "version": "6.0.2-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -9,7 +9,7 @@
     "build:client": "yarn next build",
     "prebuild:client": "tsc -p tsconfig.build.next.config.json",
     "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server-tsc-alias.json",
-    "postbuild:server": "npx -y shx echo \"Listing files under transpiled\" && npx -y shx ls transpiled && npx -y shx mv transpiled/src dist && npx -y shx cp -r transpiled/config/* config && npx -y shx cp -r src/server/views dist/server/ && npx -y shx rm -rf transpiled",
+    "postbuild:server": "npx -y shx echo \"Listing files under transpiled\" && npx -y shx ls transpiled && npx -y shx mv transpiled/src dist && npx -y shx cp -r transpiled/config/* config && npx -y shx rm -rf transpiled",
     "clean": "npx -y shx rm -rf dist transpiled",
     "prebuild": "yarn cross-env NODE_ENV=production run-p clean resources:*",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
@@ -54,7 +54,8 @@
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "next": "/Sandbox rendering is crashed with v12.3 or above ",
-    "string-width": "5.0.0 or above exports only ESM."
+    "string-width": "5.0.0 or above exports only ESM.",
+    "prom-client": "!!DO NOT REMOVE!! A peer dependency of @promster."
   },
   "dependencies": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
@@ -65,16 +66,16 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.0-RC.13",
-    "@growi/core": "^6.0.0-RC.13",
-    "@growi/hackmd": "^6.0.0-RC.13",
-    "@growi/preset-themes": "^6.0.0-RC.13",
-    "@growi/remark-drawio": "^6.0.0-RC.13",
-    "@growi/remark-growi-directive": "^6.0.0-RC.13",
-    "@growi/remark-lsx": "^6.0.0-RC.13",
-    "@growi/slack": "^6.0.0-RC.13",
-    "@promster/express": "^7.0.2",
-    "@promster/server": "^7.0.4",
+    "@growi/codemirror-textlint": "^6.0.2-RC.0",
+    "@growi/core": "^6.0.2-RC.0",
+    "@growi/hackmd": "^6.0.2-RC.0",
+    "@growi/preset-themes": "^6.0.2-RC.0",
+    "@growi/remark-drawio": "^6.0.2-RC.0",
+    "@growi/remark-growi-directive": "^6.0.2-RC.0",
+    "@growi/remark-lsx": "^6.0.2-RC.0",
+    "@growi/slack": "^6.0.2-RC.0",
+    "@promster/express": "^7.0.6",
+    "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "JSONStream": "^1.3.5",
@@ -100,7 +101,6 @@
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
     "ejs": "^3.1.8",
-    "entities": "^2.0.0",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "=4.0.0",
     "eslint-plugin-regex": "^1.8.0",
@@ -110,7 +110,6 @@
     "express-mongo-sanitize": "^2.1.0",
     "express-session": "^1.16.1",
     "express-validator": "^6.14.0",
-    "express-webpack-assets": "^0.1.0",
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
@@ -149,6 +148,7 @@
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
+    "prom-client": "^14.1.1",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react-bootstrap-typeahead": "^5.2.2",
@@ -183,7 +183,6 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swig-templates": "^2.0.2",
     "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
@@ -202,7 +201,7 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.0-RC.13",
+    "@growi/ui": "^6.0.2-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",

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

@@ -377,15 +377,12 @@
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
-    "ses_settings":"SES settings",
     "test_connection": "Test connection to mail",
     "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
     "region": "Region",
     "bucket_name": "Bucket name",
     "custom_endpoint": "Custom endpoint",
     "custom_endpoint_change": "Input the URL of the endpoint of an object storage service like MinIO that has a S3-compatible API.  Amazon S3 is used if empty.",
-    "plugin_settings": "Plugin settings",
-    "enable_plugin_loading": "Enable plugin loading",
     "load_plugins": "Load plugins",
     "enable": "Enable",
     "disable": "Disable",
@@ -851,6 +848,16 @@
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
   },
+  "plugins": {
+    "plugins": "Plugins",
+    "plugin_installer": "Plugin Installer",
+    "repository_url": "Repository URL",
+    "description": "You can install plugins by inputting the URL",
+    "plugin_card": "Plugin Card",
+    "plugin_is_not_installed": "Plugin is not installed",
+    "install": "Install",
+    "delete": "Delete"
+  },
   "cloud_setting_management": {
     "to_cloud_settings": "Open GROWI.cloud Settings"
   },
@@ -1043,6 +1050,10 @@
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "install_plugin_success": "Succeeded to install {{pluginName}}",
+    "activate_plugin_success": "Succeeded to activating {{pluginName}}",
+    "deactivate_plugin_success": "Succeeded to deactivate {{pluginName}}",
+    "remove_plugin_success": "Succeeded to removing {{pluginName}}"
   }
 }

+ 14 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -60,6 +60,7 @@
   "Presentation Mode": "Presentation",
   "The end": "The end",
   "Not available for guest": "Not available for guest",
+  "Not available in this version": "Not available in this version",
   "No users have liked this yet": "No users have liked this yet",
   "No users have liked this yet.": "No users have liked this yet.",
   "No users have bookmarked yet": "No users have bookmarked yet",
@@ -84,6 +85,7 @@
   "No diff": "No diff",
   "User ID": "User ID",
   "User Information": "User information",
+  "User Activation": "User Activation",
   "Basic Info": "Basic info",
   "Name": "Name",
   "Email": "Email",
@@ -164,6 +166,7 @@
   },
   "installer": {
     "tab": "Create account",
+    "title": "Installer",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
@@ -564,6 +567,7 @@
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
   },
   "search_result": {
+    "title": "Search",
     "result_meta": "Search results for:",
     "deletion_mode_btn_lavel": "Select and delete page",
     "cancel": "Cancel",
@@ -619,6 +623,7 @@
     }
   },
   "login": {
+    "title": "Login",
     "sign_in_error": "Login error",
     "registration_successful": "registration_successful. Please wait for administrator approval.",
     "Setup": "Setup",
@@ -626,6 +631,7 @@
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
   },
   "invited": {
+    "title": "Invited",
     "discription_heading": "Create Account",
     "discription": "Create an your account with the invited email address"
   },
@@ -664,7 +670,7 @@
     "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}",
     "successfully_send_email_auth":"We sent an email to {{email}}. Please click the URL in the email and complete the registration.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired.",
-    "user_already_loggedin": "You cannot create a new account when you are logged in.",
+    "user_already_logged_in": "You cannot create a new account when you are logged in.",
     "registration_closed": "You are not authorized to create a new account.",
     "Username has invalid characters": "Username has invalid characters.",
     "Username field is required": "User ID field is required.",
@@ -811,5 +817,12 @@
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "Edit Tags",
+    "done": "Done",
+    "tags_input": {
+      "tag_name": "tag name"
+    }
   }
 }

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

@@ -385,15 +385,12 @@
     "gridfs_label": "MongoDB(GridFS)",
     "fixed_by_env_var": "環境変数 <code>FILE_UPLOAD={{fileUploadType}}</code> により固定されています。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
-    "ses_settings": "SES設定",
     "test_connection": "接続テスト",
     "change_setting": "この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。",
     "region": "リージョン",
     "bucket_name": "バケット名",
     "custom_endpoint": "カスタムエンドポイント",
     "custom_endpoint_change": "MinIOなど、S3互換APIを持つ他のオブジェクトストレージサービスを使用する場合のみ、そのエンドポイントのURLを入力してください。空欄の場合は、Amazon S3を使用します。",
-    "plugin_settings": "プラグイン設定",
-    "enable_plugin_loading": "プラグインの読み込みを有効にします。",
     "load_plugins": "プラグインを読み込む",
     "enable": "有効",
     "disable": "無効",
@@ -859,6 +856,16 @@
       "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
   },
+  "plugins": {
+    "plugins": "プラグイン",
+    "plugin_installer": "プラグインインストーラー",
+    "repository_url": "URL",
+    "description": "リポジトリのURLの入力してください。",
+    "plugin_card": "プラグインカード",
+    "plugin_is_not_installed": "プラグインがインストールされていません",
+    "install": "インストール",
+    "delete": "削除"
+  },
   "cloud_setting_management": {
     "to_cloud_settings": "GROWI.cloud の管理画面へ"
   },
@@ -1038,6 +1045,10 @@
     "remove_user_success": "{{username}}を削除しました",
     "remove_external_user_success": "{{accountId}}を削除しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました"
+    "failed_to_reset_password":"パスワードのリセットに失敗しました",
+    "install_plugin_success": "{{pluginName}}のインストールに成功しました",
+    "activate_plugin_success": "{{pluginName}}を有効化しました",
+    "deactivate_plugin_success": "{{pluginName}}を無効化しました",
+    "remove_plugin_success": "{{pluginName}}を削除しました"
   }
 }

+ 15 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -57,6 +57,7 @@
   "Presentation Mode": "プレゼンテーション",
   "The end": "おしまい",
   "Not available for guest": "ゲストユーザーは利用できません",
+  "Not available in this version": "このバージョンでは利用できません",
   "No users have liked this yet": "いいねをしているユーザーはいません",
   "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "Create Archive Page": "アーカイブページの作成",
@@ -79,7 +80,9 @@
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "User ID": "ユーザーID",
+  "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
+  "User Activation": "ユーザーアクティベーション",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",
   "Email": "メールアドレス",
@@ -165,6 +168,7 @@
   },
   "installer": {
     "tab": "アカウント作成",
+    "title": "インストーラー",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
@@ -563,6 +567,7 @@
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
   },
   "search_result": {
+    "title": "検索",
     "result_meta": "検索結果:",
     "deletion_mode_btn_lavel": "ページを指定して削除",
     "cancel": "キャンセル",
@@ -618,6 +623,7 @@
     }
   },
   "login": {
+    "title": "ログイン",
     "sign_in_error": "ログインエラー",
     "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
@@ -625,6 +631,7 @@
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
   },
   "invited": {
+    "title": "招待",
     "discription_heading": "アカウント作成",
     "discription": "招待を受け取ったメールアドレスでアカウントを作成します"
   },
@@ -663,7 +670,7 @@
     "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}",
     "successfully_send_email_auth":"{{email}} にメールを送信しました。添付されたURLをクリックし、本登録を完了させてください",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。",
-    "user_already_loggedin": "ログイン中のため、新規アカウントを作成できませんでした。",
+    "user_already_logged_in": "ログイン中のため、新規アカウントを作成できませんでした。",
     "registration_closed": "新しいアカウントを作成する権限がありません。",
     "Username has invalid characters": "ユーザー名に不正な文字が含まれています.",
     "Username field is required": "User ID は必須項目です",
@@ -810,5 +817,12 @@
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "タグの編集",
+    "done": "完了",
+    "tags_input": {
+      "tag_name": "タグ名"
+    }
   }
 }

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

@@ -385,15 +385,12 @@
     "gridfs_label": "MongoDB(GridFS)",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
-    "ses_settings": "SES设置",
     "test_connection": "测试邮件服务器连接",
     "change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
     "region": "Region",
     "bucket_name": "Bucket name",
     "custom_endpoint": "Custom endpoint",
     "custom_endpoint_change": "输入对象存储服务(如MinIO)端点的URL,MinIO具有与S3兼容的API。如果为空,则使用Amazon S3。",
-    "plugin_settings": "插件设置",
-    "enable_plugin_loading": "启用插件加载",
     "load_plugins": "加载插件",
     "enable": "启用",
     "disable": "停用",
@@ -859,6 +856,16 @@
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
   },
+  "plugins": {
+    "plugins": "Plugins",
+    "plugin_installer": "Plugin Installer",
+    "repository_url": "Repository URL",
+    "description": "You can install plugins by inputting the URL",
+    "plugin_card": "Plugin Card",
+    "plugin_is_not_installed": "Plugin is not installed",
+    "install": "Install",
+    "delete": "Delete"
+  },
   "cloud_setting_management": {
     "to_cloud_settings": "進入 GROWI.cloud 的管理界面"
   },
@@ -1038,6 +1045,10 @@
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "install_plugin_success": "Succeeded to install {{pluginName}}",
+    "activate_plugin_success": "Succeeded to activating {{pluginName}}",
+    "deactivate_plugin_success": "Succeeded to deactivate {{pluginName}}",
+    "remove_plugin_success": "Succeeded to removing {{pluginName}}"
   }
 }

+ 15 - 2
packages/app/public/static/locales/zh_CN/translation.json

@@ -56,7 +56,8 @@
   "No_attachments_yet": "暂无附件",
 	"Presentation Mode": "演示文稿",
   "The end": "结束",
-  "Not available for guest": "Not available for guest",
+  "Not available for guest": "不提供给客人",
+  "Not available in this version": "此版本中不提供",
   "No users have liked this yet": "还没有用户喜欢这个",
   "No users have bookmarked yet": "还没有用户加入书签",
   "Create Archive Page": "创建归档页",
@@ -85,6 +86,7 @@
 	"My Drafts": "My Drafts",
 	"User Settings": "用户设置",
 	"User Information": "用户信息",
+  "User Activation": "用户激活",
 	"Basic Info": "基础信息",
 	"Name": "姓名",
 	"Email": "邮箱",
@@ -172,6 +174,7 @@
   },
 	"installer": {
     "tab": "创建账户",
+    "title": "安装",
 		"setup": "安装",
 		"create_initial_account": "创建初始用户",
 		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
@@ -569,6 +572,7 @@
     "link_sharing_is_disabled": "链接共享已被禁用"
   },
 	"search_result": {
+    "title": "搜索",
 		"result_meta": "搜索结果:",
 		"deletion_mode_btn_lavel": "选择并删除页面",
 		"cancel": "取消",
@@ -624,6 +628,7 @@
     }
   },
 	"login": {
+    "title": "登录",
 		"sign_in_error": "登录错误",
 		"registration_successful": "注册成功。请等待管理员批准",
 		"Setup": "安装程序",
@@ -631,6 +636,7 @@
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
 	},
   "invited": {
+    "invited": "邀请函",
     "discription_heading": "创建账户",
     "discription": "用被邀请的电子邮件地址创建一个你的账户"
   },
@@ -669,7 +675,7 @@
 		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}",
     "successfully_send_email_auth":"我们向 {{email}} 发送了一封电子邮件。 请点击邮件中的网址并完成注册。",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。",
-    "user_already_loggedin": "当你登录的时候,你不能创建一个新的账户。",
+    "user_already_logged_in": "当你登录的时候,你不能创建一个新的账户。",
     "registration_closed": "你无权创建一个新的账户。",
     "Username has invalid characters": "用户名有无效字符",
     "Username field is required": "用户ID字段是必需的",
@@ -816,5 +822,12 @@
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "编辑标签",
+    "done": "完毕",
+    "tags_input": {
+      "tag_name": "标签名称"
+    }
   }
 }

+ 6 - 6
packages/app/resource/locales/en_US/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, {{ email }}
+Hi, <%- email -%>
 
 You are invited to our Wiki, you can log in with following account:
 
-Email: {{ email }}
-Password: {{ password }}
+Email: <%- email -%>
+Password: <%- password -%>
 (This password was auto generated. Update required at the first time you logging in)
 
 We are waiting for you!
-{{ url }}
+<%- url -%>
 
 --
-{{ appTitle }}
-{{ url }}
+<%- appTitle -%>
+<%- url -%>

+ 8 - 8
packages/app/resource/locales/en_US/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, {{ adminUser.name }}
+Hi, <%- adminUser.name -%>
 
-A user registered to {{ appTitle }}.
+A user registered to <%- appTitle -%>.
 
 
 ====
 Created user:
 
-Name: {{ createdUser.name }}
-User Name: {{ createdUser.username }}
-Email: {{ createdUser.email }}
+Name: <%- createdUser.name -%>
+User Name: <%- createdUser.username -%>
+Email: <%- createdUser.email -%>
 ====
 
 Please do some action with following URL:
-{{ url }}/admin/users
+<%- url -%>/admin/users
 
 
 --
-{{ appTitle }}
-{{ url }}
+<%- appTitle -%>
+<%- url -%>
 

+ 3 - 3
packages/app/resource/locales/en_US/notifications/comment.txt

@@ -1,9 +1,9 @@
-{{ username }} commented on {{ path }}.
+<%- username }} commented on {{ path -%>.
 
 ----------------------
 
-{{ comment }}
+<%- comment -%>
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 4 - 4
packages/app/resource/locales/en_US/notifications/notActiveUser.txt

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

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageCreate.txt

@@ -1,5 +1,5 @@
-{{ username }} created a new page under {{ path }}.
+<%- username -%> created a new page under <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageDelete.txt

@@ -1,5 +1,5 @@
-{{ username }} deleted the page  {{ path }}.
+<%- username -%> deleted the page  <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageEdit.txt

@@ -1,5 +1,5 @@
-{{ username }} edited the page {{ path }}.
+<%- username -%> edited the page <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageLike.txt

@@ -1,5 +1,5 @@
-{{ username }} liked the page {{ path }}.
+<%- username -%> liked the page <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/en_US/notifications/pageMove.txt

@@ -1,5 +1,5 @@
-{{ username }} renamed the page {{ oldPath }} to {{ newPath }}.
+<%- username -%> renamed the page <%- oldPath -%> to <%- newPath -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 4 - 4
packages/app/resource/locales/en_US/notifications/passwordReset.txt

@@ -1,12 +1,12 @@
 Password Reset
 
-Hi, {{ email }}
+Hi, <%- email -%>
 
-A request has been received to change the password your GROWI ({{ appTitle }}) account.
+A request has been received to change the password your GROWI (<%- appTitle -%>) account.
 To reset your password, click on the link below.
 
-{{ url }}
+<%- url -%>
 
-This link will expire in 10 minutes at  {{ expiredAt }}.
+This link will expire in 10 minutes at  <%- expiredAt -%>.
 
 If you did not request a password reset, you can safely ignore this email.

+ 1 - 1
packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 Password Reset Successful
 
-Hi {{ email }}
+Hi <%- email -%>
 
 Your password has been successfully reset.
 Please log in with your new password.

+ 4 - 4
packages/app/resource/locales/en_US/notifications/userActivation.txt

@@ -1,12 +1,12 @@
 Account confirmation
 
-Hi, {{ email }}
+Hi, <%- email -%>
 
-An acount has been created in GROWI ({{ appTitle }}).
+An acount has been created in GROWI (<%- appTitle -%>).
 To activate your account, click on the link below.
 
-{{ url }}
+<%- url -%>
 
-This link will expire in 1 hour at  {{ expiredAt }}.
+This link will expire in 1 hour at  <%- expiredAt -%>.
 
 If you did not created the account, you can safely ignore this email.

+ 1 - 11
packages/app/resource/locales/en_US/sandbox.md

@@ -233,16 +233,6 @@ You can create links using `[Display text](URL)`.
 
 [Google](https://www.google.co.jp/)
 
-## Crowi compatibility
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 
 This is the most flexible linker.
@@ -254,7 +244,7 @@ Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
 [[./Bootstrap4]]  
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 
 # :memo: Lists
 

+ 6 - 6
packages/app/resource/locales/ja_JP/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, {{ email }}
+Hi, <%- email -%>
 
 You are invited to our Wiki, you can log in with following account:
 
-Email: {{ email }}
-Password: {{ password }}
+Email: <%- email -%>
+Password: <%- password -%>
 (This password was auto generated. Update required at the first time you logging in)
 
 We are waiting for you!
-{{ url }}
+<%- url -%>
 
 --
-{{ appTitle }}
-{{ url }}
+<%- appTitle -%>
+<%- url -%>

+ 8 - 8
packages/app/resource/locales/ja_JP/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, {{ adminUser.name }}
+Hi, <%- adminUser.name -%>
 
-A user registered to {{ appTitle }}.
+A user registered to <%- appTitle -%>.
 
 
 ====
 Created user:
 
-Name: {{ createdUser.name }}
-User Name: {{ createdUser.username }}
-Email: {{ createdUser.email }}
+Name: <%- createdUser.name -%>
+User Name: <%- createdUser.username -%>
+Email: <%- createdUser.email -%>
 ====
 
 Please do some action with following URL:
-{{ url }}/admin/users
+<%- url -%>/admin/users
 
 
 --
-{{ appTitle }}
-{{ url }}
+<%- appTitle -%>
+<%- url -%>
 

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/notActiveUser.txt

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

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/passwordReset.txt

@@ -1,12 +1,12 @@
 パスワード リセット
 
-こんにちは, {{ email }}
+こんにちは, <%- email -%>
 
-あなたのGROWI ({{ appTitle }}) アカウントから、パスワード再設定のリクエストがありました。
+あなたのGROWI (<%- appTitle -%>) アカウントから、パスワード再設定のリクエストがありました。
 パスワードをリセットするには、以下のリンクをクリックしてください。
 
-{{ url }}
+<%- url -%>
 
-このリンクは10分後の {{ expiredAt }} に失効します。
+このリンクは10分後の <%- expiredAt -%> に失効します。
 
 もしこのリクエストに心当たりがない場合は、このメールを無視してください。

+ 1 - 1
packages/app/resource/locales/ja_JP/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 パスワードリセットに成功
 
-こんにちは、 {{ email }}
+こんにちは、 <%- email -%>
 
 あなたのパスワードは正常にリセットされました。
 新しいパスワードでログインしてください。

+ 4 - 4
packages/app/resource/locales/ja_JP/notifications/userActivation.txt

@@ -1,13 +1,13 @@
 仮登録完了のお知らせ
 
-{{ email }} さん
+<%- email -%> さん
 
-GROWI ({{ appTitle }}) で仮登録が完了いたしました。
+GROWI (<%- appTitle -%>) で仮登録が完了いたしました。
 
 ご本人様確認のため、下記リンクをクリックし、アカウントの本登録を完了させて下さい。
 
-{{ url }}
+<%- url -%>
 
-このリンクは1時間後の {{ expiredAt }} に失効します。
+このリンクは1時間後の <%- expiredAt -%> に失効します。
 
 ※当メールの内容に心当たりがない場合は、このメールを無視してください。

+ 0 - 10
packages/app/resource/locales/ja_JP/sandbox.md

@@ -232,16 +232,6 @@ ___
 
 [Google](https://www.google.co.jp/)
 
-## Crowi 互換
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 
 最も柔軟な Linker です。

+ 6 - 6
packages/app/resource/locales/zh_CN/admin/userInvitation.txt

@@ -1,14 +1,14 @@
-Hi, {{ email }}
+Hi, <%- email -%>
 
 You are invited to our Wiki, you can log in with following account:
 
-Email: {{ email }}
-Password: {{ password }}
+Email: <%- email -%>
+Password: <%- password -%>
 (This password was auto generated. Update required at the first time you logging in)
 
 We are waiting for you!
-{{ url }}
+<%- url -%>
 
 --
-{{ appTitle }}
-{{ url }}
+<%- appTitle -%>
+<%- url -%>

+ 8 - 8
packages/app/resource/locales/zh_CN/admin/userWaitingActivation.txt

@@ -1,21 +1,21 @@
-Hi, {{ adminUser.name }}
+Hi, <%- adminUser.name -%>
 
-A user registered to {{ appTitle }}.
+A user registered to <%- appTitle -%>.
 
 
 ====
 Created user:
 
-Name: {{ createdUser.name }}
-User Name: {{ createdUser.username }}
-Email: {{ createdUser.email }}
+Name: <%- createdUser.name -%>
+User Name: <%- createdUser.username -%>
+Email: <%- createdUser.email -%>
 ====
 
 Please do some action with following URL:
-{{ url }}/admin/users
+<%- url -%>/admin/users
 
 
 --
-{{ appTitle }}
-{{ url }}
+<%- appTitle -%>
+<%- url -%>
 

+ 3 - 3
packages/app/resource/locales/zh_CN/notifications/comment.txt

@@ -1,9 +1,9 @@
-{{ username }} commented on {{ path }}.
+<%- username -%> commented on <%- path -%>.
 
 ----------------------
 
-{{ comment }}
+<%- comment -%>
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 4 - 4
packages/app/resource/locales/zh_CN/notifications/notActiveUser.txt

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

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageCreate.txt

@@ -1,5 +1,5 @@
-{{ username }} created a new page under {{ path }}.
+<%- username -%> created a new page under <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageDelete.txt

@@ -1,5 +1,5 @@
-{{ username }} deleted the page  {{ path }}.
+<%- username -%> deleted the page  <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageEdit.txt

@@ -1,5 +1,5 @@
-{{ username }} edited the page {{ path }}.
+<%- username -%> edited the page <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageLike.txt

@@ -1,5 +1,5 @@
-{{ username }} liked the page {{ path }}.
+<%- username -%> liked the page <%- path -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 2 - 2
packages/app/resource/locales/zh_CN/notifications/pageMove.txt

@@ -1,5 +1,5 @@
-{{ username }} renamed the page {{ oldPath }} to {{ newPath }}.
+<%- username -%> renamed the page <%- oldPath -%> to <%- newPath -%>.
 
 ----------------------
 
-Growi: {{ appTitle }}
+Growi: <%- appTitle -%>

+ 3 - 3
packages/app/resource/locales/zh_CN/notifications/passwordReset.txt

@@ -1,11 +1,11 @@
 重设密码
 
-嗨,{{ email }}
+嗨,<%- email -%>
 
-已收到更改您 GROWI ({{appTitle}}) 帐户 密码的请求。
+已收到更改您 GROWI (<%-appTitle-%>) 帐户 密码的请求。
 要重置密码,请单击下面的链接。
 
-{{ url }}
+<%- url -%>
 
 这个链接在10分钟后的{ expiredAt }}失效。
 

+ 1 - 1
packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt

@@ -1,6 +1,6 @@
 密码重置成功
 
-嗨, {{email}}
+嗨, <%-email-%>
 
 您的密码已成功重置。
 请使用您的新密码登录。

+ 4 - 4
packages/app/resource/locales/zh_CN/notifications/userActivation.txt

@@ -1,12 +1,12 @@
 确认账户创建
 
-致{{ email }}
+致<%- email -%>
 
-已使用 GROWI ({{ appTitle }}) 创建帐户。
+已使用 GROWI (<%- appTitle -%>) 创建帐户。
 单击下面的链接以激活您的帐户。
 
-{{ url }}
+<%- url -%>
 
-这个链接将在1小时后即{{ expiredAt }}失效。
+这个链接将在1小时后即<%- expiredAt -%>失效。
 
 如果您尚未创建,请忽略此电子邮件。

+ 1 - 11
packages/app/resource/locales/zh_CN/sandbox.md

@@ -233,16 +233,6 @@ You can create links using `[Display text](URL)`.
 
 [Google](https://www.google.co.jp/)
 
-## Crowi compatibility
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 
 This is the most flexible linker.
@@ -250,7 +240,7 @@ Both the page description and link address can be displayed on the page.
 
 ```
 [[./Bootstrap4]]
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
 [[./Bootstrap4]]  

+ 0 - 20
packages/app/src/client/services/AdminAppContainer.js

@@ -315,13 +315,6 @@ export default class AdminAppContainer extends Container {
     this.setState({ gcsReferenceFileWithRelayMode });
   }
 
-  /**
-   * Change secret key
-   */
-  changeIsEnabledPlugins(isEnabledPlugins) {
-    this.setState({ isEnabledPlugins });
-  }
-
   /**
    * Update app setting
    * @memberOf AdminAppContainer
@@ -441,19 +434,6 @@ export default class AdminAppContainer extends Container {
     return this.setState(responseParams);
   }
 
-  /**
-   * Update plugin setting
-   * @memberOf AdminAppContainer
-   * @return {Array} Appearance
-   */
-  async updatePluginSettingHandler() {
-    const response = await apiv3Put('/app-settings/plugin-setting', {
-      isEnabledPlugins: this.state.isEnabledPlugins,
-    });
-    const { pluginSettingParams } = response.data;
-    return pluginSettingParams;
-  }
-
   /**
    * Start v5 page migration
    * @memberOf AdminAppContainer

+ 2 - 3
packages/app/src/client/services/layout.ts

@@ -1,4 +1,4 @@
-import { useIsContainerFluid, useShareLinkId } from '~/stores/context';
+import { useIsContainerFluid } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useEditorMode } from '~/stores/ui';
 
@@ -17,8 +17,7 @@ export const useEditorModeClassName = (): string => {
 };
 
 export const useCurrentGrowiLayoutFluidClassName = (): string => {
-  const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { data: currentPage } = useSWRxCurrentPage();
 
   const { data: dataIsContainerFluid } = useIsContainerFluid();
 

+ 12 - 8
packages/app/src/client/services/page-operation.ts

@@ -1,3 +1,5 @@
+import { useCallback } from 'react';
+
 import { SubscriptionStatusType, Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
@@ -130,7 +132,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   /* eslint-enable react-hooks/rules-of-hooks */
 
-  return async function(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) {
+  return useCallback(async(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) => {
     const { path, pageId, revisionId } = pageInfo;
 
     const options: OptionsToSave = Object.assign({}, optionsToSave);
@@ -172,7 +174,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     await mutateIsEnabledUnsavedWarning(new Promise(r => setTimeout(() => r(false), 10)), { optimisticData: () => false });
 
     return res;
-  };
+  }, [mutateIsEnabledUnsavedWarning]);
 };
 
 export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
@@ -183,16 +185,18 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
 
-  if (pageId == null) { return }
-
   // update swr 'currentPageId', 'currentPage', remote states
-  return async() => {
-    await mutateCurrentPageId(pageId);
-    const updatedPage = await mutateCurrentPage();
+  return useCallback(async() => {
+    if (pageId == null) { return }
 
+    // update tag before page: https://github.com/weseek/growi/pull/7158
+    // !! DO NOT CHANGE THE ORDERS OF THE MUTATIONS !! -- 12.26 yuken-t
     await mutateTagsInfo(); // get from DB
     syncTagsInfoForEditor(); // sync global state for client
 
+    await mutateCurrentPageId(pageId);
+    const updatedPage = await mutateCurrentPage();
+
     if (updatedPage == null) { return }
 
     mutateEditingMarkdown(updatedPage.revision.body);
@@ -207,7 +211,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
     };
 
     setRemoteLatestPageData(remoterevisionData);
-  };
+  }, [mutateCurrentPage, mutateCurrentPageId, mutateEditingMarkdown, mutateTagsInfo, pageId, setRemoteLatestPageData, syncTagsInfoForEditor]);
 };
 
 export const unlink = async(path: string): Promise<void> => {

+ 1 - 1
packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -25,7 +25,7 @@ const SystemInformationTable = (props: Props) => {
       <tbody>
         <tr>
           <th>GROWI</th>
-          <td data-hide-in-vrt>{ growiVersion }</td>
+          <td data-vrt-blackout>{ growiVersion }</td>
         </tr>
         <tr>
           <th>node.js</th>

+ 0 - 8
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -14,7 +14,6 @@ import AppSetting from './AppSetting';
 import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
 import { MaintenanceMode } from './MaintenanceMode';
-import PluginSetting from './PluginSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 
@@ -108,13 +107,6 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
       </div>
 
-      <div className="row mt-5">
-        <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
-          <PluginSetting />
-        </div>
-      </div>
-
       <div className="row">
         <div className="col-lg-12">
           <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>

+ 1 - 1
packages/app/src/components/Admin/App/MailSetting.tsx

@@ -25,7 +25,7 @@ const MailSetting = (props: Props) => {
   async function submitHandler() {
     try {
       await adminAppContainer.updateMailSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.ses_settings'), ns: 'commons' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);

+ 0 - 66
packages/app/src/components/Admin/App/PluginSetting.tsx

@@ -1,66 +0,0 @@
-import React, { useCallback } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:app:pluginSetting');
-
-type Props = {
-  adminAppContainer: AdminAppContainer,
-}
-
-const PluginSetting = (props: Props) => {
-  const { t } = useTranslation();
-  const { adminAppContainer } = props;
-
-
-  const submitHandler = useCallback(async() => {
-    try {
-      await adminAppContainer.updatePluginSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.plugin_settings'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }, [adminAppContainer, t]);
-
-  return (
-    <>
-      <p className="card well">{t('admin:app_setting.enable_plugin_loading')}</p>
-
-      <div className="row form-group mb-5">
-        <div className="offset-3 col-6 text-left">
-          <div className="custom-control custom-checkbox custom-checkbox-success">
-            <input
-              id="isEnabledPlugins"
-              className="custom-control-input"
-              type="checkbox"
-              checked={adminAppContainer.state.isEnabledPlugins}
-              onChange={(e) => {
-                adminAppContainer.changeIsEnabledPlugins(e.target.checked);
-              }}
-            />
-            <label className="custom-control-label" htmlFor="isEnabledPlugins">{t('admin:app_setting.load_plugins')}</label>
-          </div>
-        </div>
-      </div>
-
-      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-    </>
-  );
-
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PluginSettingWrapper = withUnstatedContainers(PluginSetting, [AdminAppContainer]);
-
-export default PluginSettingWrapper;

+ 1 - 1
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -36,8 +36,8 @@ const AdminNavigation = (props) => {
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          'Plugins'}</>;
       case 'data-transfer':            return <><i className="mr-1 icon-fw icon-arrow-right"></i>{     t('g2g_data_transfer.data_transfer')}</>;
+      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */

+ 31 - 42
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -1,45 +1,34 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
-  apiv3Delete, apiv3Get, apiv3PostForm, apiv3Put,
+  apiv3Delete, apiv3PostForm, apiv3Put,
 } from '~/client/util/apiv3-client';
 import ImageCropModal from '~/components/Common/ImageCropModal';
+import { useIsDefaultLogo, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
+
 const DEFAULT_LOGO = '/images/logo.svg';
+const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 
 const CustomizeLogoSetting = (): JSX.Element => {
 
   const { t } = useTranslation();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
+  const { data: isCustomizedLogoUploaded, mutate: mutateIsCustomizedLogoUploaded } = useIsCustomizedLogoUploaded();
 
   const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
   const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
-  const [isDefaultLogo, setIsDefaultLogo] = useState<boolean>(true);
+  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
   const [retrieveError, setRetrieveError] = useState<any>();
-  const [customizedLogoSrc, setCustomizedLogoSrc] = useState< string | null >(null);
-
-  const retrieveData = useCallback(async() => {
-    try {
-      const response = await apiv3Get('/customize-setting/customize-logo');
-      const { isDefaultLogo: _isDefaultLogo, customizedLogoSrc } = response.data;
-      const isDefaultLogo = _isDefaultLogo ?? true;
-
-      setIsDefaultLogo(isDefaultLogo);
-      setCustomizedLogoSrc(customizedLogoSrc);
-    }
-    catch (err) {
-      setRetrieveError(err);
-      throw new Error('Failed to fetch data');
-    }
-  }, []);
 
-  useEffect(() => {
-    retrieveData();
-  }, [retrieveData]);
+  const currentLogo = useMemo(() => {
+    return isDefaultLogo ? DEFAULT_LOGO : CUSTOMIZED_LOGO;
+  }, [isDefaultLogo]);
 
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
     if (e.target.files != null && e.target.files.length > 0) {
@@ -52,22 +41,18 @@ const CustomizeLogoSetting = (): JSX.Element => {
 
   const onClickSubmit = useCallback(async() => {
     try {
-      const response = await apiv3Put('/customize-setting/customize-logo', {
-        isDefaultLogo,
-      });
-      const { customizedParams } = response.data;
-      setIsDefaultLogo(customizedParams.isDefaultLogo);
+      await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [t, isDefaultLogo]);
+  }, [t, isDefaultLogoSelected]);
 
   const onClickDeleteBtn = useCallback(async() => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
-      setCustomizedLogoSrc(null);
+      mutateIsCustomizedLogoUploaded(false);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     catch (err) {
@@ -75,15 +60,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       throw new Error('Failed to delete logo');
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
 
   const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
-      const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
-      setCustomizedLogoSrc(data.attachment.filePathProxied);
+      await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+      mutateIsCustomizedLogoUploaded(true);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     catch (err) {
@@ -91,7 +76,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       throw new Error('Failed to upload brand logo');
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
   return (
     <React.Fragment>
@@ -109,8 +94,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(true) }}
+                      checked={isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(true) }}
                     />
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                       {t('admin:customize_settings.default_logo')}
@@ -128,8 +113,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={!isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(false) }}
+                      checked={!isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(false) }}
                     />
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                       { t('admin:customize_settings.upload_logo') }
@@ -141,11 +126,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     { t('admin:customize_settings.current_logo') }
                   </label>
                   <div className="col-sm-8 col-12">
-                    <p><img src={customizedLogoSrc || DEFAULT_LOGO} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
-                    {(customizedLogoSrc != null) && (
-                      <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
-                        { t('admin:customize_settings.delete_logo') }
-                      </button>
+                    {isCustomizedLogoUploaded && (
+                      <>
+                        <p>
+                          <img src='/attachment/brand-logo' className="picture picture-lg " id="settingBrandLogo" width="64" />
+                        </p>
+                        <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                          { t('admin:customize_settings.delete_logo') }
+                        </button>
+                      </>
                     )}
                   </div>
                 </div>

+ 12 - 7
packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -2,9 +2,9 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
+import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { tags, attrs } from '~/services/xss/recommended-whitelist';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -16,18 +16,21 @@ class WhiteListInput extends React.Component {
     this.tagWhiteList = React.createRef();
     this.attrWhiteList = React.createRef();
 
+    this.tags = sanitizeDefaultSchema.tagNames;
+    this.attrs = JSON.stringify(sanitizeDefaultSchema.attributes);
+
     this.onClickRecommendTagButton = this.onClickRecommendTagButton.bind(this);
     this.onClickRecommendAttrButton = this.onClickRecommendAttrButton.bind(this);
   }
 
   onClickRecommendTagButton() {
-    this.tagWhiteList.current.value = tags;
-    this.props.adminMarkDownContainer.setState({ tagWhiteList: tags });
+    // this.tagWhiteList.current.value = this.tags;
+    // this.props.adminMarkDownContainer.setState({ tagWhiteList: this.tags });
   }
 
   onClickRecommendAttrButton() {
-    this.attrWhiteList.current.value = attrs;
-    this.props.adminMarkDownContainer.setState({ attrWhiteList: attrs });
+    // this.attrWhiteList.current.value = this.attrs;
+    // this.props.adminMarkDownContainer.setState({ attrWhiteList: this.attrs });
   }
 
   render() {
@@ -38,11 +41,12 @@ class WhiteListInput extends React.Component {
         <div className="mt-4">
           <div className="d-flex justify-content-between">
             {t('markdown_settings.xss_options.tag_names')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0" onClick={this.onClickRecommendTagButton}>
+            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0 disabled" onClick={this.onClickRecommendTagButton}>
               {t('markdown_settings.xss_options.import_recommended', { target: 'Tags' })}
             </p>
           </div>
           <textarea
+            disabled
             className="form-control xss-list"
             name="recommendedTags"
             rows="6"
@@ -55,11 +59,12 @@ class WhiteListInput extends React.Component {
         <div className="mt-4">
           <div className="d-flex justify-content-between">
             {t('markdown_settings.xss_options.tag_attributes')}
-            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0" onClick={this.onClickRecommendAttrButton}>
+            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0 disabled" onClick={this.onClickRecommendAttrButton}>
               {t('markdown_settings.xss_options.import_recommended', { target: 'Attrs' })}
             </p>
           </div>
           <textarea
+            disabled
             className="form-control xss-list"
             name="recommendedAttrs"
             rows="6"

+ 4 - 1
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -93,6 +93,7 @@ class XssForm extends React.Component {
           <div className="col-md-6 col-sm-12 align-self-start mb-4">
             <div className="custom-control custom-radio">
               <input
+                disabled
                 type="radio"
                 className="custom-control-input"
                 id="xssOption2"
@@ -101,7 +102,9 @@ class XssForm extends React.Component {
                 onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.CUSTOM }) }}
               />
               <label className="custom-control-label w-100" htmlFor="xssOption2">
-                <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
+                <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}
+                  <span className='text-warning'> (TBD: Currently unavailable)</span>
+                </p>
                 <WhiteListInput customizable />
               </label>
             </div>

+ 0 - 13
packages/app/src/components/Admin/PluginsExtension/Loading.js

@@ -1,13 +0,0 @@
-import {
-  Spinner,
-} from 'reactstrap';
-
-const Loading = () => {
-  return (
-    <Spinner className='d-flex justify-content-center aligh-items-center'>
-      Loading...
-    </Spinner>
-  );
-};
-
-export default Loading;

+ 92 - 49
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -1,26 +1,103 @@
-// import { faCircleArrowDown, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React, { useState } from 'react';
 
+import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 
-import styles from './PluginCard.module.scss';
+import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
 
+import styles from './PluginCard.module.scss';
 
 type Props = {
+  id: string,
   name: string,
   url: string,
-  description: string,
+  isEnalbed: boolean,
+  mutate: () => void,
+  desc?: string,
 }
 
 export const PluginCard = (props: Props): JSX.Element => {
+
   const {
-    name, url, description,
+    id, name, url, isEnalbed, desc, mutate,
   } = props;
-  // const [isEnabled, setIsEnabled] = useState(true);
 
-  // const checkboxHandler = useCallback(() => {
-  //   setIsEnabled(false);
-  // }, []);
+  const { t } = useTranslation('admin');
+
+  const PluginCardButton = (): JSX.Element => {
+    const [isEnabled, setState] = useState<boolean>(isEnalbed);
+
+    const onChangeHandler = async() => {
+      try {
+        if (isEnabled) {
+          const reqUrl = `/plugins/${id}/deactivate`;
+          const res = await apiv3Put(reqUrl);
+          setState(!isEnabled);
+          const pluginName = res.data.pluginName;
+          toastSuccess(t('toaster.deactivate_plugin_success', { pluginName }));
+        }
+        else {
+          const reqUrl = `/plugins/${id}/activate`;
+          const res = await apiv3Put(reqUrl);
+          setState(!isEnabled);
+          const pluginName = res.data.pluginName;
+          toastSuccess(t('toaster.activate_plugin_success', { pluginName }));
+        }
+      }
+      catch (err) {
+        toastError(err);
+      }
+    };
+
+    return (
+      <div className={`${styles.plugin_card}`}>
+        <div className="switch">
+          <label className="switch__label">
+            <input
+              type="checkbox"
+              className="switch__input"
+              onChange={() => onChangeHandler()}
+              checked={isEnabled}
+            />
+            <span className="switch__content"></span>
+            <span className="switch__circle"></span>
+          </label>
+        </div>
+      </div>
+    );
+  };
+
+  const PluginDeleteButton = (): JSX.Element => {
+
+    const onClickPluginDeleteBtnHandler = async() => {
+      const reqUrl = `/plugins/${id}/remove`;
+
+      try {
+        const res = await apiv3Delete(reqUrl);
+        const pluginName = res.data.pluginName;
+        toastSuccess(t('toaster.remove_plugin_success', { pluginName }));
+      }
+      catch (err) {
+        toastError(err);
+      }
+      finally {
+        mutate();
+      }
+    };
+
+    return (
+      <div className="">
+        <button
+          type="submit"
+          className="btn btn-primary"
+          onClick={() => onClickPluginDeleteBtnHandler()}
+        >
+          {t('plugins.delete')}
+        </button>
+      </div>
+    );
+  };
 
   return (
     <div className="card shadow border-0" key={name}>
@@ -30,54 +107,20 @@ export const PluginCard = (props: Props): JSX.Element => {
             <h2 className="card-title h3 border-bottom pb-2 mb-3">
               <Link href={`${url}`}>{name}</Link>
             </h2>
-            <p className="card-text text-muted">{description}</p>
+            <p className="card-text text-muted">{desc}</p>
           </div>
           <div className='col-3'>
-            <div className={`${styles.plugin_card}`}>
-              <div className="switch">
-                <label className="switch__label">
-                  <input type="checkbox" className="switch__input" checked/>
-                  <span className="switch__content"></span>
-                  <span className="switch__circle"></span>
-                </label>
-              </div>
+            <div>
+              <PluginCardButton />
+            </div>
+            <div className="mt-4">
+              <PluginDeleteButton />
             </div>
-            {/* <div className="custom-control custom-switch custom-switch-lg custom-switch-slack">
-              <input
-                type="checkbox"
-                className="custom-control-input border-0"
-                checked={isEnabled}
-                onChange={checkboxHandler}
-              />
-              <label className="custom-control-label align-center"></label>
-            </div> */}
-            {/* <Image className="mx-auto" alt="GitHub avator image" src={owner.avatar_url} width={250} height={250} /> */}
-          </div>
-        </div>
-        <div className="row">
-          <div className="col-12 d-flex flex-wrap gap-2">
-            {/* {topics?.map((topic: string) => {
-              return (
-                <span key={`${name}-${topic}`} className="badge rounded-1 mp-bg-light-blue text-dark fw-normal">
-                  {topic}
-                </span>
-              );
-            })} */}
           </div>
         </div>
       </div>
       <div className="card-footer px-5 border-top-0 mp-bg-light-blue">
         <p className="d-flex justify-content-between align-self-center mb-0">
-          <span>
-            {/* {owner.login === 'weseek' ? <FontAwesomeIcon icon={faCircleCheck} className="me-1 text-primary" /> : <></>}
-
-            <a href={owner.html_url} target="_blank" rel="noreferrer">
-              {owner.login}
-            </a> */}
-          </span>
-          {/* <span>
-            <FontAwesomeIcon icon={faCircleArrowDown} className="me-1" /> {stargazersCount}
-          </span> */}
         </p>
       </div>
     </div>

+ 17 - 36
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -1,11 +1,14 @@
 import React, { useCallback } from 'react';
 
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useTranslation } from 'next-i18next';
 
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRxPlugins } from '~/stores/plugin';
 
 export const PluginInstallerForm = (): JSX.Element => {
-  // const { t } = useTranslation('admin');
+  const { mutate } = useSWRxPlugins();
+  const { t } = useTranslation('admin');
 
   const submitHandler = useCallback(async(e) => {
     e.preventDefault();
@@ -25,59 +28,37 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
 
     try {
-      await apiv3Post('/plugins', { pluginInstallerForm });
-      toastSuccess('Plugin Install Successed!');
+      const res = await apiv3Post('/plugins', { pluginInstallerForm });
+      const pluginName = res.data.pluginName;
+      toastSuccess(t('toaster.install_plugin_success', { pluginName }));
     }
     catch (e) {
       toastError(e);
     }
-  }, []);
+    finally {
+      mutate();
+    }
+  }, [mutate, t]);
 
   return (
     <form role="form" onSubmit={submitHandler}>
       <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">GitHub Repository URL</label>
+        <label className="text-left text-md-right col-md-3 col-form-label">{t('plugins.repository_url')}</label>
         <div className="col-md-6">
           <input
             className="form-control"
             type="text"
-            // defaultValue={adminAppContainer.state.title || ''}
             name="pluginInstallerForm[url]"
-            placeholder="https://github.com/weseek/growi-plugin-lsx"
+            placeholder="https://github.com/growi/plugins"
             required
           />
-          <p className="form-text text-muted">You can install plugins by inputting the GitHub URL.</p>
-          {/* <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p> */}
+          <p className="form-text text-muted">{t('plugins.description')}</p>
         </div>
       </div>
-      {/* <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">branch</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghBranch]"
-            placeholder="main"
-          />
-          <p className="form-text text-muted">branch name</p>
-        </div>
-      </div>
-      <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">tag</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghTag]"
-            placeholder="tags"
-          />
-          <p className="form-text text-muted">tag name</p>
-        </div>
-      </div> */}
 
       <div className="row my-3">
         <div className="mx-auto">
-          <button type="submit" className="btn btn-primary">Install</button>
+          <button type="submit" className="btn btn-primary">{t('plugins.install')}</button>
         </div>
       </div>
     </form>

+ 47 - 32
packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx

@@ -1,55 +1,70 @@
 import React from 'react';
 
-import type { SearchResultItem } from '~/interfaces/github-api';
-import { useInstalledPlugins } from '~/stores/useInstalledPlugins';
+import { useTranslation } from 'next-i18next';
+import { Spinner } from 'reactstrap';
+
+import { useSWRxPlugins } from '~/stores/plugin';
 
-import Loading from './Loading';
 import { PluginCard } from './PluginCard';
 import { PluginInstallerForm } from './PluginInstallerForm';
 
-
-// TODO: i18n
+const Loading = (): JSX.Element => {
+  return (
+    <Spinner className='d-flex justify-content-center aligh-items-center'>
+      Loading...
+    </Spinner>
+  );
+};
 
 export const PluginsExtensionPageContents = (): JSX.Element => {
-  // const { data, error } = useInstalledPlugins();
+  const { t } = useTranslation('admin');
 
-  // if (data == null) {
-  //   return <Loading />;
-  // }
+  const { data, mutate } = useSWRxPlugins();
 
   return (
     <div>
-
       <div className="row mb-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">Plugin Installer</h2>
+          <h2 className="admin-setting-header">{t('plugins.plugin_installer')}</h2>
           <PluginInstallerForm />
         </div>
       </div>
 
       <div className="row mb-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">Plugins</h2>
-          <div className="d-grid gap-5">
-            <PluginCard
-              name={'growi-plugin-templates-for-office'}
-              url={'https://github.com/weseek/growi-plugin-templates-for-office'}
-              description={'GROWI markdown templates for office.'}
-            />
-            {/* <PluginCard
-              name={'growi-plugin-theme-welcome-to-fumiya-room'}
-              url={'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room'}
-              description={'Welcome to fumiya\'s room! This is very very "latest" design...'}
-            /> */}
-            <PluginCard
-              name={'growi-plugin-copy-code-to-clipboard'}
-              url={'https://github.com/weseek/growi-plugin-copy-code-to-clipboard'}
-              description={'Add copy button on code blocks.'}
-            />
-            {/* {data?.items.map((item: SearchResultItem) => {
-              return <PluginCard key={item.name} {...item} />;
-            })} */}
-          </div>
+          <h2 className="admin-setting-header">
+            {t('plugins.plugin_card')}
+            <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+              <i className="icon icon-reload"></i>
+            </button>
+          </h2>
+          {data?.plugins == null
+            ? <Loading />
+            : (
+              <div className="d-grid gap-5">
+                { data.plugins.length === 0 && (
+                  <div>{t('plugins.plugin_is_not_installed')}</div>
+                )}
+                { data.plugins.map((plugin) => {
+                  const pluginId = plugin._id;
+                  const pluginName = plugin.meta.name;
+                  const pluginUrl = plugin.origin.url;
+                  const pluginIsEnabled = plugin.isEnabled;
+                  const pluginDiscription = plugin.meta.desc;
+                  return (
+                    <PluginCard
+                      key={pluginId}
+                      id={pluginId}
+                      name={pluginName}
+                      url={pluginUrl}
+                      isEnalbed={pluginIsEnabled}
+                      desc={pluginDiscription}
+                      mutate={mutate}
+                    />
+                  );
+                })}
+              </div>
+            )}
         </div>
       </div>
 

+ 3 - 3
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -126,7 +126,7 @@ export const UserGroupPage: FC = () => {
 
   const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, transferToUserGroupId: string) => {
     try {
-      const res = await apiv3Delete(`/user-groups/${deleteGroupId}`, {
+      await apiv3Delete(`/user-groups/${deleteGroupId}`, {
         actionName,
         transferToUserGroupId,
       });
@@ -137,12 +137,12 @@ export const UserGroupPage: FC = () => {
       setSelectedUserGroup(undefined);
       setDeleteModalShown(false);
 
-      toastSuccess(`Deleted ${res.data.userGroups.length} groups.`);
+      toastSuccess(`Deleted ${selectedUserGroup?.name} group.`);
     }
     catch (err) {
       toastError(new Error('Unable to delete the groups'));
     }
-  }, [mutateUserGroups]);
+  }, [mutateUserGroups, selectedUserGroup]);
 
   return (
     <div data-testid="admin-user-groups">

+ 1 - 1
packages/app/src/components/CompleteUserRegistration.tsx

@@ -7,7 +7,7 @@ export const CompleteUserRegistration: FC = () => {
   const { t } = useTranslation();
 
   return (
-    <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+    <div className="nologin-dialog mx-auto" id="nologin-dialog">
       <div className="row mx-0">
         <div className="col-12 mb-3 text-center">
           <p className="alert alert-success">

+ 1 - 1
packages/app/src/components/CompleteUserRegistrationForm.tsx

@@ -85,7 +85,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
 
   return (
     <>
-      <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+      <div className="nologin-dialog mx-auto" id="nologin-dialog">
         <div className="row mx-0">
           <div className="col-12">
 

+ 21 - 48
packages/app/src/components/IdenticalPathPage.tsx

@@ -3,11 +3,9 @@ import React, { FC } from 'react';
 import { DevidedPagePath } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
-import { useCurrentPathname, useIsSharedUser } from '~/stores/context';
-import { useDescendantsPageListModal } from '~/stores/modal';
+import { useCurrentPathname } from '~/stores/context';
 import { useSWRxPageInfoForList, useSWRxPagesByPath } from '~/stores/page-listing';
 
-import PageListIcon from './Icons/PageListIcon';
 import { PageListItemL } from './PageList/PageListItemL';
 
 
@@ -50,16 +48,12 @@ const IdenticalPathAlert : FC<IdenticalPathAlertProps> = (props: IdenticalPathAl
 
 
 export const IdenticalPathPage = (): JSX.Element => {
-  const { t } = useTranslation();
 
   const { data: currentPath } = useCurrentPathname();
-  const { data: isSharedUser } = useIsSharedUser();
 
   const { data: pages } = useSWRxPagesByPath(currentPath);
   const { injectTo } = useSWRxPageInfoForList(null, currentPath, true, true);
 
-  const { open: openDescendantPageListModal } = useDescendantsPageListModal();
-
   if (pages == null) {
     return <></>;
   }
@@ -67,48 +61,27 @@ export const IdenticalPathPage = (): JSX.Element => {
   const injectedPages = injectTo(pages);
 
   return (
-    <div className="d-flex flex-column flex-lg-row-reverse">
-
-      <div className="grw-side-contents-container">
-        <div className={`pb-1 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-          { currentPath != null && !isSharedUser && (
-            <button
-              type="button"
-              className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between"
-              onClick={() => openDescendantPageListModal(currentPath)}
-            >
-              <PageListIcon />
-              {t('page_list')}
-              <span></span> {/* for a count badge */}
-            </button>
-          ) }
-        </div>
+    <>
+      <IdenticalPathAlert path={currentPath} />
+
+      <div className={`page-list ${styles['page-list']}`}>
+        <ul className="page-list-ul list-group list-group-flush">
+          {injectedPages.map((pageWithMeta) => {
+            const pageId = pageWithMeta.data._id;
+
+            return (
+              <PageListItemL
+                key={pageId}
+                page={pageWithMeta}
+                isSelected={false}
+                isEnableActions
+                showPageUpdatedTime
+              />
+            );
+          })}
+        </ul>
       </div>
 
-      <div className="flex-grow-1 flex-basis-0 mw-0">
-
-        <IdenticalPathAlert path={currentPath} />
-
-        <div className={`page-list ${styles['page-list']}`}>
-          <ul className="page-list-ul list-group list-group-flush">
-            {injectedPages.map((pageWithMeta) => {
-              const pageId = pageWithMeta.data._id;
-
-              return (
-                <PageListItemL
-                  key={pageId}
-                  page={pageWithMeta}
-                  isSelected={false}
-                  isEnableActions
-                  showPageUpdatedTime
-                />
-              );
-            })}
-          </ul>
-        </div>
-
-      </div>
-
-    </div>
+    </>
   );
 };

+ 4 - 1
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -26,7 +26,10 @@ export const InAppNotificationDropdown = (): JSX.Element => {
   const limit = 6;
 
   const { data: socket } = useDefaultSocket();
-  const { data: inAppNotificationData, mutate: mutateInAppNotificationData } = useSWRxInAppNotifications(limit);
+  const { data: inAppNotificationData, mutate: mutateInAppNotificationData } = useSWRxInAppNotifications(
+    limit, undefined, undefined,
+    { revalidateOnFocus: isOpen },
+  );
   const { data: inAppNotificationUnreadStatusCount, mutate: mutateInAppNotificationUnreadStatusCount } = useSWRxInAppNotificationStatus();
 
   // ripple

+ 1 - 1
packages/app/src/components/InstallerForm.tsx

@@ -94,7 +94,7 @@ const InstallerForm = memo((): JSX.Element => {
     : <span><i className="icon-fw icon-ban" />{ t('installer.unavaliable_user_id') }</span>;
 
   return (
-    <div data-testid="installerForm" className={`p-3${hasErrorClass}`}>
+    <div data-testid="installerForm" className={`nologin-dialog p-3 mx-auto${hasErrorClass}`}>
       <div className="row">
         <div className="col-md-12">
           <p className="alert alert-success">

+ 1 - 1
packages/app/src/components/InvitedForm.tsx

@@ -84,7 +84,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
   }
 
   return (
-    <div className="noLogin-dialog px-3 pb-3 mx-auto" id="noLogin-dialog">
+    <div className="nologin-dialog px-3 pb-3 mx-auto" id="nologin-dialog">
       { formNotification() }
       <form role="form" onSubmit={submitHandler} id="invited-form">
         {/* Email Form */}

+ 0 - 27
packages/app/src/components/Layout/Login.module.scss

@@ -1,27 +0,0 @@
-@use '~/styles/bootstrap/init' as bs;
-
-
-.login-page {
-  // layout
-  .main .row .login-header,
-  .login-dialog {
-    width: 320px;
-  }
-
-  .link-growi-org {
-    position: absolute;
-    bottom: 9px;
-    z-index: 3;
-  }
-
-  // To adjust the behavior, this problem is not solved.
-  // See https://github.com/AaronCCWong/react-card-flip/issues/56
-  .react-card-front,
-  .react-card-back {
-    height: 0% !important;
-  }
-}
-
-.collapse-external-auth {
-  overflow: hidden;
-}

+ 49 - 0
packages/app/src/components/Layout/MainPane.tsx

@@ -0,0 +1,49 @@
+import { ReactNode } from 'react';
+
+
+type Props = {
+  className?: string,
+  children?: ReactNode,
+  sideContents?: ReactNode,
+  footerContents?: ReactNode,
+}
+
+export const MainPane = (props: Props): JSX.Element => {
+  const {
+    className, children, sideContents, footerContents,
+  } = props;
+
+  return (
+    <>
+      <div className="flex-grow-1">
+        <div id="main" className={`main ${className}`}>
+          <div id="content-main" className="content-main grw-container-convertible">
+            { sideContents != null
+              ? (
+                <div className="d-flex flex-column flex-lg-row">
+                  <div className="flex-grow-1 flex-basis-0 mw-0">
+                    {children}
+                  </div>
+                  <div className="grw-side-contents-container d-edit-none" data-vrt-blackout-side-contents>
+                    <div className="grw-side-contents-sticky-container">
+                      {sideContents}
+                    </div>
+                  </div>
+                </div>
+              )
+              : (
+                <>{children}</>
+              )
+            }
+          </div>
+        </div>
+      </div>
+
+      { footerContents != null && (
+        <footer className="footer d-edit-none">
+          {footerContents}
+        </footer>
+      ) }
+    </>
+  );
+};

+ 32 - 46
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -2,67 +2,52 @@
 
 
 .nologin :global {
-  #page-wrapper {
-    background: none;
-  }
+  height: 100vh;
 
   // layout
-  #wrapper {
+  .page-wrapper {
+    display: flex;
+    align-items: center;
     height: 100vh;
+    margin-top: 0px;
+
+    .main {
+      width: 100vw;
 
-    #page-wrapper {
-      display: flex;
-      align-items: center;
-      height: 100vh;
-      margin-top: 0px;
-
-      .main {
-        width: 100vw;
-
-        > .row {
-          margin-right: 20px;
-          margin-left: 20px;
-        }
-
-        .noLogin-header {
-          display: flex;
-          flex-direction: column;
-          align-items: center;
-          padding-top: 30px;
-          padding-bottom: 10px;
-        }
-
-        .noLogin-form-errors {
-          width: 100%;
-
-          .alert {
-            padding: 5px;
-            margin-top: 10px;
-            margin-bottom: 0;
-
-            ul {
-              padding-left: 1.5em;
-            }
-          }
-        }
+      > .row {
+        margin-right: 20px;
+        margin-left: 20px;
       }
 
-      // .main
+      .nologin-header {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        padding-top: 30px;
+        padding-bottom: 10px;
+      }
     }
 
-    // #page-wrapper
-  }
+    .link-growi-org {
+      position: absolute;
+      bottom: 9px;
+      z-index: 3;
+    }
 
-  // #wrapper
+  }
 
   // styles
-  .noLogin-header {
+  .nologin-header {
     h1 {
       font-size: 22px;
       line-height: 1em;
     }
   }
 
+  .alert {
+    padding: 0.5em 1em 0.5em 2em;
+  }
+
   .input-group {
     margin-bottom: 10px;
 
@@ -132,10 +117,11 @@
       transition: color 0.8s;
     }
   }
-  .noLogin-header,
-  .noLogin-dialog {
+  .nologin-header,
+  .nologin-dialog {
     max-width: 480px;
   }
+
 }
 
 .link-switch {

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