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

merge master and resolve conflict

yuto-o 4 лет назад
Родитель
Сommit
413a77ca9c
100 измененных файлов с 1135 добавлено и 512 удалено
  1. 9 9
      .devcontainer/Dockerfile
  2. 12 9
      .devcontainer/devcontainer.json
  3. 0 4
      .devcontainer/docker-compose.yml
  4. 1 0
      .gitattributes
  5. 23 0
      .github/dependabot.yml
  6. 2 5
      .github/release-drafter.yml
  7. 7 6
      .github/workflows/ci-slackbot-proxy.yml
  8. 12 1
      .github/workflows/ci.yml
  9. 1 1
      .github/workflows/draft-release.yml
  10. 1 1
      .github/workflows/pr-to-master.yml
  11. 3 1
      .github/workflows/release-rc.yml
  12. 59 1
      .github/workflows/release-slackbot-proxy.yml
  13. 8 4
      .github/workflows/release.yml
  14. 144 4
      CHANGELOG.md
  15. 12 1
      SECURITY.md
  16. 11 0
      THIRD-PARTY-NOTICES.md
  17. 7 1
      bin/github-actions/bump-versions/flow/bump-versions.js
  18. 5 1
      bin/github-actions/bump-versions/step/printHelp.js
  19. 1 1
      lerna.json
  20. 9 5
      package.json
  21. 2 0
      packages/app/.env.development
  22. 1 0
      packages/app/.env.production
  23. 2 1
      packages/app/.eslintrc.js
  24. 1 1
      packages/app/.gitignore
  25. 1 12
      packages/app/bin/cdn/cdn-resources-downloader.ts
  26. 0 1
      packages/app/config/cdn.js
  27. 3 0
      packages/app/config/webpack.common.js
  28. 2 2
      packages/app/config/webpack.prod.js
  29. 7 15
      packages/app/docker/Dockerfile
  30. 6 0
      packages/app/docker/Dockerfile.dockerignore
  31. 2 2
      packages/app/docker/README.md
  32. 1 1
      packages/app/docker/docker-entrypoint.sh
  33. 6 0
      packages/app/docker/nocdn/.env.production.local
  34. 0 5
      packages/app/docker/nocdn/env.prod.js
  35. 21 5
      packages/app/jest.config.js
  36. 8 5
      packages/app/migrate-mongo-config.js
  37. 27 23
      packages/app/package.json
  38. 3 0
      packages/app/public/static/dict/base.dat.gz
  39. 3 0
      packages/app/public/static/dict/cc.dat.gz
  40. 3 0
      packages/app/public/static/dict/check.dat.gz
  41. 3 0
      packages/app/public/static/dict/tid.dat.gz
  42. 3 0
      packages/app/public/static/dict/tid_map.dat.gz
  43. 3 0
      packages/app/public/static/dict/tid_pos.dat.gz
  44. 3 0
      packages/app/public/static/dict/unk.dat.gz
  45. 3 0
      packages/app/public/static/dict/unk_char.dat.gz
  46. 3 0
      packages/app/public/static/dict/unk_compat.dat.gz
  47. 3 0
      packages/app/public/static/dict/unk_invoke.dat.gz
  48. 3 0
      packages/app/public/static/dict/unk_map.dat.gz
  49. 3 0
      packages/app/public/static/dict/unk_pos.dat.gz
  50. 13 99
      packages/app/resource/cdn-manifests.js
  51. 4 1
      packages/app/resource/locales/en_US/admin/admin.json
  52. 6 0
      packages/app/resource/locales/en_US/translation.json
  53. 11 2
      packages/app/resource/locales/ja_JP/admin/admin.json
  54. 5 0
      packages/app/resource/locales/ja_JP/translation.json
  55. 11 2
      packages/app/resource/locales/zh_CN/admin/admin.json
  56. 5 0
      packages/app/resource/locales/zh_CN/translation.json
  57. 8 3
      packages/app/src/client/app.jsx
  58. 11 0
      packages/app/src/client/services/AdminCustomizeContainer.js
  59. 21 97
      packages/app/src/client/services/AppContainer.js
  60. 1 3
      packages/app/src/client/services/CommentContainer.js
  61. 9 2
      packages/app/src/client/services/EditorContainer.js
  62. 56 25
      packages/app/src/client/services/PageContainer.js
  63. 63 0
      packages/app/src/client/util/apiv1-client.ts
  64. 0 12
      packages/app/src/client/util/apiv1ErrorHandler.js
  65. 74 0
      packages/app/src/client/util/apiv3-client.ts
  66. 0 21
      packages/app/src/client/util/apiv3ErrorHandler.js
  67. 15 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  68. 6 0
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  69. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  70. 3 0
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  71. 2 2
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  72. 1 1
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx
  73. 3 0
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  74. 35 13
      packages/app/src/components/LikeButtons.jsx
  75. 4 4
      packages/app/src/components/Me/EditorSettings.tsx
  76. 3 1
      packages/app/src/components/Navbar/GlobalSearch.jsx
  77. 5 6
      packages/app/src/components/Navbar/SubNavButtons.jsx
  78. 1 5
      packages/app/src/components/PageComment/Comment.jsx
  79. 2 5
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  80. 75 0
      packages/app/src/components/PageEditor/DownloadDictModal.tsx
  81. 8 1
      packages/app/src/components/PageEditor/DrawioModal.jsx
  82. 41 8
      packages/app/src/components/PageEditor/OptionsSelector.jsx
  83. 17 25
      packages/app/src/components/PageList.jsx
  84. 2 2
      packages/app/src/components/Sidebar/CustomSidebar.jsx
  85. 80 19
      packages/app/src/components/Sidebar/RecentChanges.jsx
  86. 4 2
      packages/app/src/components/User/SeenUserInfo.jsx
  87. 7 0
      packages/app/src/interfaces/page-tag-relation.ts
  88. 14 0
      packages/app/src/interfaces/page.ts
  89. 5 0
      packages/app/src/interfaces/paging-result.ts
  90. 9 0
      packages/app/src/interfaces/revision.ts
  91. 4 0
      packages/app/src/interfaces/tag.ts
  92. 18 0
      packages/app/src/interfaces/user.ts
  93. 2 3
      packages/app/src/migrations/20180926134048-make-email-unique.js
  94. 3 3
      packages/app/src/migrations/20180927102719-init-serverurl.js
  95. 3 4
      packages/app/src/migrations/20181019114028-abolish-page-group-relation.js
  96. 2 2
      packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  97. 3 4
      packages/app/src/migrations/20190618104011-add-config-app-installed.js
  98. 2 2
      packages/app/src/migrations/20190619055421-adjust-page-grant.js
  99. 2 2
      packages/app/src/migrations/20190624110950-fill-last-update-user.js
  100. 2 2
      packages/app/src/migrations/20190629193445-make-root-page-public.js

+ 9 - 9
.devcontainer/Dockerfile

@@ -29,15 +29,15 @@ RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
 # * any needed dependencies after executing "apt-get update". *
 # * See https://docs.docker.com/engine/reference/builder/#run *
 # *************************************************************
-# ENV DEBIAN_FRONTEND=noninteractive
-# RUN apt-get update \
-#    && apt-get -y install --no-install-recommends <your-package-list-here> \
-#    #
-#    # Clean up
-#    && apt-get autoremove -y \
-#    && apt-get clean -y \
-#    && rm -rf /var/lib/apt/lists/*
-# ENV DEBIAN_FRONTEND=dialog
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update \
+   && apt-get -y install --no-install-recommends git-lfs \
+
+   # Clean up
+   && apt-get autoremove -y \
+   && apt-get clean -y \
+   && rm -rf /var/lib/apt/lists/*
+ENV DEBIAN_FRONTEND=dialog
 
 # Uncomment to default to non-root user
 # USER $USER_UID

+ 12 - 9
.devcontainer/devcontainer.json

@@ -9,20 +9,23 @@
 
 	// Set *default* container specific settings.json values on container create.
 	"settings": {
-		"terminal.integrated.shell.linux": "/bin/bash"
+		"terminal.integrated.defaultProfile.linux": "bash"
 	},
 
 	// Add the IDs of extensions you want installed when the container is created.
 	"extensions": [
-		"dbaeumer.vscode-eslint",
-		"eamodio.gitlens",
+    "dbaeumer.vscode-eslint",
+    "mhutchie.git-graph",
+    "eamodio.gitlens",
+    "github.vscode-pull-request-github",
+    "cschleiden.vscode-github-actions",
     "firsttris.vscode-jest-runner",
-		"msjsdiag.debugger-for-chrome",
-		"firefox-devtools.vscode-firefox-debug",
-		"editorconfig.editorconfig",
-		"esbenp.prettier-vscode",
-		"shinnn.stylelint",
-		"hex-ci.stylelint-plus",
+    "msjsdiag.debugger-for-chrome",
+    "firefox-devtools.vscode-firefox-debug",
+    "editorconfig.editorconfig",
+    "esbenp.prettier-vscode",
+    "shinnn.stylelint",
+    "hex-ci.stylelint-plus",
 	],
 
 	// Uncomment the next line if you want start specific services in your Docker Compose config.

+ 0 - 4
.devcontainer/docker-compose.yml

@@ -17,10 +17,6 @@ services:
       context: .
       dockerfile: Dockerfile
 
-    ports:
-      - 3000:3000
-      - 3001:3001 # for browser-sync
-
     volumes:
       - ..:/workspace/growi:delegated
       - node_modules:/workspace/growi/node_modules

+ 1 - 0
.gitattributes

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

+ 23 - 0
.github/dependabot.yml

@@ -0,0 +1,23 @@
+version: 2
+updates:
+  - package-ecosystem: github-actions
+    directory: '/'
+    schedule:
+      interval: daily
+    commit-message:
+      prefix: ci
+      include: scope
+
+  - package-ecosystem: npm
+    directory: '/'
+    schedule:
+      interval: daily
+    commit-message:
+      prefix: ci
+      include: scope
+    ignore:
+      - dependency-name: escape-string-regexp
+      - dependency-name: string-width
+      - dependency-name: "@handsontable/react"
+      - dependency-name: handsontable
+

+ 2 - 5
.github/release-drafter.yml

@@ -33,14 +33,11 @@ autolabeler:
     branch:
       - '/^support\/.+/'
     title:
+      - '/^support/i'
+      - '/^chore/i'
       - '/^ci/i'
       - '/^docs/i'
       - '/^test/i'
-  - label: 'exclude from changelog'
-    branch:
-      - '/^chore\/.+/'
-    title:
-      - '/^chore/i'
 include-labels:
   - breaking
   - feature

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

@@ -94,6 +94,7 @@ jobs:
         cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
       env:
+        SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql
         TYPEORM_HOST: localhost
         TYPEORM_PORT: ${{ job.services.mysql.ports[3306] }}
@@ -138,6 +139,10 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Remove unnecessary packages
+      working-directory: ./packages
+      run: |
+        ls | egrep -v '^(slack|slackbot-proxy)$' | xargs rm -r
     - name: lerna bootstrap
       run: |
         npx lerna bootstrap
@@ -148,7 +153,7 @@ jobs:
         yarn list --depth=0
     - name: lerna run build
       run: |
-        yarn lerna run build --scope @growi/slack --scope @growi/slackbot-proxy
+        yarn lerna run build
     - name: lerna bootstrap --production
       run: |
         npx lerna bootstrap -- --production
@@ -163,17 +168,13 @@ jobs:
         cp config/ci/.env.local.for-ci .env.production.local
         yarn start:prod:ci
       env:
+        SERVER_URI: http://localhost:8080
         TYPEORM_CONNECTION: mysql
         TYPEORM_HOST: localhost
         TYPEORM_PORT: ${{ job.services.mysql.ports[3306] }}
         TYPEORM_DATABASE: growi-slackbot-proxy
         TYPEORM_USERNAME: root
         TYPEORM_PASSWORD:
-    - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
-      with:
-        name: Report
-        path: report
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master

+ 12 - 1
.github/workflows/ci.yml

@@ -194,6 +194,9 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Remove unnecessary packages
+      run: |
+        rm -rf packages/slackbot-proxy
     - name: lerna bootstrap
       run: |
         npx lerna bootstrap
@@ -204,7 +207,9 @@ jobs:
         yarn list --depth=0
     - name: Build
       run: |
-        yarn lerna run build --scope @growi/core --scope @growi/slack --scope @growi/plugin-* --scope @growi/app
+        yarn lerna run build
+      env:
+        ANALYZE_BUNDLE_SIZE: ${{ matrix.node-version == '14.x' }}
     - name: lerna bootstrap --production
       run: |
         npx lerna bootstrap -- --production
@@ -232,6 +237,12 @@ jobs:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
 
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Bundle Analyzing Report
+        path: packages/app/report/bundle-analyzer.html
+
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       if: failure()

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

@@ -19,7 +19,7 @@ jobs:
       - uses: actions/checkout@v2
 
       - name: Retrieve information from package.json
-        uses: myrotvorets/info-from-package-json-action@0.0.2
+        uses: myrotvorets/info-from-package-json-action@1.1.0
         id: package-json
 
       # Drafts your next Release notes as Pull Requests are merged into "master"

+ 1 - 1
.github/workflows/pr-to-master.yml

@@ -27,7 +27,7 @@ jobs:
 
     if: |
       (!contains( github.event.pull_request.labels.*.name, 'exclude from changelog' ) &&
-        !startsWith( github.ref, 'refs/heads/chore/' ))
+        !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
       - uses: amannn/action-semantic-pull-request@v3.4.2

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

@@ -13,9 +13,11 @@ jobs:
 
     steps:
     - uses: actions/checkout@v2
+      with:
+        lfs: true
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Docker meta

+ 59 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -17,8 +17,10 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
+      with:
+        workingDir: packages/slackbot-proxy
 
     - name: Docker meta
       id: meta
@@ -78,6 +80,13 @@ jobs:
         rm -rf /tmp/.buildx-cache
         mv /tmp/.buildx-cache-new /tmp/.buildx-cache
 
+    - name: Add tag
+      uses: anothrNick/github-tag-action@1.36.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
       with:
@@ -85,3 +94,52 @@ jobs:
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         repository: weseek/growi-slackbot-proxy
         readme-filepath: ./packages/slackbot-proxy/docker/README.md
+
+
+  create-pr-for-next-rc:
+    needs: build-and-push-image
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        ref: ${{ github.event.pull_request.base.ref }}
+
+    - uses: actions/setup-node@v2
+      with:
+        node-version: '14'
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    - name: Install dependencies
+      run: |
+        npx lerna bootstrap
+
+    - name: Bump versions for next RC
+      run: |
+        yarn bump-versions:slackbot-proxy
+
+    - name: Retrieve information from package.json
+      uses: myrotvorets/info-from-package-json-action@1.1.0
+      id: package-json
+      with:
+        workingDir: packages/slackbot-proxy
+
+    - name: Commit
+      uses: github-actions-x/commit@v2.8
+      with:
+        github-token: ${{ secrets.GITHUB_TOKEN }}
+        push-branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
+        commit-message: 'Bump version'
+        name: GitHub Action
+
+    - name: Create PR
+      uses: repo-sync/pull-request@v2
+      with:
+        source_branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
+        destination_branch: master
+        pr_title: Prepare v${{ steps.package-json.outputs.packageVersion }}
+        pr_label: exclude from changelog
+        pr_body: "An automated PR generated by ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+        github_token: ${{ secrets.GITHUB_TOKEN }}

+ 8 - 4
.github/workflows/release.yml

@@ -34,11 +34,11 @@ jobs:
 
     - name: Bump versions
       run: |
-        node ./bin/github-actions/bump-versions -i patch
+        yarn bump-versions:patch
         sh ./packages/app/bin/github-actions/update-readme.sh
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Update Changelog
@@ -95,10 +95,11 @@ jobs:
 
     - name: Bump versions for next RC
       run: |
-        node ./bin/github-actions/bump-versions -i prerelease
+        yarn bump-versions:rc
+        yarn bump-versions:slackbot-proxy
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Commit
@@ -133,6 +134,7 @@ jobs:
     - uses: actions/checkout@v2
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
+        lfs: true
 
     - name: Setup suffix
       id: suffix
@@ -183,6 +185,8 @@ jobs:
         file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         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
         tags: ${{ steps.meta.outputs.tags }}

+ 144 - 4
CHANGELOG.md

@@ -1,9 +1,149 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.4.3...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.4.10...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v4.4.10](https://github.com/weseek/growi/compare/v4.4.9...v4.4.10) - 2021-11-08
+
+### 🚀 Improvement
+
+- imprv: Sidebar content header style (#4526) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: /pages/info API is broken (#4602) @yuki-takei
+- fix: blackboard theme location in theme list (#4506) @ayaka0417
+
+### 🧰 Maintenance
+
+- support: Use SWR (#4487) @yuki-takei
+- support: Replaced PageList with SWR (#4498) @takayuki-t
+- support: Improve devcontainer (#4510) @yuki-takei
+- support: Update passport-ldpauth from ^2.0.0 to ^3.0.1 (#4578) @LuqmanHakim-Grune
+- ci(deps): bump validator from 13.6.0 to 13.7.0 (#4588) @dependabot
+- ci(deps-dev): bump stylelint from 13.2.0 to 14.0.1 (#4583) @dependabot
+- Bump browserslist from 4.0.1 to 4.16.6 (#3776) @dependabot
+- ci(deps-dev): bump colors from 1.2.5 to 1.4.0 (#4365) @dependabot
+- ci(deps-dev): bump on-headers from 1.0.1 to 1.0.2 (#4366) @dependabot
+- ci(deps-dev): bump jquery-ui from 1.12.1 to 1.13.0 (#4549) @dependabot
+- docs(page): Add docs to /page/info api (#4531) @Mxchaeltrxn
+
+## [v4.4.9](https://github.com/weseek/growi/compare/v4.4.8...v4.4.9) - 2021-10-18
+
+### 💎 Features
+
+- feat: blackboard theme (#4501) @ayaka0417
+- feat: jade-green theme (#4500) @ayaka0417
+- feat: fire-red theme (#4499) @ayaka0417
+- feat: Add user list for like button (#4346) @Mxchaeltrxn
+
+### 🚀 Improvement
+
+- imprv: GROWI slackbot help message (#4488) @hakumizuki
+
+### 🐛 Bug Fixes
+
+- fix: Migration update-mail-transmission (#4482) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Localize Copy bug report button (#4436) @AbiFirmandhani-Grune
+
+## [v4.4.8](https://github.com/weseek/growi/compare/v4.4.7...v4.4.8) - 2021-10-08
+
+### 🚀 Improvement
+
+- imprv: Permissions to operate comment (#4466) @yuki-takei
+- imprv: Show modal when enabling Textlint (#4373) @stevenfukase
+- imprv: Slackbot reaction to user (#4442) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Redirected to apiv3 endpoint when guest mode is enabled (#4443) @stevenfukase
+- fix: Unnecessary extra JSON.stringify for configurations for slackbot without proxy (#4467) @hakumizuki
+- fix: Migration for slackbot configurations without proxy (#4465) @hakumizuki
+- fix: Slackbot error/command handling (#4463) @hakumizuki
+- fix(slackbot): Respond bad gateway error & improved help message (#4470) @hakumizuki
+- fix(slackbot): Stop auto-join to channels with middlewarer (#4424) @yuki-takei
+
+## [v4.4.7](https://github.com/weseek/growi/compare/v4.4.6...v4.4.7) - 2021-09-29
+
+### 🚀 Improvement
+
+- imprv: Slackbot search (#4420) @yuki-takei
+- imprv: Omit textlint-rule-en-capitalization (#4403) @yuki-takei
+- imprv: Apply terminus for graceful shutdown (#4398) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: A problem that GROWI server doesn't retrieve connection status from Official bot proxy (#4416) @yuki-takei
+- fix: Dictionary path of kuromoji invalid when uploaded to server (#4381) @stevenfukase
+- fix: Copy correct dotenv file for NO_CDN docker image (#4397) @yuki-takei
+- fix: Stop using ts-node in production (#4411) @yuki-takei
+- fix: SAML setting says 'setup is not yet complete' even if setup properly (#4390) @nakashimaki
+- fix: SidebarSmall button does not keep selection on reload (#4389) @nakashimaki
+- fix: Migrations for updating data for slackbot (#4406) @yuki-takei
+- fix: Migrations do not run in production (#4395) @yuki-takei
+- fix: Migration file for mongodb 3.6 compatibility (#4413) @hakumizuki
+- fix(slackbot): Sync permission when data stored is not enough (#4417) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Install Git LFS when provisioning of devcontainer (#4405) @stevenfukase
+- chore: Add .dockerignore (#4396) @yuki-takei
+
+## [v4.4.6](https://github.com/weseek/growi/compare/v4.4.5...v4.4.6) - 2021-09-24
+
+### 🚀 Improvement
+
+- imprv: Slackbot response flow (#4296) @yuki-takei
+- imprv(slackbot-proxy): Show version on the top page (#4342) @yuto-oweseek
+
+### 🧰 Maintenance
+
+- support(slackbot-proxy): Bump slackbot proxy version independentry (#4385) @yuki-takei
+
+## [v4.4.5](https://github.com/weseek/growi/compare/v4.4.4...v4.4.5) - 2021-09-23
+
+### 🐛 Bug Fixes
+
+- fix: Revert #4347
+- fix: ERROR: Cannot find module 'tslib' on v4.4.4 (#4368) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: bump @promster/express and @promster/server (#4370) @yuki-takei
+- support: Upgrade codemirror to 5.63.0 (#4364) @yuki-takei
+- ci(deps-dev): bump codemirror from 5.48.4 to 5.58.2 (#4363) @dependabot
+
+## [v4.4.4](https://github.com/weseek/growi/compare/v4.4.3...v4.4.4) (Discontinued) - 2021-09-22
+
+### 💎 Features
+
+- feat: Add Textlint support (#4228) @kaoritokashiki
+
+### 🚀 Improvement
+
+- imprv: Highlighting searching keyword (#4327) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Backspace key on last line doesn't work in vim mode (#4347) @yuki-takei
+- fix: Recent Created of home is empty (#4345) @yuki-takei
+- fix: IME suggestion list obscures inputted text (#4335) @yuki-takei
+- fix: Highlighting section header (#4326) @yuki-takei
+
+### 🧰 Maintenance
+
+- chore: Update passport-saml 2.2.0 (#4360) @LuqmanHakim-Grune
+- ci(deps): bump http-errors from 1.6.2 to 1.8.0 (#4353) @dependabot
+- ci(deps-dev): bump @tsed/json-mapper from 6.43.0 to 6.70.1 (#4352) @dependabot
+- ci(deps): bump graceful-fs from 4.1.11 to 4.2.8 (#4351) @dependabot
+- ci(deps): bump myrotvorets/info-from-package-json-action from 0.0.2 to 1.1.0 (#4348) @dependabot
+- ci(deps): bump path-parse from 1.0.5 to 1.0.7 (#4126) @dependabot
+- ci(deps): bump tmpl from 1.0.4 to 1.0.5 (#4337) @dependabot
+
 ## [v4.4.3](https://github.com/weseek/growi/compare/v4.4.2...v4.4.3) - 2021-09-17
 
 ### 💎 Features
@@ -1441,7 +1581,7 @@ Upgrading Guide: [https://docs.growi.org/en/admin-guide/upgrading/34x.html](http
 
 - Improvement: Ensure to prevent suspending own account
 - Fix: Ensure to be able to use `.` for username when invited
-- Fix: monospace font for `<code></code>`
+- Fix: monospace font for `code` tag
 
 ## v2.1.1
 
@@ -1509,8 +1649,8 @@ Upgrading Guide: [https://docs.growi.org/en/admin-guide/upgrading/34x.html](http
 
 ## v1.2.14
 
-- Fix: Tabs(`a[data-toggle="tab"][href="#..."]`) push browser history twice
-- Fix: `a[href="#edit-form"]` still save history even when disabling pushing states option
+- Fix: Tabs(`a[data-toggle=tab][href=#...]`) push browser history twice
+- Fix: `a[href=#edit-form]` still save history even when disabling pushing states option
 
 ## v1.2.13
 

+ 12 - 1
SECURITY.md

@@ -14,9 +14,20 @@
 If you believe you have found a security vulnerability in any GROWI related repository, please report it to us using one of the methods described below.
 
   * [Join our Slack team](https://growi-slackin.weseek.co.jp/) and send DM to `@yuki` who is the lead developer
-  * Report to JPCERT/CC ([en](https://www.jpcert.or.jp/english/ir/form.html)/[ja](https://www.jpcert.or.jp/form/))
+  * Report to JPCERT/CC[^jpcertcc]
+    * [[PDF] JPCERT/CC Vulnerability Coordination and Disclosure Policy](https://www.jpcert.or.jp/english/vh/vul-coordination-disclosure-policy_2019.pdf)
 
 ## Preferred Languages
 
 Communication in English and Japanese is possible.  
 In Japanese, we can reply more quickly. 
+
+
+
+
+Some long sentence. 
+
+[^jpcertcc]: JPCERT/CC is a National CSIRT in Japan and a coordination center
+who coordinates cyber security incidents and products vulnerabilities
+with network service providers, product vendors (software/IoT/ICS etc.),
+security vendors, government agencies, as well as the industry associations.

+ 11 - 0
THIRD-PARTY-NOTICES.md

@@ -17,6 +17,7 @@ https://github.com/weseek/growi.
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 5. EmojiOne Version 3 (https://github.com/joypixels/emojione/tree/v3.1.1)
+6. Kuromoji.js (https://github.com/takuyaa/kuromoji.js)
 
 
 License Notice for Apache License, Version 2.0 Derivative Works
@@ -108,3 +109,13 @@ https://creativecommons.org/licenses/by/4.0/
 ```
 author: "EmojiOne <ryan@emojione.com> (http://emojione.com)"
 ```
+
+
+License Notice for Kuromoji.js
+------------------------
+
+https://github.com/takuyaa/kuromoji.js/blob/master/LICENSE-2.0.txt
+
+```
+author: "Takuya Asano <takuya.a@gmail.com>"
+```

+ 7 - 1
bin/github-actions/bump-versions/flow/bump-versions.js

@@ -13,6 +13,7 @@ async function bumpVersions({
   help = false,
   dir = '.',
   dryRun = false,
+  updateDependencies = true,
   increment = 'patch',
   preid = 'RC',
 }) {
@@ -26,8 +27,12 @@ async function bumpVersions({
 
   const config = await loadConfig(dir, 'bump-versions.config');
 
-  // get current version
   const { monorepo } = config;
+  if (!updateDependencies) {
+    monorepo.updateDependencies = false;
+  }
+
+  // get current version
   const currentVersion = monorepo && monorepo.mainVersionFile
     ? getCurrentVersion(dir, monorepo.mainVersionFile)
     : getCurrentVersion(dir);
@@ -55,6 +60,7 @@ const arg = {
   '--dir': String,
   '--help': Boolean,
   '--dry-run': Boolean,
+  '--update-dependencies': Boolean,
   '--increment': String,
   '--preid': String,
 

+ 5 - 1
bin/github-actions/bump-versions/step/printHelp.js

@@ -9,8 +9,9 @@ export default () => runStep({}, () => {
   const dir = `--dir ${underline('PATH')}`;
   const increment = `--increment ${underline('LEVEL')}`;
   const preId = `--preid ${underline('IDENTIFIER')}`;
+  const updateDependencies = `--update-dependencies ${underline('true/false')}`;
   const dryRun = '--dry-run';
-  const all = [help, dir, increment, preId, dryRun]
+  const all = [help, dir, increment, preId, updateDependencies, dryRun]
     .map(x => `[${x}]`)
     .join(' ');
 
@@ -46,6 +47,9 @@ export default () => runStep({}, () => {
       )} for semver.inc() with 'prerelease' type (default: 'RC').`,
     ),
     '',
+    indent(`${updateDependencies}`),
+    indent('  Update dependencies or not (default: true).'),
+    '',
     indent(`-D, ${dryRun}`),
     indent('  Displays the steps without actually doing them.'),
     '',

+ 1 - 1
lerna.json

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

+ 9 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.4.4-RC.0",
+  "version": "4.4.11-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -35,6 +35,9 @@
     "app:server": "yarn lerna run server --scope @growi/app",
     "slackbot-proxy:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
+    "bump-versions:patch": "node ./bin/github-actions/bump-versions -i patch",
+    "bump-versions:rc": "node ./bin/github-actions/bump-versions -i prerelease",
+    "bump-versions:slackbot-proxy": "node ./bin/github-actions/bump-versions -i prerelease -d packages/slackbot-proxy --preid slackbot-proxy --update-dependencies false",
     "//// scripts for backward compatibility": "",
     "build:prod": "echo !!! CAUTION !!! ==> The script 'build:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:build",
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
@@ -43,9 +46,7 @@
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "npm-run-all": "^4.1.5",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3"
+    "tslib": "^2.3.1"
   },
   "devDependencies": {
     "@types/jest": "^26.0.22",
@@ -66,7 +67,10 @@
     "lerna": "^4.0.0",
     "rewire": "^5.0.0",
     "shipjs": "^0.23.3",
-    "ts-jest": "^27.0.4"
+    "ts-jest": "^27.0.4",
+    "ts-node": "^9.1.1",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
   },
   "engines": {
     "node": "^12 || ^14",

+ 2 - 0
packages/app/.env.development

@@ -2,6 +2,8 @@
 ## Handled by Next.js with dotenv or dotenv-flow
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
+MIGRATIONS_DIR=src/migrations/
+
 FILE_UPLOAD=mongodb
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760
 MATHJAX=1

+ 1 - 0
packages/app/.env.production

@@ -3,3 +3,4 @@
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 FORMAT_NODE_LOG=false
+MIGRATIONS_DIR=dist/migrations/

+ 2 - 1
packages/app/.eslintrc.js

@@ -25,11 +25,12 @@ module.exports = {
       name: 'axios',
       message: 'Please use src/utils/axios instead.',
     }],
+    '@typescript-eslint/no-var-requires': 'off',
+
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     '@typescript-eslint/explicit-module-boundary-types': ['warn'],
     '@typescript-eslint/no-use-before-define': ['warn'],
     '@typescript-eslint/no-this-alias': ['warn'],
-    '@typescript-eslint/no-var-requires': ['warn'],
     'jest/no-done-callback': ['warn'],
   },
 };

+ 1 - 1
packages/app/.gitignore

@@ -5,8 +5,8 @@
 # dist
 /dist/
 /transpiled/
+/report/
 /public/static/js
-/public/static/dict
 /public/static/styles
 /public/uploads
 /tmp/

+ 1 - 12
packages/app/bin/cdn/cdn-resources-downloader.ts

@@ -4,9 +4,7 @@ import urljoin from 'url-join';
 import { Transform } from 'stream';
 import replaceStream from 'replacestream';
 
-import {
-  cdnLocalScriptRoot, cdnLocalDictRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot,
-} from '^/config/cdn';
+import { cdnLocalScriptRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot } from '^/config/cdn';
 import * as cdnManifests from '^/resource/cdn-manifests';
 
 import { CdnResource, CdnManifest } from '~/interfaces/cdn';
@@ -22,14 +20,6 @@ export default class CdnResourcesDownloader {
       return { manifest, outDir: cdnLocalScriptRoot };
     });
 
-    const cdnDictResources: CdnResource[] = cdnManifests.dict.map((manifest: CdnManifest) => {
-      return { manifest, outDir: cdnLocalDictRoot };
-    });
-
-    const dictExtensionOptions = {
-      ext: 'gz',
-    };
-
     const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
       return { manifest, outDir: cdnLocalStyleRoot };
     });
@@ -42,7 +32,6 @@ export default class CdnResourcesDownloader {
 
     return Promise.all([
       this.downloadScripts(cdnScriptResources),
-      this.downloadScripts(cdnDictResources, dictExtensionOptions),
       this.downloadStyles(cdnStyleResources, dlStylesOptions),
     ]);
   }

+ 0 - 1
packages/app/config/cdn.js

@@ -4,6 +4,5 @@ import { projectRoot } from '~/utils/project-dir-utils';
 
 export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
-export const cdnLocalDictRoot = path.join(projectRoot, 'public/static/dict/cdn');
 export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';

+ 3 - 0
packages/app/config/webpack.common.js

@@ -45,6 +45,9 @@ module.exports = (options) => {
       'styles/theme-antarctic':       './src/styles/theme/antarctic.scss',
       'styles/theme-spring':          './src/styles/theme/spring.scss',
       'styles/theme-hufflepuff':      './src/styles/theme/hufflepuff.scss',
+      'styles/theme-fire-red':      './src/styles/theme/fire-red.scss',
+      'styles/theme-jade-green':      './src/styles/theme/jade-green.scss',
+      'styles/theme-blackboard':      './src/styles/theme/blackboard.scss',
       // styles for external services
       'styles/style-hackmd':          './src/styles-hackmd/style.scss',
     }, options.entry || {}), // Merge with env dependent settings

+ 2 - 2
packages/app/config/webpack.prod.js

@@ -15,7 +15,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 /**
   * Webpack Constants
   */
-const { ANALYZE } = process.env;
+const { ANALYZE_BUNDLE_SIZE } = process.env;
 
 module.exports = require('./webpack.common')({
   mode: 'production',
@@ -60,7 +60,7 @@ module.exports = require('./webpack.common')({
     }),
 
     new BundleAnalyzerPlugin({
-      analyzerMode: ANALYZE ? 'static' : 'disabled',
+      analyzerMode: ANALYZE_BUNDLE_SIZE ? 'static' : 'disabled',
       reportFilename: path.resolve(__dirname, '../report/bundle-analyzer.html'),
       openAnalyzer: false,
     }),

+ 7 - 15
packages/app/docker/Dockerfile

@@ -40,10 +40,7 @@ RUN tar cf node_modules.tar \
 ## deps-resolver-prod
 ##
 FROM deps-resolver AS deps-resolver-prod
-
-# shrink dependencies for production
-RUN yarn install --production
-
+RUN npx lerna bootstrap -- --production
 # make artifacts
 RUN tar cf node_modules.tar \
   node_modules \
@@ -75,15 +72,14 @@ RUN rm node_modules.tar
 ##
 FROM prebuilder-default AS prebuilder-nocdn
 
-# replace env.prod.js for NO_CDN
-COPY docker/nocdn/env.prod.js ${appDir}/config/
+# add dotenv file for NO_CDN
+COPY packages/app/docker/nocdn/.env.production.local ${appDir}/packages/app/
 
 
 
 ##
 ## builder
 ##
-# FROM prebuilder-${flavor}
 FROM prebuilder-${flavor} AS builder
 
 ENV appDir /opt/growi
@@ -116,7 +112,8 @@ RUN tar cf packages.tar \
   packages/app/public \
   packages/app/resource \
   packages/app/tmp \
-  packages/app/.env.production \
+  packages/app/migrate-mongo-config.js \
+  packages/app/.env.production* \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.json \
   packages/*/package.json \
@@ -143,11 +140,6 @@ RUN set -eux; \
 # verify that the binary works
 	gosu nobody true
 
-# Add Tini
-ENV TINI_VERSION v0.19.0
-ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
-RUN chmod +x /tini
-
 COPY --from=deps-resolver-prod --chown=node:node \
   ${appDir}/node_modules.tar ${appDir}/
 COPY --from=builder --chown=node:node \
@@ -171,5 +163,5 @@ WORKDIR ${appDir}/packages/app
 VOLUME /data
 EXPOSE 3000
 
-ENTRYPOINT ["/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
-CMD ["node", "-r", "dotenv-flow/config", "--expose_gc", "dist/server/app.js"]
+ENTRYPOINT ["/docker-entrypoint.sh"]
+CMD ["yarn migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"]

+ 6 - 0
packages/app/docker/Dockerfile.dockerignore

@@ -0,0 +1,6 @@
+node_modules
+*/node_modules
+*/coverage
+*/dist
+*/Dockerfile
+*/*.dockerignore

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.4.3`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.3/docker/Dockerfile)
-* [`4.4.3-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.3/docker/Dockerfile)
+* [`4.4.10`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.10/docker/Dockerfile)
+* [`4.4.10-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.10/docker/Dockerfile)
 * [`4.3.3`, `4.3` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 * [`4.3.3-nocdn`, `4.3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 

+ 1 - 1
packages/app/docker/docker-entrypoint.sh

@@ -11,4 +11,4 @@ fi
 chown -R node:node /data/uploads
 chown -h node:node ./public/uploads
 
-gosu node $@
+exec gosu node /bin/bash -c "$@"

+ 6 - 0
packages/app/docker/nocdn/.env.production.local

@@ -0,0 +1,6 @@
+
+##
+## Handled by Next.js with dotenv or dotenv-flow
+## https://nextjs.org/docs/basic-features/environment-variables
+##
+NO_CDN=true

+ 0 - 5
packages/app/docker/nocdn/env.prod.js

@@ -1,5 +0,0 @@
-module.exports = {
-  NODE_ENV: 'production',
-  NO_CDN: true,
-  // FORMAT_NODE_LOG: false,
-};

+ 21 - 5
packages/app/jest.config.js

@@ -14,10 +14,22 @@ module.exports = {
 
   preset: 'ts-jest/presets/js-with-ts',
 
-  globalSetup: '<rootDir>/src/test/global-setup.js',
-  globalTeardown: '<rootDir>/src/test/global-teardown.js',
-
   projects: [
+    {
+      displayName: 'unit',
+
+      preset: 'ts-jest/presets/js-with-ts',
+
+      rootDir: '.',
+      roots: ['<rootDir>/src'],
+      testMatch: ['<rootDir>/src/test/unit/**/*.test.ts', '<rootDir>/src/test/unit/**/*.test.js'],
+
+      testEnvironment: 'node',
+
+      // Automatically clear mock calls and instances between every test
+      clearMocks: true,
+      moduleNameMapper: MODULE_NAME_MAPPING,
+    },
     {
       displayName: 'server',
 
@@ -25,9 +37,13 @@ module.exports = {
 
       rootDir: '.',
       roots: ['<rootDir>/src'],
+      testMatch: ['<rootDir>/src/test/integration/**/*.test.ts', '<rootDir>/src/test/integration/**/*.test.js'],
+
       testEnvironment: 'node',
-      setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
-      testMatch: ['<rootDir>/src/test/**/*.test.ts', '<rootDir>/src/test/**/*.test.js'],
+      globalSetup: '<rootDir>/src/test/integration/global-setup.js',
+      globalTeardown: '<rootDir>/src/test/integration/global-teardown.js',
+      setupFilesAfterEnv: ['<rootDir>/src/test/integration/setup.js'],
+
       // Automatically clear mock calls and instances between every test
       clearMocks: true,
       moduleNameMapper: MODULE_NAME_MAPPING,

+ 8 - 5
packages/app/config/migrate.js → packages/app/migrate-mongo-config.js

@@ -5,11 +5,15 @@
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
 
-import mongoose from 'mongoose';
+const { URL } = require('url');
 
-import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+// get migrationsDir from env var
+const migrationsDir = process.env.MIGRATIONS_DIR;
+if (migrationsDir == null) {
+  throw new Error('An env var MIGRATIONS_DIR must be set.');
+}
 
-const { URL } = require('url');
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('@growi/core');
 
 initMongooseGlobalSettings();
 
@@ -25,8 +29,7 @@ const mongodb = {
 };
 
 module.exports = {
-  mongoUri,
   mongodb,
-  migrationsDir: 'src/migrations/',
+  migrationsDir,
   changelogCollectionName: 'migrations',
 };

+ 27 - 23
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.4.4-RC.0",
+  "version": "4.4.11-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -14,19 +14,26 @@
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
+    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up",
     "//// for development": "",
     "dev": "run-p dev:client dev:server",
     "dev:client": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
     "dev:client:nowatch": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
     "dev:server": "yarn cross-env NODE_ENV=development ts-node-dev --inspect --expose-gc -r tsconfig-paths/register -r dotenv-flow/config --transpile-only src/server/app.ts",
     "predev:client": "yarn cross-env NODE_ENV=development run-p resources:*",
-    "predev:server": "yarn cross-env NODE_ENV=development yarn migrate",
+    "predev:server": "yarn cross-env NODE_ENV=development yarn dev:migrate:up",
+    "dev:migrate-mongo": "yarn cross-env NODE_ENV=development yarn ts-node node_modules/.bin/migrate-mongo",
+    "dev:migrate": "yarn dev:migrate:up",
+    "dev:migrate:create": "yarn dev:migrate-mongo create",
+    "dev:migrate:status": "yarn dev:migrate-mongo status",
+    "dev:migrate:up": "yarn dev:migrate-mongo up",
+    "dev:migrate:down": "yarn dev:migrate-mongo down",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
     "lint:typecheck": "npx tsc",
     "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
-    "lint:styles": "stylelint src/**/*.scss",
+    "lint:styles": "stylelint src/**/*.scss --custom-syntax postcss-scss",
     "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
@@ -39,11 +46,6 @@
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
-    "migrate": "yarn migrate:up",
-    "migrate:create": "yarn ts-node node_modules/.bin/migrate-mongo create -f config/migrate.js",
-    "migrate:status": "yarn ts-node node_modules/.bin/migrate-mongo status -f config/migrate.js",
-    "migrate:up": "yarn ts-node node_modules/.bin/migrate-mongo up -f config/migrate.js",
-    "migrate:down": "yarn ts-node node_modules/.bin/migrate-mongo down -f config/migrate.js",
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
   },
   "// comments for dependencies": {
@@ -53,14 +55,15 @@
   },
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
+    "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.4.4-RC.0",
-    "@growi/plugin-attachment-refs": "^4.4.4-RC.0",
-    "@growi/plugin-lsx": "^4.4.4-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.4-RC.0",
-    "@growi/slack": "^4.4.4-RC.0",
-    "@promster/express": "^5.0.1",
-    "@promster/server": "^6.0.0",
+    "@growi/codemirror-textlint": "^4.4.11-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.11-RC.0",
+    "@growi/plugin-lsx": "^4.4.11-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.11-RC.0",
+    "@growi/slack": "^4.4.11-RC.0",
+    "@promster/express": "^5.1.0",
+    "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
@@ -97,7 +100,7 @@
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "helmet": "^4.6.0",
-    "http-errors": "~1.6.2",
+    "http-errors": "~1.8.0",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-node-fs-backend": "^2.1.0",
@@ -122,9 +125,9 @@
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-http": "^0.3.0",
-    "passport-ldapauth": "^2.0.0",
+    "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
-    "passport-saml": "^1.0.0",
+    "passport-saml": "^2.2.0",
     "passport-twitter": "^1.0.4",
     "prom-client": "^13.0.0",
     "react-card-flip": "^1.0.10",
@@ -155,7 +158,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.4-RC.0",
+    "@growi/ui": "^4.4.11-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -166,7 +169,7 @@
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",
-    "codemirror": "^5.48.4",
+    "codemirror": "^5.63.0",
     "colors": "^1.2.5",
     "connect-browser-sync": "^2.1.0",
     "core-js": "=2.6.9",
@@ -199,7 +202,6 @@
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.10.0",
     "node-dev": "^4.0.0",
-    "node-sass": "^4.14.1",
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
@@ -221,15 +223,17 @@
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
-    "sass-loader": "^8.0.0",
+    "sass": "^1.43.4",
+    "sass-loader": "^10.1.1",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
-    "stylelint": "^13.2.0",
+    "stylelint": "^14.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger2openapi": "^5.3.1",
+    "swr": "^1.0.1",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",

+ 3 - 0
packages/app/public/static/dict/base.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0803327762e1c93ca731e4319ab8343340f2806bb84941207782cde9d2d5a8eb
+size 3956825

+ 3 - 0
packages/app/public/static/dict/cc.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02b7631be0d4de3a1a75cd9f9cc51536e4f94c9e6b389b813e06ba0f6e7de765
+size 1692067

+ 3 - 0
packages/app/public/static/dict/check.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:193ae0035fff6fe812b58d9ee730e7a7d7ee601d918481ce51075c58114f6cc9
+size 3111633

+ 3 - 0
packages/app/public/static/dict/tid.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d43d831cb6fb0f0a411739cd287a6d5e998e121a8daca614df14a81a0dcac586
+size 1605820

+ 3 - 0
packages/app/public/static/dict/tid_map.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:33efd5ffd87a70f669add093fa39dee44341d58f940844ef107c8fd98bb795b2
+size 1485576

+ 3 - 0
packages/app/public/static/dict/tid_pos.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:60dbfc99a6ab993f30c5dab648bec6ad7f9aaefa5c14e1843837d95e509f8895
+size 5916009

+ 3 - 0
packages/app/public/static/dict/unk.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f7f991cdeb9bfd3e9c0e4577cc50ee0815a11c508cccd444a9d3ab3c81521100
+size 10512

+ 3 - 0
packages/app/public/static/dict/unk_char.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a8e86fd9aff32d323fbb59f5a7006f05927a11f8173c90712cc56293aeb3225
+size 306

+ 3 - 0
packages/app/public/static/dict/unk_compat.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:50f60aa29bc2e86c2903ab8c825bb6fa604d2b294d96941c1d3924259791899d
+size 338

+ 3 - 0
packages/app/public/static/dict/unk_invoke.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6b210889548457c3006913afd12c8b525562255f2709e404604be9614a25e94c
+size 1140

+ 3 - 0
packages/app/public/static/dict/unk_map.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6df12460e5477230bb6fd9641def918b699fc0a8868016b6c9f794488630509b
+size 1190

+ 3 - 0
packages/app/public/static/dict/unk_pos.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5b183a29f281acc7e0542beca47b83f7985047c0a2d27e78a66f32276be5ad11
+size 10540

+ 13 - 99
packages/app/resource/cdn-manifests.js

@@ -55,28 +55,28 @@ module.exports = {
     },
     {
       name: 'codemirror-dialog',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/addon/dialog/dialog.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/addon/dialog/dialog.min.js',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-keymap-vim',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/keymap/vim.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/keymap/vim.min.js',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-keymap-emacs',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/keymap/emacs.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/keymap/emacs.min.js',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-keymap-sublime',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/keymap/sublime.min.js',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/keymap/sublime.min.js',
       args: {
         integrity: '',
       },
@@ -89,92 +89,6 @@ module.exports = {
       },
     },
   ],
-  dict: [
-    {
-      name: 'base.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/base.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'cc.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/cc.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'check.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/check.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'tid_map.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid_map.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'tid_pos.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid_pos.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'tid.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_char.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_char.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_compat.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_compat.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_invoke.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_invoke.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_map.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_map.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_pos.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_pos.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-  ],
   style: [
     {
       name: 'lato',
@@ -256,63 +170,63 @@ module.exports = {
     },
     {
       name: 'codemirror-dialog',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/addon/dialog/dialog.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/addon/dialog/dialog.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-eclipse',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/eclipse.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/eclipse.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-elegant',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/elegant.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/elegant.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-neo',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/neo.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/neo.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-mdn-like',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/mdn-like.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/mdn-like.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-material',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/material.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/material.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-dracula',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/dracula.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/dracula.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-monokai',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/monokai.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/monokai.min.css',
       args: {
         integrity: '',
       },
     },
     {
       name: 'codemirror-theme-twilight',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/theme/twilight.min.css',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.63.0/theme/twilight.min.css',
       args: {
         integrity: '',
       },

+ 4 - 1
packages/app/resource/locales/en_US/admin/admin.json

@@ -154,7 +154,9 @@
       "stale_notification": "Display notification on stale pages",
       "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
       "show_all_reply_comments": "Show all reply comments",
-      "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted."
+      "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted.",
+      "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
+      "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range."
     },
     "code_highlight": "Code highlight",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
@@ -349,6 +351,7 @@
       "allow_specified_long": "Allow specified (The command is allowed from only specified channels)",
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
+      "test_connection_only_public_channel":"Please test connection in a public channel",
       "error_check_logs_below": "An error has occurred. Please check the logs below.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"

+ 6 - 0
packages/app/resource/locales/en_US/translation.json

@@ -57,6 +57,7 @@
   "Presentation Mode": "Presentation",
   "The end": "The end",
   "Not available for guest": "Not available for guest",
+  "No users have liked this yet.": "No users have liked this yet.",
   "Create Archive Page": "Create Archive Page",
   "File type": "File type",
   "Target page": "Target page",
@@ -454,6 +455,11 @@
       "Post": "Post"
     }
   },
+  "modal_enable_textlint": {
+    "confirm_download_dict_and_enable_textlint": "Are you sure you want to enable Textlint? This will download 20MB of dictionary file.",
+    "enable_textlint": "Enable Textlint",
+    "dont_ask_again": "Don't ask again"
+  },
   "link_edit": {
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",

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

@@ -11,7 +11,13 @@
     "installed_version": "インストールされているバージョン",
     "list_of_env_vars": "サーバー側で設定されている環境変数一覧",
     "env_var_priority": "セキュリティに関する環境変数を除き、データベースの値が優先的に取得されます。",
-    "about_security": "セキュリティに関する環境変数は <a href='/admin/security'>セキュリティ設定画面</a> からご確認ください。"
+    "about_security": "セキュリティに関する環境変数は <a href='/admin/security'>セキュリティ設定画面</a> からご確認ください。",
+    "copy_prefilled_host_information": {
+      "default": "上記のホスト情報をコピー",
+      "done": "クリップボードにコピーしました!"
+    },
+    "bug_report": "バグを報告する",
+    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
   },
   "app_setting": {
     "site_name": "サイト名",
@@ -148,7 +154,9 @@
       "stale_notification": "古いページに通知を表示する",
       "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。",
       "show_all_reply_comments": "返信コメントを全て表示する",
-      "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。"
+      "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。",
+      "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
+      "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。"
     },
     "code_highlight": "コードハイライト",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
@@ -342,6 +350,7 @@
       "allow_specified-long": "特定のチャンネルを許可 (テキストボックスに入力されたチャンネルのみ許可されます)",
       "test_connection": "連携状況のテストをする",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
+      "test_connection_only_public_channel":"連携テストは public チャンネルで確認してください",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。",
       "send_message_to_slack_work_space": "Slack ワークスペースに送信しました",
       "add_slack_workspace": "Slackワークスペースを追加"

+ 5 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -455,6 +455,11 @@
       "Post": "投稿"
     }
   },
+  "modal_enable_textlint": {
+    "confirm_download_dict_and_enable_textlint": "Textlintを有効にしますか?20MBの辞書ファイルをダウンロードします。",
+    "enable_textlint": "Textlintを有効にする",
+    "dont_ask_again": "常に許可する"
+  },
   "link_edit": {
     "edit_link": "リンク編集",
     "set_link_and_label": "リンク情報",

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

@@ -11,7 +11,13 @@
     "installed_version": "已安装版本",
     "list_of_env_vars": "环境变量列表",
     "env_var_priority": "对于安全性以外的环境变量,优先获取数据库的值。",
-    "about_security": "检查安全环境变量的<a href='/admin/security'>安全设置</a>。"
+    "about_security": "检查安全环境变量的<a href='/admin/security'>安全设置</a>。",
+    "copy_prefilled_host_information": {
+      "default": "复制预填的主机信息",
+      "done": "复制到剪贴板!"
+    },
+    "bug_report": "提交一个错误报告",
+    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
   },
   "app_setting": {
     "site_name": "网站名称 ",
@@ -158,7 +164,9 @@
       "stale_notification": "在过期页上显示通知",
       "stale_notification_desc": "显示自上次更新以来超过1年的页面通知。",
       "show_all_reply_comments": "显示所有回复评论",
-      "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。"
+      "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。",
+      "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
+      "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。"
     },
     "code_highlight": "代码突出显示",
     "nocdn_desc": "当强制应用环境变量<code>NO_CDN=true</code><br>Github样式时,此函数被禁用。",
@@ -352,6 +360,7 @@
       "allow_specified_long": "允许指定(该命令只允许来自指定的通道)",
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
+      "test_connection_only_public_channel":"请在一个公共频道中测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "add_slack_workspace": "添加Slack Workspace"

+ 5 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -433,6 +433,11 @@
 			"Post": "提交"
 		}
 	},
+  "modal_enable_textlint": {
+    "confirm_download_dict_and_enable_textlint": "您确定要启用 Textlint 吗?这将下载 20MB 的字典文件。",
+    "enable_textlint": "启用Textlint",
+    "dont_ask_again": "不要再问"
+  },
   "link_edit": {
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",

+ 8 - 3
packages/app/src/client/app.jsx

@@ -3,7 +3,10 @@ import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 
+import { SWRConfig } from 'swr';
+
 import loggerFactory from '~/utils/logger';
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import ErrorBoundary from '../components/ErrorBoudary';
 import Sidebar from '../components/Sidebar';
@@ -156,9 +159,11 @@ Object.keys(componentMappings).forEach((key) => {
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
         <ErrorBoundary>
-          <Provider inject={injectableContainers}>
-            {componentMappings[key]}
-          </Provider>
+          <SWRConfig value={swrGlobalConfiguration}>
+            <Provider inject={injectableContainers}>
+              {componentMappings[key]}
+            </Provider>
+          </SWRConfig>
         </ErrorBoundary>
       </I18nextProvider>,
       elem,

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

@@ -35,6 +35,7 @@ export default class AdminCustomizeContainer extends Container {
 
       isEnabledStaleNotification: false,
       isAllReplyShown: false,
+      isSearchScopeChildrenAsDefault: false,
       currentHighlightJsStyleId: '',
       isHighlightJsStyleBorderEnabled: false,
       currentCustomizeTitle: '',
@@ -89,6 +90,7 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: customizeParams.pageLimitationXL,
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isAllReplyShown: customizeParams.isAllReplyShown,
+        isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         currentHighlightJsStyleId: customizeParams.styleName,
         isHighlightJsStyleBorderEnabled: customizeParams.styleBorder,
         currentCustomizeTitle: customizeParams.customizeTitle,
@@ -183,6 +185,13 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isAllReplyShown: !this.state.isAllReplyShown });
   }
 
+  /**
+   * Switch isSearchScopeChildrenAsDefault
+   */
+  switchIsSearchScopeChildrenAsDefault() {
+    this.setState({ isSearchScopeChildrenAsDefault: !this.state.isSearchScopeChildrenAsDefault });
+  }
+
   /**
    * Switch highlightJsStyle
    */
@@ -295,6 +304,7 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: this.state.pageLimitationXL,
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
         isAllReplyShown: this.state.isAllReplyShown,
+        isSearchScopeChildrenAsDefault: this.state.isSearchScopeChildrenAsDefault,
       });
       const { customizedParams } = response.data;
       this.setState({
@@ -307,6 +317,7 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: customizedParams.pageLimitationXL,
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
         isAllReplyShown: customizedParams.isAllReplyShown,
+        isSearchScopeChildrenAsDefault: customizedParams.isSearchScopeChildrenAsDefault,
       });
     }
     catch (err) {

+ 21 - 97
packages/app/src/client/services/AppContainer.js

@@ -1,10 +1,13 @@
 import { Container } from 'unstated';
 
-import urljoin from 'url-join';
-
-import axios from '~/utils/axios';
 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';
 
@@ -12,10 +15,8 @@ import {
   mediaQueryListForDarkMode,
   applyColorScheme,
 } from '../util/color-scheme';
-import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 
 import { i18nFactory } from '../util/i18n';
-import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
 
 /**
  * Service container related to options for Application
@@ -28,13 +29,11 @@ export default class AppContainer extends Container {
 
     this.state = {
       preferDarkModeByMediaQuery: false,
-
-      // stetes for contents
-      recentlyUpdatedPages: [],
     };
 
+    // get csrf token from body element
+    // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
     const body = document.querySelector('body');
-
     this.csrfToken = body.dataset.csrftoken;
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
@@ -60,17 +59,21 @@ export default class AppContainer extends Container {
     this.componentInstances = {};
     this.rendererInstances = {};
 
-    this.apiGet = this.apiGet.bind(this);
-    this.apiPost = this.apiPost.bind(this);
-    this.apiDelete = this.apiDelete.bind(this);
-    this.apiRequest = this.apiRequest.bind(this);
+    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.apiv3Root = '/_api/v3';
     this.apiv3 = {
-      get: this.apiv3Get.bind(this),
-      post: this.apiv3Post.bind(this),
-      put: this.apiv3Put.bind(this),
-      delete: this.apiv3Delete.bind(this),
+      get: apiv3Get,
+      post: apiv3Post,
+      put: apiv3Put,
+      delete: apiv3Delete,
     };
   }
 
@@ -279,11 +282,6 @@ export default class AppContainer extends Container {
     });
   }
 
-  async retrieveRecentlyUpdated() {
-    const { data } = await this.apiv3Get('/pages/recent');
-    this.setState({ recentlyUpdatedPages: data.pages });
-  }
-
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     switch (componentKind) {
@@ -304,78 +302,4 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
 
-  async apiGet(path, params) {
-    return this.apiRequest('get', path, { params });
-  }
-
-  async apiPost(path, params) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiRequest('post', path, params);
-  }
-
-  async apiDelete(path, params) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiRequest('delete', path, { data: params });
-  }
-
-  async apiRequest(method, path, params) {
-    const res = await axios[method](`/_api${path}`, params);
-    if (res.data.ok) {
-      return res.data;
-    }
-
-    // Return error code if code is exist
-    if (res.data.code != null) {
-      const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
-      throw error;
-    }
-
-    throw new Error(res.data.error);
-  }
-
-  async apiv3Request(method, path, params) {
-    try {
-      const res = await axios[method](urljoin(this.apiv3Root, path), params);
-      return res.data;
-    }
-    catch (err) {
-      const errors = apiv3ErrorHandler(err);
-      throw errors;
-    }
-  }
-
-  async apiv3Get(path, params) {
-    return this.apiv3Request('get', path, { params });
-  }
-
-  async apiv3Post(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('post', path, params);
-  }
-
-  async apiv3Put(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('put', path, params);
-  }
-
-  async apiv3Delete(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('delete', path, { params });
-  }
-
 }

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

@@ -132,11 +132,9 @@ export default class CommentContainer extends Container {
     return this.appContainer.apiPost('/comments.update', {
       commentForm: {
         comment,
-        page_id: pageId,
-        revision_id: revisionId,
         is_markdown: isMarkdown,
+        revision_id: revisionId,
         comment_id: commentId,
-        author,
       },
     })
       .then((res) => {

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

@@ -36,7 +36,9 @@ export default class EditorContainer extends Container {
 
       editorOptions: {},
       previewOptions: {},
-      isTextlintEnabled: false,
+
+      // Defaults to null to show modal when not in DB
+      isTextlintEnabled: null,
       textlintRules: [],
 
       indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
@@ -204,13 +206,18 @@ export default class EditorContainer extends Container {
    * 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;
     }
 
-    const { isTextlintEnabled = false, textlintRules = [] } = data.textlintSettings;
+    // Defaults to null to show modal when not in DB
+    const { isTextlintEnabled = null, textlintRules = [] } = data.textlintSettings;
 
     this.setState({
       isTextlintEnabled,

+ 56 - 25
packages/app/src/client/services/PageContainer.js

@@ -51,15 +51,19 @@ export default class PageContainer extends Container {
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path,
       tocHtml: '',
-      isLiked: false,
+
       isBookmarked: false,
+      sumOfBookmarks: 0,
+
       seenUsers: [],
-      seenUserIds: mainContent.getAttribute('data-page-ids-of-seen-users'),
-      countOfSeenUsers: mainContent.getAttribute('data-page-count-of-seen-users'),
+      seenUserIds: [],
+      sumOfSeenUsers: [],
 
-      likerUsers: [],
+      isLiked: false,
+      likers: [],
+      likerIds: [],
       sumOfLikers: 0,
-      sumOfBookmarks: 0,
+
       createdAt: mainContent.getAttribute('data-page-created-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       deletedAt: mainContent.getAttribute('data-page-deleted-at') || null,
@@ -109,7 +113,7 @@ export default class PageContainer extends Container {
     interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(appContainer), 900); // process as late as possible
 
     this.initStateMarkdown();
-    this.checkAndUpdateImageUrlCached(this.state.likerUsers);
+    this.checkAndUpdateImageUrlCached(this.state.likers);
 
     const { isSharedUser } = this.appContainer;
 
@@ -117,8 +121,10 @@ export default class PageContainer extends Container {
     const isAbleToGetAttachedInformationAboutPages = this.state.isPageExist && !isSharedUser;
 
     if (isAbleToGetAttachedInformationAboutPages) {
-      this.retrieveSeenUsers();
-      this.retrieveLikeInfo();
+      // We don't retrieve bookmarks in the initial page load
+      // as it is stored in a separate collection to like and seen user
+      // data so it has a separate api endpoint.
+      this.initialPageLoad();
       this.retrieveBookmarkInfo();
     }
 
@@ -219,7 +225,7 @@ export default class PageContainer extends Container {
    * whether to like button
    * not displayed on user page
    */
-  get isAbleToShowLikeButton() {
+  get isAbleToShowLikeButtons() {
     const { isUserPage } = this.state;
     const { isSharedUser } = this.appContainer;
 
@@ -264,29 +270,54 @@ export default class PageContainer extends Container {
     this.state.markdown = markdown;
   }
 
-  async retrieveSeenUsers() {
-    const { users } = await this.appContainer.apiGet('/users.list', { user_ids: this.state.seenUserIds });
 
-    this.setState({ seenUsers: users });
-    this.checkAndUpdateImageUrlCached(users);
-  }
+  async initialPageLoad() {
+    {
+      const {
+        data: {
+          likerIds, sumOfLikers, isLiked, seenUserIds, sumOfSeenUsers, isSeen,
+        },
+      } = await this.appContainer.apiv3Get('/page/info', { pageId: this.state.pageId });
 
-  async retrieveLikeInfo() {
-    const res = await this.appContainer.apiv3Get('/page/like-info', { _id: this.state.pageId });
-    const { sumOfLikers, isLiked } = res.data;
+      await this.setState({
+        sumOfLikers,
+        isLiked,
+        likerIds,
+        seenUserIds,
+        sumOfSeenUsers,
+        isSeen,
+      });
+    }
 
-    this.setState({
-      sumOfLikers,
-      isLiked,
-    });
+    await this.retrieveLikersAndSeenUsers();
   }
 
   async toggleLike() {
-    const bool = !this.state.isLiked;
-    await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool });
-    this.setState({ isLiked: bool });
+    {
+      const toggledIsLiked = !this.state.isLiked;
+      await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool: toggledIsLiked });
+
+      await this.setState(state => ({
+        isLiked: toggledIsLiked,
+        sumOfLikers: toggledIsLiked ? state.sumOfLikers + 1 : state.sumOfLikers - 1,
+        likerIds: toggledIsLiked
+          ? [...this.state.likerIds, this.appContainer.currentUserId]
+          : state.likerIds.filter(id => id !== this.appContainer.currentUserId),
+      }));
+    }
+
+    await this.retrieveLikersAndSeenUsers();
+  }
+
+  async retrieveLikersAndSeenUsers() {
+    const { users } = await this.appContainer.apiGet('/users.list', { user_ids: [...this.state.likerIds, ...this.state.seenUserIds].join(',') });
 
-    return this.retrieveLikeInfo();
+    await this.setState({
+      likers: users.filter(({ id }) => this.state.likerIds.includes(id)).slice(0, 15),
+      seenUsers: users.filter(({ id }) => this.state.seenUserIds.includes(id)).slice(0, 15),
+    });
+
+    this.checkAndUpdateImageUrlCached(users);
   }
 
   async retrieveBookmarkInfo() {

+ 63 - 0
packages/app/src/client/util/apiv1-client.ts

@@ -0,0 +1,63 @@
+import * as urljoin from 'url-join';
+
+import axios from '~/utils/axios';
+
+const apiv1Root = '/_api';
+
+// get csrf token from body element
+const body = document.querySelector('body');
+const csrfToken = body?.dataset.csrftoken;
+
+
+type ParamWithCsrfKey = {
+  _csrf: string,
+}
+
+class Apiv1ErrorHandler extends Error {
+
+  code;
+
+  constructor(message = '', code = '') {
+    super();
+
+    this.message = message;
+    this.code = code;
+  }
+
+}
+
+export async function apiRequest(method: string, path: string, params: unknown): Promise<unknown> {
+  const res = await axios[method](urljoin(apiv1Root, path), params);
+
+  if (res.data.ok) {
+    return res.data;
+  }
+
+  // Return error code if code is exist
+  if (res.data.code != null) {
+    const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
+    throw error;
+  }
+
+  throw new Error(res.data.error);
+}
+
+export async function apiGet(path: string, params: unknown = {}): Promise<unknown> {
+  return apiRequest('get', path, { params });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiPost(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiRequest('post', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiDelete(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiRequest('delete', path, { data: params });
+}

+ 0 - 12
packages/app/src/client/util/apiv1ErrorHandler.js

@@ -1,12 +0,0 @@
-class Apiv1ErrorHandler extends Error {
-
-  constructor(message = '', code = '') {
-    super();
-
-    this.message = message;
-    this.code = code;
-  }
-
-}
-
-module.exports = Apiv1ErrorHandler;

+ 74 - 0
packages/app/src/client/util/apiv3-client.ts

@@ -0,0 +1,74 @@
+import * as urljoin from 'url-join';
+
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
+
+import loggerFactory from '~/utils/logger';
+import axios from '~/utils/axios';
+import { toArrayIfNot } from '~/utils/array-utils';
+
+const apiv3Root = '/_api/v3';
+
+const logger = loggerFactory('growi:apiv3');
+
+// get csrf token from body element
+const body = document.querySelector('body');
+const csrfToken = body?.dataset.csrftoken;
+
+
+type ParamWithCsrfKey = {
+  _csrf: string,
+}
+
+const apiv3ErrorHandler = (_err) => {
+  // extract api errors from general 400 err
+  const err = _err.response ? _err.response.data.errors : _err;
+  const errs = toArrayIfNot(err);
+
+  for (const err of errs) {
+    logger.error(err.message);
+  }
+
+  return errs;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Request<T = any>(method: string, path: string, params: unknown): Promise<AxiosResponse<T>> {
+  try {
+    const res = await axios[method](urljoin(apiv3Root, path), params);
+    return res.data;
+  }
+  catch (err) {
+    const errors = apiv3ErrorHandler(err);
+    throw errors;
+  }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Get<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+  return apiv3Request('get', path, { params });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Post<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiv3Request('post', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Put<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiv3Request('put', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Delete<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiv3Request('delete', path, { params });
+}

+ 0 - 21
packages/app/src/client/util/apiv3ErrorHandler.js

@@ -1,21 +0,0 @@
-// API v3 sends an array of errors in res.data.errors.
-// API v3 errors need to extracted from an error object in order to properly handle them.
-
-import loggerFactory from '~/utils/logger';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-const logger = loggerFactory('growi:apiv3');
-
-const apiv3ErrorHandler = (_err, header = 'Error') => {
-  // extract api errors from general 400 err
-  const err = _err.response ? _err.response.data.errors : _err;
-  const errs = toArrayIfNot(err);
-
-  for (const err of errs) {
-    logger.error(err.message);
-  }
-
-  return errs;
-};
-
-export default apiv3ErrorHandler;

+ 15 - 0
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -139,6 +139,21 @@ class CustomizeFunctionSetting extends React.Component {
               </div>
             </div>
 
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isSearchScopeChildrenAsDefault"
+                  label={t('admin:customize_setting.function_options.select_search_scope_children_as_default')}
+                  isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
+                  onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.select_search_scope_children_as_default_desc')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
+
             <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
         </div>

+ 6 - 0
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -22,6 +22,10 @@ class CustomizeThemeOptions extends React.Component {
       name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
     }, {
       name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
+    }, {
+      name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
+    }, {
+      name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
     }];
 
     const uniqueTheme = [{
@@ -42,6 +46,8 @@ class CustomizeThemeOptions extends React.Component {
       name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
     }, {
       name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
+    }, {
+      name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
     }];
     /* eslint-enable no-multi-spaces */
 

+ 1 - 1
packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -65,7 +65,7 @@ class SamlSecurityManagementContents extends React.Component {
                 {t('security_setting.SAML.enable_saml')}
               </label>
             </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isSamlEnabled)
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
               && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>

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

@@ -138,6 +138,9 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         title={<><span className="mr-2">⑤</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
+        <p className="text-center text-warning">
+          <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+        </p>
         <div className="d-flex justify-content-center">
           <form className="form-row align-items-center" onSubmit={e => submitForm(e)}>
             <div className="input-group col-8">

+ 2 - 2
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -72,8 +72,8 @@ const ManageCommandsProcess = ({
     search: permissionsForBroadcastUseCommands.search,
   });
   const [permissionsForSingleUseCommandsState, setPermissionsForSingleUseCommandsState] = useState({
-    create: permissionsForSingleUseCommands.create,
-    togetter: permissionsForSingleUseCommands.togetter,
+    note: permissionsForSingleUseCommands.note,
+    keep: permissionsForSingleUseCommands.keep,
   });
   const [currentPermissionTypes, setCurrentPermissionTypes] = useState(() => {
     const initialState = {};

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

@@ -190,7 +190,7 @@ const ManageCommandsProcessWithoutProxy = ({ apiv3Put, commandPermission }) => {
       await apiv3Put('/slack-integration-settings/without-proxy/update-permissions', {
         commandPermission: editingCommandPermission,
       });
-      toastSuccess(t('toaster.update_successed', { target: 'Token' }));
+      toastSuccess(t('toaster.update_successed', { target: 'the permission for commands' }));
     }
     catch (err) {
       toastError(err);

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

@@ -264,6 +264,9 @@ const TestProcess = ({
   return (
     <>
       <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
+      <p className="text-center text-warning">
+        <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+      </p>
       <div className="d-flex justify-content-center">
         <form className="form-row justify-content-center" onSubmit={e => submitForm(e)}>
           <div className="input-group col-8">

+ 35 - 13
packages/app/src/components/LikeButton.jsx → packages/app/src/components/LikeButtons.jsx

@@ -1,22 +1,35 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { UncontrolledTooltip } from 'reactstrap';
+import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
+import UserPictureList from './User/UserPictureList';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 import { toastError } from '~/client/util/apiNotification';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
-class LikeButton extends React.Component {
+class LikeButtons extends React.Component {
 
   constructor(props) {
     super(props);
 
+    this.state = {
+      isPopoverOpen: false,
+    };
+
+    this.togglePopover = this.togglePopover.bind(this);
     this.handleClick = this.handleClick.bind(this);
   }
 
+  togglePopover() {
+    this.setState(prevState => ({
+      ...prevState,
+      isPopoverOpen: !prevState.isPopoverOpen,
+    }));
+  }
+
   async handleClick() {
     const { appContainer, pageContainer } = this.props;
     const { isGuestUser } = appContainer;
@@ -33,31 +46,40 @@ class LikeButton extends React.Component {
     }
   }
 
-
   render() {
     const { appContainer, pageContainer, t } = this.props;
     const { isGuestUser } = appContainer;
+    const {
+      state: { likers, sumOfLikers, isLiked },
+    } = pageContainer;
 
     return (
-      <div>
+      <div className="btn-group" role="group" aria-label="Like buttons">
         <button
           type="button"
           id="like-button"
           onClick={this.handleClick}
           className={`btn btn-like border-0
-          ${pageContainer.state.isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+            ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
-          <i className="icon-like mr-3"></i>
-          <span className="total-likes">
-            {pageContainer.state.sumOfLikers}
-          </span>
+          <i className="icon-like"></i>
         </button>
-
         {isGuestUser && (
           <UncontrolledTooltip placement="top" target="like-button" fade={false}>
             {t('Not available for guest')}
           </UncontrolledTooltip>
         )}
+
+        <button type="button" id="po-total-likes" className={`btn btn-like border-0 total-likes ${isLiked ? 'active' : ''}`}>
+          {sumOfLikers}
+        </button>
+        <Popover placement="bottom" isOpen={this.state.isPopoverOpen} target="po-total-likes" toggle={this.togglePopover} trigger="legacy">
+          <PopoverBody className="seen-user-popover">
+            <div className="px-2 text-right user-list-content text-truncate text-muted">
+              {likers.length ? <UserPictureList users={likers} /> : t('No users have liked this yet.')}
+            </div>
+          </PopoverBody>
+        </Popover>
       </div>
     );
   }
@@ -67,9 +89,9 @@ class LikeButton extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const LikeButtonWrapper = withUnstatedContainers(LikeButton, [AppContainer, PageContainer]);
+const LikeButtonsWrapper = withUnstatedContainers(LikeButtons, [AppContainer, PageContainer]);
 
-LikeButton.propTypes = {
+LikeButtons.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
@@ -77,4 +99,4 @@ LikeButton.propTypes = {
   size: PropTypes.string,
 };
 
-export default withTranslation()(LikeButtonWrapper);
+export default withTranslation()(LikeButtonsWrapper);

+ 4 - 4
packages/app/src/components/Me/EditorSettings.tsx

@@ -46,10 +46,10 @@ const commonRulesMenuItems = [
     name: 'sentence-length',
     description: 'editor_settings.common_settings.sentence_length',
   },
-  {
-    name: 'en-capitalization',
-    description: 'editor_settings.common_settings.en_capitalization',
-  },
+  // {  // omit because en-pos package is too big
+  //   name: 'en-capitalization',
+  //   description: 'editor_settings.common_settings.en_capitalization',
+  // },
   {
     name: 'no-unmatched-pair',
     description: 'editor_settings.common_settings.no_unmatched_pair',

+ 3 - 1
packages/app/src/components/Navbar/GlobalSearch.jsx

@@ -14,9 +14,11 @@ class GlobalSearch extends React.Component {
   constructor(props) {
     super(props);
 
+    const isSearchScopeChildrenAsDefault = this.props.appContainer.getConfig().isSearchScopeChildrenAsDefault;
+
     this.state = {
       text: '',
-      isScopeChildren: false,
+      isScopeChildren: isSearchScopeChildrenAsDefault,
     };
 
     this.onInputChange = this.onInputChange.bind(this);

+ 5 - 6
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -6,7 +6,7 @@ import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 import BookmarkButton from '../BookmarkButton';
-import LikeButton from '../LikeButton';
+import LikeButtons from '../LikeButtons';
 import PageManagement from '../Page/PageManagement';
 
 const SubnavButtons = (props) => {
@@ -21,15 +21,14 @@ const SubnavButtons = (props) => {
 
     return (
       <>
-        {pageContainer.isAbleToShowLikeButton && (
+        {pageContainer.isAbleToShowLikeButtons && (
           <span>
-            <LikeButton />
+            <LikeButtons />
           </span>
         )}
         <span>
           <BookmarkButton />
         </span>
-
       </>
     );
   };
@@ -42,8 +41,8 @@ const SubnavButtons = (props) => {
     <>
       {isViewMode && (
         <>
-          { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
-          { pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} /> }
+          {pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} />}
+          {pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} />}
         </>
       )}
     </>

+ 1 - 5
packages/app/src/components/PageComment/Comment.jsx

@@ -73,10 +73,6 @@ class Comment extends React.PureComponent {
     interceptorManager.process('postRenderCommentHtml', this.currentRenderingContext);
   }
 
-  checkPermissionToControlComment() {
-    return this.props.appContainer.isAdmin || this.isCurrentUserEqualsToAuthor();
-  }
-
   isCurrentUserEqualsToAuthor() {
     const { creator } = this.props.comment;
     if (creator == null) {
@@ -210,7 +206,7 @@ class Comment extends React.PureComponent {
                   </UncontrolledTooltip>
                 </span>
               </div>
-              {this.checkPermissionToControlComment() && (
+              {this.isCurrentUserEqualsToAuthor() && (
                 <CommentControl
                   onClickDeleteBtn={this.deleteBtnClickedHandler}
                   onClickEditBtn={() => this.setState({ isReEdit: true })}

+ 2 - 5
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -33,8 +33,9 @@ import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
 import DrawioModal from './DrawioModal';
 
-
+// Textlint
 window.JSHINT = JSHINT;
+window.kuromojin = { dicPath: '/static/dict' };
 
 // set save handler
 codemirror.commands.save = (instance) => {
@@ -154,10 +155,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
     this.cmNoCdnScriptRoot = '/static/js/cdn';
     this.cmNoCdnStyleRoot = '/static/styles/cdn';
-    window.kuromojin = this.props.noCdn
-      ? { dicPath: '/static/dict/cdn' }
-      : { dicPath: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict' };
-
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptors([
       new PreventMarkdownListInterceptor(),

+ 75 - 0
packages/app/src/components/PageEditor/DownloadDictModal.tsx

@@ -0,0 +1,75 @@
+import React, { useState, FC } from 'react';
+import PropTypes from 'prop-types';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+
+type DownloadDictModalProps = {
+  isModalOpen: boolean
+  onConfirmEnableTextlint?: (isSkipAskingAgainChecked: boolean) => void;
+  onCancel?: () => void;
+};
+
+export const DownloadDictModal: FC<DownloadDictModalProps> = (props) => {
+  const { t } = useTranslation('');
+  const [isSkipAskingAgainChecked, setIsSkipAskingAgainChecked] = useState(true);
+
+  const onCancel = () => {
+    if (props.onCancel != null) {
+      props.onCancel();
+    }
+  };
+
+  const onConfirmEnableTextlint = () => {
+    if (props.onConfirmEnableTextlint != null) {
+      props.onConfirmEnableTextlint(isSkipAskingAgainChecked);
+    }
+  };
+
+  return (
+    <Modal isOpen={props.isModalOpen} toggle={onCancel} className="">
+      <ModalHeader tag="h4" toggle={onCancel} className="bg-warning">
+        <i className="icon-fw icon-question" />
+        Warning
+      </ModalHeader>
+      <ModalBody>
+        {t('modal_enable_textlint.confirm_download_dict_and_enable_textlint')}
+      </ModalBody>
+      <ModalFooter>
+        <div className="mr-3 custom-control custom-checkbox custom-checkbox-info">
+          <input
+            type="checkbox"
+            className="custom-control-input"
+            id="dont-ask-again"
+            checked={isSkipAskingAgainChecked}
+            onChange={e => setIsSkipAskingAgainChecked(e.target.checked)}
+          />
+          <label className="custom-control-label align-center" htmlFor="dont-ask-again">
+            {t('modal_enable_textlint.dont_ask_again')}
+          </label>
+        </div>
+        <button
+          type="button"
+          className="btn btn-outline-secondary"
+          onClick={onCancel}
+        >
+          {t('Cancel')}
+        </button>
+        <button
+          type="button"
+          className="btn btn-outline-primary ml-3"
+          onClick={onConfirmEnableTextlint}
+        >
+          {t('modal_enable_textlint.enable_textlint')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+DownloadDictModal.propTypes = {
+  isModalOpen: PropTypes.bool.isRequired,
+  onConfirmEnableTextlint: PropTypes.func,
+  onCancel: PropTypes.func,
+};

+ 8 - 1
packages/app/src/components/PageEditor/DrawioModal.jsx

@@ -135,7 +135,14 @@ class DrawioModal extends React.PureComponent {
 
   render() {
     return (
-      <Modal isOpen={this.state.show} toggle={this.cancel} className="drawio-modal" size="xl" keyboard={false}>
+      <Modal
+        isOpen={this.state.show}
+        toggle={this.cancel}
+        backdrop="static"
+        className="drawio-modal"
+        size="xl"
+        keyboard={false}
+      >
         <ModalBody className="p-0">
           {/* Loading spinner */}
           <div className="w-100 h-100 position-absolute d-flex">

+ 41 - 8
packages/app/src/components/PageEditor/OptionsSelector.jsx

@@ -11,6 +11,7 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import { toastError } from '~/client/util/apiNotification';
+import { DownloadDictModal } from './DownloadDictModal';
 
 
 export const defaultEditorOptions = {
@@ -34,6 +35,8 @@ class OptionsSelector extends React.Component {
     this.state = {
       isCddMenuOpened: false,
       isMathJaxEnabled,
+      isDownloadDictModalShown: false,
+      isSkipAskingAgainChecked: false,
     };
 
     this.availableThemes = [
@@ -53,6 +56,8 @@ class OptionsSelector extends React.Component {
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.switchTextlintEnabledHandler = this.switchTextlintEnabledHandler.bind(this);
+    this.confirmEnableTextlintHandler = this.confirmEnableTextlintHandler.bind(this);
+    this.toggleTextlint = this.toggleTextlint.bind(this);
     this.updateIsTextlintEnabledToDB = this.updateIsTextlintEnabledToDB.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
     this.onChangeIndentSize = this.onChangeIndentSize.bind(this);
@@ -124,11 +129,29 @@ class OptionsSelector extends React.Component {
     }
   }
 
-  async switchTextlintEnabledHandler() {
+  toggleTextlint() {
     const { editorContainer } = this.props;
     const newVal = !editorContainer.state.isTextlintEnabled;
     editorContainer.setState({ isTextlintEnabled: newVal });
-    this.updateIsTextlintEnabledToDB(newVal);
+    if (this.state.isSkipAskingAgainChecked) {
+      this.updateIsTextlintEnabledToDB(newVal);
+    }
+  }
+
+  switchTextlintEnabledHandler() {
+    const { editorContainer } = this.props;
+    if (editorContainer.state.isTextlintEnabled === null) {
+      this.setState({ isDownloadDictModalShown: true });
+      return;
+    }
+    this.toggleTextlint();
+  }
+
+  confirmEnableTextlintHandler(isSkipAskingAgainChecked) {
+    this.setState(
+      { isSkipAskingAgainChecked, isDownloadDictModalShown: false },
+      () => this.toggleTextlint(),
+    );
   }
 
   onToggleConfigurationDropdown(newValue) {
@@ -359,12 +382,22 @@ class OptionsSelector extends React.Component {
 
   render() {
     return (
-      <div className="d-flex flex-row">
-        <span>{this.renderThemeSelector()}</span>
-        <span className="d-none d-sm-block ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
-        <span className="ml-2 ml-sm-4">{this.renderIndentSizeSelector()}</span>
-        <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
-      </div>
+      <>
+        <div className="d-flex flex-row">
+          <span>{this.renderThemeSelector()}</span>
+          <span className="d-none d-sm-block ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
+          <span className="ml-2 ml-sm-4">{this.renderIndentSizeSelector()}</span>
+          <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
+        </div>
+
+        {!this.state.isSkipAskingAgainChecked && (
+          <DownloadDictModal
+            isModalOpen={this.state.isDownloadDictModalShown}
+            onConfirmEnableTextlint={this.confirmEnableTextlintHandler}
+            onCancel={() => this.setState({ isDownloadDictModalShown: false })}
+          />
+        )}
+      </>
     );
   }
 

+ 17 - 25
packages/app/src/components/PageList.jsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useCallback, useState } from 'react';
+import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
@@ -8,39 +8,33 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
+import { useSWRxPageList } from '~/stores/page';
+
 import PaginationWrapper from './PaginationWrapper';
 
 
 const PageList = (props) => {
   const { appContainer, pageContainer, t } = props;
   const { path } = pageContainer.state;
-  const [pages, setPages] = useState(null);
-  const [isLoading, setIsLoading] = useState(true);
 
   const [activePage, setActivePage] = useState(1);
-  const [totalPages, setTotalPages] = useState(0);
-  const [limit, setLimit] = useState(Infinity);
+
+  const { data: pagesListData, error: errors } = useSWRxPageList(path, activePage);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
   }
 
-  const updatePageList = useCallback(async() => {
-    const page = activePage;
-    const res = await appContainer.apiv3Get('/pages/list', { path, page });
-
-    setPages(res.data.pages);
-    setIsLoading(false);
-    setTotalPages(res.data.totalCount);
-    setLimit(res.data.limit);
-  }, [appContainer, path, activePage]);
-
-  useEffect(() => {
-    updatePageList();
-  }, [updatePageList]);
-
+  if (errors != null) {
+    return (
+      <div className="my-5">
+        {/* eslint-disable-next-line react/no-array-index-key */}
+        {errors.map((error, index) => <div key={index} className="text-danger">{error.message}</div>)}
+      </div>
+    );
+  }
 
-  if (isLoading) {
+  if (pagesListData == null) {
     return (
       <div className="wiki">
         <div className="text-muted text-center">
@@ -51,7 +45,7 @@ const PageList = (props) => {
   }
 
   const liClasses = props.liClasses.join(' ');
-  const pageList = pages.map(page => (
+  const pageList = pagesListData.items.map(page => (
     <li key={page._id} className={liClasses}>
       <Page page={page} />
     </li>
@@ -81,14 +75,12 @@ const PageList = (props) => {
       <PaginationWrapper
         activePage={activePage}
         changePage={setPageNumber}
-        totalItemsCount={totalPages}
-        pagingLimit={limit}
+        totalItemsCount={pagesListData.totalCount}
+        pagingLimit={pagesListData.limit}
         align="center"
       />
     </div>
   );
-
-
 };
 
 const PageListWrapper = withUnstatedContainers(PageList, [AppContainer, PageContainer]);

+ 2 - 2
packages/app/src/components/Sidebar/CustomSidebar.jsx

@@ -57,11 +57,11 @@ const CustomSidebar = (props) => {
   return (
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
-        <h3 className="mb-0">
+        <h3 className="mb-0 text-nowrap">
           Custom Sidebar
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
         </h3>
-        <button type="button" className="btn btn-sm ml-auto grw-btn-reload-cs" onClick={fetchDataAndRenderHtml}>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={fetchDataAndRenderHtml}>
           <i className="icon icon-reload"></i>
         </button>
       </div>

+ 80 - 19
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -1,20 +1,23 @@
-import React from 'react';
+import React, {
+  useCallback, useEffect, useState,
+} from 'react';
 import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
+import { useTranslation, withTranslation } from 'react-i18next';
 
 import { UserPicture } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
+
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { toastError } from '~/client/util/apiNotification';
+import { useSWRxRecentlyUpdated } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import LinkedPagePath from '~/models/linked-page-path';
 
 import FootstampIcon from '../FootstampIcon';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import { toastError } from '~/client/util/apiNotification';
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
@@ -119,17 +122,82 @@ function SmallPageItem({ page }) {
 SmallPageItem.propTypes = {
   page: PropTypes.any,
 };
-class RecentChanges extends React.Component {
+
+
+const RecentChanges = () => {
+
+  const { t } = useTranslation();
+  const { data: pages, error, mutate } = useSWRxRecentlyUpdated();
+
+  if (error != null) {
+    toastError(error, 'Error occurred in updating History');
+  }
+
+  const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
+
+  const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
+    if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
+      setIsRecentChangesSidebarSmall(true);
+    }
+  });
+
+  const changeSizeHandler = useCallback((e) => {
+    setIsRecentChangesSidebarSmall(e.target.checked);
+    window.localStorage.setItem('isRecentChangesSidebarSmall', e.target.checked);
+  }, []);
+
+  // componentDidMount
+  useEffect(() => {
+    retrieveSizePreferenceFromLocalStorage();
+  }, [retrieveSizePreferenceFromLocalStorage]);
+
+  return (
+    <>
+      <div className="grw-sidebar-content-header p-3 d-flex">
+        <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+          <i className="icon icon-reload"></i>
+        </button>
+        <div className="d-flex align-items-center">
+          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-1">
+            <input
+              id="recentChangesResize"
+              className="custom-control-input"
+              type="checkbox"
+              checked={isRecentChangesSidebarSmall}
+              onChange={changeSizeHandler}
+            />
+            <label className="custom-control-label" htmlFor="recentChangesResize">
+            </label>
+          </div>
+        </div>
+      </div>
+      <div className="grw-sidebar-content-body grw-recent-changes p-3">
+        <ul className="list-group list-group-flush">
+          {(pages || []).map(page => (isRecentChangesSidebarSmall
+            ? <SmallPageItem key={page._id} page={page} />
+            : <LargePageItem key={page._id} page={page} />))}
+        </ul>
+      </div>
+    </>
+  );
+
+};
+
+// export default RecentChanges;
+
+
+class DeprecatedRecentChanges extends React.Component {
 
   static propTypes = {
     t: PropTypes.func.isRequired, // i18next
-    appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   };
 
   constructor(props) {
     super(props);
     this.state = {
       isRecentChangesSidebarSmall: false,
+      recentlyUpdatedPages: [],
     };
     this.reloadData = this.reloadData.bind(this);
   }
@@ -143,10 +211,9 @@ class RecentChanges extends React.Component {
   }
 
   async reloadData() {
-    const { appContainer } = this.props;
-
     try {
-      await appContainer.retrieveRecentlyUpdated();
+      const { data } = await apiv3Get('/pages/recent');
+      this.setState({ recentlyUpdatedPages: data.pages });
     }
     catch (error) {
       logger.error('failed to save', error);
@@ -171,7 +238,6 @@ class RecentChanges extends React.Component {
 
   render() {
     const { t } = this.props;
-    const { recentlyUpdatedPages } = this.props.appContainer.state;
 
     return (
       <>
@@ -187,7 +253,7 @@ class RecentChanges extends React.Component {
               className="custom-control-input"
               type="checkbox"
               checked={this.state.isRecentChangesSidebarSmall}
-              onChange={e => this.setState({ isRecentChangesSidebarSmall: e.target.checked })}
+              onChange={this.changeSizeHandler}
             />
             <label className="custom-control-label" htmlFor="recentChangesResize">
             </label>
@@ -195,7 +261,7 @@ class RecentChanges extends React.Component {
         </div>
         <div className="grw-sidebar-content-body grw-recent-changes p-3">
           <ul className="list-group list-group-flush">
-            {recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
+            {this.state.recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
               ? <SmallPageItem key={page._id} page={page} />
               : <LargePageItem key={page._id} page={page} />))}
           </ul>
@@ -206,10 +272,5 @@ class RecentChanges extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const RecentChangesWrapper = withUnstatedContainers(RecentChanges, [AppContainer]);
-
 
-export default withTranslation()(RecentChangesWrapper);
+export default withTranslation()(DeprecatedRecentChanges);

+ 4 - 2
packages/app/src/components/User/SeenUserInfo.jsx

@@ -22,8 +22,10 @@ const SeenUserInfo = (props) => {
   return (
     <div className="grw-seen-user-info">
       <Button id="po-seen-user" color="link" className="px-2">
-        <span className="mr-1 footstamp-icon"><FootstampIcon /></span>
-        <span className="seen-user-count">{pageContainer.state.countOfSeenUsers}</span>
+        <span className="mr-1 footstamp-icon">
+          <FootstampIcon />
+        </span>
+        <span className="seen-user-count">{pageContainer.state.sumOfSeenUsers}</span>
       </Button>
       <Popover placement="bottom" isOpen={popoverOpen} target="po-seen-user" toggle={toggle} trigger="legacy" disabled={disabled}>
         <PopoverBody className="seen-user-popover">

+ 7 - 0
packages/app/src/interfaces/page-tag-relation.ts

@@ -0,0 +1,7 @@
+import { IPage } from './page';
+import { ITag } from './tag';
+
+export type IPageTagRelation = {
+  relatedPage: IPage,
+  relatedTag: ITag,
+}

+ 14 - 0
packages/app/src/interfaces/page.ts

@@ -0,0 +1,14 @@
+import { IUser } from './user';
+import { IRevision } from './revision';
+import { ITag } from './tag';
+
+export type IPage = {
+  path: string,
+  status: string,
+  revision: IRevision,
+  tags: ITag[],
+  creator: IUser,
+  createdAt: Date,
+  updatedAt: Date,
+  seenUsers: string[]
+}

+ 5 - 0
packages/app/src/interfaces/paging-result.ts

@@ -0,0 +1,5 @@
+export type IPagingResult<T> = {
+  items: T[],
+  totalCount: number,
+  limit: number,
+}

+ 9 - 0
packages/app/src/interfaces/revision.ts

@@ -0,0 +1,9 @@
+import { IUser } from './user';
+
+export type IRevision = {
+  body: string,
+  author: IUser,
+  hasDiffToPrev: boolean;
+  createdAt: Date,
+  updatedAt: Date,
+}

+ 4 - 0
packages/app/src/interfaces/tag.ts

@@ -0,0 +1,4 @@
+export type ITag = {
+  name: string,
+  createdAt: Date;
+}

+ 18 - 0
packages/app/src/interfaces/user.ts

@@ -0,0 +1,18 @@
+export type IUser = {
+  name: string;
+  username: string;
+  imageUrlCached: string;
+  admin: boolean;
+}
+
+export type IUserGroupRelation = {
+  relatedGroup: IUserGroup,
+  relatedUser: IUser,
+  createdAt: Date,
+}
+
+export type IUserGroup = {
+  userGroupId:string;
+  name: string;
+  createdAt: Date;
+}

+ 2 - 3
packages/app/src/migrations/20180926134048-make-email-unique.js

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 const logger = loggerFactory('growi:migrate:make-email-unique');
 
@@ -10,7 +9,7 @@ module.exports = {
 
   async up(db, next) {
     logger.info('Start migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = getModelSafely('User') || require('~/server/models/user')();
 

+ 3 - 3
packages/app/src/migrations/20180927102719-init-serverurl.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:init-serverurl');
@@ -20,7 +20,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
@@ -77,7 +77,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({

+ 3 - 4
packages/app/src/migrations/20181019114028-abolish-page-group-relation.js

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 
@@ -29,7 +28,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     if (!isPagegrouprelationsExists) {
@@ -73,7 +72,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = getModelSafely('Page') || require('~/server/models/page')();
     const UserGroup = getModelSafely('UserGroup') || require('~/server/models/user-group')();

+ 2 - 2
packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 module.exports = {
   async up(db, next) {
     logger.info('Start migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     // enable passport and delete configs for crowi classic auth
     await Promise.all([

+ 3 - 4
packages/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -1,9 +1,8 @@
 import mongoose from 'mongoose';
 
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 const logger = loggerFactory('growi:migrate:add-config-app-installed');
 
@@ -19,7 +18,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const User = getModelSafely('User') || require('~/server/models/user')();
 
@@ -49,7 +48,7 @@ module.exports = {
 
   async down(db) {
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({

+ 2 - 2
packages/app/src/migrations/20190619055421-adjust-page-grant.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:adjust-page-grant');
@@ -9,7 +9,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = require('~/server/models/page')();
 

+ 2 - 2
packages/app/src/migrations/20190624110950-fill-last-update-user.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
@@ -12,7 +12,7 @@ module.exports = {
 
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = require('~/server/models/page')();
 

+ 2 - 2
packages/app/src/migrations/20190629193445-make-root-page-public.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:make-root-page-public');
@@ -8,7 +8,7 @@ const logger = loggerFactory('growi:migrate:make-root-page-public');
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
     const Page = require('~/server/models/page')();
 

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