Ver código fonte

Merge branch 'master' into imprv/vrt-page-attachment-data-bootstrap4

kaori 3 anos atrás
pai
commit
1735460417
100 arquivos alterados com 1671 adições e 858 exclusões
  1. 7 0
      .devcontainer/Dockerfile
  2. 2 2
      .devcontainer/docker-compose.yml
  3. 27 0
      .eslintrc.js
  4. 26 0
      .github/workflows/ci-app-prod.yml
  5. 19 8
      .github/workflows/ci-app.yml
  6. 14 6
      .github/workflows/ci-slackbot-proxy.yml
  7. 7 1
      .github/workflows/codeql-analysis.yml
  8. 4 3
      .github/workflows/draft-release.yml
  9. 2 2
      .github/workflows/list-unhealthy-branches.yml
  10. 3 2
      .github/workflows/pr-to-master.yml
  11. 4 16
      .github/workflows/release-rc.yml
  12. 9 16
      .github/workflows/release-slackbot-proxy.yml
  13. 10 23
      .github/workflows/release.yml
  14. 27 26
      .github/workflows/reusable-app-prod.yml
  15. 7 3
      .github/workflows/reusable-app-reg-suit.yml
  16. 238 1
      CHANGELOG.md
  17. 1 1
      lerna.json
  18. 2 2
      package.json
  19. 0 34
      packages/app/bin/shrink-emojione-strategy.js
  20. 1 0
      packages/app/config/ci/.env.local.for-auto-install-with-allowing-guest
  21. 1 1
      packages/app/config/logger/config.dev.js
  22. 2 2
      packages/app/cypress.json
  23. 3 6
      packages/app/docker/Dockerfile
  24. 4 4
      packages/app/docker/README.md
  25. 12 10
      packages/app/package.json
  26. 14 3
      packages/app/resource/Contributor.js
  27. 15 22
      packages/app/resource/cdn-manifests.js
  28. 13 2
      packages/app/resource/locales/en_US/admin/admin.json
  29. 3 1
      packages/app/resource/locales/en_US/notifications/passwordReset.txt
  30. 0 0
      packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt
  31. 3 1
      packages/app/resource/locales/en_US/notifications/userActivation.txt
  32. 12 14
      packages/app/resource/locales/en_US/sandbox.md
  33. 105 11
      packages/app/resource/locales/en_US/translation.json
  34. 13 2
      packages/app/resource/locales/ja_JP/admin/admin.json
  35. 3 1
      packages/app/resource/locales/ja_JP/notifications/passwordReset.txt
  36. 3 1
      packages/app/resource/locales/ja_JP/notifications/userActivation.txt
  37. 12 14
      packages/app/resource/locales/ja_JP/sandbox.md
  38. 105 10
      packages/app/resource/locales/ja_JP/translation.json
  39. 13 2
      packages/app/resource/locales/zh_CN/admin/admin.json
  40. 3 1
      packages/app/resource/locales/zh_CN/notifications/passwordReset.txt
  41. 0 0
      packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt
  42. 3 1
      packages/app/resource/locales/zh_CN/notifications/userActivation.txt
  43. 12 14
      packages/app/resource/locales/zh_CN/sandbox.md
  44. 105 10
      packages/app/resource/locales/zh_CN/translation.json
  45. 52 42
      packages/app/src/client/app.jsx
  46. 9 8
      packages/app/src/client/base.jsx
  47. 10 6
      packages/app/src/client/nologin.jsx
  48. 14 13
      packages/app/src/client/services/AdminAppContainer.js
  49. 6 5
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  50. 11 11
      packages/app/src/client/services/AdminCustomizeContainer.js
  51. 5 5
      packages/app/src/client/services/AdminExternalAccountsContainer.js
  52. 32 12
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  53. 6 5
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  54. 16 13
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  55. 3 4
      packages/app/src/client/services/AdminHomeContainer.js
  56. 9 7
      packages/app/src/client/services/AdminImportContainer.js
  57. 5 3
      packages/app/src/client/services/AdminLdapSecurityContainer.js
  58. 5 2
      packages/app/src/client/services/AdminLocalSecurityContainer.js
  59. 7 5
      packages/app/src/client/services/AdminMarkDownContainer.js
  60. 10 6
      packages/app/src/client/services/AdminNotificationContainer.js
  61. 6 4
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  62. 6 5
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  63. 4 2
      packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  64. 6 4
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  65. 13 8
      packages/app/src/client/services/AdminUsersContainer.js
  66. 0 95
      packages/app/src/client/services/AppContainer.js
  67. 9 6
      packages/app/src/client/services/CommentContainer.js
  68. 16 8
      packages/app/src/client/services/ContextExtractor.tsx
  69. 2 77
      packages/app/src/client/services/EditorContainer.js
  70. 10 10
      packages/app/src/client/services/PageContainer.js
  71. 3 2
      packages/app/src/client/services/PageHistoryContainer.js
  72. 22 8
      packages/app/src/client/services/PersonalContainer.js
  73. 3 2
      packages/app/src/client/services/RevisionComparerContainer.js
  74. 3 1
      packages/app/src/client/services/TagContainer.js
  75. 14 13
      packages/app/src/client/util/GrowiRenderer.js
  76. 1 1
      packages/app/src/client/util/editor.ts
  77. 0 0
      packages/app/src/client/util/emojione/emoji_strategy_shrinked.json
  78. 0 1
      packages/app/src/client/util/interceptor/detach-code-blocks.js
  79. 3 3
      packages/app/src/client/util/interceptor/drawio-interceptor.js
  80. 66 0
      packages/app/src/client/util/markdown-it/emoji-mart-data.ts
  81. 5 22
      packages/app/src/client/util/markdown-it/emoji.js
  82. 50 0
      packages/app/src/client/util/markdown-it/link-by-relative-path.ts
  83. 13 7
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  84. 3 3
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  85. 93 3
      packages/app/src/components/Admin/App/V5PageMigration.tsx
  86. 3 0
      packages/app/src/components/Admin/CustomHeaderEditor.jsx
  87. 5 4
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.jsx
  88. 11 8
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  89. 6 3
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  90. 10 7
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  91. 9 7
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  92. 5 2
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  93. 7 4
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  94. 8 5
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  95. 10 6
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  96. 7 5
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  97. 7 5
      packages/app/src/components/Admin/Security/LdapAuthTest.jsx
  98. 151 84
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  99. 8 6
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  100. 4 1
      packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx

+ 7 - 0
.devcontainer/Dockerfile

@@ -30,8 +30,15 @@ RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
 # * See https://docs.docker.com/engine/reference/builder/#run *
 # *************************************************************
 ENV DEBIAN_FRONTEND=noninteractive
+
+# Prepare to install Chrome for VRT
+RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
+RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
+
 RUN apt-get update \
    && apt-get -y install --no-install-recommends git-lfs \
+      # Chrome
+      google-chrome-stable \
       # for Cypress
       libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk \
 

+ 2 - 2
.devcontainer/docker-compose.yml

@@ -65,9 +65,9 @@ services:
       - /usr/share/elasticsearch/data
       - ../../growi-docker-compose/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
-  #need to adjust kibana version based on elasticsearch version
+  #need to adjust kibana version based on elasticsearch version (use same version as elasticsearch version)
   kibana:
-    image: docker.elastic.co/kibana/kibana:7.17.1
+    image: docker.elastic.co/kibana/kibana:7.16.1
     restart: unless-stopped
     environment:
       ELASTICSEARCH_HOSTS: 'http://elasticsearch:9200'

+ 27 - 0
.eslintrc.js

