yuto-o 4 лет назад
Родитель
Сommit
3b85c5167a
100 измененных файлов с 1072 добавлено и 427 удалено
  1. 9 9
      .devcontainer/Dockerfile
  2. 1 0
      .gitattributes
  3. 23 0
      .github/dependabot.yml
  4. 2 5
      .github/release-drafter.yml
  5. 7 6
      .github/workflows/ci-slackbot-proxy.yml
  6. 12 1
      .github/workflows/ci.yml
  7. 1 1
      .github/workflows/draft-release.yml
  8. 1 1
      .github/workflows/pr-to-master.yml
  9. 3 1
      .github/workflows/release-rc.yml
  10. 59 1
      .github/workflows/release-slackbot-proxy.yml
  11. 8 4
      .github/workflows/release.yml
  12. 81 4
      CHANGELOG.md
  13. 11 0
      THIRD-PARTY-NOTICES.md
  14. 7 1
      bin/github-actions/bump-versions/flow/bump-versions.js
  15. 5 1
      bin/github-actions/bump-versions/step/printHelp.js
  16. 1 1
      lerna.json
  17. 9 5
      package.json
  18. 2 0
      packages/app/.env.development
  19. 1 0
      packages/app/.env.production
  20. 2 1
      packages/app/.eslintrc.js
  21. 1 1
      packages/app/.gitignore
  22. 1 12
      packages/app/bin/cdn/cdn-resources-downloader.ts
  23. 0 1
      packages/app/config/cdn.js
  24. 2 2
      packages/app/config/webpack.prod.js
  25. 7 15
      packages/app/docker/Dockerfile
  26. 6 0
      packages/app/docker/Dockerfile.dockerignore
  27. 2 2
      packages/app/docker/README.md
  28. 1 1
      packages/app/docker/docker-entrypoint.sh
  29. 6 0
      packages/app/docker/nocdn/.env.production.local
  30. 0 5
      packages/app/docker/nocdn/env.prod.js
  31. 21 5
      packages/app/jest.config.js
  32. 8 5
      packages/app/migrate-mongo-config.js
  33. 21 18
      packages/app/package.json
  34. 3 0
      packages/app/public/static/dict/base.dat.gz
  35. 3 0
      packages/app/public/static/dict/cc.dat.gz
  36. 3 0
      packages/app/public/static/dict/check.dat.gz
  37. 3 0
      packages/app/public/static/dict/tid.dat.gz
  38. 3 0
      packages/app/public/static/dict/tid_map.dat.gz
  39. 3 0
      packages/app/public/static/dict/tid_pos.dat.gz
  40. 3 0
      packages/app/public/static/dict/unk.dat.gz
  41. 3 0
      packages/app/public/static/dict/unk_char.dat.gz
  42. 3 0
      packages/app/public/static/dict/unk_compat.dat.gz
  43. 3 0
      packages/app/public/static/dict/unk_invoke.dat.gz
  44. 3 0
      packages/app/public/static/dict/unk_map.dat.gz
  45. 3 0
      packages/app/public/static/dict/unk_pos.dat.gz
  46. 13 99
      packages/app/resource/cdn-manifests.js
  47. 4 1
      packages/app/resource/locales/en_US/admin/admin.json
  48. 8 1
      packages/app/resource/locales/en_US/translation.json
  49. 4 1
      packages/app/resource/locales/ja_JP/admin/admin.json
  50. 8 1
      packages/app/resource/locales/ja_JP/translation.json
  51. 4 1
      packages/app/resource/locales/zh_CN/admin/admin.json
  52. 8 1
      packages/app/resource/locales/zh_CN/translation.json
  53. 3 0
      packages/app/resource/search/mappings.json
  54. 11 0
      packages/app/src/client/services/AdminCustomizeContainer.js
  55. 9 2
      packages/app/src/client/services/EditorContainer.js
  56. 15 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  57. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  58. 3 0
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  59. 3 0
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  60. 4 4
      packages/app/src/components/Me/EditorSettings.tsx
  61. 3 1
      packages/app/src/components/Navbar/GlobalSearch.jsx
  62. 2 5
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  63. 75 0
      packages/app/src/components/PageEditor/DownloadDictModal.tsx
  64. 41 8
      packages/app/src/components/PageEditor/OptionsSelector.jsx
  65. 2 1
      packages/app/src/components/SearchForm.jsx
  66. 87 23
      packages/app/src/components/SearchPage.jsx
  67. 39 0
      packages/app/src/components/SearchPage/DeleteAllButton.jsx
  68. 42 0
      packages/app/src/components/SearchPage/IncludeSpecificPathButton.jsx
  69. 31 0
      packages/app/src/components/SearchPage/SearchControl.tsx
  70. 29 11
      packages/app/src/components/SearchPage/SearchPageForm.jsx
  71. 43 0
      packages/app/src/components/SearchPage/SearchPageLayout.tsx
  72. 9 3
      packages/app/src/components/SearchPage/SearchResult.jsx
  73. 50 0
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  74. 70 45
      packages/app/src/components/SearchPage/SearchResultList.jsx
  75. 1 1
      packages/app/src/components/Sidebar/RecentChanges.jsx
  76. 2 3
      packages/app/src/migrations/20180926134048-make-email-unique.js
  77. 3 3
      packages/app/src/migrations/20180927102719-init-serverurl.js
  78. 3 4
      packages/app/src/migrations/20181019114028-abolish-page-group-relation.js
  79. 2 2
      packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  80. 3 4
      packages/app/src/migrations/20190618104011-add-config-app-installed.js
  81. 2 2
      packages/app/src/migrations/20190619055421-adjust-page-grant.js
  82. 2 2
      packages/app/src/migrations/20190624110950-fill-last-update-user.js
  83. 2 2
      packages/app/src/migrations/20190629193445-make-root-page-public.js
  84. 2 2
      packages/app/src/migrations/20191102223900-drop-configs-indices.js
  85. 2 2
      packages/app/src/migrations/20191102223901-drop-pages-indices.js
  86. 3 3
      packages/app/src/migrations/20191126173016-adjust-pages-path.js
  87. 2 2
      packages/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js
  88. 2 3
      packages/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js
  89. 2 2
      packages/app/src/migrations/20200420160390-remove-crowi-layout.js
  90. 3 3
      packages/app/src/migrations/20200512005851-remove-behavior-type.js
  91. 2 2
      packages/app/src/migrations/20200514001356-update-theme-color-for-dark.js
  92. 2 3
      packages/app/src/migrations/20200620203632-normalize-locale-id.js
  93. 3 3
      packages/app/src/migrations/20200827045151-remove-layout-setting.js
  94. 3 3
      packages/app/src/migrations/20200828024025-copy-aws-setting.js
  95. 3 3
      packages/app/src/migrations/20200901034313-update-mail-transmission.js
  96. 2 2
      packages/app/src/migrations/20200901034314-update-mail-transmission-fix.js
  97. 3 3
      packages/app/src/migrations/20200903080025-remove-timeline-type.js.js
  98. 3 3
      packages/app/src/migrations/20200915035234-rename-s3-config.js
  99. 2 3
      packages/app/src/migrations/20210420160380-convert-double-to-date.js
  100. 35 44
      packages/app/src/migrations/20210830074539-update-configs-for-slackbot.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". *
 # * any needed dependencies after executing "apt-get update". *
 # * See https://docs.docker.com/engine/reference/builder/#run *
 # * 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
 # Uncomment to default to non-root user
 # USER $USER_UID
 # USER $USER_UID

+ 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:
     branch:
       - '/^support\/.+/'
       - '/^support\/.+/'
     title:
     title:
+      - '/^support/i'
+      - '/^chore/i'
       - '/^ci/i'
       - '/^ci/i'
       - '/^docs/i'
       - '/^docs/i'
       - '/^test/i'
       - '/^test/i'
-  - label: 'exclude from changelog'
-    branch:
-      - '/^chore\/.+/'
-    title:
-      - '/^chore/i'
 include-labels:
 include-labels:
   - breaking
   - breaking
   - feature
   - feature

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

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

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

@@ -194,6 +194,9 @@ jobs:
         cache: 'yarn'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
         cache-dependency-path: '**/yarn.lock'
 
 
+    - name: Remove unnecessary packages
+      run: |
+        rm -rf packages/slackbot-proxy
     - name: lerna bootstrap
     - name: lerna bootstrap
       run: |
       run: |
         npx lerna bootstrap
         npx lerna bootstrap
@@ -204,7 +207,9 @@ jobs:
         yarn list --depth=0
         yarn list --depth=0
     - name: Build
     - name: Build
       run: |
       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
     - name: lerna bootstrap --production
       run: |
       run: |
         npx lerna bootstrap -- --production
         npx lerna bootstrap -- --production
@@ -232,6 +237,12 @@ jobs:
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
         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
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
       if: failure()
       if: failure()

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

@@ -19,7 +19,7 @@ jobs:
       - uses: actions/checkout@v2
       - uses: actions/checkout@v2
 
 
       - name: Retrieve information from package.json
       - 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
         id: package-json
 
 
       # Drafts your next Release notes as Pull Requests are merged into "master"
       # 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: |
     if: |
       (!contains( github.event.pull_request.labels.*.name, 'exclude from changelog' ) &&
       (!contains( github.event.pull_request.labels.*.name, 'exclude from changelog' ) &&
-        !startsWith( github.ref, 'refs/heads/chore/' ))
+        !startsWith( github.head_ref, 'dependabot/' ))
 
 
     steps:
     steps:
       - uses: amannn/action-semantic-pull-request@v3.4.2
       - uses: amannn/action-semantic-pull-request@v3.4.2

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

@@ -13,9 +13,11 @@ jobs:
 
 
     steps:
     steps:
     - uses: actions/checkout@v2
     - uses: actions/checkout@v2
+      with:
+        lfs: true
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
 
 
     - name: Docker meta
     - name: Docker meta

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

@@ -17,8 +17,10 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
       id: package-json
+      with:
+        workingDir: packages/slackbot-proxy
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta
@@ -78,6 +80,13 @@ jobs:
         rm -rf /tmp/.buildx-cache
         rm -rf /tmp/.buildx-cache
         mv /tmp/.buildx-cache-new /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
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v2
       uses: peter-evans/dockerhub-description@v2
       with:
       with:
@@ -85,3 +94,52 @@ jobs:
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         repository: weseek/growi-slackbot-proxy
         repository: weseek/growi-slackbot-proxy
         readme-filepath: ./packages/slackbot-proxy/docker/README.md
         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
     - name: Bump versions
       run: |
       run: |
-        node ./bin/github-actions/bump-versions -i patch
+        yarn bump-versions:patch
         sh ./packages/app/bin/github-actions/update-readme.sh
         sh ./packages/app/bin/github-actions/update-readme.sh
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
       id: package-json
 
 
     - name: Update Changelog
     - name: Update Changelog
@@ -95,10 +95,11 @@ jobs:
 
 
     - name: Bump versions for next RC
     - name: Bump versions for next RC
       run: |
       run: |
-        node ./bin/github-actions/bump-versions -i prerelease
+        yarn bump-versions:rc
+        yarn bump-versions:slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - 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
       id: package-json
 
 
     - name: Commit
     - name: Commit
@@ -133,6 +134,7 @@ jobs:
     - uses: actions/checkout@v2
     - uses: actions/checkout@v2
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
+        lfs: true
 
 
     - name: Setup suffix
     - name: Setup suffix
       id: suffix
       id: suffix
@@ -183,6 +185,8 @@ jobs:
         file: ./packages/app/docker/Dockerfile
         file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         platforms: linux/amd64
         push: true
         push: true
+        build-args: |
+          flavor=${{ matrix.flavor }}
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
         cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
         tags: ${{ steps.meta.outputs.tags }}
         tags: ${{ steps.meta.outputs.tags }}

+ 81 - 4
CHANGELOG.md

@@ -1,9 +1,86 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.4.3...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.4.7...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [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
 ## [v4.4.3](https://github.com/weseek/growi/compare/v4.4.2...v4.4.3) - 2021-09-17
 
 
 ### 💎 Features
 ### 💎 Features
@@ -1441,7 +1518,7 @@ Upgrading Guide: [https://docs.growi.org/en/admin-guide/upgrading/34x.html](http
 
 
 - Improvement: Ensure to prevent suspending own account
 - Improvement: Ensure to prevent suspending own account
 - Fix: Ensure to be able to use `.` for username when invited
 - 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
 ## v2.1.1
 
 
@@ -1509,8 +1586,8 @@ Upgrading Guide: [https://docs.growi.org/en/admin-guide/upgrading/34x.html](http
 
 
 ## v1.2.14
 ## 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
 ## v1.2.13
 
 

+ 11 - 0
THIRD-PARTY-NOTICES.md

@@ -17,6 +17,7 @@ https://github.com/weseek/growi.
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 5. EmojiOne Version 3 (https://github.com/joypixels/emojione/tree/v3.1.1)
 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
 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)"
 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,
   help = false,
   dir = '.',
   dir = '.',
   dryRun = false,
   dryRun = false,
+  updateDependencies = true,
   increment = 'patch',
   increment = 'patch',
   preid = 'RC',
   preid = 'RC',
 }) {
 }) {
@@ -26,8 +27,12 @@ async function bumpVersions({
 
 
   const config = await loadConfig(dir, 'bump-versions.config');
   const config = await loadConfig(dir, 'bump-versions.config');
 
 
-  // get current version
   const { monorepo } = config;
   const { monorepo } = config;
+  if (!updateDependencies) {
+    monorepo.updateDependencies = false;
+  }
+
+  // get current version
   const currentVersion = monorepo && monorepo.mainVersionFile
   const currentVersion = monorepo && monorepo.mainVersionFile
     ? getCurrentVersion(dir, monorepo.mainVersionFile)
     ? getCurrentVersion(dir, monorepo.mainVersionFile)
     : getCurrentVersion(dir);
     : getCurrentVersion(dir);
@@ -55,6 +60,7 @@ const arg = {
   '--dir': String,
   '--dir': String,
   '--help': Boolean,
   '--help': Boolean,
   '--dry-run': Boolean,
   '--dry-run': Boolean,
+  '--update-dependencies': Boolean,
   '--increment': String,
   '--increment': String,
   '--preid': 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 dir = `--dir ${underline('PATH')}`;
   const increment = `--increment ${underline('LEVEL')}`;
   const increment = `--increment ${underline('LEVEL')}`;
   const preId = `--preid ${underline('IDENTIFIER')}`;
   const preId = `--preid ${underline('IDENTIFIER')}`;
+  const updateDependencies = `--update-dependencies ${underline('true/false')}`;
   const dryRun = '--dry-run';
   const dryRun = '--dry-run';
-  const all = [help, dir, increment, preId, dryRun]
+  const all = [help, dir, increment, preId, updateDependencies, dryRun]
     .map(x => `[${x}]`)
     .map(x => `[${x}]`)
     .join(' ');
     .join(' ');
 
 
@@ -46,6 +47,9 @@ export default () => runStep({}, () => {
       )} for semver.inc() with 'prerelease' type (default: 'RC').`,
       )} for semver.inc() with 'prerelease' type (default: 'RC').`,
     ),
     ),
     '',
     '',
+    indent(`${updateDependencies}`),
+    indent('  Update dependencies or not (default: true).'),
+    '',
     indent(`-D, ${dryRun}`),
     indent(`-D, ${dryRun}`),
     indent('  Displays the steps without actually doing them.'),
     indent('  Displays the steps without actually doing them.'),
     '',
     '',

+ 1 - 1
lerna.json

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

+ 9 - 5
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.4.4-RC.0",
+  "version": "4.4.8-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -35,6 +35,9 @@
     "app:server": "yarn lerna run server --scope @growi/app",
     "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:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
     "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": "",
     "//// scripts for backward compatibility": "",
     "build:prod": "echo !!! CAUTION !!! ==> The script 'build:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:build",
     "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"
     "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",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "dotenv-flow": "^3.2.0",
     "npm-run-all": "^4.1.5",
     "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": {
   "devDependencies": {
     "@types/jest": "^26.0.22",
     "@types/jest": "^26.0.22",
@@ -66,7 +67,10 @@
     "lerna": "^4.0.0",
     "lerna": "^4.0.0",
     "rewire": "^5.0.0",
     "rewire": "^5.0.0",
     "shipjs": "^0.23.3",
     "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": {
   "engines": {
     "node": "^12 || ^14",
     "node": "^12 || ^14",

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

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

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

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

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

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

+ 1 - 1
packages/app/.gitignore

@@ -5,8 +5,8 @@
 # dist
 # dist
 /dist/
 /dist/
 /transpiled/
 /transpiled/
+/report/
 /public/static/js
 /public/static/js
-/public/static/dict
 /public/static/styles
 /public/static/styles
 /public/uploads
 /public/uploads
 /tmp/
 /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 { Transform } from 'stream';
 import replaceStream from 'replacestream';
 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 * as cdnManifests from '^/resource/cdn-manifests';
 
 
 import { CdnResource, CdnManifest } from '~/interfaces/cdn';
 import { CdnResource, CdnManifest } from '~/interfaces/cdn';
@@ -22,14 +20,6 @@ export default class CdnResourcesDownloader {
       return { manifest, outDir: cdnLocalScriptRoot };
       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) => {
     const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
       return { manifest, outDir: cdnLocalStyleRoot };
       return { manifest, outDir: cdnLocalStyleRoot };
     });
     });
@@ -42,7 +32,6 @@ export default class CdnResourcesDownloader {
 
 
     return Promise.all([
     return Promise.all([
       this.downloadScripts(cdnScriptResources),
       this.downloadScripts(cdnScriptResources),
-      this.downloadScripts(cdnDictResources, dictExtensionOptions),
       this.downloadStyles(cdnStyleResources, dlStylesOptions),
       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 cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
 export const cdnLocalScriptWebRoot = '/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 cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';

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

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

+ 7 - 15
packages/app/docker/Dockerfile

@@ -40,10 +40,7 @@ RUN tar cf node_modules.tar \
 ## deps-resolver-prod
 ## deps-resolver-prod
 ##
 ##
 FROM deps-resolver AS 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
 # make artifacts
 RUN tar cf node_modules.tar \
 RUN tar cf node_modules.tar \
   node_modules \
   node_modules \
@@ -75,15 +72,14 @@ RUN rm node_modules.tar
 ##
 ##
 FROM prebuilder-default AS prebuilder-nocdn
 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
 ## builder
 ##
 ##
-# FROM prebuilder-${flavor}
 FROM prebuilder-${flavor} AS builder
 FROM prebuilder-${flavor} AS builder
 
 
 ENV appDir /opt/growi
 ENV appDir /opt/growi
@@ -116,7 +112,8 @@ RUN tar cf packages.tar \
   packages/app/public \
   packages/app/public \
   packages/app/resource \
   packages/app/resource \
   packages/app/tmp \
   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.base.json \
   packages/app/tsconfig.json \
   packages/app/tsconfig.json \
   packages/*/package.json \
   packages/*/package.json \
@@ -143,11 +140,6 @@ RUN set -eux; \
 # verify that the binary works
 # verify that the binary works
 	gosu nobody true
 	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 \
 COPY --from=deps-resolver-prod --chown=node:node \
   ${appDir}/node_modules.tar ${appDir}/
   ${appDir}/node_modules.tar ${appDir}/
 COPY --from=builder --chown=node:node \
 COPY --from=builder --chown=node:node \
@@ -171,5 +163,5 @@ WORKDIR ${appDir}/packages/app
 VOLUME /data
 VOLUME /data
 EXPOSE 3000
 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
 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.7`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.7/docker/Dockerfile)
+* [`4.4.7-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.7/docker/Dockerfile)
 * [`4.3.3`, `4.3` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/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)
 * [`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 -R node:node /data/uploads
 chown -h node:node ./public/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',
   preset: 'ts-jest/presets/js-with-ts',
 
 
-  globalSetup: '<rootDir>/src/test/global-setup.js',
-  globalTeardown: '<rootDir>/src/test/global-teardown.js',
-
   projects: [
   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',
       displayName: 'server',
 
 
@@ -25,9 +37,13 @@ module.exports = {
 
 
       rootDir: '.',
       rootDir: '.',
       roots: ['<rootDir>/src'],
       roots: ['<rootDir>/src'],
+      testMatch: ['<rootDir>/src/test/integration/**/*.test.ts', '<rootDir>/src/test/integration/**/*.test.js'],
+
       testEnvironment: 'node',
       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
       // Automatically clear mock calls and instances between every test
       clearMocks: true,
       clearMocks: true,
       moduleNameMapper: MODULE_NAME_MAPPING,
       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>
  * @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();
 initMongooseGlobalSettings();
 
 
@@ -25,8 +29,7 @@ const mongodb = {
 };
 };
 
 
 module.exports = {
 module.exports = {
-  mongoUri,
   mongodb,
   mongodb,
-  migrationsDir: 'src/migrations/',
+  migrationsDir,
   changelogCollectionName: 'migrations',
   changelogCollectionName: 'migrations',
 };
 };

+ 21 - 18
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "4.4.4-RC.0",
+  "version": "4.4.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -14,13 +14,20 @@
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server:ci": "yarn server --ci",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
+    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up",
     "//// for development": "",
     "//// for development": "",
     "dev": "run-p dev:client dev:server",
     "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": "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: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",
     "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: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": "",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
     "predev:ci": "run-p resources:*",
@@ -39,11 +46,6 @@
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "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:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.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"
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
@@ -53,14 +55,15 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
+    "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@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-pukiwiki-like-linker": "^4.4.4-RC.0",
-    "@growi/plugin-lsx": "^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.8-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.8-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.8-RC.0",
+    "@growi/plugin-lsx": "^4.4.8-RC.0",
+    "@growi/slack": "^4.4.8-RC.0",
+    "@promster/express": "^5.1.0",
+    "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
@@ -97,7 +100,7 @@
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "growi-commons": "^5.0.4",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
-    "http-errors": "~1.6.2",
+    "http-errors": "~1.8.0",
     "i18next": "^20.3.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-express-middleware": "^2.0.0",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-node-fs-backend": "^2.1.0",
@@ -124,7 +127,7 @@
     "passport-http": "^0.3.0",
     "passport-http": "^0.3.0",
     "passport-ldapauth": "^2.0.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
-    "passport-saml": "^1.0.0",
+    "passport-saml": "^2.2.0",
     "passport-twitter": "^1.0.4",
     "passport-twitter": "^1.0.4",
     "prom-client": "^13.0.0",
     "prom-client": "^13.0.0",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
@@ -154,7 +157,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.4-RC.0",
+    "@growi/ui": "^4.4.8-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
@@ -165,7 +168,7 @@
     "browser-sync": "^2.26.3",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",
     "cli": "~1.0.1",
-    "codemirror": "^5.48.4",
+    "codemirror": "^5.63.0",
     "colors": "^1.2.5",
     "colors": "^1.2.5",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
     "core-js": "=2.6.9",
     "core-js": "=2.6.9",

+ 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',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-keymap-vim',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-keymap-emacs',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-keymap-sublime',
       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: {
       args: {
         integrity: '',
         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: [
   style: [
     {
     {
       name: 'lato',
       name: 'lato',
@@ -256,63 +170,63 @@ module.exports = {
     },
     },
     {
     {
       name: 'codemirror-dialog',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-eclipse',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-elegant',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-neo',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-mdn-like',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-material',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-dracula',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-monokai',
       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: {
       args: {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
     {
     {
       name: 'codemirror-theme-twilight',
       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: {
       args: {
         integrity: '',
         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": "Display notification on stale pages",
       "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
       "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": "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",
     "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.",
     "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)",
       "allow_specified_long": "Allow specified (The command is allowed from only specified channels)",
       "test_connection": "Test Connection",
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the 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.",
       "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.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"
       "add_slack_workspace": "Add a Slack Workspace"

+ 8 - 1
packages/app/resource/locales/en_US/translation.json

@@ -454,6 +454,11 @@
       "Post": "Post"
       "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": {
   "link_edit": {
     "edit_link": "Edit Link",
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",
     "set_link_and_label": "Set link and label",
@@ -566,7 +571,9 @@
     "delete": "Delete",
     "delete": "Delete",
     "check_all": "Check all",
     "check_all": "Check all",
     "deletion_modal_header": "Delete page",
     "deletion_modal_header": "Delete page",
-    "delete_completely": "Delete completely"
+    "delete_completely": "Delete completely",
+    "include_certain_path" : "Include {{pathToInclude}} path ",
+    "delete_all_selected_page" : "Delete All"
   },
   },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "Guest users access",
     "Guest Users Access": "Guest users access",

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

@@ -148,7 +148,9 @@
       "stale_notification": "古いページに通知を表示する",
       "stale_notification": "古いページに通知を表示する",
       "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。",
       "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。",
       "show_all_reply_comments": "返信コメントを全て表示する",
       "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": "コードハイライト",
     "code_highlight": "コードハイライト",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
@@ -342,6 +344,7 @@
       "allow_specified-long": "特定のチャンネルを許可 (テキストボックスに入力されたチャンネルのみ許可されます)",
       "allow_specified-long": "特定のチャンネルを許可 (テキストボックスに入力されたチャンネルのみ許可されます)",
       "test_connection": "連携状況のテストをする",
       "test_connection": "連携状況のテストをする",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
+      "test_connection_only_public_channel":"連携テストは public チャンネルで確認してください",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。",
       "send_message_to_slack_work_space": "Slack ワークスペースに送信しました",
       "send_message_to_slack_work_space": "Slack ワークスペースに送信しました",
       "add_slack_workspace": "Slackワークスペースを追加"
       "add_slack_workspace": "Slackワークスペースを追加"

+ 8 - 1
packages/app/resource/locales/ja_JP/translation.json

@@ -455,6 +455,11 @@
       "Post": "投稿"
       "Post": "投稿"
     }
     }
   },
   },
+  "modal_enable_textlint": {
+    "confirm_download_dict_and_enable_textlint": "Textlintを有効にしますか?20MBの辞書ファイルをダウンロードします。",
+    "enable_textlint": "Textlintを有効にする",
+    "dont_ask_again": "常に許可する"
+  },
   "link_edit": {
   "link_edit": {
     "edit_link": "リンク編集",
     "edit_link": "リンク編集",
     "set_link_and_label": "リンク情報",
     "set_link_and_label": "リンク情報",
@@ -567,7 +572,9 @@
     "delete": "削除",
     "delete": "削除",
     "check_all": "すべてチェック",
     "check_all": "すべてチェック",
     "deletion_modal_header": "以下のページを削除",
     "deletion_modal_header": "以下のページを削除",
-    "delete_completely": "完全に削除する"
+    "delete_completely": "完全に削除する",
+    "include_certain_path": "{{pathToInclude}}下を含む ",
+    "delete_all_selected_page" : "一括削除"
   },
   },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Guest Users Access": "ゲストユーザーのアクセス",

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

@@ -158,7 +158,9 @@
       "stale_notification": "在过期页上显示通知",
       "stale_notification": "在过期页上显示通知",
       "stale_notification_desc": "显示自上次更新以来超过1年的页面通知。",
       "stale_notification_desc": "显示自上次更新以来超过1年的页面通知。",
       "show_all_reply_comments": "显示所有回复评论",
       "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": "代码突出显示",
     "code_highlight": "代码突出显示",
     "nocdn_desc": "当强制应用环境变量<code>NO_CDN=true</code><br>Github样式时,此函数被禁用。",
     "nocdn_desc": "当强制应用环境变量<code>NO_CDN=true</code><br>Github样式时,此函数被禁用。",
@@ -352,6 +354,7 @@
       "allow_specified_long": "允许指定(该命令只允许来自指定的通道)",
       "allow_specified_long": "允许指定(该命令只允许来自指定的通道)",
       "test_connection": "测试连接",
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
+      "test_connection_only_public_channel":"请在一个公共频道中测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "add_slack_workspace": "添加Slack Workspace"
       "add_slack_workspace": "添加Slack Workspace"

+ 8 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -433,6 +433,11 @@
 			"Post": "提交"
 			"Post": "提交"
 		}
 		}
 	},
 	},
+  "modal_enable_textlint": {
+    "confirm_download_dict_and_enable_textlint": "您确定要启用 Textlint 吗?这将下载 20MB 的字典文件。",
+    "enable_textlint": "启用Textlint",
+    "dont_ask_again": "不要再问"
+  },
   "link_edit": {
   "link_edit": {
     "edit_link": "Edit Link",
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",
     "set_link_and_label": "Set link and label",
@@ -839,7 +844,9 @@
 		"delete": "删除",
 		"delete": "删除",
 		"check_all": "全部检查",
 		"check_all": "全部检查",
 		"deletion_modal_header": "删除页",
 		"deletion_modal_header": "删除页",
-		"delete_completely": "完全删除"
+		"delete_completely": "完全删除",
+    "include_certain_path": "包含 {{pathToInclude}} 路径 ",
+    "delete_all_selected_page": "删除所有"
 	},
 	},
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"login": {
 	"login": {

+ 3 - 0
packages/app/resource/search/mappings.json

@@ -74,6 +74,9 @@
         "bookmark_count": {
         "bookmark_count": {
           "type": "integer"
           "type": "integer"
         },
         },
+        "seenUsers_count":{
+          "type": "integer"
+        },
         "like_count": {
         "like_count": {
           "type": "integer"
           "type": "integer"
         },
         },

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

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

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

@@ -36,7 +36,9 @@ export default class EditorContainer extends Container {
 
 
       editorOptions: {},
       editorOptions: {},
       previewOptions: {},
       previewOptions: {},
-      isTextlintEnabled: false,
+
+      // Defaults to null to show modal when not in DB
+      isTextlintEnabled: null,
       textlintRules: [],
       textlintRules: [],
 
 
       indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
       indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
@@ -204,13 +206,18 @@ export default class EditorContainer extends Container {
    * Retrieve Editor Settings
    * Retrieve Editor Settings
    */
    */
   async retrieveEditorSettings() {
   async retrieveEditorSettings() {
+    if (this.appContainer.isGuestUser) {
+      return;
+    }
+
     const { data } = await this.appContainer.apiv3Get('/personal-setting/editor-settings');
     const { data } = await this.appContainer.apiv3Get('/personal-setting/editor-settings');
 
 
     if (data?.textlintSettings == null) {
     if (data?.textlintSettings == null) {
       return;
       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({
     this.setState({
       isTextlintEnabled,
       isTextlintEnabled,

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

@@ -139,6 +139,21 @@ class CustomizeFunctionSetting extends React.Component {
               </div>
               </div>
             </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} />
             <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
           </div>
         </div>
         </div>

+ 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')}
                 {t('security_setting.SAML.enable_saml')}
               </label>
               </label>
             </div>
             </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 className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </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>}</>}
         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 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">
         <div className="d-flex justify-content-center">
           <form className="form-row align-items-center" onSubmit={e => submitForm(e)}>
           <form className="form-row align-items-center" onSubmit={e => submitForm(e)}>
             <div className="input-group col-8">
             <div className="input-group col-8">

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

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

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

@@ -46,10 +46,10 @@ const commonRulesMenuItems = [
     name: 'sentence-length',
     name: 'sentence-length',
     description: 'editor_settings.common_settings.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',
     name: 'no-unmatched-pair',
     description: 'editor_settings.common_settings.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) {
   constructor(props) {
     super(props);
     super(props);
 
 
+    const isSearchScopeChildrenAsDefault = this.props.appContainer.getConfig().isSearchScopeChildrenAsDefault;
+
     this.state = {
     this.state = {
       text: '',
       text: '',
-      isScopeChildren: false,
+      isScopeChildren: isSearchScopeChildrenAsDefault,
     };
     };
 
 
     this.onInputChange = this.onInputChange.bind(this);
     this.onInputChange = this.onInputChange.bind(this);

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

@@ -33,8 +33,9 @@ import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
 import EditorIcon from './EditorIcon';
 import DrawioModal from './DrawioModal';
 import DrawioModal from './DrawioModal';
 
 
-
+// Textlint
 window.JSHINT = JSHINT;
 window.JSHINT = JSHINT;
+window.kuromojin = { dicPath: '/static/dict' };
 
 
 // set save handler
 // set save handler
 codemirror.commands.save = (instance) => {
 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.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
     this.cmNoCdnScriptRoot = '/static/js/cdn';
     this.cmNoCdnScriptRoot = '/static/js/cdn';
     this.cmNoCdnStyleRoot = '/static/styles/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 = new InterceptorManager();
     this.interceptorManager.addInterceptors([
     this.interceptorManager.addInterceptors([
       new PreventMarkdownListInterceptor(),
       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,
+};

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

@@ -11,6 +11,7 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { DownloadDictModal } from './DownloadDictModal';
 
 
 
 
 export const defaultEditorOptions = {
 export const defaultEditorOptions = {
@@ -34,6 +35,8 @@ class OptionsSelector extends React.Component {
     this.state = {
     this.state = {
       isCddMenuOpened: false,
       isCddMenuOpened: false,
       isMathJaxEnabled,
       isMathJaxEnabled,
+      isDownloadDictModalShown: false,
+      isSkipAskingAgainChecked: false,
     };
     };
 
 
     this.availableThemes = [
     this.availableThemes = [
@@ -53,6 +56,8 @@ class OptionsSelector extends React.Component {
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.switchTextlintEnabledHandler = this.switchTextlintEnabledHandler.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.updateIsTextlintEnabledToDB = this.updateIsTextlintEnabledToDB.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
     this.onChangeIndentSize = this.onChangeIndentSize.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 { editorContainer } = this.props;
     const newVal = !editorContainer.state.isTextlintEnabled;
     const newVal = !editorContainer.state.isTextlintEnabled;
     editorContainer.setState({ isTextlintEnabled: newVal });
     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) {
   onToggleConfigurationDropdown(newValue) {
@@ -359,12 +382,22 @@ class OptionsSelector extends React.Component {
 
 
   render() {
   render() {
     return (
     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 })}
+          />
+        )}
+      </>
     );
     );
   }
   }
 
 

+ 2 - 1
packages/app/src/components/SearchForm.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 
 
@@ -174,4 +175,4 @@ SearchForm.defaultProps = {
   onInputChange: () => {},
   onInputChange: () => {},
 };
 };
 
 
-export default SearchFormWrapper;
+export default withTranslation()(SearchFormWrapper);

+ 87 - 23
packages/app/src/components/SearchPage.jsx

@@ -8,24 +8,31 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
-
-import SearchPageForm from './SearchPage/SearchPageForm';
-import SearchResult from './SearchPage/SearchResult';
+import SearchPageLayout from './SearchPage/SearchPageLayout';
+import SearchResultContent from './SearchPage/SearchResultContent';
+import SearchResultList from './SearchPage/SearchResultList';
+import SearchControl from './SearchPage/SearchControl';
 
 
 class SearchPage extends React.Component {
 class SearchPage extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
-
+    // NOTE : selectedPages is deletion related state, will be used later in story 77535, 77565.
+    // deletionModal, deletion related functions are all removed, add them back when necessary.
+    // i.e ) in story 77525 or any tasks implementing deletion functionalities
     this.state = {
     this.state = {
       searchingKeyword: decodeURI(this.props.query.q) || '',
       searchingKeyword: decodeURI(this.props.query.q) || '',
       searchedKeyword: '',
       searchedKeyword: '',
       searchedPages: [],
       searchedPages: [],
       searchResultMeta: {},
       searchResultMeta: {},
+      selectedPage: {},
+      selectedPages: new Set(),
     };
     };
 
 
-    this.search = this.search.bind(this);
     this.changeURL = this.changeURL.bind(this);
     this.changeURL = this.changeURL.bind(this);
+    this.search = this.search.bind(this);
+    this.selectPage = this.selectPage.bind(this);
+    this.toggleCheckBox = this.toggleCheckBox.bind(this);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -58,6 +65,7 @@ class SearchPage extends React.Component {
     }
     }
   }
   }
 
 
+
   search(data) {
   search(data) {
     const keyword = data.keyword;
     const keyword = data.keyword;
     if (keyword === '') {
     if (keyword === '') {
@@ -73,37 +81,93 @@ class SearchPage extends React.Component {
     this.setState({
     this.setState({
       searchingKeyword: keyword,
       searchingKeyword: keyword,
     });
     });
-
     this.props.appContainer.apiGet('/search', { q: keyword })
     this.props.appContainer.apiGet('/search', { q: keyword })
       .then((res) => {
       .then((res) => {
         this.changeURL(keyword);
         this.changeURL(keyword);
-
-        this.setState({
-          searchedKeyword: keyword,
-          searchedPages: res.data,
-          searchResultMeta: res.meta,
-        });
+        if (res.data.length > 0) {
+          // TODO: remove creating dummy snippet lines when the data with snippet is abole to be retrieved
+          res.data.forEach((page) => {
+            page.snippet = `dummy snippet dummpy snippet dummpy snippet dummpy snippet dummpy snippet
+            dummpy snippet dummpy snippet dummpy snippet dummpy snippet`;
+          });
+          this.setState({
+            searchedKeyword: keyword,
+            searchedPages: res.data,
+            searchResultMeta: res.meta,
+            selectedPage: res.data[0],
+          });
+        }
       })
       })
       .catch((err) => {
       .catch((err) => {
         toastError(err);
         toastError(err);
       });
       });
   }
   }
 
 
+  selectPage= (pageId) => {
+    const index = this.state.searchedPages.findIndex((page) => {
+      return page._id === pageId;
+    });
+    this.setState({
+      selectedPage: this.state.searchedPages[index],
+    });
+  }
+
+  toggleCheckBox = (page) => {
+    if (this.state.selectedPages.has(page)) {
+      this.state.selectedPages.delete(page);
+    }
+    else {
+      this.state.selectedPages.add(page);
+    }
+  }
+
+  renderSearchResultContent = () => {
+    return (
+      <SearchResultContent
+        appContainer={this.props.appContainer}
+        searchingKeyword={this.state.searchingKeyword}
+        selectedPage={this.state.selectedPage}
+      >
+      </SearchResultContent>
+    );
+  }
+
+  renderSearchResultList = () => {
+    return (
+      <SearchResultList
+        pages={this.state.searchedPages}
+        deletionMode={false}
+        selectedPage={this.state.selectedPage}
+        selectedPages={this.state.selectedPages}
+        onClickInvoked={this.selectPage}
+        onChangedInvoked={this.toggleCheckBox}
+      >
+      </SearchResultList>
+    );
+  }
+
+  renderSearchControl = () => {
+    return (
+      <SearchControl
+        searchingKeyword={this.state.searchingKeyword}
+        appContainer={this.props.appContainer}
+        onSearchInvoked={this.search}
+      >
+      </SearchControl>
+    );
+  }
+
   render() {
   render() {
     return (
     return (
       <div>
       <div>
-        <div className="search-page-input sps sps--abv">
-          <SearchPageForm
-            t={this.props.t}
-            onSearchFormChanged={this.search}
-            keyword={this.state.searchingKeyword}
-          />
-        </div>
-        <SearchResult
-          pages={this.state.searchedPages}
-          searchingKeyword={this.state.searchingKeyword}
+        <SearchPageLayout
+          SearchControl={this.renderSearchControl}
+          SearchResultList={this.renderSearchResultList}
+          SearchResultContent={this.renderSearchResultContent}
           searchResultMeta={this.state.searchResultMeta}
           searchResultMeta={this.state.searchResultMeta}
-        />
+          searchingKeyword={this.state.searchedKeyword}
+        >
+        </SearchPageLayout>
       </div>
       </div>
     );
     );
   }
   }

+ 39 - 0
packages/app/src/components/SearchPage/DeleteAllButton.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+const DeleteAllButton = (props) => {
+  const { selectedPage, checked } = props;
+  const { t } = useTranslation();
+  function deleteAllSelectedPage(pagesToDelete) {
+    // TODO: implement this function
+    // https://estoc.weseek.co.jp/redmine/issues/77543
+    // do something with pagesDelete to delete them.
+  }
+  return (
+    <div>
+      <label>
+        <input
+          type="checkbox"
+          name="check-delte-all"
+          onChange={() => {
+            if (checked) {
+              deleteAllSelectedPage(selectedPage);
+            }
+          }}
+        />
+        <span className="text-danger font-weight-light">
+          <i className="icon-trash ml-3"></i>
+          {t('search_result.delete_all_selected_page')}
+        </span>
+      </label>
+    </div>
+  );
+
+};
+
+DeleteAllButton.propTypes = {
+  selectedPage: PropTypes.array.isRequired,
+  checked: PropTypes.bool.isRequired,
+};
+export default DeleteAllButton;

+ 42 - 0
packages/app/src/components/SearchPage/IncludeSpecificPathButton.jsx

@@ -0,0 +1,42 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+const IncludeSpecificPathButton = (props) => {
+  const { pathToInclude, checked } = props;
+  const { t } = useTranslation();
+
+  // TODO : implement this function
+  // 77526 story https://estoc.weseek.co.jp/redmine/issues/77526
+  // 77535 stroy https://estoc.weseek.co.jp/redmine/issues/77535
+  function includeSpecificPathInSearchResult(pathToInclude) {
+    console.log(`now including ${pathToInclude} in search result`);
+  }
+  return (
+    <div className="border px-2 btn btn-outline-secondary">
+      <label className="mb-0">
+        <span className="font-weight-light">
+          {pathToInclude === '/user'
+            ? t('search_result.include_certain_path', { pathToInclude: '/user' }) : t('search_result.include_certain_path', { pathToInclude: '/trash' })}
+        </span>
+        <input
+          type="checkbox"
+          name="check-include-specific-path"
+          onChange={() => {
+            if (checked) {
+              includeSpecificPathInSearchResult(pathToInclude);
+            }
+          }}
+        />
+      </label>
+    </div>
+  );
+
+};
+
+IncludeSpecificPathButton.propTypes = {
+  pathToInclude: PropTypes.string.isRequired,
+  checked: PropTypes.bool.isRequired,
+};
+
+export default IncludeSpecificPathButton;

+ 31 - 0
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -0,0 +1,31 @@
+import React, { FC } from 'react';
+import SearchPageForm from './SearchPageForm';
+import AppContainer from '../../client/services/AppContainer';
+
+
+type Props = {
+  searchingKeyword: string,
+  appContainer: AppContainer,
+  onSearchInvoked: (data : any[]) => boolean,
+}
+
+const SearchControl: FC <Props> = (props: Props) => {
+  // Temporaly workaround for lint error
+  // later needs to be fixed: SearchControl to typescript componet
+  const SearchPageFormTypeAny : any = SearchPageForm;
+  return (
+    <div className="">
+      <div className="search-page-input sps sps--abv">
+        <SearchPageFormTypeAny
+          keyword={props.searchingKeyword}
+          appContainer={props.appContainer}
+          onSearchFormChanged={props.onSearchInvoked}
+        />
+      </div>
+      {/* TODO: place deleteAll button , relevance button , include specificPath button */}
+    </div>
+  );
+};
+
+
+export default SearchControl;

+ 29 - 11
packages/app/src/components/SearchPage/SearchPageForm.jsx

@@ -4,6 +4,9 @@ import PropTypes from 'prop-types';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import SearchForm from '../SearchForm';
 import SearchForm from '../SearchForm';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:searchPageForm');
 
 
 // Search.SearchForm
 // Search.SearchForm
 class SearchPageForm extends React.Component {
 class SearchPageForm extends React.Component {
@@ -21,9 +24,14 @@ class SearchPageForm extends React.Component {
   }
   }
 
 
   search() {
   search() {
-    const keyword = this.state.keyword;
-    this.props.onSearchFormChanged({ keyword });
-    this.setState({ searchedKeyword: keyword });
+    if (this.props.onSearchFormChanged != null) {
+      const keyword = this.state.keyword;
+      this.props.onSearchFormChanged({ keyword });
+      this.setState({ searchedKeyword: keyword });
+    }
+    else {
+      throw new Error('onSearchFormChanged method is null');
+    }
   }
   }
 
 
   onInputChange(input) { // for only submitting with button
   onInputChange(input) { // for only submitting with button
@@ -35,16 +43,27 @@ class SearchPageForm extends React.Component {
       <div className="grw-search-form-in-search-result-page">
       <div className="grw-search-form-in-search-result-page">
         <div className="input-group flex-nowrap">
         <div className="input-group flex-nowrap">
           <SearchForm
           <SearchForm
-            t={this.props.t}
             onSubmit={this.search}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             keyword={this.state.searchedKeyword}
             onInputChange={this.onInputChange}
             onInputChange={this.onInputChange}
           />
           />
-          <div className="input-group-append">
-            <button className="btn pr-3 pr-sm-4" type="button" id="button-addon2" onClick={this.search}>
-              <i className="icon-magnifier"></i>
-            </button>
-          </div>
+        </div>
+        <div className="input-group-append">
+          <button
+            className="btn btn-secondary"
+            type="button"
+            id="button-addon2"
+            onClick={() => {
+              try {
+                this.search();
+              }
+              catch (error) {
+                logger.error(error);
+              }
+            }}
+          >
+            <i className="icon-magnifier"></i>
+          </button>
         </div>
         </div>
       </div>
       </div>
     );
     );
@@ -58,11 +77,10 @@ class SearchPageForm extends React.Component {
 const SearchPageFormWrapper = withUnstatedContainers(SearchPageForm, [AppContainer]);
 const SearchPageFormWrapper = withUnstatedContainers(SearchPageForm, [AppContainer]);
 
 
 SearchPageForm.propTypes = {
 SearchPageForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   keyword: PropTypes.string,
   keyword: PropTypes.string,
-  onSearchFormChanged: PropTypes.func.isRequired,
+  onSearchFormChanged: PropTypes.func,
 };
 };
 SearchPageForm.defaultProps = {
 SearchPageForm.defaultProps = {
 };
 };

+ 43 - 0
packages/app/src/components/SearchPage/SearchPageLayout.tsx

@@ -0,0 +1,43 @@
+import React, { FC } from 'react';
+
+type SearchResultMeta = {
+  took : number,
+  total : number,
+  results: number
+}
+
+type Props = {
+  SearchControl: React.FunctionComponent,
+  SearchResultList: React.FunctionComponent,
+  SearchResultContent: React.FunctionComponent,
+  searchResultMeta: SearchResultMeta,
+  searchingKeyword: string
+}
+
+const SearchPageLayout: FC<Props> = (props: Props) => {
+  const { SearchResultList, SearchControl, SearchResultContent } = props;
+  return (
+    <div className="content-main">
+      <div className="search-result row" id="search-result">
+        <div className="col-lg-6  page-list search-result-list pr-0" id="search-result-list">
+          <nav><SearchControl></SearchControl></nav>
+          <div className="d-flex align-items-start justify-content-between mt-1">
+            <div className="search-result-meta">
+              <i className="icon-magnifier" /> Found {props.searchResultMeta.total} pages with &quot;{props.searchingKeyword}&quot;
+            </div>
+          </div>
+
+          <div className="page-list">
+            <ul className="page-list-ul page-list-ul-flat nav nav-pills"><SearchResultList></SearchResultList></ul>
+          </div>
+        </div>
+        <div className="col-lg-6 d-none d-lg-block search-result-content">
+          <SearchResultContent></SearchResultContent>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+
+export default SearchPageLayout;

+ 9 - 3
packages/app/src/components/SearchPage/SearchResult.jsx

@@ -10,6 +10,8 @@ import DeletePageListModal from './DeletePageListModal';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
+// NOTE : this file will be deleted in the future. Merge conflict happend in this file, so temporaly kept this left here.
+// Task 77833 deleted this file ;
 class SearchResult extends React.Component {
 class SearchResult extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -184,8 +186,11 @@ class SearchResult extends React.Component {
       // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
       // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
       const pageId = `#id_${page._id}`;
       const pageId = `#id_${page._id}`;
       return (
       return (
-        <li key={page._id} className="nav-item page-list-li w-100 m-1">
+        <li key={page._id} className="nav-item page-list-li w-100 m-0 border-bottom">
           <a className="nav-link page-list-link d-flex align-items-baseline" href={pageId}>
           <a className="nav-link page-list-link d-flex align-items-baseline" href={pageId}>
+            <div className="form-check my-auto">
+              <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
+            </div>
             <Page page={page} noLink />
             <Page page={page} noLink />
             <div className="ml-auto d-flex">
             <div className="ml-auto d-flex">
               { this.state.deletionMode
               { this.state.deletionMode
@@ -208,6 +213,7 @@ class SearchResult extends React.Component {
               </div>
               </div>
             </div>
             </div>
           </a>
           </a>
+          <div>{page.highlight['body.en']?.map(text => <p dangerouslySetInnerHTML={{ __html: text }} />)}</div>
         </li>
         </li>
       );
       );
     });
     });
@@ -292,7 +298,7 @@ class SearchResult extends React.Component {
     return (
     return (
       <div className="content-main">
       <div className="content-main">
         <div className="search-result row" id="search-result">
         <div className="search-result row" id="search-result">
-          <div className="col-lg-4 d-none d-lg-block page-list search-result-list pr-0" id="search-result-list">
+          <div className="col-lg-6 d-none d-lg-block page-list search-result-list pr-0" id="search-result-list">
             <nav>
             <nav>
               <div className="d-flex align-items-start justify-content-between mt-1">
               <div className="d-flex align-items-start justify-content-between mt-1">
                 <div className="search-result-meta">
                 <div className="search-result-meta">
@@ -309,7 +315,7 @@ class SearchResult extends React.Component {
               </div>
               </div>
             </nav>
             </nav>
           </div>
           </div>
-          <div className="col-lg-8 search-result-content" id="search-result-content">
+          <div className="col-lg-6 search-result-content" id="search-result-content">
             <SearchResultList pages={this.props.pages} searchingKeyword={this.props.searchingKeyword} />
             <SearchResultList pages={this.props.pages} searchingKeyword={this.props.searchingKeyword} />
           </div>
           </div>
         </div>
         </div>

+ 50 - 0
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -0,0 +1,50 @@
+import React, { FC } from 'react';
+
+import RevisionLoader from '../Page/RevisionLoader';
+import AppContainer from '../../client/services/AppContainer';
+
+
+type Props ={
+  appContainer: AppContainer,
+  searchingKeyword:string,
+  selectedPage : any,
+}
+const SearchResultContent: FC<Props> = (props: Props) => {
+  // Temporaly workaround for lint error
+  // later needs to be fixed: RevisoinRender to typescriptcomponet
+  const RevisionRenderTypeAny: any = RevisionLoader;
+  const renderPage = (page) => {
+    const growiRenderer = props.appContainer.getRenderer('searchresult');
+    let showTags = false;
+    if (page.tags != null && page.tags.length > 0) { showTags = true }
+    return (
+      <div key={page._id} className="search-result-page mb-5">
+        <h2>
+          <a href={page.path} className="text-break">
+            {page.path}
+          </a>
+          {showTags && (
+            <div className="mt-1 small">
+              <i className="tag-icon icon-tag"></i> {page.tags.join(', ')}
+            </div>
+          )}
+        </h2>
+        <RevisionRenderTypeAny
+          growiRenderer={growiRenderer}
+          pageId={page._id}
+          pagePath={page.path}
+          revisionId={page.revision}
+          highlightKeywords={props.searchingKeyword}
+        />
+      </div>
+    );
+  };
+  const content = renderPage(props.selectedPage);
+  return (
+
+    <div>{content}</div>
+  );
+};
+
+
+export default SearchResultContent;

+ 70 - 45
packages/app/src/components/SearchPage/SearchResultList.jsx

@@ -1,64 +1,89 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import Page from '../PageList/Page';
+import loggerFactory from '~/utils/logger';
 
 
-import RevisionLoader from '../Page/RevisionLoader';
-import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
+const logger = loggerFactory('growi:searchResultList');
 class SearchResultList extends React.Component {
 class SearchResultList extends React.Component {
 
 
-  constructor(props) {
-    super(props);
-
-    this.growiRenderer = this.props.appContainer.getRenderer('searchresult');
-  }
-
   render() {
   render() {
-    const resultList = this.props.pages.map((page) => {
-      const showTags = (page.tags != null) && (page.tags.length > 0);
-
+    return this.props.pages.map((page) => {
+      // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
+      const pageId = `#${page._id}`;
       return (
       return (
-        // Add prefix 'id_' in id attr, because scrollspy of bootstrap doesn't work when the first letter of id of target component is numeral.
-        <div id={`id_${page._id}`} key={page._id} className="search-result-page mb-5">
-          <h2>
-            <a href={page.path} className="text-break">{page.path}</a>
-            { showTags && (
-              <div className="mt-1 small"><i className="tag-icon icon-tag"></i> {page.tags.join(', ')}</div>
-            )}
-          </h2>
-          <RevisionLoader
-            growiRenderer={this.growiRenderer}
-            pageId={page._id}
-            pagePath={page.path}
-            revisionId={page.revision}
-            highlightKeywords={this.props.searchingKeyword}
-          />
-        </div>
+        <li key={page._id} className="nav-item page-list-li w-100 m-0 border-bottom">
+          <a
+            className="nav-link page-list-link d-flex align-items-baseline"
+            href={pageId}
+            onClick={() => {
+              try {
+                if (this.props.onClickInvoked == null) { throw new Error('onClickInvoked is null') }
+                this.props.onClickInvoked(page._id);
+              }
+              catch (error) {
+                logger.error(error);
+              }
+            }}
+          >
+            <div className="form-check my-auto">
+              <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
+            </div>
+            {/* TODO: remove dummy snippet and adjust style */}
+            <div className="d-block">
+              <Page page={page} noLink />
+              <div className="border-gray mt-5">{page.snippet}</div>
+            </div>
+            <div className="ml-auto d-flex">
+              {this.props.deletionMode && (
+                <div className="custom-control custom-checkbox custom-checkbox-danger">
+                  <input
+                    type="checkbox"
+                    id={`page-delete-check-${page._id}`}
+                    className="custom-control-input search-result-list-delete-checkbox"
+                    value={pageId}
+                    checked={this.props.selectedPages.has(page)}
+                    onChange={() => {
+                      try {
+                        if (this.props.onChangeInvoked == null) { throw new Error('onChnageInvoked is null') }
+                        return this.props.onChangeInvoked(page);
+                      }
+                      catch (error) {
+                        logger.error(error);
+                      }
+                    }}
+                  />
+                  <label className="custom-control-label" htmlFor={`page-delete-check-${page._id}`}></label>
+                </div>
+              )}
+              <div className="page-list-option">
+                <button
+                  type="button"
+                  className="btn btn-link p-0"
+                  value={page.path}
+                  onClick={(e) => {
+                    window.location.href = e.currentTarget.value;
+                  }}
+                >
+                  <i className="icon-login" />
+                </button>
+              </div>
+            </div>
+          </a>
+        </li>
       );
       );
     });
     });
-
-    return (
-      <div>
-        {resultList}
-      </div>
-    );
   }
   }
 
 
 }
 }
 
 
-/**
- * Wrapper component for using unstated
- */
-const SearchResultListWrapper = withUnstatedContainers(SearchResultList, [AppContainer]);
 
 
 SearchResultList.propTypes = {
 SearchResultList.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   pages: PropTypes.array.isRequired,
   pages: PropTypes.array.isRequired,
-  searchingKeyword: PropTypes.string.isRequired,
+  deletionMode: PropTypes.bool.isRequired,
+  selectedPages: PropTypes.array.isRequired,
+  onClickInvoked: PropTypes.func,
+  onChangeInvoked: PropTypes.func,
 };
 };
 
 
-SearchResultList.defaultProps = {
-};
 
 
-export default SearchResultListWrapper;
+export default SearchResultList;

+ 1 - 1
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -187,7 +187,7 @@ class RecentChanges extends React.Component {
               className="custom-control-input"
               className="custom-control-input"
               type="checkbox"
               type="checkbox"
               checked={this.state.isRecentChangesSidebarSmall}
               checked={this.state.isRecentChangesSidebarSmall}
-              onChange={e => this.setState({ isRecentChangesSidebarSmall: e.target.checked })}
+              onChange={this.changeSizeHandler}
             />
             />
             <label className="custom-control-label" htmlFor="recentChangesResize">
             <label className="custom-control-label" htmlFor="recentChangesResize">
             </label>
             </label>

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

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:make-email-unique');
 const logger = loggerFactory('growi:migrate:make-email-unique');
 
 
@@ -10,7 +9,7 @@ module.exports = {
 
 
   async up(db, next) {
   async up(db, next) {
     logger.info('Start migration');
     logger.info('Start migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     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 mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:init-serverurl');
 const logger = loggerFactory('growi:migrate:init-serverurl');
@@ -20,7 +20,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // find 'app:siteUrl'
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
     const siteUrlConfig = await Config.findOne({
@@ -77,7 +77,7 @@ module.exports = {
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({

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

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 
 
@@ -29,7 +28,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     if (!isPagegrouprelationsExists) {
     if (!isPagegrouprelationsExists) {
@@ -73,7 +72,7 @@ module.exports = {
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = getModelSafely('Page') || require('~/server/models/page')();
     const Page = getModelSafely('Page') || require('~/server/models/page')();
     const UserGroup = getModelSafely('UserGroup') || require('~/server/models/user-group')();
     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 mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 module.exports = {
 module.exports = {
   async up(db, next) {
   async up(db, next) {
     logger.info('Start migration');
     logger.info('Start migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // enable passport and delete configs for crowi classic auth
     // enable passport and delete configs for crowi classic auth
     await Promise.all([
     await Promise.all([

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

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

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

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

+ 2 - 2
packages/app/src/migrations/20191102223900-drop-configs-indices.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:drop-configs-indices');
 const logger = loggerFactory('growi:migrate:drop-configs-indices');
@@ -14,7 +14,7 @@ async function dropIndexIfExists(collection, indexName) {
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const collection = db.collection('configs');
     const collection = db.collection('configs');
     await dropIndexIfExists(collection, 'ns_1');
     await dropIndexIfExists(collection, 'ns_1');

+ 2 - 2
packages/app/src/migrations/20191102223901-drop-pages-indices.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:drop-pages-indices');
 const logger = loggerFactory('growi:migrate:drop-pages-indices');
@@ -21,7 +21,7 @@ async function dropIndexIfExists(db, collectionName, indexName) {
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await dropIndexIfExists(db, 'pages', 'lastUpdateUser_1');
     await dropIndexIfExists(db, 'pages', 'lastUpdateUser_1');
     await dropIndexIfExists(db, 'pages', 'liker_1');
     await dropIndexIfExists(db, 'pages', 'liker_1');

+ 3 - 3
packages/app/src/migrations/20191126173016-adjust-pages-path.js

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

+ 2 - 2
packages/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:drop-wrong-index-of-page-tag-relation');
 const logger = loggerFactory('growi:migrate:drop-wrong-index-of-page-tag-relation');
@@ -21,7 +21,7 @@ async function dropIndexIfExists(db, collectionName, indexName) {
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
     await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
 
 

+ 2 - 3
packages/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js

@@ -1,15 +1,14 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:remove-deleteduser-from-relationgroup');
 const logger = loggerFactory('growi:migrate:remove-deleteduser-from-relationgroup');
 
 
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     const User = getModelSafely('User') || require('~/server/models/user')();
     const UserGroupRelation = getModelSafely('UserGroupRelation') || require('~/server/models/user-group-relation')();
     const UserGroupRelation = getModelSafely('UserGroupRelation') || require('~/server/models/user-group-relation')();

+ 2 - 2
packages/app/src/migrations/20200420160390-remove-crowi-layout.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const query = { key: 'customize:layout', value: JSON.stringify('crowi') };
     const query = { key: 'customize:layout', value: JSON.stringify('crowi') };
 
 

+ 3 - 3
packages/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-behavior-type');
 const logger = loggerFactory('growi:migrate:remove-behavior-type');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-behavior-type');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Config.findOneAndDelete({ key: 'customize:behavior' }); // remove behavior
     await Config.findOneAndDelete({ key: 'customize:behavior' }); // remove behavior
 
 
@@ -19,7 +19,7 @@ module.exports = {
   async down(db, client) {
   async down(db, client) {
     // do not rollback
     // do not rollback
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
       ns: 'crowi',
       ns: 'crowi',

+ 2 - 2
packages/app/src/migrations/20200514001356-update-theme-color-for-dark.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
 const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Promise.all([
     await Promise.all([
       await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('default-dark') }, { value: JSON.stringify('default') }), // update default-dark
       await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('default-dark') }, { value: JSON.stringify('default') }), // update default-dark

+ 2 - 3
packages/app/src/migrations/20200620203632-normalize-locale-id.js

@@ -1,16 +1,15 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:normalize-locale-id');
 const logger = loggerFactory('growi:migrate:normalize-locale-id');
 
 
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     const User = getModelSafely('User') || require('~/server/models/user')();
 
 

+ 3 - 3
packages/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-layout-setting');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const layoutType = await Config.findOne({ key: 'customize:layout' });
     const layoutType = await Config.findOne({ key: 'customize:layout' });
 
 
@@ -38,7 +38,7 @@ module.exports = {
 
 
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const theme = await Config.findOne({ key: 'customize:theme' });
     const theme = await Config.findOne({ key: 'customize:theme' });
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';

+ 3 - 3
packages/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-layout-setting');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const [accessKeyId, secretAccessKey] = await Promise.all([
     const [accessKeyId, secretAccessKey] = await Promise.all([
       Config.findOne({ key: 'aws:accessKeyId' }),
       Config.findOne({ key: 'aws:accessKeyId' }),
@@ -55,7 +55,7 @@ module.exports = {
 
 
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
     await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
 
 

+ 3 - 3
packages/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:update-mail-transmission');
 const logger = loggerFactory('growi:migrate:update-mail-transmission');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:update-mail-transmission');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const sesExist = await Config.findOne({
     const sesExist = await Config.findOne({
       ns: 'crowi',
       ns: 'crowi',
@@ -33,7 +33,7 @@ module.exports = {
 
 
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // remote 'mail:transmissionMethod'
     // remote 'mail:transmissionMethod'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({

+ 2 - 2
packages/app/src/migrations/20200901034314-update-mail-transmission-fix.js

@@ -1,3 +1,4 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
@@ -5,12 +6,11 @@ import Config from '~/server/models/config';
 const logger = loggerFactory('growi:migrate:update-mail-transmission-fix');
 const logger = loggerFactory('growi:migrate:update-mail-transmission-fix');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const config = require('^/config/migrate');
 
 
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const transmissionMethod = await Config.findOne({
     const transmissionMethod = await Config.findOne({
       ns: 'crowi',
       ns: 'crowi',

+ 3 - 3
packages/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -1,3 +1,4 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
@@ -5,12 +6,11 @@ import Config from '~/server/models/config';
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const config = require('^/config/migrate');
 
 
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Config.findOneAndDelete({ key: 'customize:isEnabledTimeline' }); // remove timeline
     await Config.findOneAndDelete({ key: 'customize:isEnabledTimeline' }); // remove timeline
 
 
@@ -20,7 +20,7 @@ module.exports = {
   async down(db, client) {
   async down(db, client) {
     // do not rollback
     // do not rollback
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
       ns: 'crowi',
       ns: 'crowi',

+ 3 - 3
packages/app/src/migrations/20200915035234-rename-s3-config.js

@@ -1,3 +1,4 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
@@ -5,7 +6,6 @@ import Config from '~/server/models/config';
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const config = require('^/config/migrate');
 
 
 const awsConfigs = [
 const awsConfigs = [
   {
   {
@@ -33,7 +33,7 @@ const awsConfigs = [
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const request = awsConfigs.map((awsConfig) => {
     const request = awsConfigs.map((awsConfig) => {
       return {
       return {
@@ -52,7 +52,7 @@ module.exports = {
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
 
 
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const request = awsConfigs.map((awsConfig) => {
     const request = awsConfigs.map((awsConfig) => {
       return {
       return {

+ 2 - 3
packages/app/src/migrations/20210420160380-convert-double-to-date.js

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

+ 35 - 44
packages/app/src/migrations/20210830074539-update-configs-for-slackbot.js

@@ -1,64 +1,55 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:update-configs-for-slackbot');
 const logger = loggerFactory('growi:migrate:update-configs-for-slackbot');
 
 
+// key: oldKey, value: newKey
+const keyMap = {
+  'slackbot:proxyServerUri': 'slackbot:proxyUri',
+  'slackbot:token': 'slackbot:withoutProxy:botToken',
+  'slackbot:signingSecret': 'slackbot:withoutProxy:signingSecret',
+};
+
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
-    await Config.bulkWrite([
-      {
-        updateOne: {
-          filter: { key: 'slackbot:proxyServerUri' },
-          update: { key: 'slackbot:proxyUri' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:token' },
-          update: { key: 'slackbot:withoutProxy:botToken' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:signingSecret' },
-          update: { key: 'slackbot:withoutProxy:signingSecret' },
-        },
-      },
-    ]);
+    for await (const [oldKey, newKey] of Object.entries(keyMap)) {
+      const isExist = (await Config.count({ key: newKey })) > 0;
+
+      // remove old key
+      if (isExist) {
+        await Config.findOneAndRemove({ key: oldKey });
+      }
+      // update with new key
+      else {
+        await Config.findOneAndUpdate({ key: oldKey }, { key: newKey });
+      }
+    }
 
 
     logger.info('Migration has successfully applied');
     logger.info('Migration has successfully applied');
   },
   },
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
-
-    await Config.bulkWrite([
-      {
-        updateOne: {
-          filter: { key: 'slackbot:proxyUri' },
-          update: { key: 'slackbot:proxyServerUri' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:withoutProxy:botToken' },
-          update: { key: 'slackbot:token' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:withoutProxy:signingSecret' },
-          update: { key: 'slackbot:signingSecret' },
-        },
-      },
-    ]);
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    for await (const [oldKey, newKey] of Object.entries(keyMap)) {
+      const isExist = (await Config.count({ key: oldKey })) > 0;
+
+      // remove new key
+      if (isExist) {
+        await Config.findOneAndRemove({ key: newKey });
+      }
+      // update with old key
+      else {
+        await Config.findOneAndUpdate({ key: newKey }, { key: oldKey });
+      }
+    }
 
 
     logger.info('Migration has successfully applied');
     logger.info('Migration has successfully applied');
   },
   },

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