@@ -16,6 +16,33 @@ module.exports = {
   ],
   rules: {
     'import/prefer-default-export': 'off',
+    'import/order': [
+      'warn',
+      {
+        pathGroups: [
+          {
+            pattern: 'react',
+            group: 'builtin',
+            position: 'before',
+          },
+          {
+            pattern: '^/**',
+            group: 'parent',
+            position: 'before',
+          },
+          {
+            pattern: '~/**',
+            group: 'parent',
+            position: 'before',
+          },
+        ],
+        alphabetize: {
+          order: 'asc',
+        },
+        pathGroupsExcludedImportTypes: ['react'],
+        'newlines-between': 'always',
+      },
+    ],
     '@typescript-eslint/no-explicit-any': 'off',
     indent: [
       'error',

+ 26 - 0
.github/workflows/ci-app-prod.yml

@@ -4,10 +4,34 @@ on:
   push:
     branches:
       - master
+    paths:
+      - .github/workflows/ci-app-prod.yml
+      - .github/workflows/reusable-app-prod.yml
+      - .github/workflows/reusable-app-reg-suit.yml
+      - tsconfig.base.json
+      - yarn.lock
+      - packages/app/**
+      - '!packages/app/docker/**'
+      - packages/core/**
+      - packages/slack/**
+      - packages/ui/**
+      - packages/plugin-**
   pull_request:
     branches:
         - master
     types: [opened, reopened, synchronize]
+    paths:
+      - .github/workflows/ci-app-prod.yml
+      - .github/workflows/reusable-app-prod.yml
+      - .github/workflows/reusable-app-reg-suit.yml
+      - tsconfig.base.json
+      - yarn.lock
+      - packages/app/**
+      - '!packages/app/docker/**'
+      - packages/core/**
+      - packages/slack/**
+      - packages/ui/**
+      - packages/plugin-**
 
 jobs:
 
@@ -24,6 +48,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' ) }}
       cypress-report-artifact-name: Cypress report
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -38,6 +63,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' ) }}
       cypress-report-artifact-name: Cypress report
     secrets:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}

+ 19 - 8
.github/workflows/ci-app.yml

@@ -7,6 +7,17 @@ on:
       - rc/**
       - chore/**
       - support/prepare-v**
+    paths:
+      - .github/workflows/ci-app.yml
+      - .eslint*
+      - tsconfig.base.json
+      - yarn.lock
+      - packages/app/**
+      - '!packages/app/docker/**'
+      - packages/core/**
+      - packages/slack/**
+      - packages/ui/**
+      - packages/plugin-*/**
 
 jobs:
   lint:
@@ -17,9 +28,9 @@ jobs:
         node-version: [16.x]
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
@@ -45,7 +56,7 @@ jobs:
           yarn lerna run lint --scope @growi/plugin-*
       - name: lerna run lint for app
         run: |
-          yarn lerna run lint --scope @growi/app --scope @growi/core --scope @growi/ui
+          yarn lerna run lint --scope @growi/app --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/ui
 
       - name: Slack Notification
         uses: weseek/ghaction-slack-notification@master
@@ -71,9 +82,9 @@ jobs:
           - 27017/tcp
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
@@ -102,7 +113,7 @@ jobs:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
       - name: Upload coverage report as artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: Coverage Report
           path: packages/app/coverage
@@ -131,9 +142,9 @@ jobs:
           - 27017/tcp
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'

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

@@ -7,6 +7,14 @@ on:
       - rc/**
       - chore/**
       - support/prepare-v**
+    paths:
+      - .github/workflows/ci-slackbot-proxy.yml
+      - .eslint*
+      - tsconfig.base.json
+      - yarn.lock
+      - packages/slackbot-proxy/**
+      - '!packages/slackbot-proxy/docker/**'
+      - packages/slack/**
 
 jobs:
 
@@ -18,9 +26,9 @@ jobs:
         node-version: [16.x]
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
@@ -76,9 +84,9 @@ jobs:
           MYSQL_DATABASE: growi-slackbot-proxy
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'
@@ -141,9 +149,9 @@ jobs:
           MYSQL_DATABASE: growi-slackbot-proxy
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}
         cache: 'yarn'

+ 7 - 1
.github/workflows/codeql-analysis.yml

@@ -14,9 +14,15 @@ name: "CodeQL"
 on:
   push:
     branches: [ master, dev/*, release/current ]
+    paths:
+      - .github/workflows/codeql-analysis.yml
+      - packages/**
   pull_request:
     # The branches below must be a subset of the branches above
     branches: [ master ]
+    paths:
+      - .github/workflows/codeql-analysis.yml
+      - packages/**
   schedule:
     - cron: '28 20 * * 6'
 
@@ -35,7 +41,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL

+ 4 - 3
.github/workflows/draft-release.yml

@@ -16,7 +16,7 @@ jobs:
       RELEASE_DRAFT_BODY: ${{ steps.release-drafter.outputs.body }}
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Retrieve information from package.json
         uses: myrotvorets/info-from-package-json-action@1.1.0
@@ -40,7 +40,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           fetch-depth: 0
 
@@ -50,8 +50,9 @@ jobs:
           RELEASE_VERSION=`npx semver -i patch ${{ needs.update-release-draft.outputs.CURRENT_VERSION }}`
           echo ::set-output name=RELEASE_VERSION::$RELEASE_VERSION
 
+      # See: https://github.com/bakunyo/git-pr-release-action/issues/15, https://github.com/samunohito/SimpleVolumeMixer/commit/2059044c71236509466cf9b1bb2d56d515274938
       - name: Create/Update Pull Request
-        uses: bakunyo/git-pr-release-action@master
+        uses: bakunyo/git-pr-release-action@281e1fe424fac01f3992542266805e4202a22fe0
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GIT_PR_RELEASE_BRANCH_PRODUCTION: release/current

+ 2 - 2
.github/workflows/list-unhealthy-branches.yml

@@ -10,11 +10,11 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         fetch-depth: 0
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: '16'
 

+ 3 - 2
.github/workflows/pr-to-master.yml

@@ -13,7 +13,8 @@ jobs:
   auto-labeling:
     runs-on: ubuntu-latest
 
-    if: ${{ !contains(github.event.pull_request.labels.*.name, 'exclude from changelog') }}
+    if: |
+      !contains(github.event.pull_request.labels.*.name, 'exclude from changelog')
 
     steps:
       - uses: release-drafter/release-drafter@v5
@@ -30,7 +31,7 @@ jobs:
         !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
-      - uses: amannn/action-semantic-pull-request@v3.4.2
+      - uses: amannn/action-semantic-pull-request@v4.2.0
         with:
           types: |
             feat

+ 4 - 16
.github/workflows/release-rc.yml

@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         lfs: true
 
@@ -43,14 +43,6 @@ jobs:
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v1
 
-    - name: Cache Docker layers
-      uses: actions/cache@v3
-      with:
-        path: /tmp/.buildx-cache
-        key: ${{ runner.os }}-buildx-app-${{ github.sha }}
-        restore-keys: |
-          ${{ runner.os }}-buildx-app-
-
     - name: Build and push
       uses: docker/build-push-action@v2
       with:
@@ -58,11 +50,7 @@ jobs:
         file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         push: true
-        cache-from: type=local,src=/tmp/.buildx-cache
-        cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
+        builder: ${{ steps.buildx.outputs.name }}
+        cache-from: type=gha
+        cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
-
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache

+ 9 - 16
.github/workflows/release-slackbot-proxy.yml

@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
@@ -56,14 +56,6 @@ jobs:
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v1
 
-    - name: Cache Docker layers
-      uses: actions/cache@v3
-      with:
-        path: /tmp/.buildx-cache
-        key: ${{ runner.os }}-buildx-slackbot-proxy-${{ github.sha }}
-        restore-keys: |
-          ${{ runner.os }}-buildx-slackbot-proxy-
-
     - name: Build and push
       uses: docker/build-push-action@v2
       with:
@@ -71,8 +63,9 @@ jobs:
         file: ./packages/slackbot-proxy/docker/Dockerfile
         platforms: linux/amd64
         push: true
-        cache-from: type=local,src=/tmp/.buildx-cache
-        cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
+        builder: ${{ steps.buildx.outputs.name }}
+        cache-from: type=gha
+        cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
 
     - name: Move cache
@@ -81,14 +74,14 @@ jobs:
         mv /tmp/.buildx-cache-new /tmp/.buildx-cache
 
     - name: Add tag
-      uses: anothrNick/github-tag-action@1.36.0
+      uses: anothrNick/github-tag-action@1.38.0
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         CUSTOM_TAG: v${{ steps.package-json.outputs.packageVersion }}
         VERBOSE : true
 
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v2
+      uses: peter-evans/dockerhub-description@v3
       with:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
@@ -102,11 +95,11 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: '16'
         cache: 'yarn'
@@ -127,7 +120,7 @@ jobs:
         workingDir: packages/slackbot-proxy
 
     - name: Commit
-      uses: github-actions-x/commit@v2.8
+      uses: github-actions-x/commit@v2.9
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         push-branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}

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

@@ -18,11 +18,11 @@ jobs:
       RELEASED_VERSION: ${{ steps.package-json.outputs.packageVersion }}
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         ref: ${{ github.event.pull_request.base.ref }}
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: '16'
         cache: 'yarn'
@@ -79,11 +79,11 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: '16'
         cache: 'yarn'
@@ -103,7 +103,7 @@ jobs:
       id: package-json
 
     - name: Commit
-      uses: github-actions-x/commit@v2.8
+      uses: github-actions-x/commit@v2.9
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         push-branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
@@ -131,7 +131,7 @@ jobs:
         flavor: [default, nocdn]
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         lfs: true
@@ -169,15 +169,6 @@ jobs:
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v1
 
-    - name: Cache Docker layers
-      uses: actions/cache@v3
-      with:
-        path: /tmp/.buildx-cache
-        key: ${{ runner.os }}-buildx-app-${{ matrix.flavor }}-${{ github.sha }}
-        restore-keys: |
-          ${{ runner.os }}-buildx-app-${{ matrix.flavor }}-
-          ${{ runner.os }}-buildx-app-
-
     - name: Build and push
       uses: docker/build-push-action@v2
       with:
@@ -187,17 +178,13 @@ jobs:
         push: true
         build-args: |
           flavor=${{ matrix.flavor }}
-        cache-from: type=local,src=/tmp/.buildx-cache
-        cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
+        builder: ${{ steps.buildx.outputs.name }}
+        cache-from: type=gha
+        cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
 
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache
-
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v2
+      uses: peter-evans/dockerhub-description@v3
       with:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

+ 27 - 26
.github/workflows/reusable-app-prod.yml

@@ -23,9 +23,9 @@ jobs:
       PROD_FILES: ${{ steps.archive-prod-files.outputs.file }}
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
@@ -63,13 +63,13 @@ jobs:
         echo ::set-output name=file::production.tar
 
     - name: Upload production files as artifact
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: Production Files
         path: ${{ steps.archive-prod-files.outputs.file }}
 
     - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: Bundle Analyzing Report
         path: packages/app/report/bundle-analyzer.html
@@ -103,9 +103,9 @@ jobs:
           discovery.type: single-node
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
@@ -140,7 +140,7 @@ jobs:
         npx lerna bootstrap -- --production
 
     - name: Download production files artifact
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: Production Files
 
@@ -175,17 +175,12 @@ jobs:
     if: ${{ !inputs.skip-cypress }}
 
     runs-on: ubuntu-latest
-    container:
-      image: cypress/base:16.13.0
-      # solve permissions issue
-      # see: https://github.com/cypress-io/github-action/issues/446#issuecomment-987015822
-      options: --user 1001
 
     strategy:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['1', '2', '3']
+        spec-group: ['10', '20', '21', '30', '40', '60']
 
     services:
       mongodb:
@@ -200,12 +195,13 @@ jobs:
           discovery.type: single-node
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
-    - name: Get yarn cache dir
-      id: yarn-cache-dir
-      run: |
-        echo "::set-output name=value::`yarn cache dir --silent`"
+    - uses: actions/setup-node@v3
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
 
     - name: Cache/Restore dependencies
       uses: actions/cache@v3
@@ -213,7 +209,6 @@ jobs:
         path: |
           **/node_modules
           ~/.cache/Cypress
-          ${{ steps.yarn-cache-dir.outputs.value }}
         key: deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
         restore-keys: |
           deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-
@@ -224,7 +219,7 @@ jobs:
         npx lerna bootstrap -- --frozen-lockfile
 
     - name: Download production files artifact
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: Production Files
 
@@ -244,26 +239,32 @@ jobs:
         cat config/ci/.env.local.for-ci >> .env.production.local
 
     - name: Copy dotenv file for automatic installation
-      if: ${{ matrix.spec-group != '1' }}
+      if: ${{ matrix.spec-group != '10' }}
       working-directory: ./packages/app
       run: |
         cat config/ci/.env.local.for-auto-install >> .env.production.local
 
+    - name: Copy dotenv file for automatic installation with allowing guest mode
+      if: ${{ matrix.spec-group == '21' }}
+      working-directory: ./packages/app
+      run: |
+        cat config/ci/.env.local.for-auto-install-with-allowing-guest >> .env.production.local
+
     - name: Cypress Run
-      uses: cypress-io/github-action@v2
+      uses: cypress-io/github-action@v3
       with:
+        browser: chrome
         working-directory: ./packages/app
-        install: false
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         start: yarn server
         wait-on: 'http://localhost:3000'
       env:
-        MONGO_URI: mongodb://mongodb:27017/growi-vrt
-        ELASTICSEARCH_URI: http://elasticsearch:9200/growi
+        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
+        ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
 
     - name: Upload results
       if: always()
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: ${{ inputs.cypress-report-artifact-name }}
         path: |

+ 7 - 3
.github/workflows/reusable-app-reg-suit.yml

@@ -9,6 +9,8 @@ on:
       checkout-ref:
         type: string
         default: ${{ github.head_ref }}
+      skip-reg-suit:
+        type: boolean
       cypress-report-artifact-name:
         required: true
         type: string
@@ -33,6 +35,8 @@ jobs:
     # https://github.com/weseek/growi/settings/environments/376165508/edit
     environment: VRT
 
+    if: ${{ !inputs.skip-reg-suit }}
+
     env:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
       AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -45,12 +49,12 @@ jobs:
       EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         ref: ${{ inputs.checkout-ref }}
         fetch-depth: 0
 
-    - uses: actions/setup-node@v2
+    - uses: actions/setup-node@v3
       with:
         node-version: ${{ inputs.node-version }}
         cache: 'yarn'
@@ -71,7 +75,7 @@ jobs:
         npx lerna bootstrap -- --frozen-lockfile
 
     - name: Download screenshots taken by cypress
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: ${{ inputs.cypress-report-artifact-name }}
         path: packages/app/test/cypress

+ 238 - 1
CHANGELOG.md

@@ -1,9 +1,246 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.5.14...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.0.6...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.0.6](https://github.com/weseek/growi/compare/v5.0.5...v5.0.6) - 2022-05-27
+
+### 💎 Features
+
+- feat: Emoji - replace emojione to emojimart (#5668) @kaoritokashiki
+- feat: Show username suggestion for mention in comment (#5856) @mudana-grune
+- feat: Send in-app notification when containing username mention in comment  (#5906) @mudana-grune
+- feat: Customize menu in navbar for guest user (#5858) @yukendev
+- feat: Admin only page convert by path (#5902) @hakumizuki
+- feat: Fix grant alert (#5903) @hakumizuki
+
+### 🚀 Improvement
+
+- imprv: Automatic login after registration (#5860) @hiroki-hgs
+- imprv: Add tooltip to SubNavButtons (#5887) @miya
+- imprv: Mixin of argument-of-override-list-group-item-for-pagetree for dark theme (#5904) @shukmos
+- imprv: Move code to the appropriate place for fix browser auto-complete email wiith username (#5892) @Yohei-Shiina
+- imprv: Initial rendering when opening Custom Sidebar (#5880) @Kami-jo
+- imprv: Add contributors to staff credit (#5841) @hiroki-hgs
+
+### 🐛 Bug Fixes
+
+- fix: Can not toggle textlint function on v5.0.x (#5854) @kaoritokashiki
+- fix(google-oauth2): Automatically bind external accounts  does not work on v5.0.x (#5886) @kaoritokashiki
+
+## [v5.0.5](https://github.com/weseek/growi/compare/v5.0.4...v5.0.5) - 2022-05-16
+
+### 💎 Features
+
+- feat: Empty trash button in trash page (#5816) @yukendev
+
+### 🚀 Improvement
+
+- imprv: Count badge colors (#5835) @shukmos
+- imprv: List group background colors on PageTree (#5812) @shukmos
+- imprv: Page path auto complete function for page rename modal (#5805) @kaoritokashiki
+- imprv: Show toastr when converting is completed on Private Legacy Page (#5810) @yukendev
+- imprv: Create parent pages as needed by path that includes slash (#5809) @kaoritokashiki
+
+### 🐛 Bug Fixes
+
+- fix: Change the execution user of the official docker image to root (#5846) @yuki-takei
+- fix: Display admin link only with logged in (#5799) @hirokei-camel
+- fix: Error when renaming (#5793) @miya
+
+### 🧰 Maintenance
+
+- support: Typescriptize tag model (#5778) @kaoritokashiki
+
+## [v5.0.4](https://github.com/weseek/growi/compare/v5.0.3...v5.0.4) - 2022-04-28
+
+### 💎 Features
+
+- feat: Private legacy pages convert by path (#5787) @hakumizuki
+- feat: Generate activity when page is created (#5765) @miya
+- feat: Private legacy pages convert by path API (#5760) @hakumizuki
+- feat:  Create notification when page is reverted (#5756) @miya
+- feat: Create notification when page is duplicated (#5749) @miya
+- feat: Add count badge to Page List button and Comment button (#5740) @yukendev
+- feat: Infinite scroll for Recent Changes in Sidebar (#5647) @mudana-grune
+
+### 🚀 Improvement
+
+- imprv: Change GET method to POST for logout operation (#5751) @kaoritokashiki
+- imprv: Redesign tags (#5730) @miya
+- imprv: i18n for already_exists error in PutBackPageModal (#5747) @kaoritokashiki
+
+### 🐛 Bug Fixes
+
+- fix: Default markdown linker with relative path does not respect the current page path (#5788) @yuki-takei
+- fix: Include any public pages as applicable ancestors (#5786) @hakumizuki
+- fix: Not create unnecessary empty pages when ancestors are public (#5774) @hakumizuki
+- fix: Too many footstamps icons are shown by lsx output 2 (#5763) @yuki-takei
+- fix:  footstamp-icon size (#5759) @kaoritokashiki
+
+## [v4.5.19](https://github.com/weseek/growi/compare/v4.5.18...v4.5.19) - 2022-04-28
+
+### 🐛 Bug Fixes
+
+- fix: Swiping to previous/next page for Mac users (4.5.x) (#5758) @hirokei-camel
+- fix: Get attachment list api without "page" parameter returns 500 response (#5726) @miya
+
+## [v5.0.3](https://github.com/weseek/growi/compare/v5.0.2...v5.0.3) - 2022-04-21
+
+### 💎 Features
+
+- feat: Search on private legacy pages (#5723) @hakumizuki
+
+### 🚀 Improvement
+
+- imprv: Dark theme color optimization (#5737) @shukmos
+- imprv: Change the order of menu items (#5722) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Get attachment list api without "page" parameter returns 500 response (#5726) @miya
+- fix: New user notification email is also sent TO: deleted_at_<epoch_time>@deleted (#5735) @yuki-takei
+- fix: Too many footstamps icons are shown by lsx output (#5727) @yuki-takei
+
+## [v5.0.2](https://github.com/weseek/growi/compare/v5.0.1...v5.0.2) - 2022-04-15
+
+### 🐛 Bug Fixes
+
+- fix: Edit button to open built-in editor does not work when HackMD is disabled (#5719) @yuki-takei
+- fix: Share link list occures error when related page is not found (#5718) @yuki-takei
+
+## [v5.0.1](https://github.com/weseek/growi/compare/v5.0.0...v5.0.1) - 2022-04-15
+
+### 💎 Features
+
+- feat: Input Slack member ID (#5412) @mudana-grune
+- feat: Remove child group from parent group (#5600) @miya
+
+### 🚀 Improvement
+
+- imprv: Add spinner to tag sidebar (#5700) @miya
+- imprv: Adjust pagelist and comment position (#5682) @Yohei-Shiina
+- imprv: Adjust layout for PageTree Descendant Count (#5666) @miya
+- imprv: adjust spaces in page item control and subnav btn (#5655) @Yohei-Shiina
+- imprv: Clickable area of PageListItemL (#5665) @yuki-takei
+- imprv: Add an expiration date for the link in the email (#5660) @miya
+- imprv: remove min-width from search-sort-option-btn (#5656) @kaoritokashiki
+
+### 🐛 Bug Fixes
+
+- fix: Correction of expiredAt attached to email (#5715) @miya
+- fix: Normalize parent so it does not include siblings (#5678) @hakumizuki
+- fix: Prevent auto completing email with username stored by browser in /me page (#5702) @Yohei-Shiina
+- fix: Do not include granted users if change page permission restricted (#5693) @miya
+- fix: Do not include in search results if the page grant is restricted (#5691) @miya
+- fix: Password reset gives error update password failed when submitting a new password (#5685) @kaoritokashiki
+- fix: Cannot register new users (#5683) @kaoritokashiki
+- fix: Sync change of count for both like and bookmark in search page (#5667) @Yohei-Shiina
+- imprv: Adjust layout for PageTree Descendant Count (#5666) @miya
+- fix: HackMD disabled tooltip on mobile (#5658) @yuki-takei
+- fix: One Time Token is not available (#5654) @miya
+- fix: Page items disappear when dnd (#5651) @miya
+
+### 🧰 Maintenance
+
+- ci(deps): bump anothrNick/github-tag-action from 1.36.0 to 1.38.0 (#5271) @dependabot
+- ci(deps): bump amannn/action-semantic-pull-request from 3.4.5 to 4.2.0 (#5627) @dependabot
+- ci(deps): bump actions/upload-artifact from 2 to 3 (#5686) @dependabot
+- ci(deps): bump actions/download-artifact from 2 to 3 (#5687) @dependabot
+- support: Migration for setting sparce option to slack member id (#5694) @kaoritokashiki
+- support: Update eslint-config-weseek (#5673) @yuki-takei
+
+## [v4.5.18](https://github.com/weseek/growi/compare/v4.5.17...v4.5.18) - 2022-04-15
+
+### 🐛 Bug Fixes
+
+- fix: One Time Token is not available for v4.5.x (#5713) @miya
+- fix: Prevent auto completing email with username stored by browser in /me page for v4.5.x (#5703) @Yohei-Shiina
+- fix: Page view count stops at 15 (#5705) @miya
+
+## [v4.5.17](https://github.com/weseek/growi/compare/v4.5.16...v4.5.17) - 2022-04-07
+
+### 🐛 Bug Fixes
+
+- fix: Elasticsearch doesn't work properly on production (#5676) @Yohei-Shiina
+
+## [v4.5.16](https://github.com/weseek/growi/compare/v4.5.15...v4.5.16) - 2022-04-06
+
+### 💎 Features
+
+- feat: Support Elasticsearch 7 (#5613) @Yohei-Shiina
+
+### 🐛 Bug Fixes
+
+- fix: Domain whitelist is not respected (fix #5408) (#5488) @yuto-oweseek
+- fix: Add tags to pages restricted by specified groups on View mode (for v4.5.x) (#5487) @yuto-oweseek
+
+## [v5.0.0](https://github.com/weseek/growi/compare/v4.5.15...v5.0.0) - 2022-04-01
+
+### 💎 Features
+
+- feat: Support Elasticsearch 7 (#5080) @yuki-takei
+- feat: Elasticsearch reindex on boot (#5149) @LuqmanHakim-Grune
+- feat: PageTree and re-impl SearchResult with list group (#5286) @yuki-takei
+- feat: Rename(Move) by Drag & Drop (#5292) @hakumizuki
+- feat: Maintenance mode (#5486) @hakumizuki
+- feat: Delete permission config (#5527) @hakumizuki
+
+### 🚀 Improvement
+
+- imprv: Show comments in search page result (#5645) @yuki-takei
+- imprv: Add description for user addition (#5614) @hakumizuki
+- imprv: Validate deletion settings (#5581) @hakumizuki
+
+### 🐛 Bug Fixes
+
+- fix: Swiping to previous/next page for Mac users (5.0.x) (#5491) @hakumizuki
+- fix: Guest User Access Dropdown shows wrong value (#5643) @miya
+- fix: Show full text on presentation mode (#5636) @hakumizuki
+- fix: Displaying minimum length of password (#5630) @Yohei-Shiina
+- fix: Domain whitelist is not respected (fix #5408) (#5470) @yuto-oweseek
+- fix: Add tags to pages restricted by specified groups on View mode (#5457) @yuto-oweseek
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump plantuml-encoder from 1.2.5 to 1.4.0 (#5633) @dependabot
+- ci(deps-dev): bump codemirror from 5.63.0 to 5.64.0 (#4777) @dependabot
+- ci(deps): bump nanoid from 3.1.30 to 3.2.0 (#5142) @dependabot
+- support: Upgrade openid client (#5185) @mudana-grune
+- ci(deps): bump amannn/action-semantic-pull-request from 3.4.2 to 3.4.5 (#4559) @dependabot
+- ci(deps): bump extend from 3.0.1 to 3.0.2 (#5222) @dependabot
+- ci(deps-dev): bump jquery-ui from 1.12.1 to 1.13.0 (#4548) @dependabot
+- ci(deps): bump actions/setup-node from 2 to 3 (#5437) @dependabot
+- ci(deps): bump actions/checkout from 2 to 3 (#5462) @dependabot
+- ci(deps): bump peter-evans/dockerhub-description from 2 to 3 (#5615) @dependabot
+- ci(deps): bump actions/cache from 2 to 3 (#5584) @dependabot
+- ci(deps-dev): bump reveal.js from 3.6.0 to 4.3.1 (#5603) @dependabot
+- support: Update yarn git-hosted-info v2.8.8 to v2.8.9 (#5215) @LuqmanHakim-Grune
+- support: dependabot trim-off-newlines (#5336) @mudana-grune
+- support: dependabot @npmcli/git (#5337) @mudana-grune
+- support: dependabot highlight.js (#5352) @mudana-grune
+- support: dependabot extend (#5335) @mudana-grune
+- support: dependabot ajv (#5333) @mudana-grune
+- support: dependabot dot-drop (#5204) @LuqmanHakim-Grune
+- support: update nanoid yarn.lock v3.1.30 to v3.2.0 (#5216) @LuqmanHakim-Grune
+- support: update validator version (#5562) @LuqmanHakim-Grune
+
+## [v4.5.15](https://github.com/weseek/growi/compare/v4.5.14...v4.5.15) - 2022-02-17
+
+### 🚀 Improvement
+
+- imprv: Hide forgot password when localstrategy is disabled (#5380) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: The condition to attempt to reconnect to Elasticsearch (#5344) @yuki-takei
+- fix: Highlight-addons and drawio-viewer for view missing (#5376) @yuki-takei
+
+### 🧰 Maintenance
+
+- support:  modify docker-compose indent (#5322) @yuto-oweseek
+
 ## [v4.5.14](https://github.com/weseek/growi/compare/v4.5.13...v4.5.14) - 2022-02-10
 
 ### 💎 Features

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.0.0-RC.11",
+  "version": "5.0.7-RC.0",
   "packages": [
     "packages/*"
   ]

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.0.0-RC.11",
+  "version": "5.0.7-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -59,7 +59,7 @@
     "@typescript-eslint/parser": "^4.28.5",
     "cypress": "^9.2.0",
     "eslint": "^7.31.0",
-    "eslint-config-weseek": "^1.1.0",
+    "eslint-config-weseek": "^2.1.0",
     "eslint-import-resolver-typescript": "^2.4.0",
     "eslint-plugin-import": "^2.23.4",
     "eslint-plugin-jest": "^24.3.2",

+ 0 - 34
packages/app/bin/shrink-emojione-strategy.js

@@ -1,34 +0,0 @@
-/**
- * the tool to shrink emojione/emoji_strategy.json and output
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-/*
-require('module-alias/register');
-
-const fs = require('graceful-fs');
-
-const helpers = require('@commons/util/helpers');
-
-const emojiStrategy = require('emojione/emoji_strategy.json');
-const markdownItEmojiFull = require('markdown-it-emoji/lib/data/full.json');
-
-const OUT = helpers.root('tmp/emoji_strategy_shrinked.json');
-
-const shrinkedMap = {};
-Object.keys(emojiStrategy).forEach((unicode) => {
-  const data = emojiStrategy[unicode];
-  const shortname = data.shortname.replace(/:/g, '');
-
-  // ignore if it isn't included in markdownItEmojiFull
-  if (markdownItEmojiFull[shortname] == null) {
-    return;
-  }
-
-  // add
-  shrinkedMap[unicode] = data;
-});
-
-// write
-fs.writeFileSync(OUT, JSON.stringify(shrinkedMap));
-*/

+ 1 - 0
packages/app/config/ci/.env.local.for-auto-install-with-allowing-guest

@@ -0,0 +1 @@
+AUTO_INSTALL_ALLOW_GUEST_MODE=true

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

@@ -12,7 +12,7 @@ module.exports = {
   // 'growi:crow:dev': 'debug',
   'growi:crowi:express-init': 'debug',
   'growi:models:external-account': 'debug',
-  // 'growi:routes:login': 'debug',
+  'growi:routes:login': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:middleware:safe-redirect': 'debug',
   'growi:service:PassportService': 'debug',

+ 2 - 2
packages/app/cypress.json

@@ -10,8 +10,8 @@
   "pluginsFile": "test/cypress/plugins/index.ts",
   "testFiles": "**/*.spec.ts",
 
-  "viewportWidth": 1440,
-  "viewportHeight": 1200,
+  "viewportWidth": 1400,
+  "viewportHeight": 1024,
 
   "experimentalSessionSupport": true
 }

+ 3 - 6
packages/app/docker/Dockerfile

@@ -1,4 +1,4 @@
-# syntax = docker/dockerfile:1
+# syntax = docker/dockerfile:1.4
 
 ARG flavor=default
 
@@ -157,12 +157,9 @@ RUN tar -xf node_modules.tar
 RUN tar -xf packages.tar
 RUN rm node_modules.tar packages.tar
 
-USER root
-
-COPY packages/app/docker/docker-entrypoint.sh /
-RUN chmod 700 /docker-entrypoint.sh
-RUN chown node:node ${appDir}
+COPY --chown=node:node --chmod=700 packages/app/docker/docker-entrypoint.sh /
 
+USER root
 WORKDIR ${appDir}/packages/app
 
 VOLUME /data

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

@@ -10,10 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.0.0`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.0/docker/Dockerfile)
-* [`5.0.0-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.0/docker/Dockerfile)
-* [`4.5.14`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.14/docker/Dockerfile)
-* [`4.5.14-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.14/docker/Dockerfile)
+* [`5.0.6`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.6/docker/Dockerfile)
+* [`5.0.6-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.6/docker/Dockerfile)
+* [`4.5.15`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
+* [`4.5.15-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 

+ 12 - 10
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.0-RC.11",
+  "version": "5.0.7-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -28,7 +28,7 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status",
     "dev:migrate:up": "yarn dev:migrate-mongo up",
     "dev:migrate:down": "yarn dev:migrate-mongo down",
-    "cy:run": "cypress run --headless",
+    "cy:run": "cypress run --browser chrome",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
@@ -62,11 +62,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.0-RC.11",
-    "@growi/plugin-attachment-refs": "^5.0.0-RC.11",
-    "@growi/plugin-lsx": "^5.0.0-RC.11",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.0-RC.11",
-    "@growi/slack": "^5.0.0-RC.11",
+    "@growi/codemirror-textlint": "^5.0.7-RC.0",
+    "@growi/plugin-attachment-refs": "^5.0.7-RC.0",
+    "@growi/plugin-lsx": "^5.0.7-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.7-RC.0",
+    "@growi/slack": "^5.0.7-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -127,7 +127,7 @@
     "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
-    "openid-client": "=2.5.0",
+    "openid-client": "^5.1.2",
     "p-retry": "^4.0.0",
     "passport": "^0.5.0",
     "passport-github": "^1.1.0",
@@ -167,7 +167,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.0.0-RC.11",
+    "@growi/ui": "^5.0.7-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -179,7 +179,7 @@
     "browser-sync": "^2.27.7",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",
-    "codemirror": "^5.63.0",
+    "codemirror": "^5.64.0",
     "colors": "=1.4.0",
     "connect-browser-sync": "^2.1.0",
     "core-js": "=2.6.9",
@@ -187,6 +187,8 @@
     "csv-to-markdown-table": "^1.0.1",
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.1.0",
+    "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
+    "markdown-it-emoji-mart": "^0.1.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
     "file-loader": "^5.0.2",

+ 14 - 3
packages/app/resource/Contributor.js

@@ -12,6 +12,11 @@ const contributors = [
           { position: 'Soncho 2nd', name: 'yusuketk' },
           { position: 'Paladin', name: 'itizawa' },
           { position: 'Valkyrie', name: 'kaoritokashiki' },
+          { position: 'Slime', name: 'TatsuyaIse' },
+          { position: 'Knight', name: 'Yohei-Shiina' },
+          { position: 'Titan', name: 'ryoh15' },
+          { position: 'Haberion', name: 'hakumizuki' },
+          { position: 'Undefined', name: 'miya' },
         ],
       },
       {
@@ -19,7 +24,6 @@ const contributors = [
         members: [
           { name: 'utsushiiro' },
           { name: 'mayumorita' },
-          { name: 'TatsuyaIse' },
           { name: 'shinoka7' },
           { name: 'SeiyaTashiro' },
           { name: 'TsuyoshiSuzukief' },
@@ -30,13 +34,13 @@ const contributors = [
           { name: 'kaishuu0123' },
           { name: 'kouki-o' },
           { name: 'Angola' },
-          { name: 'Yohei-Shiina' },
           { name: 'shukmos' },
           { name: 'sooouh' },
           { name: 'ryouhek' },
           { name: 'ryuichi-e' },
           { name: 'N1koge' },
           { name: 'Ertai87' },
+          { name: 'takayuki-t' },
           { name: 'zahmis' },
           { name: 'takeru0001' },
           { name: 'Shu Katabe' },
@@ -44,8 +48,15 @@ const contributors = [
           { name: 'makotoshiraishi' },
           { name: 'yamagai' },
           { name: 'stevenfukase' },
-          { name: 'miya' },
           { name: 'kaho819' },
+          { name: 'yuto-oweseek' },
+          { name: 'maow89126' },
+          { name: 'kntowd' },
+          { name: 'yukendev' },
+          { name: 'asami-n' },
+          { name: 'yoshiro-s' },
+          { name: 'kuimac' },
+          { name: 'akira-sugiyama' },
         ],
       },
     ],

+ 15 - 22
packages/app/resource/cdn-manifests.js

@@ -3,7 +3,7 @@ module.exports = {
     {
       name: 'basis',
       // eslint-disable-next-line max-len
-      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
+      url: 'https://cdn.jsdelivr.net/combine/npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
       groups: ['basis'],
       args: {
         integrity: '',
@@ -55,28 +55,28 @@ module.exports = {
     },
     {
       name: 'codemirror-dialog',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/addon/dialog/dialog.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/addon/dialog/dialog.min.js',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-keymap-vim',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/keymap/vim.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/keymap/vim.min.js',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-keymap-emacs',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/keymap/emacs.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/keymap/emacs.min.js',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-keymap-sublime',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/keymap/sublime.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/keymap/sublime.min.js',
       args: {
         integrity: '',
       },
@@ -138,14 +138,7 @@ module.exports = {
         integrity: '',
       },
     },
-    {
-      name: 'emojione',
-      url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
+
     {
       name: 'animate.css',
       url: 'https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css',
@@ -170,63 +163,63 @@ module.exports = {
     },
     {
       name: 'codemirror-dialog',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/addon/dialog/dialog.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/addon/dialog/dialog.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-eclipse',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/eclipse.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/eclipse.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-elegant',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/elegant.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/elegant.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-neo',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/neo.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/neo.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-mdn-like',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/mdn-like.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/mdn-like.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-material',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/material.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/material.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-dracula',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/dracula.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/dracula.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-monokai',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/monokai.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/monokai.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-twilight',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/twilight.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/twilight.min.css',
       args: {
         integrity: '',
       },

+ 13 - 2
packages/app/resource/locales/en_US/admin/admin.json

@@ -28,7 +28,10 @@
     "modal_migration_warning": "This process may take long. It is highly recommended that administrators tell users not to create, modify, or delete pages during the conversion.",
     "start_upgrading": "Start converting to v5 compatibility",
     "successfully_started": "Succeeded to start the conversion",
-    "already_upgraded": "You have already completed the conversion to v5 compatibility"
+    "already_upgraded": "You have already completed the conversion to v5 compatibility",
+    "header_upgrading_progress": "Upgrade Progress",
+    "migration_succeeded": "Your upgrade has been successfully completed! Exit maintenance mode and GROWI can be used.",
+    "migration_failed": "Upgrade failed. Please refer to the GROWI docs for information on what to do in the event of failure."
   },
   "maintenance_mode": {
     "maintenance_mode": "Maintenance Mode",
@@ -469,6 +472,7 @@
   "user_group_management": {
     "create_group": "Create new group",
     "add_child_group": "Add child group",
+    "remove_child_group": "Remove",
     "deny_create_group": "You can't create a new group with the current settings.",
     "group_name": "Group name",
     "group_example": "e.g. : Group1",
@@ -476,6 +480,7 @@
     "select_parent_group": "Select Parent Group",
     "release_parent_group": "Release parent group",
     "add_modal": {
+      "description": "The added user will also be added to all parent groups.",
       "add_user": "Add a user to the created group",
       "search_option": "Search option",
       "enable_option": "Enable {{option}}",
@@ -486,7 +491,6 @@
     "group_list": "Group list",
     "child_group_list": "Child group list",
     "back_to_list": "Go back to group list",
-    "back_to_ancestors_group": "Go back to ancestors group",
     "basic_info": "Basic info",
     "user_list": "User list",
     "created_group": "Group was created",
@@ -502,6 +506,13 @@
       "publish_pages": "Publish all",
       "delete_pages": "Delete all",
       "transfer_pages": "Transfer to another group"
+    },
+    "update_parent_confirm_modal": {
+      "header": "The parent of the group will be changed",
+      "caution_change_parent": "This operation will change the parent of the group \"{{groupName}}\".",
+      "danger_message": "Note that this affects the permissions to view all pages associated with this group.",
+      "force_update_parents_label": "Forcibly add missing users",
+      "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
     }
   }
 }

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

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

+ 0 - 0
packages/app/resource/locales/en_US/notifications/PasswordResetSuccessful.txt → packages/app/resource/locales/en_US/notifications/passwordResetSuccessful.txt


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

@@ -2,9 +2,11 @@ Account confirmation
 
 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 }}
 
+This link will expire in 1 hour at  {{ expiredAt }}.
+
 If you did not created the account, you can safely ignore this email.

+ 12 - 14
packages/app/resource/locales/en_US/sandbox.md

@@ -12,7 +12,7 @@
   </div>
 </div>
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 ## Headers
 
@@ -160,7 +160,7 @@ ___
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 ## Strong Text
 
@@ -200,7 +200,7 @@ This is ___Italic & Bold___.
 This is ***Italic & Bold***.
 This is ___Italic & Bold___.
 
-# :pencil: Images
+# :memo: Images
 
 You can insert `<img>` tag using `![description](URL)`.
 
@@ -221,7 +221,7 @@ The size of the image can be set by using an HTML image tag
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
-# :pencil: Link
+# :memo: Link
 
 ## Markdown standard
 
@@ -256,10 +256,10 @@ Both the page description and link address can be displayed on the page.
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
-[[../Bootstrap4]]  
+[[./Bootstrap4]]  
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 
-# :pencil: Lists
+# :memo: Lists
 
 ## Ul Bulleted list
 
@@ -319,7 +319,7 @@ The numbers don’t have to be in numerical order, but the list should start wit
 - [x] Task2
 
 
-# :pencil: Table
+# :memo: Table
 
 ## Markdown Standard
 
@@ -415,7 +415,7 @@ Content Cell,Content Cell
 :::
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
@@ -428,15 +428,13 @@ Long footnotes can be written as [^longnote].
     Subsequent paragraphs are indented and belong to the previous footnote.
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
@@ -444,7 +442,7 @@ See [emojione](https://www.emojione.com/)
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 # :heavy_plus_sign: More..

+ 105 - 11
packages/app/resource/locales/en_US/translation.json

@@ -15,8 +15,6 @@
   "Move/Rename": "Move/Rename",
   "Redirected": "Redirected",
   "Unlinked": "Unlinked",
-  "Like!": "Like!",
-  "Seen by": "Seen by",
   "Done": "Done",
   "Cancel": "Cancel",
   "Create": "Create",
@@ -100,6 +98,7 @@
   "Connected": "Connected",
   "Show": "Show",
   "Hide": "Hide",
+  "Loading": "Loading...",
   "Disclose E-mail": "Disclose E-mail",
   "page exists": "this page already exists",
   "Error occurred": "Error occurred",
@@ -148,6 +147,7 @@
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
   "Add tags for this page": "Add tags for this page",
+  "popular_tags": "Popular tags",
   "Check All tags": "check all tags",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
   "Show latest": "Show latest",
@@ -169,6 +169,9 @@
   "Link sharing is disabled": "Link sharing is disabled",
   "successfully_saved_the_page": "Successfully saved the page",
   "you_can_not_create_page_with_this_name": "You can not create page with this name",
+  "not_allowed_to_see_this_page": "You cannot see this page",
+  "Confirm": "Confirm",
+  "Successfully requested": "Successfully requested.",
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
@@ -181,9 +184,7 @@
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
     "invalid_syntax": "The syntax of %s is invalid.",
-    "title_required": "Title is required.",
-    "slashed_are_not_yet_supported": "Titles containing slashes are not yet supported"
-
+    "title_required": "Title is required."
   },
   "not_found_page": {
     "Create Page": "Create Page",
@@ -211,7 +212,7 @@
     },
     "form_help": {
       "email": "You must have email address which listed below to sign up to this wiki.",
-      "password": "Your password must be at least 8 characters long.",
+      "password": "Your password must be at least {{target}} characters long.",
       "user_id": "The URL of pages you create will contain your User ID. Your User ID can consist of letters, numbers, and some symbols."
     }
   },
@@ -387,11 +388,12 @@
     }
   },
   "page_comment": {
-    "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
+    "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
+    "no_user_found": "No user found"
   },
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
-    "already_exists": "New page is already exists.",
+    "already_exists": "Page with the path already exists.",
     "outdated": "Page is updated someone and now outdated.",
     "user_not_admin": "Only admin user can delete"
   },
@@ -435,11 +437,15 @@
     "recursively": "Delete pages under this path recursively.",
     "completely": "Delete completely instead of putting it into trash."
   },
+  "deleted_page": "Moved to the trash",
   "deleted_pages": "{{path}} has been deleted",
   "deleted_pages_completely": "{{path}} has been deleted completely",
   "renamed_pages": "{{path}} has been renamed",
+  "empty_trash": "The trash has been emptied",
   "modal_empty":{
     "empty_the_trash": "Empty The Trash",
+    "empty_the_trash_button": "Empty The Trash",
+    "not_deletable_notice": "Some pages cannot be removed due to lack of permission.",
     "notice": "The pages deleted completely are unrecoverable."
   },
   "modal_duplicate": {
@@ -637,7 +643,8 @@
   "private_legacy_pages": {
     "bulk_operation": "Bulk operation",
     "convert_all_selected_pages": "Convert all to new v5 compatible format",
-    "alert_title": "You are viewing old v4 compatible private pages.",
+    "input_path_to_convert": "Input a path to convert pages",
+    "alert_title": "Old v4 compatible format private pages exist.",
     "alert_desc1": "On this page, you can select pages with the checkbox and batch convert to the new v5 compatible format from the \"Bulk operation\" button at the top of the screen.",
     "nopages_title": "Congratulations. Ready to use GROWI v5!",
     "nopages_desc1": "Now all the pages you can manage seem to be in v5 compatible format.",
@@ -648,11 +655,28 @@
       "convert_recursively_label": "Convert child pages recursively.",
       "convert_recursively_desc": "Convert pages under this path recursively.",
       "button_label": "Convert"
+    },
+    "toaster": {
+      "page_migration_succeeded": "Conversion of selected page to v5 has been successfully completed.",
+      "page_migration_failed_with_paths": "Conversion of {{paths}} to v5 has been failed.",
+      "page_migration_failed": "Conversion of page to v5 has been failed."
+    },
+    "by_path_modal": {
+      "title": "Convert to new v5 compatible format",
+      "alert": "This operation cannot be undone, and pages that the user cannot view are also subject to processing.",
+      "checkbox_label": "Understood",
+      "description": "Enter a path and all pages under that path will be converted to v5 compatible format.",
+      "button_label": "Convert",
+      "success": "Successfully requested conversion.",
+      "error": "Failed to request conversion.",
+      "error_grant_invalid": "Page permissions are incorrect. Please correct it and try again.",
+      "error_page_not_found": "Page not found.",
+      "error_duplicate_pages_found": "Multiple pages with the same path name were found. Please rename or delete and try again."
     }
   },
   "security_setting": {
     "Guest Users Access": "Guest users access",
-    "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
+    "Fixed by env var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
     "Register limitation": "Register limitation",
     "Register limitation desc": "Restriction of new users' registration",
     "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
@@ -684,7 +708,8 @@
     "max_age": "Max age (msec)",
     "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
     "max_age_caution": "Restarting the server is required after you modify this value.",
-    "page_delete_rights_caution": "The \"operation including the descendants\" setting is forced to be stronger than the \"operation for only the selected page\" setting.",
+    "forced_update_desc": "Settings have been forcibly changed. Previous setting: ",
+    "page_delete_rights_caution": "The \"Delete / Delete All\" permission (including descendant pages) is forced to be stronger than the \"Delete / Completely Delete\" permission. <br> <br> Admin only > Admin and autor > Anyone",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
@@ -999,6 +1024,36 @@
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
   },
+  "emoji" :{
+    "title": "Pick an Emoji",
+    "search": "Search",
+    "clear": "Clear",
+    "notfound": "No Emoji Found",
+    "skintext": "Choose your default skin tone",
+    "categories": {
+      "search": "Search Results",
+      "recent": "Frequently Used",
+      "smileys": "Smileys & Emotion",
+      "people": "People & Body",
+      "nature": "Animals & Nature",
+      "foods": "Food & Drink",
+      "activity": "Activity",
+      "places": "Travel & Places",
+      "objects": "Objects",
+      "symbols": "Symbols",
+      "flags": "Flags",
+      "custom": "Custom"
+    },
+    "categorieslabel": "Emoji categories",
+    "skintones": {
+      "1": "Default Skin Tone",
+      "2": "Light Skin Tone",
+      "3": "Medium-Light Skin Tone",
+      "4": "Medium Skin Tone",
+      "5": "Medium-Dark Skin Tone",
+      "6": "Dark Skin Tone"
+    }
+  },
   "maintenance_mode":{
     "maintenance_mode": "Maintenance Mode",
     "growi_is_under_maintenance": "GROWI is under maintenance. Please wait until it ends.",
@@ -1016,5 +1071,44 @@
     "same_page_name_exists": "Same page name exits as「{{pageName}}」",
     "same_page_name_exists_at_path" : "Same page name as {{pageName}} exists at {{path}} ",
     "select_page_to_see" : "Select a page to see"
+  },
+  "user_group": {
+    "select_group": "Select group",
+    "belonging_to_no_group": "Could not find the groups you belong to.",
+    "manage_user_groups": "Manage user groups"
+  },
+  "fix_page_grant": {
+    "modal": {
+      "no_grant_available": "The list of selectable permissions could not be found. Please modify the permissions on the parent page first and try again.",
+      "need_to_fix_grant": "The permissions associated with this page must be modified in order to use the functionality correctly. <br> Please select from the options below to make the change.",
+      "grant_label": {
+        "isForbidden": "Authority not allowed to view",
+        "currentPageGrantLabel": "Authorization for this page: ",
+        "parentPageGrantLabel": "Authority of parent page: ",
+        "docLink": "For more information on modifying permissions, please refer to <a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのリンク</a>"
+      },
+      "radio_btn": {
+        "restrected": "Only those who know the link",
+        "only_me": "only to oneself",
+        "grant_group": "Only specific groups"
+      },
+      "select_group_default_text": "Select Group",
+      "alert_message_select_group": "No group selected",
+      "btn_label": "Conversion",
+      "title": "Modify authority"
+    },
+    "alert": {
+      "description": "You need to modify the permission settings for this page.",
+      "btn_label": "Revision"
+    }
+  },
+  "tooltip": {
+    "like": "Like!",
+    "cancel_like": "Cancel Like",
+    "bookmark": "Bookmark",
+    "cancel_bookmark": "Cancel Bookmark",
+    "receive_notifications": "Receive Notifications",
+    "stop_notification": "Stop Notification",
+    "footprints": "Footprints"
   }
 }

+ 13 - 2
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -28,7 +28,10 @@
     "modal_migration_warning": "管理者はユーザーに、v5 互換形式への変換中はページを作成・変更・削除しないように伝えることを強くお勧めします。",
     "start_upgrading": "v5 互換形式への変換を開始",
     "successfully_started": "正常に v5 互換形式への変換が開始されました",
-    "already_upgraded": "v5 互換形式への変換は既に完了しています"
+    "already_upgraded": "v5 互換形式への変換は既に完了しています",
+    "header_upgrading_progress": "アップグレード進行度",
+    "migration_succeeded": "アップグレードが正常に完了しました!メンテナンスモードを終了して、GROWI を使用することができます。",
+    "migration_failed": "アップグレードが失敗しました。失敗した場合の対処法は GROWI docs を参照してください。"
   },
   "maintenance_mode": {
     "maintenance_mode": "メンテナンスモード",
@@ -468,6 +471,7 @@
   "user_group_management": {
     "create_group": "新規グループの作成",
     "add_child_group": "子グループの追加",
+    "remove_child_group": "解除",
     "deny_create_group": "新規グループの作成はできません。",
     "group_name": "グループ名",
     "group_example": "例: Group1",
@@ -475,6 +479,7 @@
     "select_parent_group": "親グループを選択",
     "release_parent_group": "親グループの解除",
     "add_modal": {
+      "description": "追加したユーザーは、親グループにも追加されます。",
       "add_user": "グループへのユーザー追加",
       "search_option": "検索オプション",
       "enable_option": "{{option}}を有効にする",
@@ -485,7 +490,6 @@
     "group_list": "グループ一覧",
     "child_group_list": "子グループ一覧",
     "back_to_list": "グループ一覧に戻る",
-    "back_to_ancestors_group": "祖先グループに戻る",
     "basic_info": "基本情報",
     "user_list": "ユーザー一覧",
     "created_group": "グループを作成しました",
@@ -501,6 +505,13 @@
       "publish_pages": "全て公開する",
       "delete_pages": "全て削除する",
       "transfer_pages": "全て他のグループに移譲する"
+    },
+    "update_parent_confirm_modal": {
+      "header": "グループの親が変更されます",
+      "caution_change_parent": "この操作はグループ \"{{groupName}}\" の親を変更します。",
+      "danger_message": "このグループに関連する全てのページの閲覧権限に影響があることに注意してください。",
+      "force_update_parents_label": "強制的に足りないユーザーを追加する",
+      "force_update_parents_description": "このオプションを有効化すると、親グループ変更後に祖先グループに足りないユーザーが存在した場合にそれらのユーザーを強制的に追加することができます"
     }
   }
 }

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

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

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

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

+ 12 - 14
packages/app/resource/locales/ja_JP/sandbox.md

@@ -12,7 +12,7 @@
   </div>
 </div>
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 ## Headers 見出し
 
@@ -159,7 +159,7 @@ ___
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 ## 強調
 
@@ -199,7 +199,7 @@ ___
 これは ***イタリック&ボールド*** です
 これは ___イタリック&ボールド___ です
 
-# :pencil: Images
+# :memo: Images
 
 `![Alt文字列](URL)` で`<img>`タグを挿入できます。
 
@@ -220,7 +220,7 @@ ___
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
-# :pencil: Link
+# :memo: Link
 
 ## Markdown 標準
 
@@ -255,10 +255,10 @@ ___
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 ```
 
-[[../Bootstrap4]]  
+[[./Bootstrap4]]  
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 
-# :pencil: Lists
+# :memo: Lists
 
 ## Ul 箇条書きリスト
 
@@ -318,7 +318,7 @@ Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 - [x] タスク2
 
 
-# :pencil: Table
+# :memo: Table
 
 ## Markdown 標準
 
@@ -414,7 +414,7 @@ Content Cell,Content Cell
 :::
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
 
@@ -427,15 +427,13 @@ Content Cell,Content Cell
     後続の段落はインデントされて、前の脚注に属します。
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
@@ -443,7 +441,7 @@ See [emojione](https://www.emojione.com/)
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 

+ 105 - 10
packages/app/resource/locales/ja_JP/translation.json

@@ -15,8 +15,6 @@
   "Move/Rename": "移動/名前変更",
   "Redirected": "リダイレクトされました",
   "Unlinked": "リダイレクト削除",
-  "Like!": "いいね!",
-  "Seen by": "見た人",
   "Done": "完了",
   "Cancel": "キャンセル",
   "Create": "作成",
@@ -100,6 +98,7 @@
   "Connected": "接続されています",
   "Show": "公開",
   "Hide": "非公開",
+  "Loading": "読み込み中...",
   "Disclose E-mail": "メールアドレスの公開",
   "page exists": "このページはすでに存在しています",
   "Error occurred": "エラーが発生しました",
@@ -147,7 +146,8 @@
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Add tags for this page": "タグを付ける",
-  "Check All tags": "全てのタグをチェックする",
+  "popular_tags": "人気のタグ",
+  "Check All tags": "全てのタグを見る",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
   "Show latest": "最新のページを表示",
   "Load latest": "最新版を読み込む",
@@ -171,6 +171,9 @@
   "Link sharing is disabled": "リンクのシェアは無効化されています",
   "successfully_saved_the_page": "ページが正常に保存されました",
   "you_can_not_create_page_with_this_name": "この名前でページを作成することはできません",
+  "not_allowed_to_see_this_page": "このページは閲覧できません",
+  "Confirm": "確認",
+  "Successfully requested": "正常に処理を受け付けました",
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -183,8 +186,7 @@
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
     "invalid_syntax": "%sの構文が不正です",
-    "title_required": "タイトルを入力してください",
-    "slashed_are_not_yet_supported": "スラッシュを含むタイトルにはまだ対応していません"
+    "title_required": "タイトルを入力してください"
   },
   "not_found_page": {
     "Create Page": "ページを作成する",
@@ -212,7 +214,7 @@
     },
     "form_help": {
       "email": "この Wiki では以下のメールアドレスのみ登録可能です。",
-      "password": "パスワードには、6文字以上の半角英数字または記号等を設定してください。",
+      "password": "パスワードには、{{target}}文字以上の半角英数字または記号等を設定してください。",
       "user_id": "ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。"
     }
   },
@@ -386,11 +388,12 @@
     }
   },
   "page_comment": {
-    "display_the_page_when_posting_this_comment": "投稿時のページを表示する"
+    "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
+    "no_user_found": "ユーザー名が見つかりません"
   },
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
-    "already_exists": "新しいページが既に存在しています。",
+    "already_exists": "そのパスを持つページは既に存在しています。",
     "outdated": "ページが他のユーザーによって更新されました。",
     "user_not_admin": "権限のあるユーザーのみが削除できます"
   },
@@ -434,11 +437,15 @@
     "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
+  "deleted_page": "ゴミ箱に入れました",
   "deleted_pages": "{{path}} をゴミ箱に入れました",
   "deleted_pages_completely": "{{path}} を完全に削除しました",
   "renamed_pages": "{{path}} を移動/名前変更しました",
+  "empty_trash": "ゴミ箱を空にしました",
   "modal_empty":{
     "empty_the_trash": "ゴミ箱を空にする",
+    "empty_the_trash_button": "空にする",
+    "not_deletable_notice": "権限がないため、いくつかのページは削除できません",
     "notice": "完全削除したページは元に戻すことができません"
   },
   "modal_duplicate": {
@@ -636,7 +643,8 @@
   "private_legacy_pages": {
     "bulk_operation": "一括操作",
     "convert_all_selected_pages": "新しい v5 互換形式に一括変換",
-    "alert_title": "古い v4 互換形式のプライベートページを表示しています",
+    "input_path_to_convert": "パスを入力して変換",
+    "alert_title": "古い v4 互換形式のプライベートページが存在します",
     "alert_desc1": "このページでは、チェックボックスでページを選択し、画面上部の「一括操作」ボタンから新しい v5 互換形式に一括変換できます。",
     "nopages_title": "おめでとうございます。GROWI v5 を使う準備が完了しました!",
     "nopages_desc1": "今あなたが管理可能なページはすべて v5 互換形式になっているようです。",
@@ -647,6 +655,23 @@
       "convert_recursively_label": "再起的に変換",
       "convert_recursively_desc": "このページの配下のページを再起的に変換します",
       "button_label": "変換"
+    },
+    "toaster": {
+      "page_migration_succeeded": "選択されたページの v5 互換形式への変換が正常に終了しました。",
+      "page_migration_failed_with_paths": "{{paths}} の v5 互換形式への変換中にエラーが発生しました。",
+      "page_migration_failed": "ページの v5 互換形式への変換中にエラーが発生しました。"
+    },
+    "by_path_modal": {
+      "title": "新しい v5 互換形式への変換",
+      "alert": "この操作は取り消すことができず、ユーザーが閲覧できないページも処理の対象になります。",
+      "checkbox_label": "理解しました",
+      "description": "パスを入力することで、そのパスの配下のページを全て v5 互換形式に変換します",
+      "button_label": "変換",
+      "success": "正常に変換を開始しました",
+      "error": "変換を開始できませんでした",
+      "error_grant_invalid": "ページの権限が正しくありません。修正してから再度実行してください",
+      "error_page_not_found": "ページが見つかりませんでした",
+      "error_duplicate_pages_found": "同名のパスを持つページが複数見つかりました。リネームまたは削除してから再度実行してください"
     }
   },
   "security_setting": {
@@ -683,7 +708,8 @@
     "max_age": "有効期間 (ミリ秒)",
     "max_age_desc": "ユーザーのセッション情報の有効期間をミリ秒で指定できます。<br>デフォルト値: 2592000000 (30日間)",
     "max_age_caution": "この値を変更した後は、サーバーを再起動する必要があります。",
-    "page_delete_rights_caution": "「子孫を含む操作」の設定値は、「単体のみの操作」の設定値よりも強いものに強制されます。",
+    "forced_update_desc": "設定が強制変更されました。前回の設定: ",
+    "page_delete_rights_caution": "「(子孫ページを含む)ゴミ箱に入れる操作 / 完全に削除する」の権限は、「ゴミ箱に入れる操作 / 完全に削除する」よりも強い権限になるように強制されます。 <br><br> 管理者のみ可能 > 管理者とページ作者が可能 > 誰でも可能",
     "Authentication mechanism settings": "認証機構設定",
     "setup_is_not_yet_complete":"セットアップはまだ完了してません",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
@@ -991,6 +1017,36 @@
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
   },
+  "emoji" :{
+    "title": "絵文字を選択",
+    "search": "探す",
+    "clear": "リセット",
+    "notfound": "絵文字が見つかりません",
+    "skintext": "デフォルトの肌の色を選択",
+    "categories": {
+      "search": "検索結果",
+      "recent": "最新履歴",
+      "smileys": "スマイリーと感情",
+      "people": "人と体",
+      "nature": "動物と自然",
+      "foods": "食べ物や飲み物",
+      "activity": "アクティビティ",
+      "places": "旅行と場所",
+      "objects": "オブジェクト",
+      "symbols": "シンボル",
+      "flags": "国旗",
+      "custom": "カスタマイズ"
+    },
+    "categorieslabel": "絵文字カテゴリ",
+    "skintones": {
+      "1": "デフォルトの肌の色",
+      "2": "明るい肌のトーン",
+      "3": "ミディアム-明るい肌のトーン",
+      "4": "ミディアムスキントーン",
+      "5": "ミディアムダークスキントーン",
+      "6": "肌の色が濃い"
+    }
+  },
   "maintenance_mode":{
     "maintenance_mode": "メンテナンスモード",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",
@@ -1008,5 +1064,44 @@
     "same_page_name_exists": "ページ名 「{{pageName}}」が重複しています",
     "same_page_name_exists_at_path" : "”{{path}}” において ”{{pageName}}”というページは複数存在しています。",
     "select_page_to_see" : "以下から遷移するページを選択してください。"
+  },
+  "user_group": {
+    "select_group": "グループを選ぶ",
+    "belonging_to_no_group": "所属しているグループが見つかりませんでした。",
+    "manage_user_groups": "グループ管理"
+  },
+  "fix_page_grant": {
+    "modal": {
+      "no_grant_available": "選択可能な権限のリストが見つかりませんでした。まず親ページの権限を修正したのちに再試行してください。",
+      "need_to_fix_grant": "正しく機能を使用するためにはこのページに紐づく権限を修正する必要があります。 <br> 下記の選択肢から選んで変更してください。",
+      "grant_label": {
+        "isForbidden": "権限の閲覧が許可されていません",
+        "currentPageGrantLabel": "このページの権限: ",
+        "parentPageGrantLabel": "親のページの権限: ",
+        "docLink": "権限の修正についての詳細は<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのリンク</a>を参照してください"
+      },
+      "radio_btn": {
+        "restrected": "リンクを知っている人のみ",
+        "only_me": "自分のみ",
+        "grant_group": "特定グループのみ"
+      },
+      "select_group_default_text": "グループを選択",
+      "alert_message_select_group": "グループが選択されていません",
+      "btn_label": "変換",
+      "title": "権限を修正"
+    },
+    "alert": {
+      "description": "このページの権限設定を修正する必要があります。",
+      "btn_label": "修正"
+    }
+  },
+  "tooltip": {
+    "like": "いいね!",
+    "cancel_like": "いいねを取り消す",
+    "bookmark": "ブックマーク",
+    "cancel_bookmark": "ブックマークを取り消す",
+    "receive_notifications": "通知を受け取る",
+    "stop_notification": "通知を止める",
+    "footprints": "足跡"
   }
 }

+ 13 - 2
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -28,7 +28,10 @@
     "modal_migration_warning": "这个过程可能需要很长时间。强烈建议管理员告诉用户在转换期间不要创建、修改或删除页面。",
     "start_upgrading": "开始转换为v5兼容性",
     "successfully_started": "成功开始转换",
-    "already_upgraded": "你已经完成了向v5兼容性的转换"
+    "already_upgraded": "你已经完成了向v5兼容性的转换",
+    "header_upgrading_progress": "升级进度",
+    "migration_succeeded": "您的升级已经成功完成! 退出维护模式,可以使用GROWI。",
+    "migration_failed": "升级失败。请参考GROWI的文档,了解在失败情况下该如何处理。"
   },
   "maintenance_mode": {
     "maintenance_mode": "维护模式",
@@ -478,6 +481,7 @@
   "user_group_management": {
     "create_group": "创建新组",
     "add_child_group": "添加一个子组",
+    "remove_child_group": "移除",
     "deny_create_group": "不能用当前设置创建新组。",
     "group_name": "组名",
     "group_example": "e.g.:第1组",
@@ -485,6 +489,7 @@
     "select_parent_group": "选择父组",
     "release_parent_group": "Release parent group",
     "add_modal": {
+      "description": "添加的用户也将被添加到所有的父组。",
       "add_user": "将用户添加到创建的组",
       "search_option": "搜索选项",
       "enable_option": "启用{{option}",
@@ -495,7 +500,6 @@
     "group_list": "组列表",
     "child_group_list": "儿童组名单",
     "back_to_list": "返回组列表",
-    "back_to_ancestors_group": "返回到祖先组",
     "basic_info": "基本信息",
     "user_list": "用户列表",
     "created_group": "已创建组",
@@ -511,6 +515,13 @@
       "publish_pages": "全部发布",
       "delete_pages": "全部删除",
       "transfer_pages": "转移到另一组"
+    },
+    "update_parent_confirm_modal": {
+      "header": "该组的父组被改变",
+      "caution_change_parent": "该操作改变了组的父级,即 \"{{groupName}}\" 。",
+      "danger_message": "注意,查看与该组相关的所有页面的权限会受到影响。",
+      "force_update_parents_label": "强行添加失踪的用户",
+      "force_update_parents_description": "激活这个选项,如果在父组改变后,在祖先组中有缺失的用户,可以强制添加这些用户"
     }
   }
 }

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

@@ -2,9 +2,11 @@
 
 嗨,{{ email }}
 
-已收到更改您 GROWI 帐户 {{appTitle}} 密码的请求。
+已收到更改您 GROWI ({{appTitle}}) 帐户 密码的请求。
 要重置密码,请单击下面的链接。
 
 {{ url }}
 
+这个链接在10分钟后的{ expiredAt }}失效。
+
 如果您没有要求重置密码,则可以放心地忽略此电子邮件。

+ 0 - 0
packages/app/resource/locales/zh_CN/notifications/PasswordResetSuccessful.txt → packages/app/resource/locales/zh_CN/notifications/passwordResetSuccessful.txt


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

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

+ 12 - 14
packages/app/resource/locales/zh_CN/sandbox.md

@@ -12,7 +12,7 @@
   </div>
 </div>
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 ## Headers
 
@@ -160,7 +160,7 @@ ___
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 ## Strong Text
 
@@ -200,7 +200,7 @@ This is ___Italic & Bold___.
 This is ***Italic & Bold***.
 This is ___Italic & Bold___.
 
-# :pencil: Images
+# :memo: Images
 
 You can insert `<img>` tag using `![description](URL)`.
 
@@ -221,7 +221,7 @@ The size of the image can be set by using an HTML image tag
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
-# :pencil: Link
+# :memo: Link
 
 ## Markdown standard
 
@@ -256,10 +256,10 @@ Both the page description and link address can be displayed on the page.
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 ```
 
-[[../Bootstrap4]]  
+[[./Bootstrap4]]  
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 
-# :pencil: Lists
+# :memo: Lists
 
 ## Ul Bulleted list
 
@@ -319,7 +319,7 @@ The numbers don’t have to be in numerical order, but the list should start wit
 - [x] Task2
 
 
-# :pencil: Table
+# :memo: Table
 
 ## Markdown Standard
 
@@ -415,7 +415,7 @@ Content Cell,Content Cell
 :::
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
@@ -428,15 +428,13 @@ Long footnotes can be written as [^longnote].
     Subsequent paragraphs are indented and belong to the previous footnote.
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
@@ -444,7 +442,7 @@ See [emojione](https://www.emojione.com/)
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 # :heavy_plus_sign: More..

+ 105 - 10
packages/app/resource/locales/zh_CN/translation.json

@@ -16,8 +16,6 @@
 	"Move/Rename": "移动/重命名",
 	"Redirected": "重定向",
 	"Unlinked": "Unlinked",
-	"Like!": "Like!",
-	"Seen by": "Seen by",
   "Done": "Done",
   "Cancel": "取消",
 	"Create": "创建",
@@ -107,6 +105,7 @@
 	"Connected": "Connected",
 	"Show": "显示",
 	"Hide": "隐藏",
+  "Loading": "加载...",
 	"Reset": "重置",
 	"Disclose E-mail": "显示邮箱",
 	"page exists": "页面已存在",
@@ -156,6 +155,7 @@
 	"Shareable link": "可分享链接",
 	"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
 	"Add tags for this page": "添加标签",
+  "popular_tags": "流行标签",
   "Check All tags": "检查所有标签",
 	"You have no tag, You can set tags on pages": "你没有标签,可以在页面上设置标签",
 	"Show latest": "显示最新",
@@ -177,12 +177,14 @@
   "Link sharing is disabled": "你不允许分享该链接",
   "successfully_saved_the_page": "成功地保存了该页面",
   "you_can_not_create_page_with_this_name": "您无法使用此名称创建页面",
+  "not_allowed_to_see_this_page": "你不能看到这个页面",
+  "Confirm": "确定",
+  "Successfully requested": "进程成功接受",
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
 		"invalid_syntax": "%s的语法无效。",
-    "title_required": "标题是必需的。",
-    "slashed_are_not_yet_supported": "スラッシュを含むタイトルにはまだ対応していません"
+    "title_required": "标题是必需的。"
   },
   "not_found_page": {
     "Create Page": "创建页面",
@@ -210,7 +212,7 @@
 		},
 		"form_help": {
 			"email": "您必须有下面列出的电子邮件地址才能注册此wiki。",
-			"password": "密码长度必须至少为6个字符。",
+			"password": "密码长度必须至少为8个字符。",
 			"user_id": "您创建的网页的URL将包含您的用户ID。您的用户ID可以由字母、数字和一些符号组成。"
 		}
 	},
@@ -365,11 +367,12 @@
 		}
   },
   "page_comment": {
-    "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
+    "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
+    "no_user_found": "未找到用户名"
   },
 	"page_api_error": {
 		"notfound_or_forbidden": "未找到或禁止原始页。",
-		"already_exists": "新建页面已存在",
+		"already_exists": "具有该路径的页面已存在",
 		"outdated": "页面已被某人更新,现在已过时。",
 		"user_not_admin": "仅管理员用户可以删除"
   },
@@ -413,11 +416,15 @@
 		"recursively": "Delete children of <code>%s</code> recursively.",
 		"completely": "Delete completely instead of putting it into trash."
   },
+  "deleted_page": "移到了垃圾箱。",
   "deleted_pages": "将 {{path}} 放入垃圾箱",
   "deleted_pages_completely": "{{path}} 已被完全删除",
   "renamed_pages": "移动/重命名 {{path}}",
+  "empty_trash": "清空垃圾",
 	"modal_empty": {
-		"empty_the_trash": "Empty The Trash",
+		"empty_the_trash": "清空垃圾",
+    "empty_the_trash_button": "清空垃圾",
+    "not_deletable_notice": "由于缺乏权限,一些页面不能被删除",
 		"notice": "完全删除的页面是不可恢复的。"
 	},
 	"modal_duplicate": {
@@ -642,7 +649,8 @@
     "max_age": "有效期间  (msec)",
     "max_age_desc": "指定使用户会话过期的数量(以毫秒为单位)。<br>默认值: 2592000000 (30天)",
     "max_age_caution": "修改该值后需要重启服务器。",
-    "page_delete_rights_caution": "\"包括后代的操作\" 的设置被迫强于 \"只对选定的页面进行操作\" 的设置。",
+    "forced_update_desc": "设置已被强行更改。以前的设置: ",
+    "page_delete_rights_caution": "\"删除/全部删除\"权限(包括后代页面)被强制强于\"删除/完全删除\"权限。 <br> <br> 仅管理员 > 管理员|作者 > 何人",
 		"Authentication mechanism settings": "身份验证机制设置",
 		"setup_is_not_yet_complete": "安装尚未完成",
 		"alert_siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
@@ -922,7 +930,8 @@
   "private_legacy_pages": {
     "bulk_operation": "批量操作",
     "convert_all_selected_pages": "全部转换为新的v5兼容格式",
-    "alert_title": "你正在查看旧的v4兼容的私人网页。",
+		"input_path_to_convert": "输入一个转换页面的路径",
+    "alert_title": "存在旧的v4兼容格式的私人网页。",
     "alert_desc1": "在这一页,你可以用复选框选择页面,并通过屏幕上方的批量操作按钮批量转换为新的v5兼容格式。",
     "nopages_title": "恭喜你。准备使用GROWI v5!",
     "nopages_desc1": "现在你能管理的所有页面似乎都是v5兼容的格式。",
@@ -933,6 +942,23 @@
       "convert_recursively_label": "递归地转换子页面。",
       "convert_recursively_desc": "递归地转换该路径下的页面。",
       "button_label": "转换"
+    },
+    "toaster": {
+      "page_migration_succeeded": "已成功将所选页面转换为 v5 兼容格式。",
+      "page_migration_failed_with_paths": "将 {{paths}} 转换为 v5 兼容格式时出错",
+      "page_migration_failed": "将页面转换为 v5 兼容格式时出错。"
+    },
+    "by_path_modal": {
+      "title": "转换为新的v5兼容格式",
+      "alert": "这一操作不能被撤销,用户不能查看的页面也要进行处理。",
+      "checkbox_label": "明白了",
+      "description": "输入一个路径,该路径下的所有页面将被转换为v5兼容格式。",
+      "button_label": "转换",
+      "success": "成功地请求转换。",
+      "error": "请求转换失败。",
+      "error_grant_invalid": "页面权限不正确。请更正并重试。",
+      "error_page_not_found": "没有找到页面。",
+      "error_duplicate_pages_found": "发现多个具有相同路径名称的页面。请重新命名或删除并重试。"
     }
   },
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
@@ -1001,6 +1027,36 @@
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
   },
+  "emoji" :{
+    "title": "选择一个表情符号",
+    "search": "搜索",
+    "clear": "重置",
+    "notfound": "找不到表情符号",
+    "skintext": "选择您的默认肤色",
+    "categories": {
+      "search": "搜索结果",
+      "recent": "经常使用",
+      "smileys": "笑脸和情感",
+      "people": "人和身体",
+      "nature": "动物与自然",
+      "foods": "食物和饮料",
+      "activity": "活动",
+      "places": "旅行和地方",
+      "objects": "对象",
+      "symbols": "符号",
+      "flags": "旗帜",
+      "custom": "定制"
+    },
+    "categorieslabel": "表情符号类别",
+    "skintones": {
+      "1": "默认肤色",
+      "2": "浅肤色",
+      "3": "中浅肤色",
+      "4": "中等肤色",
+      "5": "中深肤色",
+      "6": "深色肤色"
+    }
+  },
   "maintenance_mode":{
     "maintenance_mode": "维护模式",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",
@@ -1018,5 +1074,44 @@
     "same_page_name_exists": "页面名称「{{pageName}}」是重复的",
     "same_page_name_exists_at_path" : "在”{{path}}” 中,有不止一个名为”{{pageName}}”的页面",
     "select_page_to_see" : "请在下面选择你想去的页面。"
+  },
+  "user_group": {
+    "select_group": "选择组别",
+    "belonging_to_no_group": "无法找到你所属的团体。",
+    "manage_user_groups": "管理用户组"
+  },
+  "fix_page_grant": {
+    "modal": {
+      "no_grant_available": "无法找到可选择的权限列表。 请先修改父页的权限,然后再试一次。",
+      "need_to_fix_grant": "为了正确使用该功能,需要修改与该页面相关的权限。 <br> 请从以下选项中选择进行更改。",
+      "grant_label": {
+        "isForbidden": "无权查看的机构",
+        "currentPageGrantLabel": "本页的权限: ",
+        "parentPageGrantLabel": "父页的权限: ",
+        "docLink": "关于修改授权的更多信息,请参见此<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>此链接</a>"
+      },
+      "radio_btn": {
+        "restrected": "只有那些知道链接的人",
+        "only_me": "只对自己说",
+        "grant_group": "仅限特定群体"
+      },
+      "select_group_default_text": "选择组别",
+      "alert_message_select_group": "未选择组别",
+      "btn_label": "蜕变",
+      "title": "修改后的授权书"
+    },
+    "alert": {
+      "description": "本页的授权设置需要修改。",
+      "btn_label": "修改"
+    }
+  },
+  "tooltip": {
+    "like": "很好!",
+    "cancel_like": "取消喜欢",
+    "bookmark": "书签",
+    "cancel_bookmark": "取消书签",
+    "receive_notifications": "接收通知",
+    "stop_notification": "停止通知",
+    "footprints": "脚印"
   }
 }

+ 52 - 42
packages/app/src/client/app.jsx

@@ -1,57 +1,58 @@
 import React from 'react';
-import ReactDOM from 'react-dom';
-import { Provider } from 'unstated';
-import { I18nextProvider } from 'react-i18next';
+
 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 CommentContainer from '~/client/services/CommentContainer';
+import ContextExtractor from '~/client/services/ContextExtractor';
+import EditorContainer from '~/client/services/EditorContainer';
+import PageContainer from '~/client/services/PageContainer';
+import PageHistoryContainer from '~/client/services/PageHistoryContainer';
+import PersonalContainer from '~/client/services/PersonalContainer';
+import RevisionComparerContainer from '~/client/services/RevisionComparerContainer';
+import TagContainer from '~/client/services/TagContainer';
+import IdenticalPathPage from '~/components/IdenticalPathPage';
+import PrivateLegacyPages from '~/components/PrivateLegacyPages';
 import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
-import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
 import ErrorBoundary from '../components/ErrorBoudary';
-import Sidebar from '../components/Sidebar';
-import { SearchPage } from '../components/SearchPage';
-import TagsList from '../components/TagsList';
-import DisplaySwitcher from '../components/Page/DisplaySwitcher';
-import { defaultEditorOptions, defaultPreviewOptions } from '../components/PageEditor/OptionsSelector';
-import Page from '../components/Page';
-import PageComments from '../components/PageComments';
-import PageContentFooter from '../components/PageContentFooter';
-import PageTimeline from '../components/PageTimeline';
-import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
-import ShareLinkAlert from '../components/Page/ShareLinkAlert';
-import RedirectedAlert from '../components/Page/RedirectedAlert';
-import TrashPageList from '../components/TrashPageList';
-import TrashPageAlert from '../components/Page/TrashPageAlert';
-import NotFoundPage from '../components/NotFoundPage';
-import NotFoundAlert from '../components/Page/NotFoundAlert';
+import Fab from '../components/Fab';
 import ForbiddenPage from '../components/ForbiddenPage';
-import PageStatusAlert from '../components/PageStatusAlert';
-import RecentCreated from '../components/RecentCreated/RecentCreated';
 import RecentlyCreatedIcon from '../components/Icons/RecentlyCreatedIcon';
-import MyDraftList from '../components/MyDraftList/MyDraftList';
-import BookmarkList from '../components/PageList/BookmarkList';
-import Fab from '../components/Fab';
+import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
+import MaintenanceModeContent from '../components/MaintenanceModeContent';
 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 IdenticalPathPage from '~/components/IdenticalPathPage';
-
-import ContextExtractor from '~/client/services/ContextExtractor';
-import PageContainer from '~/client/services/PageContainer';
-import PageHistoryContainer from '~/client/services/PageHistoryContainer';
-import RevisionComparerContainer from '~/client/services/RevisionComparerContainer';
-import CommentContainer from '~/client/services/CommentContainer';
-import EditorContainer from '~/client/services/EditorContainer';
-import TagContainer from '~/client/services/TagContainer';
-import PersonalContainer from '~/client/services/PersonalContainer';
+import NotFoundPage from '../components/NotFoundPage';
+import Page from '../components/Page';
+import DisplaySwitcher from '../components/Page/DisplaySwitcher';
+import FixPageGrantAlert from '../components/Page/FixPageGrantAlert';
+import NotFoundAlert from '../components/Page/NotFoundAlert';
+import RedirectedAlert from '../components/Page/RedirectedAlert';
+import ShareLinkAlert from '../components/Page/ShareLinkAlert';
+import TrashPageAlert from '../components/Page/TrashPageAlert';
+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 TagPage from '../components/TagPage';
+import TrashPageList from '../components/TrashPageList';
 
 import { appContainer, componentMappings } from './base';
 import { toastError } from './util/apiNotification';
-import { PrivateLegacyPages } from '~/components/PrivateLegacyPages';
+
 
 const logger = loggerFactory('growi:cli:app');
 
@@ -65,7 +66,7 @@ const pageContainer = new PageContainer(appContainer);
 const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContainer);
 const revisionComparerContainer = new RevisionComparerContainer(appContainer, pageContainer);
 const commentContainer = new CommentContainer(appContainer);
-const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
+const editorContainer = new EditorContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
@@ -90,12 +91,16 @@ Object.assign(componentMappings, {
   'identical-path-page': <IdenticalPathPage />,
 
   // 'revision-history': <PageHistory pageId={pageId} />,
-  'tags-page': <TagsList crowi={appContainer} />,
+  'tags-page': <TagPage />,
 
   'grw-page-status-alert-container': <PageStatusAlert />,
 
+  'maintenance-mode-content': <MaintenanceModeContent />,
+
   'trash-page-alert': <TrashPageAlert />,
 
+  'fix-page-grant-alert': <FixPageGrantAlert />,
+
   'trash-page-list-container': <TrashPageList />,
 
   'not-found-page': <NotFoundPage />,
@@ -120,9 +125,14 @@ Object.assign(componentMappings, {
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
-    'page-comments-list': <PageComments />,
-    'page-comment-write': <CommentEditorLazyRenderer />,
-    'page-content-footer': <PageContentFooter />,
+    '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 />,
   });

+ 9 - 8
packages/app/src/client/base.jsx

@@ -1,22 +1,22 @@
 import React from 'react';
 
+import AppContainer from '~/client/services/AppContainer';
+import SocketIoContainer from '~/client/services/SocketIoContainer';
+import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
+import PutbackPageModal from '~/components/PutbackPageModal';
 import Xss from '~/services/xss';
 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 HotkeysManager from '../components/Hotkeys/HotkeysManager';
+import PageAccessoriesModal from '../components/PageAccessoriesModal';
 import PageCreateModal from '../components/PageCreateModal';
 import PageDeleteModal from '../components/PageDeleteModal';
 import PageDuplicateModal from '../components/PageDuplicateModal';
-import PageRenameModal from '../components/PageRenameModal';
 import PagePresentationModal from '../components/PagePresentationModal';
-import PageAccessoriesModal from '../components/PageAccessoriesModal';
-import PutbackPageModal from '~/components/PutbackPageModal';
-
-import AppContainer from '~/client/services/AppContainer';
-import SocketIoContainer from '~/client/services/SocketIoContainer';
-import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
+import PageRenameModal from '../components/PageRenameModal';
 
 const logger = loggerFactory('growi:cli:app');
 
@@ -48,6 +48,7 @@ const componentMappings = {
 
   'page-create-modal': <PageCreateModal />,
   'page-delete-modal': <PageDeleteModal />,
+  'empty-trash-modal': <EmptyTrashModal />,
   'page-duplicate-modal': <PageDuplicateModal />,
   'page-rename-modal': <PageRenameModal />,
   'page-presentation-modal': <PagePresentationModal />,

+ 10 - 6
packages/app/src/client/nologin.jsx

@@ -1,17 +1,19 @@
 import React from 'react';
+
 import ReactDOM from 'react-dom';
-import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
+import { Provider } from 'unstated';
 
-import { i18nFactory } from './util/i18n';
 
 import AppContainer from '~/client/services/AppContainer';
+import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
 
 import InstallerForm from '../components/InstallerForm';
 import LoginForm from '../components/LoginForm';
-import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
 import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
-import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
+import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
+
+import { i18nFactory } from './util/i18n';
 
 const i18n = i18nFactory();
 
@@ -85,10 +87,12 @@ if (loginFormElem) {
   );
 }
 
-// render PasswordResetRequestForm
-const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
 const appContainer = new AppContainer();
 appContainer.initApp();
+
+
+// render PasswordResetRequestForm
+const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
 if (passwordResetRequestFormElem) {
 
   ReactDOM.render(

+ 14 - 13
packages/app/src/client/services/AdminAppContainer.js

@@ -1,15 +1,16 @@
 import { Container } from 'unstated';
 
+import { apiv3Get, apiv3Post, apiv3Put } from '../util/apiv3-client';
+
 /**
  * Service container for admin app setting page (AppSettings.jsx)
  * @extends {Container} unstated Container
  */
 export default class AdminAppContainer extends Container {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
-    this.appContainer = appContainer;
     this.dummyTitle = 0;
     this.dummyTitleForError = 1;
 
@@ -75,7 +76,7 @@ export default class AdminAppContainer extends Container {
    * retrieve app sttings data
    */
   async retrieveAppSettingsData() {
-    const response = await this.appContainer.apiv3.get('/app-settings/');
+    const response = await apiv3Get('/app-settings/');
     const { appSettingsParams } = response.data;
 
     this.setState({
@@ -326,7 +327,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    */
   async updateAppSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/app-setting', {
+    const response = await apiv3Put('/app-settings/app-setting', {
       title: this.state.title,
       confidential: this.state.confidential,
       globalLang: this.state.globalLang,
@@ -344,7 +345,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    */
   async updateSiteUrlSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/site-url-setting', {
+    const response = await apiv3Put('/app-settings/site-url-setting', {
       siteUrl: this.state.siteUrl,
     });
     const { siteUrlSettingParams } = response.data;
@@ -369,7 +370,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    */
   async updateSmtpSetting() {
-    const response = await this.appContainer.apiv3.put('/app-settings/smtp-setting', {
+    const response = await apiv3Put('/app-settings/smtp-setting', {
       fromAddress: this.state.fromAddress,
       transmissionMethod: this.state.transmissionMethod,
       smtpHost: this.state.smtpHost,
@@ -388,7 +389,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    */
   async updateSesSetting() {
-    const response = await this.appContainer.apiv3.put('/app-settings/ses-setting', {
+    const response = await apiv3Put('/app-settings/ses-setting', {
       fromAddress: this.state.fromAddress,
       transmissionMethod: this.state.transmissionMethod,
       sesAccessKeyId: this.state.sesAccessKeyId,
@@ -404,7 +405,7 @@ export default class AdminAppContainer extends Container {
    * @memberOf AdminAppContainer
    */
   async sendTestEmail() {
-    return this.appContainer.apiv3.post('/app-settings/smtp-test');
+    return apiv3Post('/app-settings/smtp-test');
   }
 
   /**
@@ -434,7 +435,7 @@ export default class AdminAppContainer extends Container {
       requestParams.s3ReferenceFileWithRelayMode = this.state.s3ReferenceFileWithRelayMode;
     }
 
-    const response = await this.appContainer.apiv3.put('/app-settings/file-upload-setting', requestParams);
+    const response = await apiv3Put('/app-settings/file-upload-setting', requestParams);
     const { responseParams } = response.data;
     return this.setState(responseParams);
   }
@@ -445,7 +446,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    */
   async updatePluginSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/plugin-setting', {
+    const response = await apiv3Put('/app-settings/plugin-setting', {
       isEnabledPlugins: this.state.isEnabledPlugins,
     });
     const { pluginSettingParams } = response.data;
@@ -457,17 +458,17 @@ export default class AdminAppContainer extends Container {
    * @memberOf AdminAppContainer
    */
   async v5PageMigrationHandler() {
-    const response = await this.appContainer.apiv3.post('/app-settings/v5-schema-migration');
+    const response = await apiv3Post('/app-settings/v5-schema-migration');
     const { isV5Compatible } = response.data;
     return { isV5Compatible };
   }
 
   async startMaintenanceMode() {
-    await this.appContainer.apiv3.post('/app-settings/maintenance-mode', { flag: true });
+    await apiv3Post('/app-settings/maintenance-mode', { flag: true });
   }
 
   async endMaintenanceMode() {
-    await this.appContainer.apiv3.post('/app-settings/maintenance-mode', { flag: false });
+    await apiv3Post('/app-settings/maintenance-mode', { flag: false });
   }
 
 }

+ 6 - 5
packages/app/src/client/services/AdminBasicSecurityContainer.js

@@ -1,8 +1,10 @@
 import { Container } from 'unstated';
-import loggerFactory from '~/utils/logger';
 
+import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
 
 /**
@@ -11,10 +13,9 @@ const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
  */
 export default class AdminBasicSecurityContainer extends Container {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
-    this.appContainer = appContainer;
     this.dummyIsSameUsernameTreatedAsIdenticalUser = 0;
     this.dummyIsSameUsernameTreatedAsIdenticalUserForError = 1;
 
@@ -31,7 +32,7 @@ export default class AdminBasicSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { basicAuth } = response.data.securityParams;
       this.setState({
         isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
@@ -65,7 +66,7 @@ export default class AdminBasicSecurityContainer extends Container {
     let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/basic', requestParams);
+    const response = await apiv3Put('/security-setting/basic', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({

+ 11 - 11
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminCustomizeContainer');
@@ -13,10 +14,9 @@ const logger = loggerFactory('growi:services:AdminCustomizeContainer');
  */
 export default class AdminCustomizeContainer extends Container {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
-    this.appContainer = appContainer;
     this.dummyCurrentTheme = 0;
     this.dummyCurrentThemeForError = 1;
 
@@ -76,7 +76,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async retrieveCustomizeData() {
     try {
-      const response = await this.appContainer.apiv3.get('/customize-setting/');
+      const response = await apiv3Get('/customize-setting/');
       const { customizeParams } = response.data;
 
       this.setState({
@@ -246,7 +246,7 @@ export default class AdminCustomizeContainer extends Container {
   async previewTheme(themeName) {
     try {
       // get theme asset path
-      const response = await this.appContainer.apiv3.get('/customize-setting/theme/asset-path', { themeName });
+      const response = await apiv3Get('/customize-setting/theme/asset-path', { themeName });
       const { assetPath } = response.data;
 
       const themeLink = document.getElementById('grw-theme-link');
@@ -274,7 +274,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateCustomizeTheme() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/theme', {
+      const response = await apiv3Put('/customize-setting/theme', {
         themeType: this.state.currentTheme,
       });
       const { customizedParams } = response.data;
@@ -294,7 +294,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateCustomizeFunction() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/function', {
+      const response = await apiv3Put('/customize-setting/function', {
         isEnabledTimeline: this.state.isEnabledTimeline,
         isSavedStatesOfTabChanges: this.state.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
@@ -332,7 +332,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateHighlightJsStyle() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/highlight', {
+      const response = await apiv3Put('/customize-setting/highlight', {
         highlightJsStyle: this.state.currentHighlightJsStyleId,
         highlightJsStyleBorder: this.state.isHighlightJsStyleBorderEnabled,
       });
@@ -354,7 +354,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateCustomizeTitle() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-title', {
+      const response = await apiv3Put('/customize-setting/customize-title', {
         customizeTitle: this.state.currentCustomizeTitle,
       });
       const { customizedParams } = response.data;
@@ -374,7 +374,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateCustomizeHeader() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-header', {
+      const response = await apiv3Put('/customize-setting/customize-header', {
         customizeHeader: this.state.currentCustomizeHeader,
       });
       const { customizedParams } = response.data;
@@ -394,7 +394,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateCustomizeCss() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-css', {
+      const response = await apiv3Put('/customize-setting/customize-css', {
         customizeCss: this.state.currentCustomizeCss,
       });
       const { customizedParams } = response.data;
@@ -415,7 +415,7 @@ export default class AdminCustomizeContainer extends Container {
    */
   async updateCustomizeScript() {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-script', {
+      const response = await apiv3Put('/customize-setting/customize-script', {
         customizeScript: this.state.currentCustomizeScript,
       });
       const { customizedParams } = response.data;

+ 5 - 5
packages/app/src/client/services/AdminExternalAccountsContainer.js

@@ -2,6 +2,8 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
+import { apiv3Delete, apiv3Get } from '../util/apiv3-client';
+
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
@@ -12,11 +14,9 @@ const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
  */
 export default class AdminExternalAccountsContainer extends Container {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
-    this.appContainer = appContainer;
-
     this.state = {
       externalAccounts: [],
       totalAccounts: 0,
@@ -42,7 +42,7 @@ export default class AdminExternalAccountsContainer extends Container {
   async retrieveExternalAccountsByPagingNum(selectedPage) {
 
     const params = { page: selectedPage };
-    const { data } = await this.appContainer.apiv3.get('/users/external-accounts', params);
+    const { data } = await apiv3Get('/users/external-accounts', params);
 
     if (data.paginateResult == null) {
       throw new Error('data must conclude \'paginateResult\' property.');
@@ -64,7 +64,7 @@ export default class AdminExternalAccountsContainer extends Container {
    * @param {string} externalAccountId id of the External Account to be removed
    */
   async removeExternalAccountById(externalAccountId) {
-    const res = await this.appContainer.apiv3.delete(`/users/external-accounts/${externalAccountId}/remove`);
+    const res = await apiv3Delete(`/users/external-accounts/${externalAccountId}/remove`);
     const deletedUserData = res.data.externalAccount;
     await this.retrieveExternalAccountsByPagingNum(this.state.activePage);
     return deletedUserData.accountId;

+ 32 - 12
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -4,9 +4,11 @@ import {
   PageSingleDeleteConfigValue, PageSingleDeleteCompConfigValue,
   PageRecursiveDeleteConfigValue, PageRecursiveDeleteCompConfigValue,
 } from '~/interfaces/page-delete-config';
-import { toastError } from '../util/apiNotification';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { toastError } from '../util/apiNotification';
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 /**
  * Service container for admin security page (SecuritySetting.jsx)
  * @extends {Container} unstated Container
@@ -16,7 +18,6 @@ export default class AdminGeneralSecurityContainer extends Container {
   constructor(appContainer) {
     super();
 
-    this.appContainer = appContainer;
     this.dummyCurrentRestrictGuestMode = 0;
     this.dummyCurrentRestrictGuestModeForError = 1;
 
@@ -30,6 +31,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageRecursiveDeletionAuthority: PageRecursiveDeleteConfigValue.Inherit,
       currentPageCompleteDeletionAuthority: PageSingleDeleteCompConfigValue.AdminOnly,
       currentPageRecursiveCompleteDeletionAuthority: PageRecursiveDeleteCompConfigValue.Inherit,
+      previousPageRecursiveDeletionAuthority: null,
+      previousPageRecursiveCompleteDeletionAuthority: null,
       expandOtherOptionsForDeletion: false,
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
@@ -55,12 +58,14 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.changePageCompleteDeletionAuthority = this.changePageCompleteDeletionAuthority.bind(this);
     this.changePageRecursiveDeletionAuthority = this.changePageRecursiveDeletionAuthority.bind(this);
     this.changePageRecursiveCompleteDeletionAuthority = this.changePageRecursiveCompleteDeletionAuthority.bind(this);
+    this.changePreviousPageRecursiveDeletionAuthority = this.changePreviousPageRecursiveDeletionAuthority.bind(this);
+    this.changePreviousPageRecursiveCompleteDeletionAuthority = this.changePreviousPageRecursiveCompleteDeletionAuthority.bind(this);
 
   }
 
   async retrieveSecurityData() {
     await this.retrieveSetupStratedies();
-    const response = await this.appContainer.apiv3.get('/security-setting/');
+    const response = await apiv3Get('/security-setting/');
     const { generalSetting, shareLinkSetting, generalAuth } = response.data.securityParams;
     this.setState({
       currentRestrictGuestMode: generalSetting.restrictGuestMode,
@@ -149,18 +154,33 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ currentPageRecursiveCompleteDeletionAuthority: val });
   }
 
+  /**
+   * Change previousPageRecursiveDeletionAuthority
+   */
+  changePreviousPageRecursiveDeletionAuthority(val) {
+    this.setState({ previousPageRecursiveDeletionAuthority: val });
+  }
+
+
+  /**
+   * Change previousPageRecursiveCompleteDeletionAuthority
+   */
+  changePreviousPageRecursiveCompleteDeletionAuthority(val) {
+    this.setState({ previousPageRecursiveCompleteDeletionAuthority: val });
+  }
+
   /**
    * Switch ExpandOtherOptionsForDeletion
    */
-  switchExpandOtherOptionsForDeletion() {
-    this.setState({ expandOtherOptionsForDeletion:  !this.state.expandOtherOptionsForDeletion });
+  switchExpandOtherOptionsForDeletion(bool) {
+    this.setState({ expandOtherOptionsForDeletion: bool });
   }
 
   /**
    * Switch ExpandOtherOptionsForDeletion
    */
-  switchExpandOtherOptionsForCompleteDeletion() {
-    this.setState({ expandOtherOptionsForCompleteDeletion:  !this.state.expandOtherOptionsForCompleteDeletion });
+  switchExpandOtherOptionsForCompleteDeletion(bool) {
+    this.setState({ expandOtherOptionsForCompleteDeletion: bool });
   }
 
   /**
@@ -196,7 +216,7 @@ export default class AdminGeneralSecurityContainer extends Container {
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/general-setting', requestParams);
+    const response = await apiv3Put('/security-setting/general-setting', requestParams);
     const { securitySettingParams } = response.data;
     return securitySettingParams;
   }
@@ -208,7 +228,7 @@ export default class AdminGeneralSecurityContainer extends Container {
     const requestParams = {
       disableLinkSharing: !this.state.disableLinkSharing,
     };
-    const response = await this.appContainer.apiv3.put('/security-setting/share-link-setting', requestParams);
+    const response = await apiv3Put('/security-setting/share-link-setting', requestParams);
     this.setDisableLinkSharing(!this.state.disableLinkSharing);
     return response;
   }
@@ -219,7 +239,7 @@ export default class AdminGeneralSecurityContainer extends Container {
   async switchAuthentication(stateVariableName, authId) {
     const isEnabled = !this.state[stateVariableName];
     try {
-      await this.appContainer.apiv3.put('/security-setting/authentication/enabled', {
+      await apiv3Put('/security-setting/authentication/enabled', {
         isEnabled,
         authId,
       });
@@ -236,7 +256,7 @@ export default class AdminGeneralSecurityContainer extends Container {
    */
   async retrieveSetupStratedies() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/authentication');
+      const response = await apiv3Get('/security-setting/authentication');
       const { setupStrategies } = response.data;
       this.setState({ setupStrategies });
     }
@@ -254,7 +274,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       page,
     };
 
-    const { data } = await this.appContainer.apiv3.get('/security-setting/all-share-links', params);
+    const { data } = await apiv3Get('/security-setting/all-share-links', params);
 
     if (data.paginateResult == null) {
       throw new Error('data must conclude \'paginateResult\' property.');

+ 6 - 5
packages/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminGitHubSecurityContainer');
 
 /**
@@ -16,7 +18,6 @@ export default class AdminGitHubSecurityContainer extends Container {
   constructor(appContainer) {
     super();
 
-    this.appContainer = appContainer;
     this.dummyGithubClientId = 0;
     this.dummyGithubClientIdForError = 1;
 
@@ -36,7 +37,7 @@ export default class AdminGitHubSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { githubOAuth } = response.data.securityParams;
       this.setState({
         githubClientId: githubOAuth.githubClientId,
@@ -88,7 +89,7 @@ export default class AdminGitHubSecurityContainer extends Container {
     let requestParams = { githubClientId, githubClientSecret, isSameUsernameTreatedAsIdenticalUser };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/github-oauth', requestParams);
+    const response = await apiv3Put('/security-setting/github-oauth', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({

+ 16 - 13
packages/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminGoogleSecurityContainer');
 
 /**
@@ -16,7 +18,6 @@ export default class AdminGoogleSecurityContainer extends Container {
   constructor(appContainer) {
     super();
 
-    this.appContainer = appContainer;
     this.dummyGoogleClientId = 0;
     this.dummyGoogleClientIdForError = 1;
 
@@ -26,7 +27,7 @@ export default class AdminGoogleSecurityContainer extends Container {
       // set dummy value tile for using suspense
       googleClientId: this.dummyGoogleClientId,
       googleClientSecret: '',
-      isSameUsernameTreatedAsIdenticalUser: false,
+      isSameEmailTreatedAsIdenticalUser: false,
     };
 
 
@@ -37,12 +38,12 @@ export default class AdminGoogleSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { googleOAuth } = response.data.securityParams;
       this.setState({
         googleClientId: googleOAuth.googleClientId,
         googleClientSecret: googleOAuth.googleClientSecret,
-        isSameUsernameTreatedAsIdenticalUser: googleOAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser: googleOAuth.isSameEmailTreatedAsIdenticalUser,
       });
     }
     catch (err) {
@@ -74,30 +75,32 @@ export default class AdminGoogleSecurityContainer extends Container {
   }
 
   /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
+   * Switch isSameEmailTreatedAsIdenticalUser
    */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  switchIsSameEmailTreatedAsIdenticalUser() {
+    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
   }
 
+
   /**
    * Update googleSetting
    */
   async updateGoogleSetting() {
-    const { googleClientId, googleClientSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
+    const { googleClientId, googleClientSecret, isSameEmailTreatedAsIdenticalUser } = this.state;
+    console.log('updateGoogleSetting', isSameEmailTreatedAsIdenticalUser);
 
     let requestParams = {
-      googleClientId, googleClientSecret, isSameUsernameTreatedAsIdenticalUser,
+      googleClientId, googleClientSecret, isSameEmailTreatedAsIdenticalUser,
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/google-oauth', requestParams);
+    const response = await apiv3Put('/security-setting/google-oauth', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({
       googleClientId: securitySettingParams.googleClientId,
       googleClientSecret: securitySettingParams.googleClientSecret,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
     });
     return response;
   }

+ 3 - 4
packages/app/src/client/services/AdminHomeContainer.js

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
+import { apiv3Get } from '../util/apiv3-client';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminHomeContainer');
@@ -13,11 +14,9 @@ const logger = loggerFactory('growi:services:AdminHomeContainer');
  */
 export default class AdminHomeContainer extends Container {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
-    this.appContainer = appContainer;
-
     this.copyStateValues = {
       DEFAULT: 'default',
       DONE: 'done',
@@ -53,7 +52,7 @@ export default class AdminHomeContainer extends Container {
    */
   async retrieveAdminHomeData() {
     try {
-      const response = await this.appContainer.apiv3.get('/admin-home/');
+      const response = await apiv3Get('/admin-home/');
       const { adminHomeParams } = response.data;
 
       this.setState(prevState => ({

+ 9 - 7
packages/app/src/client/services/AdminImportContainer.js

@@ -3,6 +3,8 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 
 import { toastSuccess, toastError } from '../util/apiNotification';
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Get } from '../util/apiv3-client';
 
 const logger = loggerFactory('growi:appSettings');
 
@@ -48,7 +50,7 @@ export default class AdminImportContainer extends Container {
    * retrieve app sttings data
    */
   async retrieveImportSettingsData() {
-    const response = await this.appContainer.apiv3.get('/import/');
+    const response = await apiv3Get('/import/');
     const {
       importSettingsParams,
     } = response.data;
@@ -73,7 +75,7 @@ export default class AdminImportContainer extends Container {
         'importer:esa:team_name': this.state.esaTeamName,
         'importer:esa:access_token': this.state.esaAccessToken,
       };
-      await this.appContainer.apiPost('/admin/import/esa', params);
+      await apiPost('/admin/import/esa', params);
       toastSuccess('Import posts from esa success.');
     }
     catch (err) {
@@ -88,7 +90,7 @@ export default class AdminImportContainer extends Container {
         'importer:esa:team_name': this.state.esaTeamName,
         'importer:esa:access_token': this.state.esaAccessToken,
       };
-      await this.appContainer.apiPost('/admin/import/testEsaAPI', params);
+      await apiPost('/admin/import/testEsaAPI', params);
       toastSuccess('Test connection to esa success.');
     }
     catch (error) {
@@ -102,7 +104,7 @@ export default class AdminImportContainer extends Container {
       'importer:esa:access_token': this.state.esaAccessToken,
     };
     try {
-      await this.appContainer.apiPost('/admin/settings/importerEsa', params);
+      await apiPost('/admin/settings/importerEsa', params);
       toastSuccess('Updated');
     }
     catch (err) {
@@ -117,7 +119,7 @@ export default class AdminImportContainer extends Container {
         'importer:qiita:team_name': this.state.qiitaTeamName,
         'importer:qiita:access_token': this.state.qiitaAccessToken,
       };
-      await this.appContainer.apiPost('/admin/import/qiita', params);
+      await apiPost('/admin/import/qiita', params);
       toastSuccess('Import posts from qiita:team success.');
     }
     catch (err) {
@@ -133,7 +135,7 @@ export default class AdminImportContainer extends Container {
         'importer:qiita:team_name': this.state.qiitaTeamName,
         'importer:qiita:access_token': this.state.qiitaAccessToken,
       };
-      await this.appContainer.apiPost('/admin/import/testQiitaAPI', params);
+      await apiPost('/admin/import/testQiitaAPI', params);
       toastSuccess('Test connection to qiita:team success.');
     }
     catch (err) {
@@ -148,7 +150,7 @@ export default class AdminImportContainer extends Container {
       'importer:qiita:access_token': this.state.qiitaAccessToken,
     };
     try {
-      await this.appContainer.apiPost('/admin/settings/importerQiita', params);
+      await apiPost('/admin/settings/importerQiita', params);
       toastSuccess('Updated');
     }
     catch (err) {

+ 5 - 3
packages/app/src/client/services/AdminLdapSecurityContainer.js

@@ -1,8 +1,10 @@
 import { Container } from 'unstated';
-import loggerFactory from '~/utils/logger';
 
+import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
 
 /**
@@ -42,7 +44,7 @@ export default class AdminLdapSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { ldapAuth } = response.data.securityParams;
       this.setState({
         serverUrl: ldapAuth.serverUrl,
@@ -183,7 +185,7 @@ export default class AdminLdapSecurityContainer extends Container {
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/ldap', requestParams);
+    const response = await apiv3Put('/security-setting/ldap', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({

+ 5 - 2
packages/app/src/client/services/AdminLocalSecurityContainer.js

@@ -1,6 +1,9 @@
 import { Container } from 'unstated';
+
 import loggerFactory from '~/utils/logger';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminLocalSecurityContainer');
 /**
@@ -30,7 +33,7 @@ export default class AdminLocalSecurityContainer extends Container {
 
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { localSetting } = response.data.securityParams;
       this.setState({
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
@@ -89,7 +92,7 @@ export default class AdminLocalSecurityContainer extends Container {
    */
   async updateLocalSecuritySetting() {
     const { registrationWhiteList, isPasswordResetEnabled, isEmailAuthenticationEnabled } = this.state;
-    const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
+    const response = await apiv3Put('/security-setting/local-setting', {
       registrationMode: this.state.registrationMode,
       registrationWhiteList,
       isPasswordResetEnabled,

+ 7 - 5
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -1,5 +1,7 @@
 import { Container } from 'unstated';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 /**
  * Service container for admin markdown setting page (MarkDownSetting.jsx)
  * @extends {Container} unstated Container
@@ -43,7 +45,7 @@ export default class AdminMarkDownContainer extends Container {
    * retrieve markdown data
    */
   async retrieveMarkdownData() {
-    const response = await this.appContainer.apiv3.get('/markdown-setting/');
+    const response = await apiv3Get('/markdown-setting/');
     const { markdownParams } = response.data;
 
     this.setState({
@@ -93,7 +95,7 @@ export default class AdminMarkDownContainer extends Container {
    */
   async updateLineBreakSetting() {
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/lineBreak', {
+    const response = await apiv3Put('/markdown-setting/lineBreak', {
       isEnabledLinebreaks: this.state.isEnabledLinebreaks,
       isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
     });
@@ -106,7 +108,7 @@ export default class AdminMarkDownContainer extends Container {
    */
   async updateIndentSetting() {
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/indent', {
+    const response = await apiv3Put('/markdown-setting/indent', {
       adminPreferredIndentSize: this.state.adminPreferredIndentSize,
       isIndentSizeForced: this.state.isIndentSizeForced,
     });
@@ -123,7 +125,7 @@ export default class AdminMarkDownContainer extends Container {
     tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
     attrWhiteList = Array.isArray(attrWhiteList) ? attrWhiteList : attrWhiteList.split(',');
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/xss', {
+    const response = await apiv3Put('/markdown-setting/xss', {
       isEnabledXss: this.state.isEnabledXss,
       xssOption: this.state.xssOption,
       tagWhiteList,
@@ -138,7 +140,7 @@ export default class AdminMarkDownContainer extends Container {
    */
   async updatePresentationSetting() {
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/presentation', {
+    const response = await apiv3Put('/markdown-setting/presentation', {
       pageBreakSeparator: this.state.pageBreakSeparator,
       pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
     });

+ 10 - 6
packages/app/src/client/services/AdminNotificationContainer.js

@@ -1,5 +1,9 @@
 import { Container } from 'unstated';
 
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '../util/apiv3-client';
+
 /**
  * Service container for admin Notification setting page (NotificationSetting.jsx)
  * @extends {Container} unstated Container
@@ -37,7 +41,7 @@ export default class AdminNotificationContainer extends Container {
    * Retrieve notificationData
    */
   async retrieveNotificationData() {
-    const response = await this.appContainer.apiv3.get('/notification-setting/');
+    const response = await apiv3Get('/notification-setting/');
     const { notificationParams } = response.data;
 
     this.setState({
@@ -57,7 +61,7 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async updateSlackAppConfiguration() {
-    const response = await this.appContainer.apiv3.put('/notification-setting/slack-configuration', {
+    const response = await apiv3Put('/notification-setting/slack-configuration', {
       webhookUrl: this.state.webhookUrl,
       isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
       slackToken: this.state.slackToken,
@@ -71,7 +75,7 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async addNotificationPattern(pathPattern, channel) {
-    const response = await this.appContainer.apiv3.post('/notification-setting/user-notification', {
+    const response = await apiv3Post('/notification-setting/user-notification', {
       pathPattern,
       channel,
     });
@@ -83,7 +87,7 @@ export default class AdminNotificationContainer extends Container {
    * Delete user trigger notification pattern
    */
   async deleteUserTriggerNotificationPattern(notificatiionId) {
-    const response = await this.appContainer.apiv3.delete(`/notification-setting/user-notification/${notificatiionId}`);
+    const response = await apiv3Delete(`/notification-setting/user-notification/${notificatiionId}`);
     const deletedNotificaton = response.data;
     await this.retrieveNotificationData();
     return deletedNotificaton;
@@ -108,7 +112,7 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async updateGlobalNotificationForPages() {
-    const response = await this.appContainer.apiv3.put('/notification-setting/notify-for-page-grant/', {
+    const response = await apiv3Put('/notification-setting/notify-for-page-grant/', {
       isNotificationForOwnerPageEnabled: this.state.isNotificationForOwnerPageEnabled,
       isNotificationForGroupPageEnabled: this.state.isNotificationForGroupPageEnabled,
     });
@@ -120,7 +124,7 @@ export default class AdminNotificationContainer extends Container {
    * Delete global notification pattern
    */
   async deleteGlobalNotificationPattern(notificatiionId) {
-    const response = await this.appContainer.apiv3.delete(`/notification-setting/global-notification/${notificatiionId}`);
+    const response = await apiv3Delete(`/notification-setting/global-notification/${notificatiionId}`);
     const deletedNotificaton = response.data;
     await this.retrieveNotificationData();
     return deletedNotificaton;

+ 6 - 4
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
 
 /**
@@ -51,7 +53,7 @@ export default class AdminOidcSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { oidcAuth } = response.data.securityParams;
       this.setState({
         oidcProviderName: oidcAuth.oidcProviderName,
@@ -261,7 +263,7 @@ export default class AdminOidcSecurityContainer extends Container {
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/oidc', requestParams);
+    const response = await apiv3Put('/security-setting/oidc', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({

+ 6 - 5
packages/app/src/client/services/AdminSamlSecurityContainer.js

@@ -1,11 +1,12 @@
-import { Container } from 'unstated';
-
-
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminSamlSecurityContainer');
 
 /**
@@ -57,7 +58,7 @@ export default class AdminSamlSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { samlAuth } = response.data.securityParams;
       this.setState({
         missingMandatoryConfigKeys: samlAuth.missingMandatoryConfigKeys,
@@ -195,7 +196,7 @@ export default class AdminSamlSecurityContainer extends Container {
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/saml', requestParams);
+    const response = await apiv3Put('/security-setting/saml', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({

+ 4 - 2
packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js

@@ -1,5 +1,7 @@
 import { Container } from 'unstated';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 /**
  * Service container for admin LegacySlackIntegration setting page (LegacySlackIntegration.jsx)
  * @extends {Container} unstated Container
@@ -35,7 +37,7 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
    * Retrieve notificationData
    */
   async retrieveData() {
-    const response = await this.appContainer.apiv3.get('/slack-integration-legacy-settings/');
+    const response = await apiv3Get('/slack-integration-legacy-settings/');
     const { slackIntegrationParams } = response.data;
 
     this.setState({
@@ -79,7 +81,7 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async updateSlackAppConfiguration() {
-    const response = await this.appContainer.apiv3.put('/slack-integration-legacy-settings/', {
+    const response = await apiv3Put('/slack-integration-legacy-settings/', {
       webhookUrl: this.state.webhookUrl,
       isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
       slackToken: this.state.slackToken,

+ 6 - 4
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
 
 /**
@@ -36,7 +38,7 @@ export default class AdminTwitterSecurityContainer extends Container {
    */
   async retrieveSecurityData() {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { twitterOAuth } = response.data.securityParams;
       this.setState({
         twitterConsumerKey: twitterOAuth.twitterConsumerKey,
@@ -88,7 +90,7 @@ export default class AdminTwitterSecurityContainer extends Container {
     let requestParams = { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/twitter-oauth', requestParams);
+    const response = await apiv3Put('/security-setting/twitter-oauth', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({

+ 13 - 8
packages/app/src/client/services/AdminUsersContainer.js

@@ -1,7 +1,12 @@
-import { Container } from 'unstated';
 import { debounce } from 'throttle-debounce';
+import { Container } from 'unstated';
+
 import loggerFactory from '~/utils/logger';
 
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '../util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminUserGroupDetailContainer');
 
@@ -135,7 +140,7 @@ export default class AdminUsersContainer extends Container {
       // Even if email is hidden, it will be displayed on admin page.
       forceIncludeAttributes: ['email'],
     };
-    const { data } = await this.appContainer.apiv3.get('/users', params);
+    const { data } = await apiv3Get('/users', params);
 
     if (data.paginateResult == null) {
       throw new Error('data must conclude \'paginateResult\' property.');
@@ -159,7 +164,7 @@ export default class AdminUsersContainer extends Container {
    * @param {bool} sendEmail
    */
   async createUserInvited(shapedEmailList, sendEmail) {
-    const response = await this.appContainer.apiv3.post('/users/invite', {
+    const response = await apiv3Post('/users/invite', {
       shapedEmailList,
       sendEmail,
     });
@@ -205,7 +210,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    */
   async giveUserAdmin(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/giveAdmin`);
+    const response = await apiv3Put(`/users/${userId}/giveAdmin`);
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
@@ -218,7 +223,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    */
   async removeUserAdmin(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/removeAdmin`);
+    const response = await apiv3Put(`/users/${userId}/removeAdmin`);
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
@@ -231,7 +236,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    */
   async activateUser(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/activate`);
+    const response = await apiv3Put(`/users/${userId}/activate`);
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
@@ -244,7 +249,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    */
   async deactivateUser(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/deactivate`);
+    const response = await apiv3Put(`/users/${userId}/deactivate`);
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
@@ -257,7 +262,7 @@ export default class AdminUsersContainer extends Container {
    * @return {object} removedUserData
    */
   async removeUser(userId) {
-    const response = await this.appContainer.apiv3.delete(`/users/${userId}/remove`);
+    const response = await apiv3Delete(`/users/${userId}/remove`);
     const removedUserData = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return removedUserData;

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

@@ -2,20 +2,7 @@ import { Container } from 'unstated';
 
 import InterceptorManager from '~/services/interceptor-manager';
 
-import {
-  apiDelete, apiGet, apiPost, apiRequest,
-} from '../util/apiv1-client';
-import {
-  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
-} from '../util/apiv3-client';
-import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
-
-import {
-  mediaQueryListForDarkMode,
-  applyColorScheme,
-} from '../util/color-scheme';
-
 import { i18nFactory } from '../util/i18n';
 
 /**
@@ -27,10 +14,6 @@ export default class AppContainer extends Container {
   constructor() {
     super();
 
-    this.state = {
-      preferDarkModeByMediaQuery: false,
-    };
-
     // get csrf token from body element
     // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
     const body = document.querySelector('body');
@@ -38,9 +21,6 @@ export default class AppContainer extends Container {
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
 
-    const userAgent = window.navigator.userAgent.toLowerCase();
-    this.isMobile = /iphone|ipad|android/.test(userAgent);
-
     const currentUserElem = document.getElementById('growi-current-user');
     if (currentUserElem != null) {
       this.currentUser = JSON.parse(currentUserElem.textContent);
@@ -58,23 +38,6 @@ export default class AppContainer extends Container {
     this.containerInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
-
-    this.apiGet = apiGet;
-    this.apiPost = apiPost;
-    this.apiDelete = apiDelete;
-    this.apiRequest = apiRequest;
-
-    this.apiv3Get = apiv3Get;
-    this.apiv3Post = apiv3Post;
-    this.apiv3Put = apiv3Put;
-    this.apiv3Delete = apiv3Delete;
-
-    this.apiv3 = {
-      get: apiv3Get,
-      post: apiv3Post,
-      put: apiv3Put,
-      delete: apiv3Delete,
-    };
   }
 
   /**
@@ -85,27 +48,18 @@ export default class AppContainer extends Container {
   }
 
   initApp() {
-    this.initMediaQueryForColorScheme();
-
     this.injectToWindow();
   }
 
   initContents() {
     const body = document.querySelector('body');
 
-    this.isAdmin = body.dataset.isAdmin === 'true';
-
     this.isDocSaved = true;
 
     this.originRenderer = new GrowiRenderer(this);
 
     this.interceptorManager = new InterceptorManager();
 
-    if (this.currentUser != null) {
-      // remove old user cache
-      this.removeOldUserCache();
-    }
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
       this.initPlugins();
@@ -114,18 +68,6 @@ export default class AppContainer extends Container {
     this.injectToWindow();
   }
 
-  async initMediaQueryForColorScheme() {
-    const switchStateByMediaQuery = async(mql) => {
-      const preferDarkMode = mql.matches;
-      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
-
-      applyColorScheme();
-    };
-
-    // add event listener
-    mediaQueryListForDarkMode.addListener(switchStateByMediaQuery);
-  }
-
   initPlugins() {
     const growiPlugin = window.growiPlugin;
     growiPlugin.installAll(this, this.originRenderer);
@@ -222,28 +164,6 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
-  /**
-   *
-   * @param {string} breakpoint id of breakpoint
-   * @param {function} handler event handler for media query
-   * @param {boolean} invokeOnInit invoke handler after the initialization if true
-   */
-  addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
-    document.addEventListener('DOMContentLoaded', () => {
-      // get the value of '--breakpoint-*'
-      const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
-
-      const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
-
-      // add event listener
-      mediaQuery.addListener(handler);
-      // initialize
-      if (invokeOnInit) {
-        handler(mediaQuery);
-      }
-    });
-  }
-
   getOriginRenderer() {
     return this.originRenderer;
   }
@@ -266,21 +186,6 @@ export default class AppContainer extends Container {
     return renderer;
   }
 
-  getEmojiStrategy() {
-    return emojiStrategy;
-  }
-
-  removeOldUserCache() {
-    if (window.localStorage.userByName == null) {
-      return;
-    }
-
-    const keys = ['userByName', 'userById', 'users', 'lastFetched'];
-
-    keys.forEach((key) => {
-      window.localStorage.removeItem(key);
-    });
-  }
 
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;

+ 9 - 6
packages/app/src/client/services/CommentContainer.js

@@ -2,6 +2,9 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
+import { apiGet, apiPost } from '../util/apiv1-client';
+import { apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:services:CommentContainer');
 
 /**
@@ -67,7 +70,7 @@ export default class CommentContainer extends Container {
     const { pageId } = this.getPageContainer().state;
 
     // get data (desc order array)
-    const res = await this.appContainer.apiGet('/comments.get', { page_id: pageId });
+    const res = await apiGet('/comments.get', { page_id: pageId });
     if (res.ok) {
       const comments = res.comments;
       this.setState({ comments });
@@ -89,7 +92,7 @@ export default class CommentContainer extends Container {
     }
 
     try {
-      await this.appContainer.apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
+      await apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
     }
     catch (err) {
       // Error alert doesn't apear, because user don't need to notice this error.
@@ -103,7 +106,7 @@ export default class CommentContainer extends Container {
   postComment(comment, isMarkdown, replyTo, isSlackEnabled, slackChannels) {
     const { pageId, revisionId } = this.getPageContainer().state;
 
-    return this.appContainer.apiPost('/comments.add', {
+    return apiPost('/comments.add', {
       commentForm: {
         comment,
         page_id: pageId,
@@ -129,7 +132,7 @@ export default class CommentContainer extends Container {
   putComment(comment, isMarkdown, commentId, author) {
     const { pageId, revisionId } = this.getPageContainer().state;
 
-    return this.appContainer.apiPost('/comments.update', {
+    return apiPost('/comments.update', {
       commentForm: {
         comment,
         is_markdown: isMarkdown,
@@ -145,7 +148,7 @@ export default class CommentContainer extends Container {
   }
 
   deleteComment(comment) {
-    return this.appContainer.apiPost('/comments.remove', { comment_id: comment._id })
+    return apiPost('/comments.remove', { comment_id: comment._id })
       .then((res) => {
         if (res.ok) {
           this.findAndSplice(comment);
@@ -163,7 +166,7 @@ export default class CommentContainer extends Container {
     formData.append('path', pagePath);
     formData.append('page_id', pageId);
 
-    return this.appContainer.apiPost(endpoint, formData);
+    return apiPost(endpoint, formData);
   }
 
 }

+ 16 - 8
packages/app/src/client/services/ContextExtractor.tsx

@@ -1,22 +1,25 @@
 import React, { FC, useEffect, useState } from 'react';
+
 import { pagePathUtils } from '@growi/core';
 
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import {
+  useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
+  useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+} from '~/stores/ui';
+import { useSetupGlobalSocket, useSetupGlobalAdminSocket } from '~/stores/websocket';
+
 import {
   useSiteUrl,
   useCurrentCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd,
   useIsDeleted, useIsNotCreatable, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
-  useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath,
+  useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
+  useDefaultIndentSize, useIsIndentSizeForced,
 } from '../../stores/context';
-import {
-  useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-  useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
-} from '~/stores/ui';
-import { useSetupGlobalSocket } from '~/stores/websocket';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
 
@@ -68,6 +71,7 @@ const ContextExtractorOnce: FC = () => {
   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);
@@ -107,6 +111,8 @@ const ContextExtractorOnce: FC = () => {
   useIsSearchServiceConfigured(configByContextHydrate.isSearchServiceConfigured);
   useIsSearchServiceReachable(configByContextHydrate.isSearchServiceReachable);
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
+  useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
+  useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
 
 
   // Page
@@ -139,6 +145,7 @@ const ContextExtractorOnce: FC = () => {
   useNotFoundTargetPathOrId(notFoundTargetPathOrId);
   useIsNotFoundPermalink(isNotFoundPermalink);
   useIsSearchPage(isSearchPage);
+  useHasParent(hasParent);
 
   // Navigation
   usePreferDrawerModeByUser();
@@ -161,6 +168,7 @@ const ContextExtractorOnce: FC = () => {
 
   // Global Socket
   useSetupGlobalSocket();
+  useSetupGlobalAdminSocket();
 
   return null;
 };

+ 2 - 77
packages/app/src/client/services/EditorContainer.js

@@ -4,46 +4,27 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:services:EditorContainer');
 
+
 /**
  * Service container related to options for Editor/Preview
  * @extends {Container} unstated Container
  */
 export default class EditorContainer extends Container {
 
-  constructor(appContainer, defaultEditorOptions, defaultPreviewOptions) {
+  constructor(appContainer) {
     super();
 
     this.appContainer = appContainer;
     this.appContainer.registerContainer(this);
-    this.retrieveEditorSettings = this.retrieveEditorSettings.bind(this);
-
-    const mainContent = document.querySelector('#content-main');
-
-    if (mainContent == null) {
-      logger.debug('#content-main element is not exists');
-      return;
-    }
 
     this.state = {
       tags: null,
-
-      editorOptions: {},
-      previewOptions: {},
-
-      // Defaults to null to show modal when not in DB
-      isTextlintEnabled: null,
-      textlintRules: [],
-
-      indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
     };
 
     this.isSetBeforeunloadEventHandler = false;
 
     this.initDrafts();
 
-    this.editorOptions = null;
-    this.initEditorOptions('editorOptions', 'editorOptions', defaultEditorOptions);
-    this.initEditorOptions('previewOptions', 'previewOptions', defaultPreviewOptions);
   }
 
   /**
@@ -78,30 +59,6 @@ export default class EditorContainer extends Container {
     }
   }
 
-  initEditorOptions(stateKey, localStorageKey, defaultOptions) {
-    // load from localStorage
-    const optsStr = window.localStorage[localStorageKey];
-
-    let loadedOpts = {};
-    // JSON.parseparse
-    if (optsStr != null) {
-      try {
-        loadedOpts = JSON.parse(optsStr);
-      }
-      catch (e) {
-        this.localStorage.removeItem(localStorageKey);
-      }
-    }
-
-    // set to state obj
-    this.state[stateKey] = Object.assign(defaultOptions, loadedOpts);
-  }
-
-  saveOptsToLocalStorage() {
-    window.localStorage.setItem('editorOptions', JSON.stringify(this.state.editorOptions));
-    window.localStorage.setItem('previewOptions', JSON.stringify(this.state.previewOptions));
-  }
-
   setCaretLine(line) {
     const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     if (pageEditor != null) {
@@ -116,19 +73,11 @@ export default class EditorContainer extends Container {
     }
   }
 
-  // TODO: Remove when SWR is complete
   getCurrentOptionsToSave() {
     const opt = {
-      // isSlackEnabled: this.state.isSlackEnabled,
-      // slackChannels: this.state.slackChannels,
-      // grant: this.state.grant,
       pageTags: this.state.tags,
     };
 
-    // if (this.state.grantGroupId != null) {
-    //   opt.grantUserGroupId = this.state.grantGroupId;
-    // }
-
     return opt;
   }
 
@@ -175,28 +124,4 @@ export default class EditorContainer extends Container {
     return null;
   }
 
-
-  /**
-   * Retrieve Editor Settings
-   */
-  async retrieveEditorSettings() {
-    if (this.appContainer.isGuestUser) {
-      return;
-    }
-
-    const { data } = await this.appContainer.apiv3Get('/personal-setting/editor-settings');
-
-    if (data?.textlintSettings == null) {
-      return;
-    }
-
-    // Defaults to null to show modal when not in DB
-    const { isTextlintEnabled = null, textlintRules = [] } = data.textlintSettings;
-
-    this.setState({
-      isTextlintEnabled,
-      textlintRules,
-    });
-  }
-
 }

+ 10 - 10
packages/app/src/client/services/PageContainer.js

@@ -1,19 +1,19 @@
-import { Container } from 'unstated';
-
-
+import { pagePathUtils } from '@growi/core';
 import * as entities from 'entities';
 import * as toastr from 'toastr';
-import { pagePathUtils } from '@growi/core';
+import { Container } from 'unstated';
+
 
-import loggerFactory from '~/utils/logger';
 import { EditorMode } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Post } from '../util/apiv3-client';
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
 } from '../util/interceptor/detach-code-blocks';
-
 import {
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
@@ -120,7 +120,7 @@ export default class PageContainer extends Container {
     if (unlinkPageButton != null) {
       unlinkPageButton.addEventListener('click', async() => {
         try {
-          const res = await this.appContainer.apiPost('/pages.unlink', { path });
+          const res = await apiPost('/pages.unlink', { path });
           window.location.href = encodeURI(`${res.path}?unlinked=true`);
         }
         catch (err) {
@@ -194,7 +194,7 @@ export default class PageContainer extends Container {
     this.setState(newState);
   }
 
-  setTocHtml(tocHtml) {
+  async setTocHtml(tocHtml) {
     if (this.state.tocHtml !== tocHtml) {
       this.setState({ tocHtml });
     }
@@ -350,7 +350,7 @@ export default class PageContainer extends Container {
       body: markdown,
     });
 
-    const res = await this.appContainer.apiv3Post('/pages/', params);
+    const res = await apiv3Post('/pages/', params);
     const { page, tags, revision } = res.data;
 
     return { page, tags, revision };
@@ -366,7 +366,7 @@ export default class PageContainer extends Container {
       body: markdown,
     });
 
-    const res = await this.appContainer.apiPost('/pages.update', params);
+    const res = await apiPost('/pages.update', params);
     if (!res.ok) {
       throw new Error(res.error);
     }

+ 3 - 2
packages/app/src/client/services/PageHistoryContainer.js

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
+import { apiv3Get } from '../util/apiv3-client';
 
 const logger = loggerFactory('growi:PageHistoryContainer');
 
@@ -60,7 +61,7 @@ export default class PageHistoryContainer extends Container {
     }
 
     // Get one more for the bottom display
-    const res = await this.appContainer.apiv3Get('/revisions/list', {
+    const res = await apiv3Get('/revisions/list', {
       pageId, shareLinkId, page, limit: pagingLimitForApiParam,
     });
     const rev = res.data.docs;
@@ -147,7 +148,7 @@ export default class PageHistoryContainer extends Container {
     }
 
     try {
-      const res = await this.appContainer.apiv3Get(`/revisions/${revision._id}`, { pageId, shareLinkId });
+      const res = await apiv3Get(`/revisions/${revision._id}`, { pageId, shareLinkId });
       this.setState({
         revisions: this.state.revisions.map((rev) => {
           // comparing ObjectId

+ 22 - 8
packages/app/src/client/services/PersonalContainer.js

@@ -2,6 +2,9 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:PersonalContainer');
 
@@ -30,6 +33,7 @@ export default class PersonalContainer extends Container {
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       externalAccounts: [],
       apiToken: '',
+      slackMemberId: '',
     };
 
   }
@@ -46,7 +50,7 @@ export default class PersonalContainer extends Container {
    */
   async retrievePersonalData() {
     try {
-      const response = await this.appContainer.apiv3.get('/personal-setting/');
+      const response = await apiv3Get('/personal-setting/');
       const { currentUser } = response.data;
       this.setState({
         name: currentUser.name,
@@ -55,6 +59,7 @@ export default class PersonalContainer extends Container {
         lang: currentUser.lang,
         isGravatarEnabled: currentUser.isGravatarEnabled,
         apiToken: currentUser.apiToken,
+        slackMemberId: currentUser.slackMemberId,
       });
     }
     catch (err) {
@@ -88,7 +93,7 @@ export default class PersonalContainer extends Container {
    */
   async retrieveExternalAccounts() {
     try {
-      const response = await this.appContainer.apiv3.get('/personal-setting/external-accounts');
+      const response = await apiv3Get('/personal-setting/external-accounts');
       const { externalAccounts } = response.data;
 
       this.setState({ externalAccounts });
@@ -114,6 +119,13 @@ export default class PersonalContainer extends Container {
     this.setState({ email: inputValue });
   }
 
+  /**
+   * Change Slack Member ID
+   */
+  changeSlackMemberId(inputValue) {
+    this.setState({ slackMemberId: inputValue });
+  }
+
   /**
    * Change isEmailPublished
    */
@@ -142,11 +154,12 @@ export default class PersonalContainer extends Container {
    */
   async updateBasicInfo() {
     try {
-      const response = await this.appContainer.apiv3.put('/personal-setting/', {
+      const response = await apiv3Put('/personal-setting/', {
         name: this.state.name,
         email: this.state.email,
         isEmailPublished: this.state.isEmailPublished,
         lang: this.state.lang,
+        slackMemberId: this.state.slackMemberId,
       });
       const { updatedUser } = response.data;
 
@@ -155,6 +168,7 @@ export default class PersonalContainer extends Container {
         email: updatedUser.email,
         isEmailPublished: updatedUser.isEmailPublished,
         lang: updatedUser.lang,
+        slackMemberId: updatedUser.slackMemberId,
       });
     }
     catch (err) {
@@ -170,7 +184,7 @@ export default class PersonalContainer extends Container {
    */
   async updateProfileImage() {
     try {
-      const response = await this.appContainer.apiv3.put('/personal-setting/image-type', {
+      const response = await apiv3Put('/personal-setting/image-type', {
         isGravatarEnabled: this.state.isGravatarEnabled,
       });
       const { userData } = response.data;
@@ -193,7 +207,7 @@ export default class PersonalContainer extends Container {
       const formData = new FormData();
       formData.append('file', file);
       formData.append('_csrf', this.appContainer.csrfToken);
-      const response = await this.appContainer.apiPost('/attachments.uploadProfileImage', formData);
+      const response = await apiPost('/attachments.uploadProfileImage', formData);
       this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
     }
     catch (err) {
@@ -208,7 +222,7 @@ export default class PersonalContainer extends Container {
    */
   async deleteProfileImage() {
     try {
-      await this.appContainer.apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
+      await apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
       this.setState({ isUploadedPicture: false, uploadedPictureSrc: DEFAULT_IMAGE });
     }
     catch (err) {
@@ -223,7 +237,7 @@ export default class PersonalContainer extends Container {
    */
   async associateLdapAccount(account) {
     try {
-      await this.appContainer.apiv3.put('/personal-setting/associate-ldap', account);
+      await apiv3Put('/personal-setting/associate-ldap', account);
     }
     catch (err) {
       this.setState({ retrieveError: err });
@@ -237,7 +251,7 @@ export default class PersonalContainer extends Container {
    */
   async disassociateLdapAccount(account) {
     try {
-      await this.appContainer.apiv3.put('/personal-setting/disassociate-ldap', account);
+      await apiv3Put('/personal-setting/disassociate-ldap', account);
     }
     catch (err) {
       this.setState({ retrieveError: err });

+ 3 - 2
packages/app/src/client/services/RevisionComparerContainer.js

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
+import { apiv3Get } from '../util/apiv3-client';
 
 const logger = loggerFactory('growi:PageHistoryContainer');
 
@@ -75,7 +76,7 @@ export default class RevisionComparerContainer extends Container {
     const { pageId, shareLinkId } = this.pageContainer.state;
 
     try {
-      const res = await this.appContainer.apiv3Get('/revisions/list', {
+      const res = await apiv3Get('/revisions/list', {
         pageId, shareLinkId, page: 1, limit: 1,
       });
       return res.data.docs[0];
@@ -96,7 +97,7 @@ export default class RevisionComparerContainer extends Container {
     const { pageId, shareLinkId } = this.pageContainer.state;
 
     try {
-      const res = await this.appContainer.apiv3Get(`/revisions/${revisionId}`, {
+      const res = await apiv3Get(`/revisions/${revisionId}`, {
         pageId, shareLinkId,
       });
       return res.data.revision;

+ 3 - 1
packages/app/src/client/services/TagContainer.js

@@ -2,6 +2,8 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
+import { apiGet } from '../util/apiv1-client';
+
 const logger = loggerFactory('growi:services:TagContainer');
 
 /**
@@ -48,7 +50,7 @@ export default class TagContainer extends Container {
     let tags = [];
     // when the page exists or shared page
     if (pageId != null && shareLinkId == null) {
-      const res = await this.appContainer.apiGet('/pages.getPageTag', { pageId });
+      const res = await apiGet('/pages.getPageTag', { pageId });
       tags = res.tags;
     }
     // when the page not exist

+ 14 - 13
packages/app/src/client/util/GrowiRenderer.js

@@ -2,24 +2,24 @@ import MarkdownIt from 'markdown-it';
 
 import loggerFactory from '~/utils/logger';
 
-import Linker from './PreProcessor/Linker';
 import CsvToTable from './PreProcessor/CsvToTable';
 import EasyGrid from './PreProcessor/EasyGrid';
+import Linker from './PreProcessor/Linker';
 import XssFilter from './PreProcessor/XssFilter';
-
+import BlockdiagConfigurer from './markdown-it/blockdiag';
+import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
 import EmojiConfigurer from './markdown-it/emoji';
 import FooternoteConfigurer from './markdown-it/footernote';
-import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
 import HeaderConfigurer from './markdown-it/header';
+import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
+import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
+import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
 import MathJaxConfigurer from './markdown-it/mathjax';
 import PlantUMLConfigurer from './markdown-it/plantuml';
 import TableConfigurer from './markdown-it/table';
+import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
 import TaskListsConfigurer from './markdown-it/task-lists';
 import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
-import BlockdiagConfigurer from './markdown-it/blockdiag';
-import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
-import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
-import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
 
 const logger = loggerFactory('growi:util:GrowiRenderer');
 
@@ -68,6 +68,7 @@ export default class GrowiRenderer {
     this.isMarkdownItConfigured = false;
 
     this.markdownItConfigurers = [
+      new LinkerByRelativePathConfigurer(appContainer),
       new TaskListsConfigurer(appContainer),
       new HeaderConfigurer(appContainer),
       new EmojiConfigurer(appContainer),
@@ -135,29 +136,29 @@ export default class GrowiRenderer {
     }
   }
 
-  preProcess(markdown) {
+  preProcess(markdown, context) {
     let processed = markdown;
     for (let i = 0; i < this.preProcessors.length; i++) {
       if (!this.preProcessors[i].process) {
         continue;
       }
-      processed = this.preProcessors[i].process(processed);
+      processed = this.preProcessors[i].process(processed, context);
     }
 
     return processed;
   }
 
-  process(markdown) {
-    return this.md.render(markdown);
+  process(markdown, context) {
+    return this.md.render(markdown, context);
   }
 
-  postProcess(html) {
+  postProcess(html, context) {
     let processed = html;
     for (let i = 0; i < this.postProcessors.length; i++) {
       if (!this.postProcessors[i].process) {
         continue;
       }
-      processed = this.postProcessors[i].process(processed);
+      processed = this.postProcessors[i].process(processed, context);
     }
 
     return processed;

+ 1 - 1
packages/app/src/client/util/editor.ts

@@ -4,7 +4,7 @@ type OptionsToSave = {
   isSlackEnabled: boolean;
   slackChannels: string;
   grant: number;
-  pageTags: string[];
+  pageTags: string[] | null;
   grantUserGroupId: string | null;
   grantUserGroupName: string | null;
 };

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
packages/app/src/client/util/emojione/emoji_strategy_shrinked.json


+ 0 - 1
packages/app/src/client/util/interceptor/detach-code-blocks.js

@@ -47,7 +47,6 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
 
     const context = Object.assign(args[0]); // clone
     const targetKey = this.getTargetKey(contextName);
-    const currentPagePath = context.currentPagePath; // eslint-disable-line no-unused-vars
 
     context.dcbContextMap = {};
 

+ 3 - 3
packages/app/src/client/util/interceptor/drawio-interceptor.js

@@ -1,8 +1,9 @@
 /* eslint-disable import/prefer-default-export */
 import React from 'react';
+
+import { BasicInterceptor } from '@growi/core';
 import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
-import { BasicInterceptor } from '@growi/core';
 
 import Drawio from '~/components/Drawio';
 
@@ -103,8 +104,7 @@ export class DrawioInterceptor extends BasicInterceptor {
    */
   drawioPostRender(contextName, context) {
     const isPreview = (contextName === 'postRenderPreviewHtml');
-    const editorContainer = this.appContainer.getContainer('EditorContainer');
-    const renderDrawioInRealtime = editorContainer.state.previewOptions.renderDrawioInRealtime;
+    const renderDrawioInRealtime = context.renderDrawioInRealtime;
 
     Object.keys(context.DrawioMap).forEach((domId) => {
       const elem = document.getElementById(domId);

+ 66 - 0
packages/app/src/client/util/markdown-it/emoji-mart-data.ts

@@ -0,0 +1,66 @@
+import { Emoji } from 'emoji-mart';
+import data from 'emoji-mart/data/apple.json';
+
+const DEFAULT_EMOJI_SIZE = 24;
+
+
+type EmojiMap = {
+  [key: string]: string,
+};
+
+/**
+ *
+ * Get native emoji with skin tone
+ * @param skin number
+ * @returns emoji data with skin tone
+ */
+const getEmojiSkinTone = (emojiName: string): EmojiMap => {
+  const emojiData = {};
+  [...Array(6).keys()].forEach((index) => {
+    if (index > 0) {
+      const elem = Emoji({
+        emoji: emojiName,
+        skin: index + 1,
+        size: DEFAULT_EMOJI_SIZE,
+      });
+      if (elem) {
+        emojiData[`${emojiName}::skin-tone-${index + 1}`] = elem.props['aria-label'].split(',')[0];
+      }
+    }
+  });
+  return emojiData;
+};
+
+/**
+ * Get native emoji from emoji array
+ * @returns emoji data
+ */
+
+const getNativeEmoji = (): EmojiMap => {
+  const emojiData = {};
+  Object.entries(data.emojis).forEach((emoji) => {
+    const emojiName = emoji[0];
+    const hasSkinVariation = 'skin_variations' in emoji[1];
+
+    const elem = Emoji({
+      emoji: emojiName,
+      size: DEFAULT_EMOJI_SIZE,
+    });
+
+    if (elem != null) {
+      emojiData[emojiName] = elem.props['aria-label'].split(',')[0];
+      if (hasSkinVariation) {
+        const emojiWithSkinTone = getEmojiSkinTone(emojiName);
+        Object.assign(emojiData, emojiWithSkinTone);
+      }
+    }
+  });
+
+  return emojiData;
+};
+
+/**
+ * Get native emoji mart data
+ * @returns native emoji mart data
+ */
+export const emojiMartData = getNativeEmoji();

+ 5 - 22
packages/app/src/client/util/markdown-it/emoji.js

@@ -1,29 +1,12 @@
-export default class EmojiConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
+import markdownItEmojiMart from 'markdown-it-emoji-mart';
 
-  configure(md) {
-    const emojiStrategy = this.crowi.getEmojiStrategy();
+import { emojiMartData } from './emoji-mart-data';
 
-    const emojiShortnameUnicodeMap = {};
 
-    /* eslint-disable guard-for-in, no-restricted-syntax */
-    for (const unicode in emojiStrategy) {
-      const data = emojiStrategy[unicode];
-      const shortname = data.shortname.replace(/:/g, '');
-      emojiShortnameUnicodeMap[shortname] = String.fromCharCode(unicode);
-    }
-    /* eslint-enable guard-for-in, no-restricted-syntax */
-
-    md.use(require('markdown-it-emoji'), { defs: emojiShortnameUnicodeMap });
+export default class EmojiConfigurer {
 
-    // integrate markdown-it-emoji and emojione
-    md.renderer.rules.emoji = (token, idx) => {
-      const shortname = `:${token[idx].markup}:`;
-      return emojione.shortnameToImage(shortname);
-    };
+  configure(md) {
+    md.use(markdownItEmojiMart, { defs: emojiMartData });
   }
 
 }

+ 50 - 0
packages/app/src/client/util/markdown-it/link-by-relative-path.ts

@@ -0,0 +1,50 @@
+import path from 'path';
+
+// https://regex101.com/r/vV8LUe/1
+const PATTERN_RELATIVE_PATH = new RegExp(/^(\.{1,2})(\/.*)?$/);
+
+export default class LinkerByRelativePathConfigurer {
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  appContainer: any;
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  constructor(appContainer) {
+    this.appContainer = appContainer;
+  }
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  configure(md): void {
+    const pageContainer = this.appContainer.getContainer('PageContainer');
+
+    // Remember old renderer, if overridden, or proxy to default renderer
+    const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
+      return self.renderToken(tokens, idx, options);
+    };
+
+    md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
+      if (tokens[idx] == null || (typeof tokens[idx].attrIndex !== 'function')) {
+        return defaultRender(tokens, idx, options, env, self);
+      }
+
+      // get href
+      const hrefIndex = tokens[idx].attrIndex('href');
+
+      if (hrefIndex != null && hrefIndex >= 0) {
+        const href: string = tokens[idx].attrs[hrefIndex][1];
+        const currentPath: string | null = pageContainer?.state.path;
+
+        // resolve relative path and replace
+        if (PATTERN_RELATIVE_PATH.test(href) && currentPath != null) {
+          const newHref = path.resolve(path.dirname(currentPath), href);
+          tokens[idx].attrs[hrefIndex][1] = newHref;
+        }
+      }
+
+      // pass token to default renderer.
+      return defaultRender(tokens, idx, options, env, self);
+    };
+
+  }
+
+}

+ 13 - 7
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -1,3 +1,8 @@
+import markdownItEmojiMart from 'markdown-it-emoji-mart';
+import markdownItToc from 'markdown-it-toc-and-anchor-with-slugid';
+
+import { emojiMartData } from './emoji-mart-data';
+
 export default class TocAndAnchorConfigurer {
 
   constructor(crowi, setHtml) {
@@ -6,13 +11,14 @@ export default class TocAndAnchorConfigurer {
   }
 
   configure(md) {
-    md.use(require('markdown-it-toc-and-anchor-with-slugid').default, {
-      tocLastLevel: 3,
-      anchorLinkBefore: false,
-      anchorLinkSymbol: '',
-      anchorLinkSymbolClassName: 'icon-link',
-      anchorClassName: 'revision-head-link',
-    });
+    md.use(markdownItEmojiMart, { defs: emojiMartData })
+      .use(markdownItToc, {
+        tocLastLevel: 3,
+        anchorLinkBefore: false,
+        anchorLinkSymbol: '',
+        anchorLinkSymbolClassName: 'icon-link',
+        anchorClassName: 'revision-head-link',
+      });
 
     // set toc render function
     if (this.setHtml != null) {

+ 3 - 3
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -66,15 +66,15 @@
         interceptorManager.process('preRender', context)
           .then(() => { return interceptorManager.process('prePreProcess', context) })
           .then(() => {
-            context.markdown = growiRenderer.preProcess(context.markdown);
+            context.markdown = growiRenderer.preProcess(context.markdown, context);
           })
           .then(() => { return interceptorManager.process('postPreProcess', context) })
           .then(() => {
-            context.parsedHTML = growiRenderer.process(context.markdown);
+            context.parsedHTML = growiRenderer.process(context.markdown, context);
           })
           .then(() => { return interceptorManager.process('prePostProcess', context) })
           .then(() => {
-            context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+            context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
           })
           .then(() => { return interceptorManager.process('postPostProcess', context) })
           .then(() => { return interceptorManager.process('preRenderHtml', context) })

+ 93 - 3
packages/app/src/components/Admin/App/V5PageMigration.tsx

@@ -1,19 +1,75 @@
-import React, { FC, useState } from 'react';
+import React, {
+  FC, useCallback, useEffect, useState,
+} from 'react';
 import { useTranslation } from 'react-i18next';
 import { ConfirmModal } from './ConfirmModal';
 import AdminAppContainer from '../../../client/services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../client/util/apiNotification';
+import { useGlobalAdminSocket } from '~/stores/websocket';
+import LabeledProgressBar from '../Common/LabeledProgressBar';
+import {
+  SocketEventName, PMStartedData, PMMigratingData, PMErrorCountData, PMEndedData,
+} from '~/interfaces/websocket';
 
 type Props = {
   adminAppContainer: typeof AdminAppContainer & { v5PageMigrationHandler: () => Promise<{ isV5Compatible: boolean }> },
 }
 
 const V5PageMigration: FC<Props> = (props: Props) => {
+  // Modal
   const [isV5PageMigrationModalShown, setIsV5PageMigrationModalShown] = useState(false);
-  const { adminAppContainer } = props;
+  // Progress bar
+  const [isInProgress, setProgressing] = useState<boolean | undefined>(undefined); // use false as ended
+  const [total, setTotal] = useState<number>(0);
+  const [skip, setSkip] = useState<number>(0);
+  const [current, setCurrent] = useState<number>(0);
+  const [isSucceeded, setSucceeded] = useState<boolean | undefined>(undefined);
+
+  const { data: adminSocket } = useGlobalAdminSocket();
   const { t } = useTranslation();
 
+  const { adminAppContainer } = props;
+
+  /*
+   * Local components
+   */
+  const renderResultMessage = useCallback((isSucceeded: boolean) => {
+    return (
+      <>
+        {
+          isSucceeded
+            ? <p className="text-success p-1">{t('admin:v5_page_migration.migration_succeeded')}</p>
+            : <p className="text-danger p-1">{t('admin:v5_page_migration.migration_failed')}</p>
+        }
+      </>
+    );
+  }, [t]);
+
+  const renderProgressBar = () => {
+    if (isInProgress == null) {
+      return <></>;
+    }
+
+    return (
+      <>
+        {
+          isSucceeded != null && renderResultMessage(isSucceeded)
+        }
+        <LabeledProgressBar
+          header={t('admin:v5_page_migration.header_upgrading_progress')}
+          currentCount={current}
+          errorsCount={skip}
+          totalCount={total}
+          isInProgress={isInProgress}
+        />
+      </>
+    );
+  };
+
+  /*
+   * Functions
+   */
   const onConfirm = async() => {
     setIsV5PageMigrationModalShown(false);
     try {
@@ -29,6 +85,39 @@ const V5PageMigration: FC<Props> = (props: Props) => {
     }
   };
 
+  /*
+   * Use Effect
+   */
+  // Setup Admin Socket
+  useEffect(() => {
+    adminSocket?.once(SocketEventName.PMStarted, (data: PMStartedData) => {
+      setProgressing(true);
+      setTotal(data.total);
+    });
+
+    adminSocket?.on(SocketEventName.PMMigrating, (data: PMMigratingData) => {
+      setProgressing(true);
+      setCurrent(data.count);
+    });
+
+    adminSocket?.on(SocketEventName.PMErrorCount, (data: PMErrorCountData) => {
+      setProgressing(true);
+      setSkip(data.skip);
+    });
+
+    adminSocket?.once(SocketEventName.PMEnded, (data: PMEndedData) => {
+      setProgressing(false);
+      setSucceeded(data.isSucceeded);
+    });
+
+    return () => {
+      adminSocket?.off(SocketEventName.PMStarted);
+      adminSocket?.off(SocketEventName.PMMigrating);
+      adminSocket?.off(SocketEventName.PMErrorCount);
+      adminSocket?.off(SocketEventName.PMEnded);
+    };
+  }, [adminSocket]);
+
   return (
     <>
       <ConfirmModal
@@ -48,9 +137,10 @@ const V5PageMigration: FC<Props> = (props: Props) => {
           {t('admin:v5_page_migration.migration_note')}
         </span>
       </p>
+      {renderProgressBar()}
       <div className="row my-3">
         <div className="mx-auto">
-          <button type="button" className="btn btn-warning" onClick={() => setIsV5PageMigrationModalShown(true)}>
+          <button type="button" className="btn btn-warning" onClick={() => setIsV5PageMigrationModalShown(true)} disabled={isInProgress != null}>
             {t('admin:v5_page_migration.upgrade_to_v5')}
           </button>
         </div>

+ 3 - 0
packages/app/src/components/Admin/CustomHeaderEditor.jsx

@@ -7,6 +7,8 @@ require('codemirror/addon/hint/show-hint');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/closebrackets');
 require('codemirror/mode/htmlmixed/htmlmixed');
+require('codemirror/addon/hint/html-hint');
+require('codemirror/addon/edit/closetag');
 require('~/client/util/codemirror/autorefresh.ext');
 
 require('jquery-ui/ui/widgets/resizable');
@@ -22,6 +24,7 @@ export default class CustomHeaderEditor extends React.Component {
         detach
         options={{
           mode: 'htmlmixed',
+          autoCloseTags: true,
           lineNumbers: true,
           tabSize: 2,
           indentUnit: 2,

+ 5 - 4
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -1,10 +1,11 @@
 import React, { useCallback, useEffect, useState } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
-
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
 
 const isDarkMode = isDarkModeByUtil();
@@ -18,14 +19,14 @@ const CustomizeLayoutSetting = (props) => {
 
   const retrieveData = useCallback(async() => {
     try {
-      const res = await appContainer.apiv3Get('/customize-setting/layout');
+      const res = await apiv3Get('/customize-setting/layout');
       setIsContainerFluid(res.data.isContainerFluid);
     }
     catch (err) {
       setRetrieveError(err);
       toastError(err);
     }
-  }, [appContainer]);
+  }, []);
 
   useEffect(() => {
     retrieveData();
@@ -33,7 +34,7 @@ const CustomizeLayoutSetting = (props) => {
 
   const onClickSubmit = async() => {
     try {
-      await appContainer.apiv3Put('/customize-setting/layout', { isContainerFluid });
+      await apiv3Put('/customize-setting/layout', { isContainerFluid });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.layout') }));
       retrieveData();
     }

+ 11 - 8
packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx

@@ -1,16 +1,19 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import StatusTable from './StatusTable';
-import ReconnectControls from './ReconnectControls';
 import NormalizeIndicesControls from './NormalizeIndicesControls';
 import RebuildIndexControls from './RebuildIndexControls';
+import ReconnectControls from './ReconnectControls';
+import StatusTable from './StatusTable';
 
 class ElasticsearchManagement extends React.Component {
 
@@ -70,7 +73,7 @@ class ElasticsearchManagement extends React.Component {
     const { appContainer } = this.props;
 
     try {
-      const { data } = await appContainer.apiv3Get('/search/indices');
+      const { data } = await apiv3Get('/search/indices');
       const { info } = data;
 
       this.setState({
@@ -105,7 +108,7 @@ class ElasticsearchManagement extends React.Component {
     this.setState({ isReconnectingProcessing: true });
 
     try {
-      await appContainer.apiv3Post('/search/connection');
+      await apiv3Post('/search/connection');
     }
     catch (e) {
       toastError(e);
@@ -120,7 +123,7 @@ class ElasticsearchManagement extends React.Component {
     const { appContainer } = this.props;
 
     try {
-      await appContainer.apiv3Put('/search/indices', { operation: 'normalize' });
+      await apiv3Put('/search/indices', { operation: 'normalize' });
     }
     catch (e) {
       toastError(e);
@@ -137,7 +140,7 @@ class ElasticsearchManagement extends React.Component {
     this.setState({ isRebuildingProcessing: true });
 
     try {
-      await appContainer.apiv3Put('/search/indices', { operation: 'rebuild' });
+      await apiv3Put('/search/indices', { operation: 'rebuild' });
       toastSuccess('Rebuilding is requested');
     }
     catch (e) {

+ 6 - 3
packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import {
@@ -6,8 +7,10 @@ import {
 } from 'reactstrap';
 import * as toastr from 'toastr';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
+import { apiPost } from '~/client/util/apiv1-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
@@ -67,8 +70,8 @@ class SelectCollectionsModal extends React.Component {
     e.preventDefault();
 
     try {
-      // TODO: use appContainer.apiv3.post
-      const result = await this.props.appContainer.apiPost('/v3/export', { collections: Array.from(this.state.selectedCollections) });
+      // TODO: use apiv3Post
+      const result = await apiPost('/v3/export', { collections: Array.from(this.state.selectedCollections) });
       // TODO: toastSuccess, toastError
 
       if (!result.ok) {

+ 10 - 7
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -1,19 +1,22 @@
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 
 
+import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { apiDelete, apiGet } from '~/client/util/apiv1-client';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
 
 import LabeledProgressBar from './Common/LabeledProgressBar';
-
-import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
+import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
+
 
 const IGNORED_COLLECTION_NAMES = [
   'sessions',
@@ -45,8 +48,8 @@ class ExportArchiveDataPage extends React.Component {
     // TODO:: use apiv3.get
     // eslint-disable-next-line no-unused-vars
     const [{ collections }, { status }] = await Promise.all([
-      this.props.appContainer.apiGet('/v3/mongo/collections', {}),
-      this.props.appContainer.apiGet('/v3/export/status', {}),
+      apiGet('/v3/mongo/collections', {}),
+      apiGet('/v3/export/status', {}),
     ]);
     // TODO: toastSuccess, toastError
 
@@ -118,7 +121,7 @@ class ExportArchiveDataPage extends React.Component {
 
   async onZipFileStatRemove(fileName) {
     try {
-      await this.props.appContainer.apiDelete(`/v3/export/${fileName}`, {});
+      await apiDelete(`/v3/export/${fileName}`, {});
 
       this.setState((prevState) => {
         return {

+ 9 - 7
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -1,20 +1,22 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
 
 import { withUnstatedContainers } from '../../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
-import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
 import ErrorViewer from './ErrorViewer';
+import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
+import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
 
 
 const GROUPS_PAGE = [
@@ -300,8 +302,8 @@ class ImportForm extends React.Component {
     });
 
     try {
-      // TODO: use appContainer.apiv3.post
-      await appContainer.apiv3Post('/import', {
+      // TODO: use apiv3Post
+      await apiv3Post('/import', {
         fileName,
         collections: Array.from(selectedCollections),
         optionsMap,

+ 5 - 2
packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -1,10 +1,13 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../../UnstatedUtils';
 
 class UploadForm extends React.Component {
 
@@ -32,7 +35,7 @@ class UploadForm extends React.Component {
     formData.append('file', this.inputRef.current.files[0]);
 
     try {
-      const { data } = await this.props.appContainer.apiv3Post('/import/upload', formData);
+      const { data } = await apiv3Post('/import/upload', formData);
       // TODO: toastSuccess, toastError
       this.props.onUpload(data);
     }

+ 7 - 4
packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -1,14 +1,17 @@
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
+import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import UploadForm from './GrowiArchive/UploadForm';
 import ImportForm from './GrowiArchive/ImportForm';
+import UploadForm from './GrowiArchive/UploadForm';
 
 class GrowiArchiveSection extends React.Component {
 
@@ -32,7 +35,7 @@ class GrowiArchiveSection extends React.Component {
 
   async componentWillMount() {
     // get uploaded file status
-    const res = await this.props.appContainer.apiv3Get('/import/status');
+    const res = await apiv3Get('/import/status');
 
     if (res.data.zipFileStat != null) {
       const { fileName, innerFileStats } = res.data.zipFileStat;
@@ -55,7 +58,7 @@ class GrowiArchiveSection extends React.Component {
   async discardData() {
     try {
       const { fileName } = this.state;
-      await this.props.appContainer.apiv3Delete('/import/all');
+      await apiv3Delete('/import/all');
       this.resetState();
 
       // TODO: toastSuccess, toastError

+ 8 - 5
packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx

@@ -1,18 +1,21 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
-import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import loggerFactory from '~/utils/logger';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import NotificationDeleteModal from './NotificationDeleteModal';
 import NotificationTypeIcon from './NotificationTypeIcon';
 
+
 const logger = loggerFactory('growi:GolobalNotificationList');
 
 class GlobalNotificationList extends React.Component {
@@ -34,7 +37,7 @@ class GlobalNotificationList extends React.Component {
     const { t } = this.props;
     const isEnabled = !notification.isEnabled;
     try {
-      await this.props.appContainer.apiv3.put(`/notification-setting/global-notification/${notification._id}/enabled`, {
+      await apiv3Put(`/notification-setting/global-notification/${notification._id}/enabled`, {
         isEnabled,
       });
       toastSuccess(t('notification_setting.toggle_notification', { path: notification.triggerPath }));

+ 10 - 6
packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -1,16 +1,20 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
+import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 
-import { toastError } from '~/client/util/apiNotification';
 
-import TriggerEventCheckBox from './TriggerEventCheckBox';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+import TriggerEventCheckBox from './TriggerEventCheckBox';
+
 
 const logger = loggerFactory('growi:manageGlobalNotification');
 
@@ -81,10 +85,10 @@ class ManageGlobalNotification extends React.Component {
 
     try {
       if (this.state.globalNotificationId != null) {
-        await this.props.appContainer.apiv3.put(`/notification-setting/global-notification/${this.state.globalNotificationId}`, requestParams);
+        await apiv3Put(`/notification-setting/global-notification/${this.state.globalNotificationId}`, requestParams);
       }
       else {
-        await this.props.appContainer.apiv3.post('/notification-setting/global-notification', requestParams);
+        await apiv3Post('/notification-setting/global-notification', requestParams);
       }
       window.location.href = urljoin(window.location.origin, '/admin/notification#global-notification');
     }

+ 7 - 5
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -1,14 +1,16 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class GoogleSecurityManagementContents extends React.Component {
 
@@ -135,8 +137,8 @@ class GoogleSecurityManagementContents extends React.Component {
                     id="bindByUserNameGoogle"
                     className="custom-control-input"
                     type="checkbox"
-                    checked={adminGoogleSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminGoogleSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                    checked={adminGoogleSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
+                    onChange={() => { adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                   />
                   <label
                     className="custom-control-label"

+ 7 - 5
packages/app/src/components/Admin/Security/LdapAuthTest.jsx

@@ -1,13 +1,15 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiPost } from '~/client/util/apiv1-client';
+import loggerFactory from '~/utils/logger';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
 
@@ -41,7 +43,7 @@ class LdapAuthTest extends React.Component {
    */
   async testLdapCredentials() {
     try {
-      const response = await this.props.appContainer.apiPost('/login/testLdap', {
+      const response = await apiPost('/login/testLdap', {
         loginForm: {
           username: this.props.username,
           password: this.props.password,

+ 151 - 84
packages/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { Collapse } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 
-import { validateDeleteConfigs } from '~/utils/page-delete-config';
+import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
@@ -39,6 +39,20 @@ const getDeletionTypeForT = (deletionType) => {
   }
 };
 
+const getDeleteConfigValueForT = (DeleteConfigValue) => {
+  switch (DeleteConfigValue) {
+    case PageDeleteConfigValue.Anyone:
+    case null:
+      return 'security_setting.anyone';
+    case PageDeleteConfigValue.Inherit:
+      return 'security_setting.inherit';
+    case PageDeleteConfigValue.AdminOnly:
+      return 'security_setting.admin_only';
+    case PageDeleteConfigValue.AdminAndAuthor:
+      return 'security_setting.admin_and_author';
+  }
+};
+
 /**
  * Return true if "deletionType" is DeletionType.RecursiveDeletion or DeletionType.RecursiveCompleteDeletion.
  * @param deletionType Deletion type
@@ -62,9 +76,16 @@ class SecuritySetting extends React.Component {
   constructor(props) {
     super(props);
 
+    // functions
     this.putSecuritySetting = this.putSecuritySetting.bind(this);
     this.getRecursiveDeletionConfigState = this.getRecursiveDeletionConfigState.bind(this);
+    this.previousPageRecursiveAuthorityState = this.previousPageRecursiveAuthorityState.bind(this);
+    this.setPagePreviousRecursiveAuthorityState = this.setPagePreviousRecursiveAuthorityState.bind(this);
+    this.expantDeleteOptionsState = this.expantDeleteOptionsState.bind(this);
+    this.setExpantOtherDeleteOptionsState = this.setExpantOtherDeleteOptionsState.bind(this);
     this.setDeletionConfigState = this.setDeletionConfigState.bind(this);
+
+    // render
     this.renderPageDeletePermission = this.renderPageDeletePermission.bind(this);
     this.renderPageDeletePermissionDropdown = this.renderPageDeletePermissionDropdown.bind(this);
   }
@@ -96,25 +117,67 @@ class SecuritySetting extends React.Component {
     ];
   }
 
+  previousPageRecursiveAuthorityState(deletionType) {
+    const { adminGeneralSecurityContainer } = this.props;
+
+    return isTypeDeletion(deletionType)
+      ? adminGeneralSecurityContainer.state.previousPageRecursiveDeletionAuthority
+      : adminGeneralSecurityContainer.state.previousPageRecursiveCompleteDeletionAuthority;
+  }
+
+  setPagePreviousRecursiveAuthorityState(deletionType, previousState) {
+    const { adminGeneralSecurityContainer } = this.props;
+
+    if (isTypeDeletion(deletionType)) {
+      adminGeneralSecurityContainer.changePreviousPageRecursiveDeletionAuthority(previousState);
+      return;
+    }
+
+    adminGeneralSecurityContainer.changePreviousPageRecursiveCompleteDeletionAuthority(previousState);
+  }
+
+  expantDeleteOptionsState(deletionType) {
+    const { adminGeneralSecurityContainer } = this.props;
+
+    return isTypeDeletion(deletionType)
+      ? adminGeneralSecurityContainer.state.expandOtherOptionsForDeletion
+      : adminGeneralSecurityContainer.state.expandOtherOptionsForCompleteDeletion;
+  }
+
+  setExpantOtherDeleteOptionsState(deletionType, bool) {
+    const { adminGeneralSecurityContainer } = this.props;
+
+    if (isTypeDeletion(deletionType)) {
+      adminGeneralSecurityContainer.switchExpandOtherOptionsForDeletion(bool);
+      return;
+    }
+    adminGeneralSecurityContainer.switchExpandOtherOptionsForCompleteDeletion(bool);
+    return;
+  }
+
   /**
    * Force update deletion config for recursive operation when the deletion config for general operation is updated.
    * @param deletionType Deletion type
    */
   setDeletionConfigState(newState, setState, deletionType) {
-    if (isRecursiveDeletion(deletionType)) {
-      setState(newState);
+    setState(newState);
+
+    if (this.previousPageRecursiveAuthorityState(deletionType) !== null) {
+      this.setPagePreviousRecursiveAuthorityState(deletionType, null);
+    }
 
+    if (isRecursiveDeletion(deletionType)) {
       return;
     }
 
     const [recursiveState, setRecursiveState] = this.getRecursiveDeletionConfigState(deletionType);
-    const shouldForceUpdate = !validateDeleteConfigs(newState, recursiveState);
+
+    const calculableValue = prepareDeleteConfigValuesForCalc(newState, recursiveState);
+    const shouldForceUpdate = !validateDeleteConfigs(calculableValue[0], calculableValue[1]);
     if (shouldForceUpdate) {
-      setState(newState);
       setRecursiveState(newState);
-    }
-    else {
-      setState(newState);
+      this.setPagePreviousRecursiveAuthorityState(deletionType, recursiveState);
+      this.setExpantOtherDeleteOptionsState(deletionType, true);
     }
 
     return;
@@ -133,10 +196,7 @@ class SecuritySetting extends React.Component {
           aria-expanded="true"
         >
           <span className="float-left">
-            {currentState === PageDeleteConfigValue.Inherit && t('security_setting.inherit')}
-            {(currentState === PageDeleteConfigValue.Anyone || currentState == null) && t('security_setting.anyone')}
-            {currentState === PageDeleteConfigValue.AdminOnly && t('security_setting.admin_only')}
-            {currentState === PageDeleteConfigValue.AdminAndAuthor && t('security_setting.admin_and_author')}
+            {t(getDeleteConfigValueForT(currentState))}
           </span>
         </button>
         <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
@@ -184,20 +244,9 @@ class SecuritySetting extends React.Component {
   }
 
   renderPageDeletePermission(currentState, setState, deletionType, isButtonDisabled) {
-    const { t, adminGeneralSecurityContainer } = this.props;
-
-    const expandOtherOptions = isTypeDeletion(deletionType)
-      ? adminGeneralSecurityContainer.state.expandOtherOptionsForDeletion
-      : adminGeneralSecurityContainer.state.expandOtherOptionsForCompleteDeletion;
+    const { t } = this.props;
 
-    const setExpantOtherOptions = () => {
-      if (isTypeDeletion(deletionType)) {
-        adminGeneralSecurityContainer.switchExpandOtherOptionsForDeletion();
-        return;
-      }
-      adminGeneralSecurityContainer.switchExpandOtherOptionsForCompleteDeletion();
-      return;
-    };
+    const expantDeleteOptionsState = this.expantDeleteOptionsState(deletionType);
 
     return (
       <div key={`page-delete-permission-dropdown-${deletionType}`} className="row">
@@ -223,13 +272,30 @@ class SecuritySetting extends React.Component {
                     type="button"
                     className="btn btn-link p-0 mb-4"
                     aria-expanded="false"
-                    onClick={() => setExpantOtherOptions()}
+                    onClick={() => this.setExpantOtherDeleteOptionsState(deletionType, !expantDeleteOptionsState)}
                   >
-                    <i className={`fa fa-fw fa-arrow-right ${expandOtherOptions ? 'fa-rotate-90' : ''}`}></i>
+                    <i className={`fa fa-fw fa-arrow-right ${expantDeleteOptionsState ? 'fa-rotate-90' : ''}`}></i>
                     { t('security_setting.other_options') }
                   </button>
-                  <Collapse isOpen={expandOtherOptions}>
+                  <Collapse isOpen={expantDeleteOptionsState}>
                     <div className="pb-4">
+                      <p className="card well">
+                        <span className="text-warning">
+                          <i className="icon-info"></i>
+                          {/* eslint-disable-next-line react/no-danger */}
+                          <span dangerouslySetInnerHTML={{ __html: t('security_setting.page_delete_rights_caution') }} />
+                        </span>
+                      </p>
+                      { this.previousPageRecursiveAuthorityState(deletionType) !== null && (
+                        <div className="mb-3">
+                          <strong>
+                            {t('security_setting.forced_update_desc')}
+                          </strong>
+                          <code>
+                            {t(getDeleteConfigValueForT(this.previousPageRecursiveAuthorityState(deletionType)))}
+                          </code>
+                        </div>
+                      )}
                       {this.renderPageDeletePermissionDropdown(currentState, setState, deletionType, isButtonDisabled)}
                     </div>
                   </Collapse>
@@ -269,58 +335,60 @@ class SecuritySetting extends React.Component {
         )}
 
         <h4 className="mt-4">{ t('security_setting.page_list_and_search_results') }</h4>
-        <table className="table table-bordered col-lg-9 mb-5">
-          <thead>
-            <tr>
-              <th scope="col">{ t('scope_of_page_disclosure') }</th>
-              <th scope="col">{ t('set_point') }</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr>
-              <th scope="row">{ t('Public') }</th>
-              <td>{ t('always_displayed') }</td>
-            </tr>
-            <tr>
-              <th scope="row">{ t('Anyone with the link') }</th>
-              <td>{ t('always_hidden') }</td>
-            </tr>
-            <tr>
-              <th scope="row">{ t('Only me') }</th>
-              <td>
-                <div className="custom-control custom-switch custom-checkbox-success">
-                  <input
-                    type="checkbox"
-                    className="custom-control-input"
-                    id="isShowRestrictedByOwner"
-                    checked={adminGeneralSecurityContainer.state.isShowRestrictedByOwner}
-                    onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
-                  />
-                  <label className="custom-control-label" htmlFor="isShowRestrictedByOwner">
-                    {t('displayed_or_hidden')}
-                  </label>
-                </div>
-              </td>
-            </tr>
-            <tr>
-              <th scope="row">{ t('Only inside the group') }</th>
-              <td>
-                <div className="custom-control custom-switch custom-checkbox-success">
-                  <input
-                    type="checkbox"
-                    className="custom-control-input"
-                    id="isShowRestrictedByGroup"
-                    checked={adminGeneralSecurityContainer.state.isShowRestrictedByGroup}
-                    onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
-                  />
-                  <label className="custom-control-label" htmlFor="isShowRestrictedByGroup">
-                    {t('displayed_or_hidden')}
-                  </label>
-                </div>
-              </td>
-            </tr>
-          </tbody>
-        </table>
+        <div className="row justify-content-md-center">
+          <table className="table table-bordered col-lg-9 mb-5">
+            <thead>
+              <tr>
+                <th scope="col">{ t('scope_of_page_disclosure') }</th>
+                <th scope="col">{ t('set_point') }</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <th scope="row">{ t('Public') }</th>
+                <td>{ t('always_displayed') }</td>
+              </tr>
+              <tr>
+                <th scope="row">{ t('Anyone with the link') }</th>
+                <td>{ t('always_hidden') }</td>
+              </tr>
+              <tr>
+                <th scope="row">{ t('Only me') }</th>
+                <td>
+                  <div className="custom-control custom-switch custom-checkbox-success">
+                    <input
+                      type="checkbox"
+                      className="custom-control-input"
+                      id="isShowRestrictedByOwner"
+                      checked={adminGeneralSecurityContainer.state.isShowRestrictedByOwner}
+                      onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
+                    />
+                    <label className="custom-control-label" htmlFor="isShowRestrictedByOwner">
+                      {t('displayed_or_hidden')}
+                    </label>
+                  </div>
+                </td>
+              </tr>
+              <tr>
+                <th scope="row">{ t('Only inside the group') }</th>
+                <td>
+                  <div className="custom-control custom-switch custom-checkbox-success">
+                    <input
+                      type="checkbox"
+                      className="custom-control-input"
+                      id="isShowRestrictedByGroup"
+                      checked={adminGeneralSecurityContainer.state.isShowRestrictedByGroup}
+                      onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
+                    />
+                    <label className="custom-control-label" htmlFor="isShowRestrictedByGroup">
+                      {t('displayed_or_hidden')}
+                    </label>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
 
         <h4>{t('security_setting.page_access_rights')}</h4>
         <div className="row mb-4">
@@ -353,13 +421,13 @@ class SecuritySetting extends React.Component {
               </div>
             </div>
             {adminGeneralSecurityContainer.isWikiModeForced && (
-              <p className="alert alert-warning mt-2 text-left offset-3 col-6">
+              <p className="alert alert-warning mt-2 col-6">
                 <i className="icon-exclamation icon-fw">
                 </i><b>FIXED</b><br />
                 <b
                   dangerouslySetInnerHTML={{
                     __html: t('security_setting.Fixed by env var',
-                      { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
+                      { key: 'FORCE_WIKI_MODE', value: adminGeneralSecurityContainer.state.wikiMode }),
                   }}
                 />
               </p>
@@ -368,8 +436,7 @@ class SecuritySetting extends React.Component {
         </div>
 
         <h4>{t('security_setting.page_delete_rights')}</h4>
-        <div className="row mb-4"></div>
-        {/* Render PageDeletePermissionDropdown */}
+        {/* Render PageDeletePermission */}
         {
           [
             [currentPageDeletionAuthority, adminGeneralSecurityContainer.changePageDeletionAuthority, DeletionType.Deletion, false],

+ 8 - 6
packages/app/src/components/Admin/Security/ShareLinkSetting.jsx

@@ -1,17 +1,19 @@
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Delete } from '~/client/util/apiv3-client';
 
 import PaginationWrapper from '../../PaginationWrapper';
+import ShareLinkList from '../../ShareLink/ShareLinkList';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';
-import ShareLinkList from '../../ShareLink/ShareLinkList';
 
 
 const Pager = (props) => {
@@ -80,7 +82,7 @@ class ShareLinkSetting extends React.Component {
     const { t, appContainer } = this.props;
 
     try {
-      const res = await appContainer.apiv3Delete('/share-links/all');
+      const res = await apiv3Delete('/share-links/all');
       const { deletedCount } = res.data;
       toastSuccess(t('toaster.remove_share_link', { count: deletedCount }));
     }
@@ -95,7 +97,7 @@ class ShareLinkSetting extends React.Component {
     const { shareLinksActivePage } = adminGeneralSecurityContainer.state;
 
     try {
-      const res = await appContainer.apiv3Delete(`/share-links/${shareLinkId}`);
+      const res = await apiv3Delete(`/share-links/${shareLinkId}`);
       const { deletedShareLink } = res.data;
       toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
     }

+ 4 - 1
packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -69,7 +69,10 @@ const BotTypeCard = (props) => {
       <div className="card-body p-4">
         <div className="card-text">
           <div className="my-2">
-            <img className="d-block mx-auto mb-4" src={`/images/slack-integration/slackbot-difficulty-level-${botDetails[props.botType].setUp}.svg`}></img>
+            <img
+              className="bot-difficulty-icon d-block mx-auto mb-4"
+              src={`/images/slack-integration/slackbot-difficulty-level-${botDetails[props.botType].setUp}.svg`}
+            />
             <div className="d-flex justify-content-between mb-3">
               <span>{t('admin:slack_integration.selecting_bot_types.multiple_workspaces_integration')}</span>
               <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].multiWSIntegration}.png`} alt="" />

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff