Przeglądaj źródła

Merge remote-tracking branch 'origin/master' into imprv/107127-refactoring-adminPagesMap

kaori 3 lat temu
rodzic
commit
3950f61a29
49 zmienionych plików z 531 dodań i 445 usunięć
  1. 4 2
      .github/dependabot.yml
  2. 8 0
      .github/workflows/ci-app-prod.yml
  3. 2 2
      .github/workflows/draft-release.yml
  4. 4 4
      .github/workflows/list-unhealthy-branches.yml
  5. 1 1
      .github/workflows/release-rc.yml
  6. 2 2
      .github/workflows/release-slackbot-proxy.yml
  7. 3 3
      .github/workflows/release.yml
  8. 8 4
      .github/workflows/reusable-app-prod.yml
  9. 0 0
      packages/app/_obsolete/src/client/services/AppContainer.js
  10. 0 0
      packages/app/_obsolete/src/client/services/PageContainer.js
  11. 9 9
      packages/app/package.json
  12. 0 2
      packages/app/public/static/locales/en_US/admin.json
  13. 5 1
      packages/app/public/static/locales/en_US/translation.json
  14. 0 2
      packages/app/public/static/locales/ja_JP/admin.json
  15. 5 1
      packages/app/public/static/locales/ja_JP/translation.json
  16. 0 2
      packages/app/public/static/locales/zh_CN/admin.json
  17. 5 1
      packages/app/public/static/locales/zh_CN/translation.json
  18. 4 45
      packages/app/src/client/util/smooth-scroll.ts
  19. 2 4
      packages/app/src/components/Admin/FullTextSearchManagement.tsx
  20. 35 37
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  21. 22 31
      packages/app/src/components/ContentLinkButtons.tsx
  22. 35 24
      packages/app/src/components/Fab.tsx
  23. 1 1
      packages/app/src/components/IdenticalPathPage.tsx
  24. 39 19
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  25. 19 2
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  26. 4 2
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  27. 2 1
      packages/app/src/components/Page/DisplaySwitcher.tsx
  28. 14 8
      packages/app/src/components/Page/RevisionLoader.tsx
  29. 9 0
      packages/app/src/components/PageAttachment/DeleteAttachmentModal.module.scss
  30. 9 1
      packages/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  31. 60 56
      packages/app/src/components/PageComment.tsx
  32. 0 1
      packages/app/src/components/PageComment/Comment.tsx
  33. 1 3
      packages/app/src/components/PageComment/ReplyComments.tsx
  34. 0 1
      packages/app/src/components/PageTimeline.tsx
  35. 14 9
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  36. 2 1
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  37. 58 30
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  38. 2 2
      packages/app/src/components/Sidebar/PageTree.tsx
  39. 2 1
      packages/app/src/pages/[[...path]].page.tsx
  40. 2 1
      packages/app/src/pages/_app.page.tsx
  41. 33 4
      packages/app/src/pages/trash.page.tsx
  42. 3 2
      packages/app/src/pages/utils/commons.ts
  43. 36 33
      packages/app/src/server/models/page.ts
  44. 2 2
      packages/app/src/server/service/page.ts
  45. 11 9
      packages/app/src/services/renderer/rehype-plugins/keyword-highlighter.ts
  46. 4 0
      packages/app/src/stores/context.tsx
  47. 0 10
      packages/app/src/styles/_attachments.scss
  48. 2 1
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  49. 48 68
      yarn.lock

+ 4 - 2
.github/dependabot.yml

@@ -2,16 +2,18 @@ version: 2
 updates:
 updates:
   - package-ecosystem: github-actions
   - package-ecosystem: github-actions
     directory: '/'
     directory: '/'
+    open-pull-requests-limit: 0
     schedule:
     schedule:
-      interval: daily
+      interval: monthly
     commit-message:
     commit-message:
       prefix: ci
       prefix: ci
       include: scope
       include: scope
 
 
   - package-ecosystem: npm
   - package-ecosystem: npm
     directory: '/'
     directory: '/'
+    open-pull-requests-limit: 0
     schedule:
     schedule:
-      interval: daily
+      interval: weekly
     commit-message:
     commit-message:
       prefix: ci
       prefix: ci
       include: scope
       include: scope

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

@@ -36,6 +36,13 @@ on:
       - packages/slack/**
       - packages/slack/**
       - packages/ui/**
       - packages/ui/**
       - packages/plugin-**
       - packages/plugin-**
+  workflow_call:
+    inputs:
+      cypress-config-video:
+        description: 'Enable video when running Cypress test'
+        type: boolean
+        default: false
+
 
 
 jobs:
 jobs:
 
 
@@ -54,6 +61,7 @@ jobs:
       node-version: 16.x
       node-version: 16.x
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
       cypress-report-artifact-name: Cypress report
       cypress-report-artifact-name: Cypress report
+      cypress-config-video: ${{ inputs.cypress-config-video || false }}
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 

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

@@ -19,7 +19,7 @@ jobs:
       - uses: actions/checkout@v3
       - uses: actions/checkout@v3
 
 
       - name: Retrieve information from package.json
       - name: Retrieve information from package.json
-        uses: myrotvorets/info-from-package-json-action@1.1.0
+        uses: myrotvorets/info-from-package-json-action@1.2.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"
@@ -48,7 +48,7 @@ jobs:
         id: release-version
         id: release-version
         run: |
         run: |
           RELEASE_VERSION=`npx semver -i patch ${{ needs.update-release-draft.outputs.CURRENT_VERSION }}`
           RELEASE_VERSION=`npx semver -i patch ${{ needs.update-release-draft.outputs.CURRENT_VERSION }}`
-          echo ::set-output name=RELEASE_VERSION::$RELEASE_VERSION
+          echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT
 
 
       # See: https://github.com/bakunyo/git-pr-release-action/issues/15, https://github.com/samunohito/SimpleVolumeMixer/commit/2059044c71236509466cf9b1bb2d56d515274938
       # See: https://github.com/bakunyo/git-pr-release-action/issues/15, https://github.com/samunohito/SimpleVolumeMixer/commit/2059044c71236509466cf9b1bb2d56d515274938
       - name: Create/Update Pull Request
       - name: Create/Update Pull Request

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

@@ -23,10 +23,10 @@ jobs:
       run: |
       run: |
         export SLACK_ATTACHMENTS_ILLEGAL=`node bin/github-actions/list-branches --illegal`
         export SLACK_ATTACHMENTS_ILLEGAL=`node bin/github-actions/list-branches --illegal`
         export SLACK_ATTACHMENTS_INACTIVE=`node bin/github-actions/list-branches --inactive`
         export SLACK_ATTACHMENTS_INACTIVE=`node bin/github-actions/list-branches --inactive`
-        echo ::set-output name=SLACK_ATTACHMENTS_ILLEGAL::$SLACK_ATTACHMENTS_ILLEGAL
-        echo ::set-output name=SLACK_ATTACHMENTS_INACTIVE::$SLACK_ATTACHMENTS_INACTIVE
-        echo ::set-output name=SLACK_ATTACHMENTS_LENGTH_ILLEGAL::$(echo $SLACK_ATTACHMENTS_ILLEGAL | jq '. | length')
-        echo ::set-output name=SLACK_ATTACHMENTS_LENGTH_INACTIVE::$(echo $SLACK_ATTACHMENTS_INACTIVE | jq '. | length')
+        echo "SLACK_ATTACHMENTS_ILLEGAL=$SLACK_ATTACHMENTS_ILLEGAL" >> $GITHUB_OUTPUT
+        echo "SLACK_ATTACHMENTS_INACTIVE=$SLACK_ATTACHMENTS_INACTIVE" >> $GITHUB_OUTPUT
+        echo "SLACK_ATTACHMENTS_LENGTH_ILLEGAL=$(echo $SLACK_ATTACHMENTS_ILLEGAL | jq '. | length')" >> $GITHUB_OUTPUT
+        echo "SLACK_ATTACHMENTS_LENGTH_INACTIVE=$(echo $SLACK_ATTACHMENTS_INACTIVE | jq '. | length')" >> $GITHUB_OUTPUT
 
 
     - name: Slack Notification for illegal named branches
     - name: Slack Notification for illegal named branches
       if: steps.list-branches.outputs.SLACK_ATTACHMENTS_LENGTH_ILLEGAL > 0
       if: steps.list-branches.outputs.SLACK_ATTACHMENTS_LENGTH_ILLEGAL > 0

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

@@ -17,7 +17,7 @@ jobs:
         lfs: true
         lfs: true
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.1.0
+      uses: myrotvorets/info-from-package-json-action@1.2.0
       id: package-json
       id: package-json
 
 
     - name: Docker meta
     - name: Docker meta

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

@@ -17,7 +17,7 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.1.0
+      uses: myrotvorets/info-from-package-json-action@1.2.0
       id: package-json
       id: package-json
       with:
       with:
         workingDir: packages/slackbot-proxy
         workingDir: packages/slackbot-proxy
@@ -115,7 +115,7 @@ jobs:
         yarn bump-versions:slackbot-proxy
         yarn bump-versions:slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.1.0
+      uses: myrotvorets/info-from-package-json-action@1.2.0
       id: package-json
       id: package-json
       with:
       with:
         workingDir: packages/slackbot-proxy
         workingDir: packages/slackbot-proxy

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

@@ -38,7 +38,7 @@ jobs:
         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@1.1.0
+      uses: myrotvorets/info-from-package-json-action@1.2.0
       id: package-json
       id: package-json
 
 
     - name: Update Changelog
     - name: Update Changelog
@@ -99,7 +99,7 @@ jobs:
         yarn bump-versions:slackbot-proxy
         yarn bump-versions:slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@1.1.0
+      uses: myrotvorets/info-from-package-json-action@1.2.0
       id: package-json
       id: package-json
 
 
     - name: Commit
     - name: Commit
@@ -140,7 +140,7 @@ jobs:
       id: suffix
       id: suffix
       run: |
       run: |
         [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
         [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
-        echo "::set-output name=SUFFIX::$suffix"
+        echo "SUFFIX=$suffix" >> $GITHUB_OUTPUT
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta

+ 8 - 4
.github/workflows/reusable-app-prod.yml

@@ -10,6 +10,9 @@ on:
         type: boolean
         type: boolean
       cypress-report-artifact-name:
       cypress-report-artifact-name:
         type: string
         type: string
+      cypress-config-video:
+        type: boolean
+        default: false
     secrets:
     secrets:
       SLACK_WEBHOOK_URL:
       SLACK_WEBHOOK_URL:
         required: true
         required: true
@@ -70,7 +73,7 @@ jobs:
           packages/app/.env.production* \
           packages/app/.env.production* \
           packages/*/package.json \
           packages/*/package.json \
           packages/*/dist
           packages/*/dist
-        echo ::set-output name=file::production.tar.gz
+        echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
 
     - name: Upload production files as artifact
     - name: Upload production files as artifact
       uses: actions/upload-artifact@v3
       uses: actions/upload-artifact@v3
@@ -126,8 +129,8 @@ jobs:
     - name: Get Date
     - name: Get Date
       id: get-date
       id: get-date
       run: |
       run: |
-        echo "::set-output name=dateYmdHM::$(/bin/date -u "+%Y%m%d%H%M")"
-        echo "::set-output name=dateYm::$(/bin/date -u "+%Y%m")"
+        echo "dateYmdHM=$(/bin/date -u "+%Y%m%d%H%M")" >> $GITHUB_OUTPUT
+        echo "dateYm=$(/bin/date -u "+%Y%m")" >> $GITHUB_OUTPUT
 
 
     - name: Cache/Restore node_modules (not reused)
     - name: Cache/Restore node_modules (not reused)
       id: cache-dependencies
       id: cache-dependencies
@@ -247,7 +250,7 @@ jobs:
       id: determine-spec-exp
       id: determine-spec-exp
       run: |
       run: |
         SPEC=`node bin/github-actions/generate-cypress-spec-arg.js --prefix="test/cypress/integration/" --suffix="-*/**" "${{ matrix.spec-group }}"`
         SPEC=`node bin/github-actions/generate-cypress-spec-arg.js --prefix="test/cypress/integration/" --suffix="-*/**" "${{ matrix.spec-group }}"`
-        echo "::set-output name=value::$SPEC"
+        echo "value=$SPEC" >> $GITHUB_OUTPUT
 
 
     - name: Copy dotenv file for ci
     - name: Copy dotenv file for ci
       working-directory: ./packages/app
       working-directory: ./packages/app
@@ -274,6 +277,7 @@ jobs:
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         start: yarn server
         start: yarn server
         wait-on: 'http://localhost:3000'
         wait-on: 'http://localhost:3000'
+        config: video=${{ inputs.cypress-config-video }}
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
         ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi

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


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


+ 9 - 9
packages/app/package.json

@@ -116,9 +116,9 @@
     "hast-util-select": "^5.0.2",
     "hast-util-select": "^5.0.2",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "http-errors": "^2.0.0",
-    "i18next-chained-backend": "^3.0.2",
-    "i18next-http-backend": "^1.4.1",
-    "i18next-localstorage-backend": "^3.1.3",
+    "i18next-chained-backend": "^4.0.0",
+    "i18next-http-backend": "^2.0.0",
+    "i18next-localstorage-backend": "^4.0.0",
     "is-absolute-url": "^4.0.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "is-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
     "lucene-query-parser": "^1.2.0",
@@ -133,7 +133,7 @@
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "next": "^12.2.5",
     "next": "^12.2.5",
-    "next-i18next": "^11.3.0",
+    "next-i18next": "^12.1.0",
     "next-superjson": "^0.0.4",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
     "next-themes": "^0.2.0",
     "nocache": "^3.0.1",
     "nocache": "^3.0.1",
@@ -162,9 +162,9 @@
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
     "react-markdown": "^8.0.3",
     "react-markdown": "^8.0.3",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
+    "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
     "react-use-ripple": "^1.5.2",
     "react-use-ripple": "^1.5.2",
-    "react-scroll": "^1.8.7",
     "reactstrap": "^8.9.0",
     "reactstrap": "^8.9.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
@@ -192,20 +192,19 @@
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
+    "unstated": "^2.1.1",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",
     "validator": "^13.7.0",
     "ws": "^8.3.0",
     "ws": "^8.3.0",
-    "xss": "^1.0.6",
-    "unstated": "^2.1.1"
+    "xss": "^1.0.6"
   },
   },
   "// comments for defDependencies": {
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@alienfast/i18next-loader": "^1.1.4",
     "@growi/ui": "^6.0.0-RC.7",
     "@growi/ui": "^6.0.0-RC.7",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
@@ -214,6 +213,7 @@
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/jquery": "^3.5.8",
     "@types/jquery": "^3.5.8",
     "@types/multer": "^1.4.5",
     "@types/multer": "^1.4.5",
+    "@types/react-scroll": "^1.8.4",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
     "bootstrap": "^4.6.1",
     "bootstrap": "^4.6.1",
@@ -231,7 +231,7 @@
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "font-awesome": "^4.7.0",
     "font-awesome": "^4.7.0",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
-    "i18next-hmr": "^1.7.7",
+    "i18next-hmr": "^1.11.0",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "jshint": "^2.13.0",

+ 0 - 2
packages/app/public/static/locales/en_US/admin.json

@@ -290,8 +290,6 @@
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>then submit your issue to GitHub.</a>"
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>then submit your issue to GitHub.</a>"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
-    "page_tree_not_avaliable" : "Page tree feature is not available yet.",
-    "go_to_settings": "Go to settings to enable the feature",
     "migration_desc": "There are some pages with old v4 compatibility. To take advantage of new features such as page trees and easy renaming, please convert all your pages to v5 compatibility.",
     "migration_desc": "There are some pages with old v4 compatibility. To take advantage of new features such as page trees and easy renaming, please convert all your pages to v5 compatibility.",
     "migration_note": "Note: You will lose unique constraints from the page paths.",
     "migration_note": "Note: You will lose unique constraints from the page paths.",
     "upgrade_to_v5": "Convert to v5 compatibility",
     "upgrade_to_v5": "Convert to v5 compatibility",

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

@@ -115,7 +115,7 @@
   "Create under": "Create page under below:",
   "Create under": "Create page under below:",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "See more detail on <a href='#'>{{url}}</a> <i class='icon-share-alt'></i> ",
+  "See_more_detail_on_new_schema": "See more detail on <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a> <i class='icon-share-alt'></i> ",
   "Site URL settings": "Site URL settings",
   "Site URL settings": "Site URL settings",
   "external_account_management": "External Account Management",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "UserGroup": "UserGroup",
@@ -863,5 +863,9 @@
   "footer": {
   "footer": {
     "bookmarks": "Bookmarks",
     "bookmarks": "Bookmarks",
     "recently_created": "Recently Created"
     "recently_created": "Recently Created"
+  },
+  "v5_page_migration": {
+    "page_tree_not_avaliable" : "Page tree feature is not available yet.",
+    "go_to_settings": "Go to settings to enable the feature"
   }
   }
 }
 }

+ 0 - 2
packages/app/public/static/locales/ja_JP/admin.json

@@ -322,8 +322,6 @@
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
-    "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
-    "go_to_settings": "設定する",
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",
     "migration_note": "注意: ページパスからユニーク制約が失われます。",
     "migration_note": "注意: ページパスからユニーク制約が失われます。",
     "upgrade_to_v5": "v5 互換形式 へ変換",
     "upgrade_to_v5": "v5 互換形式 へ変換",

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

@@ -109,7 +109,7 @@
   "Create under": "ページを以下に作成",
   "Create under": "ページを以下に作成",
   "V5 Page Migration": "V5 互換形式 への変換",
   "V5 Page Migration": "V5 互換形式 への変換",
   "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
   "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
-  "See_more_detail_on_new_schema": "詳しくは<a href='#'>{{url}}</a><i class='icon-share-alt'></i>を参照ください。",
+  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><i class='icon-share-alt'></i>を参照ください。",
   "Site URL settings": "サイトURL設定",
   "Site URL settings": "サイトURL設定",
   "external_account_management": "外部アカウント管理",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
   "UserGroup": "グループ",
@@ -854,5 +854,9 @@
   "footer": {
   "footer": {
     "bookmarks": "ブックマーク",
     "bookmarks": "ブックマーク",
     "recently_created": "最近作成したページ"
     "recently_created": "最近作成したページ"
+  },
+  "v5_page_migration": {
+    "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
+    "go_to_settings": "設定する"
   }
   }
 }
 }

+ 0 - 2
packages/app/public/static/locales/zh_CN/admin.json

@@ -277,8 +277,6 @@
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
-    "page_tree_not_avaliable": "Page Tree 功能不可用",
-    "go_to_settings": "进入设置,启用该功能",
     "migration_desc": "有一些页面具有旧的v4兼容性。为了利用新的功能,如页面树和容易重命名,请将您的所有页面转换为v5兼容性。",
     "migration_desc": "有一些页面具有旧的v4兼容性。为了利用新的功能,如页面树和容易重命名,请将您的所有页面转换为v5兼容性。",
     "migration_note": "注意:你将失去页面路径的唯一约束。",
     "migration_note": "注意:你将失去页面路径的唯一约束。",
     "upgrade_to_v5": "转换为v5兼容性",
     "upgrade_to_v5": "转换为v5兼容性",

+ 5 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -117,7 +117,7 @@
 	"Create under": "Create page under below:",
 	"Create under": "Create page under below:",
   "V5 Page Migration": "转换为V5的兼容性",
   "V5 Page Migration": "转换为V5的兼容性",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "更多详情请见<a href='#'>{{url}}</a> <i class='icon-share-alt'></i> ",
+  "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <i class='icon-share-alt'></i> ",
 	"Site URL settings": "主页URL设置",
 	"Site URL settings": "主页URL设置",
 	"Markdown Settings": "Markdown设置",
 	"Markdown Settings": "Markdown设置",
 	"Notification Settings": "通知设置",
 	"Notification Settings": "通知设置",
@@ -909,5 +909,9 @@
   "footer": {
   "footer": {
     "bookmarks": "书签",
     "bookmarks": "书签",
     "recently_created": "最近创建页面"
     "recently_created": "最近创建页面"
+  },
+  "v5_page_migration": {
+    "page_tree_not_avaliable": "Page Tree 功能不可用",
+    "go_to_settings": "进入设置,启用该功能"
   }
   }
 }
 }

+ 4 - 45
packages/app/src/client/util/smooth-scroll.ts

@@ -1,46 +1,5 @@
-const WIKI_HEADER_LINK = 120;
-
-export const smoothScrollIntoView = (
-    element: HTMLElement = window.document.body, offsetTop = 0, scrollElement: HTMLElement | Window = window,
-): void => {
-
-  // get the distance to the target element top
-  const rectTop = element.getBoundingClientRect().top;
-
-  const top = window.pageYOffset + rectTop - offsetTop;
-
-  scrollElement.scrollTo({
-    top,
-    behavior: 'smooth',
-  });
-};
-
-export type SmoothScrollEventCallback = (elem: HTMLElement) => void;
-
-export const addSmoothScrollEvent = (elements: HTMLAnchorElement[], callback?: SmoothScrollEventCallback): void => {
-  elements.forEach((link) => {
-    const href = link.getAttribute('href');
-
-    if (href == null) {
-      return;
-    }
-
-    link.addEventListener('click', (e) => {
-      e.preventDefault();
-
-      // modify location.hash without scroll
-      window.history.pushState({}, '', link.href);
-
-      // smooth scroll
-      const elemId = href.replace('#', '');
-      const targetDom = document.getElementById(elemId);
-      if (targetDom != null) {
-        smoothScrollIntoView(targetDom, WIKI_HEADER_LINK);
-
-        if (callback != null) {
-          callback(targetDom);
-        }
-      }
-    });
-  });
+// option object for react-scroll
+export const DEFAULT_AUTO_SCROLL_OPTS = {
+  smooth: 'easeOutQuint',
+  duration: 1200,
 };
 };

+ 2 - 4
packages/app/src/components/Admin/FullTextSearchManagement.tsx

@@ -4,8 +4,8 @@ import { useTranslation } from 'next-i18next';
 
 
 import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
 import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
 
 
-const FullTextSearchManagement = (): JSX.Element => {
-  const { t } = useTranslation();
+export const FullTextSearchManagement = (): JSX.Element => {
+  const { t } = useTranslation('admin');
 
 
   return (
   return (
     <div data-testid="admin-full-text-search">
     <div data-testid="admin-full-text-search">
@@ -14,5 +14,3 @@ const FullTextSearchManagement = (): JSX.Element => {
     </div>
     </div>
   );
   );
 };
 };
-
-export default FullTextSearchManagement;

+ 35 - 37
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -103,54 +103,48 @@ class ImportForm extends React.Component {
   setupWebsocketEventHandler() {
   setupWebsocketEventHandler() {
     const { socket } = this.props;
     const { socket } = this.props;
 
 
-    if (socket != null) {
-      // websocket event
-      // eslint-disable-next-line object-curly-newline
-      socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
-        const { progressMap, errorsMap } = this.state;
-        progressMap[collectionName] = collectionProgress;
-
-        const errors = errorsMap[collectionName] || [];
-        errorsMap[collectionName] = errors.concat(appendedErrors);
-
-        this.setState({
-          isImporting: true,
-          progressMap,
-          errorsMap,
-        });
+    // websocket event
+    // eslint-disable-next-line object-curly-newline
+    socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
+      const { progressMap, errorsMap } = this.state;
+      progressMap[collectionName] = collectionProgress;
+
+      const errors = errorsMap[collectionName] || [];
+      errorsMap[collectionName] = errors.concat(appendedErrors);
+
+      this.setState({
+        isImporting: true,
+        progressMap,
+        errorsMap,
       });
       });
+    });
 
 
-      // websocket event
-      socket.on('admin:onTerminateForImport', () => {
-        this.setState({
-          isImporting: false,
-          isImported: true,
-        });
-
-        toastSuccess(undefined, 'Import process has completed.');
+    // websocket event
+    socket.on('admin:onTerminateForImport', () => {
+      this.setState({
+        isImporting: false,
+        isImported: true,
       });
       });
 
 
-      // websocket event
-      socket.on('admin:onErrorForImport', (err) => {
-        this.setState({
-          isImporting: false,
-          isImported: false,
-        });
+      toastSuccess(undefined, 'Import process has completed.');
+    });
 
 
-        toastError(err, 'Import process has failed.');
+    // websocket event
+    socket.on('admin:onErrorForImport', (err) => {
+      this.setState({
+        isImporting: false,
+        isImported: false,
       });
       });
 
 
-    }
-
+      toastError(err, 'Import process has failed.');
+    });
   }
   }
 
 
   teardownWebsocketEventHandler() {
   teardownWebsocketEventHandler() {
     const { socket } = this.props;
     const { socket } = this.props;
 
 
-    if (socket != null) {
-      socket.removeAllListeners('admin:onProgressForImport');
-      socket.removeAllListeners('admin:onTerminateForImport');
-    }
+    socket.removeAllListeners('admin:onProgressForImport');
+    socket.removeAllListeners('admin:onTerminateForImport');
   }
   }
 
 
   async toggleCheckbox(collectionName, bool) {
   async toggleCheckbox(collectionName, bool) {
@@ -500,7 +494,7 @@ class ImportForm extends React.Component {
 
 
 ImportForm.propTypes = {
 ImportForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  socket: PropTypes.object,
+  socket: PropTypes.object.isRequired,
 
 
   fileName: PropTypes.string,
   fileName: PropTypes.string,
   innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
   innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -512,6 +506,10 @@ const ImportFormWrapperFc = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { data: socket } = useAdminSocket();
   const { data: socket } = useAdminSocket();
 
 
+  if (socket == null) {
+    return;
+  }
+
   return <ImportForm t={t} socket={socket} {...props} />;
   return <ImportForm t={t} socket={socket} {...props} />;
 };
 };
 
 

+ 22 - 31
packages/app/src/components/ContentLinkButtons.tsx

@@ -1,31 +1,27 @@
-import React, { useCallback } from 'react';
+import React from 'react';
 
 
 import { IUserHasId } from '@growi/core';
 import { IUserHasId } from '@growi/core';
+import { Link as ScrollLink } from 'react-scroll';
 
 
-import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
+import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 
 
 import styles from './ContentLinkButtons.module.scss';
 import styles from './ContentLinkButtons.module.scss';
 
 
-const WIKI_HEADER_LINK = 120;
+const OFFSET = -120;
 
 
 const BookMarkLinkButton = React.memo(() => {
 const BookMarkLinkButton = React.memo(() => {
 
 
-  const BookMarkLinkButtonClickHandler = useCallback(() => {
-    const getBookMarkListHeaderDom = document.getElementById('bookmarks-list');
-    if (getBookMarkListHeaderDom == null) { return }
-    smoothScrollIntoView(getBookMarkListHeaderDom, WIKI_HEADER_LINK);
-  }, []);
-
   return (
   return (
-    <button
-      type="button"
-      className="btn btn-outline-secondary btn-sm px-2"
-      onClick={BookMarkLinkButtonClickHandler}
-    >
-      <i className="fa fa-fw fa-bookmark-o"></i>
-      <span>Bookmarks</span>
-    </button>
+    <ScrollLink to="bookmarks-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+      <button
+        type="button"
+        className="btn btn-outline-secondary btn-sm px-2"
+      >
+        <i className="fa fa-fw fa-bookmark-o"></i>
+        <span>Bookmarks</span>
+      </button>
+    </ScrollLink>
   );
   );
 });
 });
 
 
@@ -33,21 +29,16 @@ BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 
 
 const RecentlyCreatedLinkButton = React.memo(() => {
 const RecentlyCreatedLinkButton = React.memo(() => {
 
 
-  const RecentlyCreatedListButtonClickHandler = useCallback(() => {
-    const getRecentlyCreatedListHeaderDom = document.getElementById('recently-created-list');
-    if (getRecentlyCreatedListHeaderDom == null) { return }
-    smoothScrollIntoView(getRecentlyCreatedListHeaderDom, WIKI_HEADER_LINK);
-  }, []);
-
   return (
   return (
-    <button
-      type="button"
-      className="btn btn-outline-secondary btn-sm px-3"
-      onClick={RecentlyCreatedListButtonClickHandler}
-    >
-      <i className={`${styles['grw-icon-container-recently-created']} grw-icon-container-recently-created mr-2`}><RecentlyCreatedIcon /></i>
-      <span>Recently Created</span>
-    </button>
+    <ScrollLink to="recently-created-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+      <button
+        type="button"
+        className="btn btn-outline-secondary btn-sm px-3"
+      >
+        <i className={`${styles['grw-icon-container-recently-created']} grw-icon-container-recently-created mr-2`}><RecentlyCreatedIcon /></i>
+        <span>Recently Created</span>
+      </button>
+    </ScrollLink>
   );
   );
 });
 });
 
 

+ 35 - 24
packages/app/src/components/Fab.tsx

@@ -1,11 +1,12 @@
 import React, {
 import React, {
-  useState, useCallback, useEffect, useRef,
+  useState, useCallback, useRef,
 } from 'react';
 } from 'react';
 
 
+import { animateScroll } from 'react-scroll';
 import { useRipple } from 'react-use-ripple';
 import { useRipple } from 'react-use-ripple';
 import StickyEvents from 'sticky-events';
 import StickyEvents from 'sticky-events';
 
 
-import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
+import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { useCurrentPagePath, useCurrentUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentUser } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -59,39 +60,49 @@ export const Fab = (): JSX.Element => {
   //   };
   //   };
   // }, [stickyChangeHandler]);
   // }, [stickyChangeHandler]);
 
 
-  if (currentPath == null) {
-    return <></>;
-  }
-
-  const renderPageCreateButton = () => {
+  const PageCreateButton = useCallback(() => {
     return (
     return (
-      <>
-        <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
-          <button
-            type="button"
-            className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 ${buttonClasses}`}
-            ref={createBtnRef}
-            onClick={() => openCreateModal(currentPath)}
-          >
-            <CreatePageIcon />
-          </button>
-        </div>
-      </>
+      <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
+        <button
+          type="button"
+          className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 ${buttonClasses}`}
+          ref={createBtnRef}
+          onClick={currentPath != null
+            ? () => openCreateModal(currentPath)
+            : undefined}
+        >
+          <CreatePageIcon />
+        </button>
+      </div>
     );
     );
-  };
+  }, [animateClasses, buttonClasses, currentPath, openCreateModal]);
 
 
-  return (
-    <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab-container">
-      {currentUser != null && renderPageCreateButton()}
+  const ScrollToTopButton = useCallback(() => {
+    const clickHandler = () => {
+      animateScroll.scrollToTop(DEFAULT_AUTO_SCROLL_OPTS);
+    };
+
+    return (
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }} data-testid="grw-fab-return-to-top">
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }} data-testid="grw-fab-return-to-top">
         <button
         <button
           type="button"
           type="button"
           className={`btn btn-light btn-scroll-to-top rounded-circle p-0 ${buttonClasses}`}
           className={`btn btn-light btn-scroll-to-top rounded-circle p-0 ${buttonClasses}`}
-          onClick={() => smoothScrollIntoView()}
+          onClick={clickHandler}
         >
         >
           <ReturnTopIcon />
           <ReturnTopIcon />
         </button>
         </button>
       </div>
       </div>
+    );
+  }, [animateClasses, buttonClasses]);
+
+  if (currentPath == null) {
+    return <></>;
+  }
+
+  return (
+    <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab-container">
+      {currentUser != null && <PageCreateButton />}
+      <ScrollToTopButton />
     </div>
     </div>
   );
   );
 
 

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

@@ -40,7 +40,7 @@ const IdenticalPathAlert : FC<IdenticalPathAlertProps> = (props: IdenticalPathAl
           { path: _path, pageName: _pageName })}<br />
           { path: _path, pageName: _pageName })}<br />
         <span
         <span
           // eslint-disable-next-line react/no-danger
           // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('See_more_detail_on_new_schema', { url: t('GROWI.5.0_new_schema') }) }}
+          dangerouslySetInnerHTML={{ __html: t('See_more_detail_on_new_schema', { title: t('GROWI.5.0_new_schema') }) }}
         />
         />
       </p>
       </p>
       <p className="mb-1">{t('duplicated_page_alert.select_page_to_see')}</p>
       <p className="mb-1">{t('duplicated_page_alert.select_page_to_see')}</p>

+ 39 - 19
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -61,25 +61,19 @@ const AuthorInfo = dynamic(() => import('./AuthorInfo'), {
   loading: AuthorInfoSkelton,
   loading: AuthorInfoSkelton,
 });
 });
 
 
-type AdditionalMenuItemsProps = {
+type PageOperationMenuItemsProps = {
   pageId: string,
   pageId: string,
   revisionId: string,
   revisionId: string,
   isLinkSharingDisabled?: boolean,
   isLinkSharingDisabled?: boolean,
-  onClickTemplateMenuItem: (isPageTemplateModalShown: boolean) => void,
-
 }
 }
 
 
-const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
+const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    pageId, revisionId, isLinkSharingDisabled, onClickTemplateMenuItem,
+    pageId, revisionId, isLinkSharingDisabled,
   } = props;
   } = props;
 
 
-  const openPageTemplateModalHandler = () => {
-    onClickTemplateMenuItem(true);
-  };
-
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
 
 
@@ -151,9 +145,25 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
         </span>
         </span>
         {t('share_links.share_link_management')}
         {t('share_links.share_link_management')}
       </DropdownItem>
       </DropdownItem>
+    </>
+  );
+};
 
 
-      <DropdownItem divider />
+type CreateTemplateMenuItemsProps = {
+  onClickTemplateMenuItem: (isPageTemplateModalShown: boolean) => void,
+}
 
 
+const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { onClickTemplateMenuItem } = props;
+
+  const openPageTemplateModalHandler = () => {
+    onClickTemplateMenuItem(true);
+  };
+
+  return (
+    <>
       {/* Create template */}
       {/* Create template */}
       <DropdownItem
       <DropdownItem
         onClick={openPageTemplateModalHandler}
         onClick={openPageTemplateModalHandler}
@@ -175,7 +185,6 @@ type GrowiContextualSubNavigationProps = {
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 
 
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const path = currentPage?.path;
 
 
   const revision = currentPage?.revision;
   const revision = currentPage?.revision;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
@@ -203,6 +212,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: templateTagData } = useTemplateTagData();
   const { data: templateTagData } = useTemplateTagData();
 
 
+  const path = currentPage?.path ?? currentPathname;
 
 
   useEffect(() => {
   useEffect(() => {
     // Run only when tagsInfoData has been updated
     // Run only when tagsInfoData has been updated
@@ -306,15 +316,25 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const RightComponent = useCallback(() => {
   const RightComponent = useCallback(() => {
     const additionalMenuItemsRenderer = () => {
     const additionalMenuItemsRenderer = () => {
       if (revisionId == null || pageId == null) {
       if (revisionId == null || pageId == null) {
-        return <></>;
+        return (
+          <>
+            <CreateTemplateMenuItems
+              onClickTemplateMenuItem={templateMenuItemClickHandler}
+            />
+          </>);
       }
       }
       return (
       return (
-        <AdditionalMenuItems
-          pageId={pageId}
-          revisionId={revisionId}
-          isLinkSharingDisabled={isLinkSharingDisabled}
-          onClickTemplateMenuItem={templateMenuItemClickHandler}
-        />
+        <>
+          <PageOperationMenuItems
+            pageId={pageId}
+            revisionId={revisionId}
+            isLinkSharingDisabled={isLinkSharingDisabled}
+          />
+          <DropdownItem divider />
+          <CreateTemplateMenuItems
+            onClickTemplateMenuItem={templateMenuItemClickHandler}
+          />
+        </>
       );
       );
     };
     };
 
 
@@ -377,7 +397,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       </>
       </>
     );
     );
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [isCompactMode, isViewMode, pageId, revisionId, shareLinkId, path, isSharedUser, isAbleToShowPageManagement, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, isAbleToShowPageEditorModeManager, isGuestUser, editorMode, isAbleToShowPageAuthors, currentPage, currentUser, isPageTemplateModalShown, isLinkSharingDisabled, templateMenuItemClickHandler, mutateEditorMode]);
+  }, [isCompactMode, isViewMode, pageId, revisionId, shareLinkId, path, currentPathname, isSharedUser, isAbleToShowPageManagement, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, isAbleToShowPageEditorModeManager, isGuestUser, editorMode, isAbleToShowPageAuthors, currentPage, currentUser, isPageTemplateModalShown, isLinkSharingDisabled, templateMenuItemClickHandler, mutateEditorMode]);
 
 
 
 
   const pagePath = isNotFound
   const pagePath = isNotFound

+ 19 - 2
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -5,12 +5,13 @@ import React, {
 import { isServer } from '@growi/core';
 import { isServer } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Image from 'next/image';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import {
 import {
-  useIsSearchPage, useCurrentPagePath, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential,
+  useIsSearchPage, useCurrentPagePath, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
@@ -119,6 +120,21 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 });
 });
 Confidential.displayName = 'Confidential';
 Confidential.displayName = 'Confidential';
 
 
+interface NavbarLogoProps {
+  logoSrc?: string,
+}
+
+const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
+  const { logoSrc } = props;
+
+  return logoSrc != null
+    // eslint-disable-next-line @next/next/no-img-element
+    ? (<img src={logoSrc} alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />)
+    : <GrowiLogo />;
+});
+
+GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
+
 export const GrowiNavbar = (): JSX.Element => {
 export const GrowiNavbar = (): JSX.Element => {
 
 
   const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });
   const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });
@@ -128,6 +144,7 @@ export const GrowiNavbar = (): JSX.Element => {
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isSearchPage } = useIsSearchPage();
   const { data: isSearchPage } = useIsSearchPage();
+  const { data: customizedLogoSrc } = useCustomizedLogoSrc();
 
 
   return (
   return (
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
@@ -135,7 +152,7 @@ export const GrowiNavbar = (): JSX.Element => {
       <div className="navbar-brand mr-0">
       <div className="navbar-brand mr-0">
         <Link href="/" prefetch={false}>
         <Link href="/" prefetch={false}>
           <a className="grw-logo d-block">
           <a className="grw-logo d-block">
-            <GrowiLogo />
+            <GrowiNavbarLogo logoSrc={customizedLogoSrc}/>
           </a>
           </a>
         </Link>
         </Link>
       </div>
       </div>

+ 4 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -32,7 +32,7 @@ export type GrowiSubNavigationProps = {
   isCompactMode?: boolean,
   isCompactMode?: boolean,
   tags?: string[],
   tags?: string[],
   tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
   tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
-  rightComponent: React.FunctionComponent,
+  rightComponent?: React.FunctionComponent,
   additionalClasses?: string[],
   additionalClasses?: string[],
 }
 }
 
 
@@ -81,7 +81,9 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
         </div>
         </div>
       </div>
       </div>
       {/* Right side. */}
       {/* Right side. */}
-      <RightComponent />
+      { RightComponent && (
+        <RightComponent />
+      ) }
     </div>
     </div>
   );
   );
 };
 };

+ 2 - 1
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -5,6 +5,7 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { Link } from 'react-scroll';
 import { Link } from 'react-scroll';
 
 
+import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import {
 import {
   useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
   useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 } from '~/stores/context';
@@ -81,7 +82,7 @@ const PageView = React.memo((): JSX.Element => {
             {/* Comments */}
             {/* Comments */}
             { !isTopPagePath && (
             { !isTopPagePath && (
               <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
               <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-                <Link to={'page-comments'} smooth="easeOutQuart" offset={-100} duration={800}>
+                <Link to={'page-comments'} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
                   <button
                   <button
                     type="button"
                     type="button"
                     className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
                     className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"

+ 14 - 8
packages/app/src/components/Page/RevisionLoader.tsx

@@ -10,19 +10,23 @@ import loggerFactory from '~/utils/logger';
 
 
 import RevisionRenderer from './RevisionRenderer';
 import RevisionRenderer from './RevisionRenderer';
 
 
+export const ROOT_ELEM_ID = 'revision-loader' as const;
+
 export type RevisionLoaderProps = {
 export type RevisionLoaderProps = {
   rendererOptions: RendererOptions,
   rendererOptions: RendererOptions,
   pageId: string,
   pageId: string,
   revisionId: Ref<IRevision>,
   revisionId: Ref<IRevision>,
   lazy?: boolean,
   lazy?: boolean,
   onRevisionLoaded?: (revision: IRevisionHasId) => void,
   onRevisionLoaded?: (revision: IRevisionHasId) => void,
-
-  pagePath: string,
-  highlightKeywords?: string[],
 }
 }
 
 
 const logger = loggerFactory('growi:Page:RevisionLoader');
 const logger = loggerFactory('growi:Page:RevisionLoader');
 
 
+// Always render '#revision-loader' for MutationObserver of SearchResultContent
+const RevisionLoaderRoot = (props: React.HTMLAttributes<HTMLDivElement>): JSX.Element => (
+  <div id={ROOT_ELEM_ID} {...props}>{props.children}</div>
+);
+
 /**
 /**
  * Load data from server and render RevisionBody component
  * Load data from server and render RevisionBody component
  */
  */
@@ -81,7 +85,7 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   if (lazy && !isLoaded) {
   if (lazy && !isLoaded) {
     return (
     return (
       <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px">
       <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px">
-        <div className="wiki"></div>
+        <></>
       </Waypoint>
       </Waypoint>
     );
     );
   }
   }
@@ -110,9 +114,11 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <RevisionRenderer
-      rendererOptions={rendererOptions}
-      markdown={markdown}
-    />
+    <RevisionLoaderRoot>
+      <RevisionRenderer
+        rendererOptions={rendererOptions}
+        markdown={markdown}
+      />
+    </RevisionLoaderRoot>
   );
   );
 };
 };

+ 9 - 0
packages/app/src/components/PageAttachment/DeleteAttachmentModal.module.scss

@@ -0,0 +1,9 @@
+.attachment-delete-modal :global {
+  .attachment-delete-image {
+    text-align: center;
+
+    img {
+      max-width: 100%;
+    }
+  }
+}

+ 9 - 1
packages/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -10,6 +10,8 @@ import {
 
 
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
+import styles from './DeleteAttachmentModal.module.scss';
+
 
 
 function iconNameByFormat(format: string): string {
 function iconNameByFormat(format: string): string {
   if (format.match(/image\/.+/i)) {
   if (format.match(/image\/.+/i)) {
@@ -74,7 +76,13 @@ export const DeleteAttachmentModal = (props: Props): JSX.Element => {
 
 
 
 
   return (
   return (
-    <Modal isOpen={isOpen} className="attachment-delete-modal" size="lg" aria-labelledby="contained-modal-title-lg" fade={false}>
+    <Modal
+      isOpen={isOpen}
+      className={`${styles['attachment-delete-modal']} attachment-delete-modal`}
+      size="lg"
+      aria-labelledby="contained-modal-title-lg"
+      fade={false}
+    >
       <ModalHeader tag="h4" toggle={toggle} className="bg-danger text-light">
       <ModalHeader tag="h4" toggle={toggle} className="bg-danger text-light">
         <span id="contained-modal-title-lg">Delete attachment?</span>
         <span id="contained-modal-title-lg">Delete attachment?</span>
       </ModalHeader>
       </ModalHeader>

+ 60 - 56
packages/app/src/components/PageComment.tsx

@@ -27,6 +27,14 @@ const DeleteCommentModal = dynamic<DeleteCommentModalProps>(
   () => import('./PageComment/DeleteCommentModal').then(mod => mod.DeleteCommentModal), { ssr: false },
   () => import('./PageComment/DeleteCommentModal').then(mod => mod.DeleteCommentModal), { ssr: false },
 );
 );
 
 
+export const ROOT_ELEM_ID = 'page-comments' as const;
+
+// Always render '#page-comments' for MutationObserver of SearchResultContent
+const PageCommentRoot = (props: React.HTMLAttributes<HTMLDivElement>): JSX.Element => (
+  <div id={ROOT_ELEM_ID} {...props}>{props.children}</div>
+);
+
+
 export type PageCommentProps = {
 export type PageCommentProps = {
   rendererOptions?: RendererOptions,
   rendererOptions?: RendererOptions,
   pageId: string,
   pageId: string,
@@ -34,7 +42,6 @@ export type PageCommentProps = {
   currentUser: any,
   currentUser: any,
   isReadOnly: boolean,
   isReadOnly: boolean,
   titleAlign?: 'center' | 'left' | 'right',
   titleAlign?: 'center' | 'left' | 'right',
-  highlightKeywords?: string[],
   hideIfEmpty?: boolean,
   hideIfEmpty?: boolean,
 }
 }
 
 
@@ -42,7 +49,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
 
 
   const {
   const {
     rendererOptions: rendererOptionsByProps,
     rendererOptions: rendererOptionsByProps,
-    pageId, revision, currentUser, highlightKeywords, isReadOnly, titleAlign, hideIfEmpty,
+    pageId, revision, currentUser, isReadOnly, titleAlign, hideIfEmpty,
   } = props;
   } = props;
 
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
   const { data: comments, mutate } = useSWRxPageComment(pageId);
@@ -103,7 +110,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
   }, []);
   }, []);
 
 
   if (hideIfEmpty && comments?.length === 0) {
   if (hideIfEmpty && comments?.length === 0) {
-    return <></>;
+    return <PageCommentRoot />;
   }
   }
 
 
   let commentTitleClasses = 'border-bottom py-3 mb-3';
   let commentTitleClasses = 'border-bottom py-3 mb-3';
@@ -113,7 +120,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
 
 
   if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
   if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
     if (hideIfEmpty) {
     if (hideIfEmpty) {
-      return <></>;
+      return <PageCommentRoot />;
     }
     }
     return (
     return (
       <PageCommentSkelton commentTitleClasses={commentTitleClasses}/>
       <PageCommentSkelton commentTitleClasses={commentTitleClasses}/>
@@ -131,7 +138,6 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
       revisionCreatedAt={revisionCreatedAt as Date}
       revisionCreatedAt={revisionCreatedAt as Date}
       currentUser={currentUser}
       currentUser={currentUser}
       isReadOnly={isReadOnly}
       isReadOnly={isReadOnly}
-      highlightKeywords={highlightKeywords}
       deleteBtnClicked={onClickDeleteButton}
       deleteBtnClicked={onClickDeleteButton}
       onComment={mutate}
       onComment={mutate}
     />
     />
@@ -151,57 +157,55 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
   );
   );
 
 
   return (
   return (
-    <>
-      <div id="page-comments" className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
-        <div className="container-lg">
-          <div className="page-comments">
-            <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
-            <div className="page-comments-list" id="page-comments-list">
-              { commentsExceptReply.map((comment) => {
-
-                const defaultCommentThreadClasses = 'page-comment-thread pb-5';
-                const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
-
-                let commentThreadClasses = '';
-                commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
-
-                return (
-                  <div key={comment._id} className={commentThreadClasses}>
-                    {generateCommentElement(comment)}
-                    {hasReply && generateReplyCommentsElement(allReplies[comment._id])}
-                    {(!isReadOnly && !showEditorIds.has(comment._id)) && (
-                      <div className="text-right">
-                        <Button
-                          outline
-                          color="secondary"
-                          size="sm"
-                          className="btn-comment-reply"
-                          onClick={() => {
-                            setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
-                          }}
-                        >
-                          <i className="icon-fw icon-action-undo"></i> Reply
-                        </Button>
-                      </div>
-                    )}
-                    {(!isReadOnly && showEditorIds.has(comment._id)) && (
-                      <CommentEditor
-                        pageId={pageId}
-                        replyTo={comment._id}
-                        onCancelButtonClicked={() => {
-                          removeShowEditorId(comment._id);
-                        }}
-                        onCommentButtonClicked={() => {
-                          removeShowEditorId(comment._id);
-                          mutate();
+    <PageCommentRoot className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
+      <div className="container-lg">
+        <div className="page-comments">
+          <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
+          <div className="page-comments-list" id="page-comments-list">
+            { commentsExceptReply.map((comment) => {
+
+              const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+              const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
+
+              let commentThreadClasses = '';
+              commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
+
+              return (
+                <div key={comment._id} className={commentThreadClasses}>
+                  {generateCommentElement(comment)}
+                  {hasReply && generateReplyCommentsElement(allReplies[comment._id])}
+                  {(!isReadOnly && !showEditorIds.has(comment._id)) && (
+                    <div className="text-right">
+                      <Button
+                        outline
+                        color="secondary"
+                        size="sm"
+                        className="btn-comment-reply"
+                        onClick={() => {
+                          setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
                         }}
                         }}
-                      />
-                    )}
-                  </div>
-                );
-
-              })}
-            </div>
+                      >
+                        <i className="icon-fw icon-action-undo"></i> Reply
+                      </Button>
+                    </div>
+                  )}
+                  {(!isReadOnly && showEditorIds.has(comment._id)) && (
+                    <CommentEditor
+                      pageId={pageId}
+                      replyTo={comment._id}
+                      onCancelButtonClicked={() => {
+                        removeShowEditorId(comment._id);
+                      }}
+                      onCommentButtonClicked={() => {
+                        removeShowEditorId(comment._id);
+                        mutate();
+                      }}
+                    />
+                  )}
+                </div>
+              );
+
+            })}
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -214,7 +218,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
           confirmToDelete={onDeleteComment}
           confirmToDelete={onDeleteComment}
         />
         />
       )}
       )}
-    </>
+    </PageCommentRoot>
   );
   );
 });
 });
 
 

+ 0 - 1
packages/app/src/components/PageComment/Comment.tsx

@@ -29,7 +29,6 @@ type CommentProps = {
   revisionCreatedAt: Date,
   revisionCreatedAt: Date,
   currentUser: IUser,
   currentUser: IUser,
   isReadOnly: boolean,
   isReadOnly: boolean,
-  highlightKeywords?: string[],
   deleteBtnClicked: (comment: ICommentHasId) => void,
   deleteBtnClicked: (comment: ICommentHasId) => void,
   onComment: () => void,
   onComment: () => void,
 }
 }

+ 1 - 3
packages/app/src/components/PageComment/ReplyComments.tsx

@@ -21,7 +21,6 @@ type ReplycommentsProps = {
   revisionCreatedAt: Date,
   revisionCreatedAt: Date,
   currentUser: IUser,
   currentUser: IUser,
   replyList: ICommentHasIdList,
   replyList: ICommentHasIdList,
-  highlightKeywords?: string[],
   deleteBtnClicked: (comment: ICommentHasId) => void,
   deleteBtnClicked: (comment: ICommentHasId) => void,
   onComment: () => void,
   onComment: () => void,
 }
 }
@@ -29,7 +28,7 @@ type ReplycommentsProps = {
 export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
 export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
 
 
   const {
   const {
-    rendererOptions, isReadOnly, revisionId, revisionCreatedAt, currentUser, replyList, highlightKeywords,
+    rendererOptions, isReadOnly, revisionId, revisionCreatedAt, currentUser, replyList,
     deleteBtnClicked, onComment,
     deleteBtnClicked, onComment,
   } = props;
   } = props;
 
 
@@ -47,7 +46,6 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
           revisionCreatedAt={revisionCreatedAt}
           revisionCreatedAt={revisionCreatedAt}
           currentUser={currentUser}
           currentUser={currentUser}
           isReadOnly={isReadOnly}
           isReadOnly={isReadOnly}
-          highlightKeywords={highlightKeywords}
           deleteBtnClicked={deleteBtnClicked}
           deleteBtnClicked={deleteBtnClicked}
           onComment={onComment}
           onComment={onComment}
         />
         />

+ 0 - 1
packages/app/src/components/PageTimeline.tsx

@@ -65,7 +65,6 @@ export const PageTimeline = (): JSX.Element => {
                   lazy
                   lazy
                   rendererOptions={rendererOptions}
                   rendererOptions={rendererOptions}
                   pageId={page._id}
                   pageId={page._id}
-                  pagePath={page.path}
                   revisionId={page.revision}
                   revisionId={page.revision}
                 />
                 />
               </div>
               </div>

+ 14 - 9
packages/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 
 
 import EventEmitter from 'events';
 import EventEmitter from 'events';
 
 
@@ -57,19 +57,24 @@ export const Header = (props: HeaderProps): JSX.Element => {
 
 
   const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
   const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
 
 
-  // update isActive when hash is changed
+  const activateByHash = useCallback((url: string) => {
+    const hash = (new URL(url, 'https://example.com')).hash.slice(1);
+    setActive(hash === id);
+  }, [id]);
+
+  // init
   useEffect(() => {
   useEffect(() => {
-    const handler = (url: string) => {
-      const hash = (new URL(url, 'https://example.com')).hash.slice(1);
-      setActive(hash === id);
-    };
+    activateByHash(window.location.href);
+  }, [activateByHash]);
 
 
-    router.events.on('hashChangeComplete', handler);
+  // update isActive when hash is changed
+  useEffect(() => {
+    router.events.on('hashChangeComplete', activateByHash);
 
 
     return () => {
     return () => {
-      router.events.off('hashChangeComplete', handler);
+      router.events.off('hashChangeComplete', activateByHash);
     };
     };
-  }, [id, router.events]);
+  }, [activateByHash, router.events]);
 
 
   return (
   return (
     <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>
     <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>

+ 2 - 1
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -1,6 +1,7 @@
 import Link, { LinkProps } from 'next/link';
 import Link, { LinkProps } from 'next/link';
 import { Link as ScrollLink } from 'react-scroll';
 import { Link as ScrollLink } from 'react-scroll';
 
 
+import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { useSiteUrl } from '~/stores/context';
 import { useSiteUrl } from '~/stores/context';
 
 
 const isAnchorLink = (href: string): boolean => {
 const isAnchorLink = (href: string): boolean => {
@@ -35,7 +36,7 @@ export const NextLink = ({
     const to = href.slice(1);
     const to = href.slice(1);
     return (
     return (
       <Link href={href} scroll={false}>
       <Link href={href} scroll={false}>
-        <ScrollLink href={href} to={to} className={className} smooth="easeOutQuart" offset={-100} duration={800}>
+        <ScrollLink href={href} to={to} className={className} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
           {children}
           {children}
         </ScrollLink>
         </ScrollLink>
       </Link>
       </Link>

+ 58 - 30
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -1,15 +1,16 @@
 import React, {
 import React, {
-  FC, useCallback, useEffect, useRef,
+  FC, useCallback, useEffect, useRef, useState,
 } from 'react';
 } from 'react';
 
 
 import { getIdForRef } from '@growi/core';
 import { getIdForRef } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
+
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/apiNotification';
 import { toastSuccess } from '~/client/util/apiNotification';
-import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
 import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
@@ -24,8 +25,8 @@ import { useFullTextSearchTermManager } from '~/stores/search';
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
 import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
-import { RevisionLoaderProps } from '../Page/RevisionLoader';
-import { PageCommentProps } from '../PageComment';
+import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, RevisionLoaderProps } from '../Page/RevisionLoader';
+import { ROOT_ELEM_ID as PageCommentRootElemId, PageCommentProps } from '../PageComment';
 import { PageContentFooterProps } from '../PageContentFooter';
 import { PageContentFooterProps } from '../PageContentFooter';
 
 
 
 
@@ -57,8 +58,8 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   );
   );
 };
 };
 
 
-const SCROLL_OFFSET_TOP = 175; // approximate height of (navigation + subnavigation)
-const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true };
+const SCROLL_OFFSET_TOP = 30;
+const MUTATION_OBSERVER_CONFIG = { childList: true }; // omit 'subtree: true'
 
 
 type Props ={
 type Props ={
   pageWithMeta : IPageWithSearchMeta,
   pageWithMeta : IPageWithSearchMeta,
@@ -67,28 +68,26 @@ type Props ={
   forceHideMenuItems?: ForceHideMenuItems,
   forceHideMenuItems?: ForceHideMenuItems,
 }
 }
 
 
-const scrollTo = (scrollElement:HTMLElement) => {
+const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): boolean => {
   // use querySelector to intentionally get the first element found
   // use querySelector to intentionally get the first element found
-  const highlightedKeyword = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
-  if (highlightedKeyword != null) {
-    smoothScrollIntoView(highlightedKeyword, SCROLL_OFFSET_TOP, scrollElement);
+  const toElem = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
+  if (toElem == null) {
+    return false;
   }
   }
-};
 
 
-const generateObserverCallback = (doScroll: ()=>void) => {
-  return (mutationRecords:MutationRecord[]) => {
-    mutationRecords.forEach((record:MutationRecord) => {
-      const target = record.target as HTMLElement;
-      const targetId = target.id as string;
-      if (targetId !== 'wiki') return;
-      doScroll();
-    });
-  };
+  animateScroll.scrollTo(toElem.offsetTop - SCROLL_OFFSET_TOP, {
+    containerId: scrollElement.id,
+    duration: 200,
+  });
+  return true;
 };
 };
 
 
 export const SearchResultContent: FC<Props> = (props: Props) => {
 export const SearchResultContent: FC<Props> = (props: Props) => {
 
 
-  const scrollElementRef = useRef(null);
+  const scrollElementRef = useRef<HTMLDivElement|null>(null);
+
+  const [isRevisionLoaded, setRevisionLoaded] = useState(false);
+  const [isPageCommentLoaded, setPageCommentLoaded] = useState(false);
 
 
   // for mutation
   // for mutation
   const { advance: advancePt } = usePageTreeTermManager();
   const { advance: advancePt } = usePageTreeTermManager();
@@ -97,19 +96,49 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
 
 
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
   useEffect(() => {
-    const scrollElement = scrollElementRef.current as HTMLElement | null;
+    const scrollElement = scrollElementRef.current;
     if (scrollElement == null) return;
     if (scrollElement == null) return;
 
 
-    const observerCallback = generateObserverCallback(() => {
-      scrollTo(scrollElement);
-    });
+    const observerCallback = (mutationRecords:MutationRecord[], thisObs: MutationObserver) => {
+      mutationRecords.forEach((record:MutationRecord) => {
+        const target = record.target as HTMLElement;
+
+        // turn on boolean if loaded
+        Array.from(target.children).forEach((child) => {
+          const childId = (child as HTMLElement).id;
+          if (childId === RevisionLoaderRoomElemId) {
+            setRevisionLoaded(true);
+          }
+          else if (childId === PageCommentRootElemId) {
+            setPageCommentLoaded(true);
+          }
+        });
+      });
+    };
 
 
     const observer = new MutationObserver(observerCallback);
     const observer = new MutationObserver(observerCallback);
     observer.observe(scrollElement, MUTATION_OBSERVER_CONFIG);
     observer.observe(scrollElement, MUTATION_OBSERVER_CONFIG);
     return () => {
     return () => {
       observer.disconnect();
       observer.disconnect();
     };
     };
-  });
+  }, []);
+
+  useEffect(() => {
+    if (!isRevisionLoaded || !isPageCommentLoaded) {
+      return;
+    }
+    if (scrollElementRef.current == null) {
+      return;
+    }
+
+    const scrollElement = scrollElementRef.current;
+    const isScrollProcessed = scrollToFirstHighlightedKeyword(scrollElement);
+    // retry after 1000ms if highlighted element is absense
+    if (!isScrollProcessed) {
+      setTimeout(() => scrollToFirstHighlightedKeyword(scrollElement), 1000);
+    }
+
+  }, [isPageCommentLoaded, isRevisionLoaded]);
   // *******************************  end  *******************************
   // *******************************  end  *******************************
 
 
   const {
   const {
@@ -211,20 +240,19 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           additionalClasses={['px-4']}
           additionalClasses={['px-4']}
         />
         />
       </div>
       </div>
-      <div className="search-result-content-body-container" ref={scrollElementRef}>
+      <div id="search-result-content-body-container" className="search-result-content-body-container" ref={scrollElementRef}>
+        {/* RevisionLoader will render '#revision-loader' after loaded */}
         <RevisionLoader
         <RevisionLoader
           rendererOptions={rendererOptions}
           rendererOptions={rendererOptions}
           pageId={page._id}
           pageId={page._id}
-          pagePath={page.path}
           revisionId={page.revision}
           revisionId={page.revision}
-          highlightKeywords={highlightKeywords}
         />
         />
+        {/* PageComment will render '#page-comment' after loaded */}
         <PageComment
         <PageComment
           rendererOptions={rendererOptions}
           rendererOptions={rendererOptions}
           pageId={page._id}
           pageId={page._id}
           revision={page.revision}
           revision={page.revision}
           currentUser={currentUser}
           currentUser={currentUser}
-          highlightKeywords={highlightKeywords}
           isReadOnly
           isReadOnly
           hideIfEmpty
           hideIfEmpty
         />
         />

+ 2 - 2
packages/app/src/components/Sidebar/PageTree.tsx

@@ -43,8 +43,8 @@ const PageTree: FC = memo(() => {
           <h3 className="mb-0">{t('Page Tree')}</h3>
           <h3 className="mb-0">{t('Page Tree')}</h3>
         </div>
         </div>
         <div className="mt-5 mx-2 text-center">
         <div className="mt-5 mx-2 text-center">
-          <h3 className="text-gray">{t('admin:v5_page_migration.page_tree_not_avaliable')}</h3>
-          <a href="/admin">{t('admin:v5_page_migration.go_to_settings')}</a>
+          <h3 className="text-gray">{t('v5_page_migration.page_tree_not_avaliable')}</h3>
+          <a href="/admin">{t('v5_page_migration.go_to_settings')}</a>
         </div>
         </div>
       </>
       </>
     );
     );

+ 2 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -65,7 +65,7 @@ import {
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useLayoutSetting,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useLayoutSetting, useCustomizedLogoSrc,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import {
 import {
@@ -187,6 +187,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // commons
   // commons
   useEditorConfig(props.editorConfig);
   useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
+  useCustomizedLogoSrc(props.customizedLogoSrc);
 
 
   // UserUISettings
   // UserUISettings
   usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
   usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);

+ 2 - 1
packages/app/src/pages/_app.page.tsx

@@ -9,7 +9,7 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 
 
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
 import {
-  useAppTitle, useConfidential, useGrowiTheme, useGrowiVersion, useSiteUrl,
+  useAppTitle, useConfidential, useGrowiTheme, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
 } from '~/stores/context';
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
@@ -53,6 +53,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useConfidential(commonPageProps.confidential);
   useConfidential(commonPageProps.confidential);
   useGrowiTheme(commonPageProps.theme);
   useGrowiTheme(commonPageProps.theme);
   useGrowiVersion(commonPageProps.growiVersion);
   useGrowiVersion(commonPageProps.growiVersion);
+  useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
 
 
   return (
   return (
     <SWRConfig value={swrConfig}>
     <SWRConfig value={swrConfig}>

+ 33 - 4
packages/app/src/pages/trash.page.tsx

@@ -5,16 +5,20 @@ import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
+import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
+import {
+  useCurrentProductNavWidth, useCurrentSidebarContents, useDrawerMode, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
+} from '~/stores/ui';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
-import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import {
 import {
   useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
   useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL,
+  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import {
 import {
@@ -30,8 +34,12 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
-  userUISettings?: IUserUISettings,
   showPageLimitationXL: number,
   showPageLimitationXL: number,
+
+  // UI
+  userUISettings?: IUserUISettings
+  // Sidebar
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const TrashPage: NextPage<CommonProps> = (props: Props) => {
 const TrashPage: NextPage<CommonProps> = (props: Props) => {
@@ -46,13 +54,29 @@ const TrashPage: NextPage<CommonProps> = (props: Props) => {
   useCurrentPathname('/trash');
   useCurrentPathname('/trash');
   useCurrentPagePath('/trash');
   useCurrentPagePath('/trash');
 
 
+  // UserUISettings
+  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
+  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
+  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
+  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
+  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+
   useShowPageLimitationXL(props.showPageLimitationXL);
   useShowPageLimitationXL(props.showPageLimitationXL);
 
 
+  const { data: isDrawerMode } = useDrawerMode();
+  const { data: isGuestUser } = useIsGuestUser();
+
   return (
   return (
     <>
     <>
       <BasicLayout title={useCustomTitle(props, 'GROWI')} >
       <BasicLayout title={useCustomTitle(props, 'GROWI')} >
         <header className="py-0 position-relative">
         <header className="py-0 position-relative">
-          <GrowiContextualSubNavigation isLinkSharingDisabled={false} />
+          <GrowiSubNavigation
+            pagePath="/trash"
+            showDrawerToggler={isDrawerMode}
+            isGuestUser={isGuestUser}
+            isDrawerMode={isDrawerMode}
+            additionalClasses={['container-fluid']}
+          />
         </header>
         </header>
 
 
         <div className="grw-container-convertible mb-5 pb-5">
         <div className="grw-container-convertible mb-5 pb-5">
@@ -93,6 +117,11 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
+
+  props.sidebarConfig = {
+    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
 }
 }
 
 
 /**
 /**

+ 3 - 2
packages/app/src/pages/utils/commons.ts

@@ -21,6 +21,7 @@ export type CommonProps = {
   growiVersion: string,
   growiVersion: string,
   isMaintenanceMode: boolean,
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   redirectDestination: string | null,
+  customizedLogoSrc?: string,
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
 // eslint-disable-next-line max-len
 // eslint-disable-next-line max-len
@@ -53,6 +54,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     growiVersion: crowi.version,
     growiVersion: crowi.version,
     isMaintenanceMode,
     isMaintenanceMode,
     redirectDestination,
     redirectDestination,
+    customizedLogoSrc: configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
   };
   };
 
 
   return { props };
   return { props };
@@ -76,12 +78,11 @@ export const getNextI18NextConfig = async(
     ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
     ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
     ?? Lang.en_US;
     ?? Lang.en_US;
 
 
-  // TODO: Consider to not include translation as default or other architecture idea
-  // see: https://redmine.weseek.co.jp/issues/107092
   const namespaces = ['commons'];
   const namespaces = ['commons'];
   if (namespacesRequired != null) {
   if (namespacesRequired != null) {
     namespaces.push(...namespacesRequired);
     namespaces.push(...namespacesRequired);
   }
   }
+  // TODO: deprecate 'translation.json' in the future
   else {
   else {
     namespaces.push('translation');
     namespaces.push('translation');
   }
   }

+ 36 - 33
packages/app/src/server/models/page.ts

@@ -2,7 +2,7 @@
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
-import { pagePathUtils, pathUtils } from '@growi/core';
+import { HasObjectId, pagePathUtils, pathUtils } from '@growi/core';
 import escapeStringRegexp from 'escape-string-regexp';
 import escapeStringRegexp from 'escape-string-regexp';
 import mongoose, {
 import mongoose, {
   Schema, Model, Document, AnyObject,
   Schema, Model, Document, AnyObject,
@@ -58,13 +58,13 @@ export type CreateMethod = (path: string, body: string, user, options: PageCreat
 export interface PageModel extends Model<PageDocument> {
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete static methods
   [x: string]: any; // for obsolete static methods
   findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
   findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
-  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: true, includeEmpty?: boolean): Promise<PageDocument | PageDocument[] | null>
-  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: false, includeEmpty?: boolean): Promise<PageDocument[]>
+  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: true, includeEmpty?: boolean): Promise<PageDocument & HasObjectId | null>
+  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: false, includeEmpty?: boolean): Promise<(PageDocument & HasObjectId)[]>
   countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
   countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findRecentUpdatedPages(path: string, user, option, includeEmpty?: boolean): Promise<PaginatedPages>
   findRecentUpdatedPages(path: string, user, option, includeEmpty?: boolean): Promise<PaginatedPages>
   generateGrantCondition(
   generateGrantCondition(
-    user, userGroups, showAnyoneKnowsLink?: boolean, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
+    user, userGroups, includeAnyoneWithTheLink?: boolean, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
   ): { $or: any[] }
   ): { $or: any[] }
 
 
   PageQueryBuilder: typeof PageQueryBuilder
   PageQueryBuilder: typeof PageQueryBuilder
@@ -140,7 +140,7 @@ export class PageQueryBuilder {
    * @param pathsToFilter The paths to have additional filters as to be applicable
    * @param pathsToFilter The paths to have additional filters as to be applicable
    * @returns PageQueryBuilder
    * @returns PageQueryBuilder
    */
    */
-  addConditionToFilterByApplicableAncestors(pathsToFilter: string[]) {
+  addConditionToFilterByApplicableAncestors(pathsToFilter: string[]): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and(
       .and(
         {
         {
@@ -156,7 +156,7 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  addConditionToExcludeTrashed() {
+  addConditionToExcludeTrashed(): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and({
       .and({
         $or: [
         $or: [
@@ -172,7 +172,7 @@ export class PageQueryBuilder {
    * generate the query to find the pages '{path}/*' and '{path}' self.
    * generate the query to find the pages '{path}/*' and '{path}' self.
    * If top page, return without doing anything.
    * If top page, return without doing anything.
    */
    */
-  addConditionToListWithDescendants(path: string, option?) {
+  addConditionToListWithDescendants(path: string, option?): PageQueryBuilder {
     // No request is set for the top page
     // No request is set for the top page
     if (isTopPage(path)) {
     if (isTopPage(path)) {
       return this;
       return this;
@@ -198,7 +198,7 @@ export class PageQueryBuilder {
    * generate the query to find the pages '{path}/*' (exclude '{path}' self).
    * generate the query to find the pages '{path}/*' (exclude '{path}' self).
    * If top page, return without doing anything.
    * If top page, return without doing anything.
    */
    */
-  addConditionToListOnlyDescendants(path, option) {
+  addConditionToListOnlyDescendants(path, option): PageQueryBuilder {
     // No request is set for the top page
     // No request is set for the top page
     if (isTopPage(path)) {
     if (isTopPage(path)) {
       return this;
       return this;
@@ -215,7 +215,7 @@ export class PageQueryBuilder {
 
 
   }
   }
 
 
-  addConditionToListOnlyAncestors(path) {
+  addConditionToListOnlyAncestors(path): PageQueryBuilder {
     const pathNormalized = pathUtils.normalizePath(path);
     const pathNormalized = pathUtils.normalizePath(path);
     const ancestorsPaths = extractToAncestorsPaths(pathNormalized);
     const ancestorsPaths = extractToAncestorsPaths(pathNormalized);
 
 
@@ -299,7 +299,7 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  async addConditionForParentNormalization(user) {
+  async addConditionForParentNormalization(user): Promise<PageQueryBuilder> {
     // determine UserGroup condition
     // determine UserGroup condition
     let userGroups;
     let userGroups;
     if (user != null) {
     if (user != null) {
@@ -332,7 +332,7 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  async addConditionAsMigratablePages(user) {
+  async addConditionAsMigratablePages(user): Promise<PageQueryBuilder> {
     this.query = this.query
     this.query = this.query
       .and({
       .and({
         $or: [
         $or: [
@@ -349,19 +349,21 @@ export class PageQueryBuilder {
   }
   }
 
 
   // add viewer condition to PageQueryBuilder instance
   // add viewer condition to PageQueryBuilder instance
-  async addViewerCondition(user, userGroups = null): Promise<PageQueryBuilder> {
+  async addViewerCondition(user, userGroups = null, includeAnyoneWithTheLink = false): Promise<PageQueryBuilder> {
     let relatedUserGroups = userGroups;
     let relatedUserGroups = userGroups;
     if (user != null && relatedUserGroups == null) {
     if (user != null && relatedUserGroups == null) {
       const UserGroupRelation: any = mongoose.model('UserGroupRelation');
       const UserGroupRelation: any = mongoose.model('UserGroupRelation');
       relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
       relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
     }
 
 
-    this.addConditionToFilteringByViewer(user, relatedUserGroups, false);
+    this.addConditionToFilteringByViewer(user, relatedUserGroups, includeAnyoneWithTheLink);
     return this;
     return this;
   }
   }
 
 
-  addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false) {
-    const condition = generateGrantCondition(user, userGroups, showAnyoneKnowsLink, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
+  addConditionToFilteringByViewer(
+      user, userGroups, includeAnyoneWithTheLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false,
+  ): PageQueryBuilder {
+    const condition = generateGrantCondition(user, userGroups, includeAnyoneWithTheLink, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
 
 
     this.query = this.query
     this.query = this.query
       .and(condition);
       .and(condition);
@@ -369,27 +371,27 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  addConditionToPagenate(offset, limit, sortOpt?) {
+  addConditionToPagenate(offset, limit, sortOpt?): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .sort(sortOpt).skip(offset).limit(limit); // eslint-disable-line newline-per-chained-call
       .sort(sortOpt).skip(offset).limit(limit); // eslint-disable-line newline-per-chained-call
 
 
     return this;
     return this;
   }
   }
 
 
-  addConditionAsNonRootPage() {
+  addConditionAsNonRootPage(): PageQueryBuilder {
     this.query = this.query.and({ path: { $ne: '/' } });
     this.query = this.query.and({ path: { $ne: '/' } });
 
 
     return this;
     return this;
   }
   }
 
 
-  addConditionAsNotMigrated() {
+  addConditionAsNotMigrated(): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and({ parent: null });
       .and({ parent: null });
 
 
     return this;
     return this;
   }
   }
 
 
-  addConditionAsOnTree() {
+  addConditionAsOnTree(): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and(
       .and(
         {
         {
@@ -406,25 +408,25 @@ export class PageQueryBuilder {
   /*
   /*
    * Add this condition when get any ancestor pages including the target's parent
    * Add this condition when get any ancestor pages including the target's parent
    */
    */
-  addConditionToSortPagesByDescPath() {
+  addConditionToSortPagesByDescPath(): PageQueryBuilder {
     this.query = this.query.sort('-path');
     this.query = this.query.sort('-path');
 
 
     return this;
     return this;
   }
   }
 
 
-  addConditionToSortPagesByAscPath() {
+  addConditionToSortPagesByAscPath(): PageQueryBuilder {
     this.query = this.query.sort('path');
     this.query = this.query.sort('path');
 
 
     return this;
     return this;
   }
   }
 
 
-  addConditionToMinimizeDataForRendering() {
+  addConditionToMinimizeDataForRendering(): PageQueryBuilder {
     this.query = this.query.select('_id path isEmpty grant revision descendantCount');
     this.query = this.query.select('_id path isEmpty grant revision descendantCount');
 
 
     return this;
     return this;
   }
   }
 
 
-  addConditionToListByPathsArray(paths) {
+  addConditionToListByPathsArray(paths): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and({
       .and({
         path: {
         path: {
@@ -435,7 +437,7 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  addConditionToListByPageIdsArray(pageIds) {
+  addConditionToListByPageIdsArray(pageIds): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and({
       .and({
         _id: {
         _id: {
@@ -446,7 +448,7 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  addConditionToExcludeByPageIdsArray(pageIds) {
+  addConditionToExcludeByPageIdsArray(pageIds): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .and({
       .and({
         _id: {
         _id: {
@@ -457,7 +459,7 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  populateDataToList(userPublicFields) {
+  populateDataToList(userPublicFields): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .populate({
       .populate({
         path: 'lastUpdateUser',
         path: 'lastUpdateUser',
@@ -466,12 +468,12 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
-  populateDataToShowRevision(userPublicFields) {
+  populateDataToShowRevision(userPublicFields): PageQueryBuilder {
     this.query = populateDataToShowRevision(this.query, userPublicFields);
     this.query = populateDataToShowRevision(this.query, userPublicFields);
     return this;
     return this;
   }
   }
 
 
-  addConditionToFilteringByParentId(parentId) {
+  addConditionToFilteringByParentId(parentId): PageQueryBuilder {
     this.query = this.query.and({ parent: parentId });
     this.query = this.query.and({ parent: parentId });
     return this;
     return this;
   }
   }
@@ -561,16 +563,17 @@ schema.statics.findByIdsAndViewer = async function(pageIds: string[], user, user
  * Find a page by path and viewer. Pass false to useFindOne to use findOne method.
  * Find a page by path and viewer. Pass false to useFindOne to use findOne method.
  */
  */
 schema.statics.findByPathAndViewer = async function(
 schema.statics.findByPathAndViewer = async function(
-    path: string | null, user, userGroups = null, useFindOne = true, includeEmpty = false,
-): Promise<PageDocument | PageDocument[] | null> {
+    path: string | null, user, userGroups = null, useFindOne = false, includeEmpty = false,
+): Promise<(PageDocument | PageDocument[]) & HasObjectId | null> {
   if (path == null) {
   if (path == null) {
     throw new Error('path is required.');
     throw new Error('path is required.');
   }
   }
 
 
   const baseQuery = useFindOne ? this.findOne({ path }) : this.find({ path });
   const baseQuery = useFindOne ? this.findOne({ path }) : this.find({ path });
+  const includeAnyoneWithTheLink = useFindOne;
   const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
   const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
 
 
-  await queryBuilder.addViewerCondition(user, userGroups);
+  await queryBuilder.addViewerCondition(user, userGroups, includeAnyoneWithTheLink);
 
 
   return queryBuilder.query.exec();
   return queryBuilder.query.exec();
 };
 };
@@ -889,14 +892,14 @@ schema.statics.findParent = async function(pageId): Promise<PageDocument | null>
 schema.statics.PageQueryBuilder = PageQueryBuilder as any; // mongoose does not support constructor type as statics attrs type
 schema.statics.PageQueryBuilder = PageQueryBuilder as any; // mongoose does not support constructor type as statics attrs type
 
 
 export function generateGrantCondition(
 export function generateGrantCondition(
-    user, userGroups, showAnyoneKnowsLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false,
+    user, userGroups, includeAnyoneWithTheLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false,
 ): { $or: any[] } {
 ): { $or: any[] } {
   const grantConditions: AnyObject[] = [
   const grantConditions: AnyObject[] = [
     { grant: null },
     { grant: null },
     { grant: GRANT_PUBLIC },
     { grant: GRANT_PUBLIC },
   ];
   ];
 
 
-  if (showAnyoneKnowsLink) {
+  if (includeAnyoneWithTheLink) {
     grantConditions.push({ grant: GRANT_RESTRICTED });
     grantConditions.push({ grant: GRANT_RESTRICTED });
   }
   }
 
 

+ 2 - 2
packages/app/src/server/service/page.ts

@@ -224,9 +224,9 @@ class PageService {
       pageId: string, path: string, user: IUserHasId, includeEmpty = false, isSharedPage = false,
       pageId: string, path: string, user: IUserHasId, includeEmpty = false, isSharedPage = false,
   ): Promise<IPageWithMeta<IPageInfoAll>|null> {
   ): Promise<IPageWithMeta<IPageInfoAll>|null> {
 
 
-    const Page = this.crowi.model('Page');
+    const Page = this.crowi.model('Page') as PageModel;
 
 
-    let page: PageModel & PageDocument & HasObjectId;
+    let page: PageDocument & HasObjectId | null;
     if (pageId != null) { // prioritized
     if (pageId != null) { // prioritized
       page = await Page.findByIdAndViewer(pageId, user, null, includeEmpty);
       page = await Page.findByIdAndViewer(pageId, user, null, includeEmpty);
     }
     }

+ 11 - 9
packages/app/src/services/renderer/rehype-plugins/keyword-highlighter.ts

@@ -10,7 +10,7 @@ import { Plugin } from 'unified';
  * @param value
  * @param value
  * @returns
  * @returns
  */
  */
-function splitWithKeyword(keyword: string, value: string): string[] {
+function splitWithKeyword(lowercasedKeyword: string, value: string): string[] {
   if (value.length === 0) {
   if (value.length === 0) {
     return [];
     return [];
   }
   }
@@ -21,7 +21,7 @@ function splitWithKeyword(keyword: string, value: string): string[] {
   const splitted: string[] = [];
   const splitted: string[] = [];
 
 
   do {
   do {
-    cursorEnd = value.indexOf(keyword, cursorStart);
+    cursorEnd = value.toLowerCase().indexOf(lowercasedKeyword, cursorStart);
 
 
     // not found
     // not found
     if (cursorEnd === -1) {
     if (cursorEnd === -1) {
@@ -29,7 +29,7 @@ function splitWithKeyword(keyword: string, value: string): string[] {
     }
     }
     // keyword is found
     // keyword is found
     else if (cursorEnd === cursorStart) {
     else if (cursorEnd === cursorStart) {
-      cursorEnd += keyword.length;
+      cursorEnd += lowercasedKeyword.length;
     }
     }
 
 
     splitted.push(value.slice(cursorStart, cursorEnd));
     splitted.push(value.slice(cursorStart, cursorEnd));
@@ -50,17 +50,17 @@ function wrapWithEm(textElement: Text): Element {
   };
   };
 }
 }
 
 
-function highlight(keyword: string, node: Text, index: number, parent: Root | Element): void {
-  if (node.value.includes(keyword)) {
-    const splitted = splitWithKeyword(keyword, node.value);
+function highlight(lowercasedKeyword: string, node: Text, index: number, parent: Root | Element): void {
+  if (node.value.toLowerCase().includes(lowercasedKeyword)) {
+    const splitted = splitWithKeyword(lowercasedKeyword, node.value);
 
 
     parent.children[index] = {
     parent.children[index] = {
       type: 'element',
       type: 'element',
       tagName: 'span',
       tagName: 'span',
       properties: {},
       properties: {},
       children: splitted.map((text) => {
       children: splitted.map((text) => {
-        return text === keyword
-          ? wrapWithEm({ type: 'text', value: keyword })
+        return text.toLowerCase() === lowercasedKeyword
+          ? wrapWithEm({ type: 'text', value: text })
           : { type: 'text', value: text };
           : { type: 'text', value: text };
       }),
       }),
     };
     };
@@ -79,11 +79,13 @@ export const rehypePlugin: Plugin<[KeywordHighlighterPluginParams]> = (options)
 
 
   const keywords = (typeof options.keywords === 'string') ? [options.keywords] : options.keywords;
   const keywords = (typeof options.keywords === 'string') ? [options.keywords] : options.keywords;
 
 
+  const lowercasedKeywords = keywords.map(keyword => keyword.toLowerCase());
+
   // return rehype-rewrite with hithlighter
   // return rehype-rewrite with hithlighter
   return rehypeRewrite.bind(this)({
   return rehypeRewrite.bind(this)({
     rewrite: (node, index, parent) => {
     rewrite: (node, index, parent) => {
       if (parent != null && index != null && node.type === 'text') {
       if (parent != null && index != null && node.type === 'text') {
-        keywords.forEach(keyword => highlight(keyword, node, index, parent));
+        lowercasedKeywords.forEach(keyword => highlight(keyword, node, index, parent));
       }
       }
     },
     },
   });
   });

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -271,6 +271,10 @@ export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Err
   return useStaticSWR('CustomizeTitle', initialData);
   return useStaticSWR('CustomizeTitle', initialData);
 };
 };
 
 
+export const useCustomizedLogoSrc = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR('customizedLogoSrc', initialData);
+};
+
 /** **********************************************************
 /** **********************************************************
  *                     Computed contexts
  *                     Computed contexts
  *********************************************************** */
  *********************************************************** */

+ 0 - 10
packages/app/src/styles/_attachments.scss

@@ -1,13 +1,3 @@
-.attachment-delete-modal {
-  .attachment-delete-image {
-    text-align: center;
-
-    img {
-      max-width: 100%;
-    }
-  }
-}
-
 .attachment-userpicture .picture {
 .attachment-userpicture .picture {
   vertical-align: text-bottom;
   vertical-align: text-bottom;
 }
 }

+ 2 - 1
packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts

@@ -98,10 +98,11 @@ context('Access to special pages', () => {
   });
   });
 
 
   it('/tags is successfully loaded', () => {
   it('/tags is successfully loaded', () => {
-    cy.visit('/tags');
 
 
     // open sidebar
     // open sidebar
     cy.collapseSidebar(false);
     cy.collapseSidebar(false);
+
+    cy.visit('/tags');
     // select tags
     // select tags
     cy.getByTestid('grw-sidebar-nav-primary-tags').click();
     cy.getByTestid('grw-sidebar-nav-primary-tags').click();
     cy.getByTestid('grw-sidebar-content-tags').should('be.visible');
     cy.getByTestid('grw-sidebar-content-tags').should('be.visible');

+ 48 - 68
yarn.lock

@@ -10,16 +10,6 @@
     plantuml-encoder "^1.4.0"
     plantuml-encoder "^1.4.0"
     unist-util-visit "^2.0.2"
     unist-util-visit "^2.0.2"
 
 
-"@alienfast/i18next-loader@^1.1.4":
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/@alienfast/i18next-loader/-/i18next-loader-1.1.4.tgz#213a6cd77222900a61b1635a212051193bcd5d1f"
-  integrity sha512-8H+pIHIPwsjr1ip4bpCHnZtmR1z/K4KPpmD/fUL+kLug/2usATVmRi3IcZogy70Olqo3eH+qoKvWf+ROJbwoUA==
-  dependencies:
-    glob-all "^3.1.0"
-    js-yaml "^3.13.1"
-    loader-utils "^1.2.3"
-    lodash "^4.17.15"
-
 "@ampproject/remapping@^2.1.0":
 "@ampproject/remapping@^2.1.0":
   version "2.2.0"
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
   resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@@ -1448,7 +1438,7 @@
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.4"
     regenerator-runtime "^0.13.4"
 
 
-"@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
+"@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
   version "7.18.6"
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
   integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
   integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
@@ -1462,10 +1452,10 @@
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.4"
     regenerator-runtime "^0.13.4"
 
 
-"@babel/runtime@^7.18.6":
-  version "7.19.0"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
-  integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
+"@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4":
+  version "7.19.4"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78"
+  integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.4"
     regenerator-runtime "^0.13.4"
 
 
@@ -4490,6 +4480,13 @@
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
   integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
   integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
 
 
+"@types/react-scroll@^1.8.4":
+  version "1.8.4"
+  resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.4.tgz#2b6258fb34104d3fcc7a22e8eceaadc669ba3ad1"
+  integrity sha512-DpHA9PYw42/rBrfKbGE/kAEvHRfyDL/ACfKB/ORWUYuCLi/yGrntxSzYXmg/7TLgQsJ5ma13GCDOzFSOz+8XOA==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react@*":
 "@types/react@*":
   version "16.9.23"
   version "16.9.23"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c"
@@ -11007,14 +11004,6 @@ glam@^5.0.1:
     fbjs "^0.8.16"
     fbjs "^0.8.16"
     inline-style-prefixer "^3.0.8"
     inline-style-prefixer "^3.0.8"
 
 
-glob-all@^3.1.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.3.0.tgz#2019896fbaeb37bc451809cf0cb1e5d2b3e345b2"
-  integrity sha512-30gCh9beSb+YSAh0vsoIlBRm4bSlyMa+5nayax1EJhjwYrCohX0aDxcxvWVe3heOrJikbHgRs75Af6kPLcumew==
-  dependencies:
-    glob "^7.1.2"
-    yargs "^15.3.1"
-
 glob-base@^0.3.0:
 glob-base@^0.3.0:
   version "0.3.0"
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -11051,7 +11040,7 @@ glob2base@^0.0.12:
   dependencies:
   dependencies:
     find-index "^0.1.1"
     find-index "^0.1.1"
 
 
-glob@7.1.6, glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6:
+glob@7.1.6, glob@^7.0.0, glob@^7.1.3, glob@^7.1.6:
   version "7.1.6"
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -11928,41 +11917,41 @@ hyphenate-style-name@^1.0.2:
   resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
   resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
   integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
   integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
 
 
-i18next-chained-backend@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-3.0.2.tgz#8968c9e12412d24fd23eec109f0340386154384a"
-  integrity sha512-0dd/7oVtPHJnCDMuDvjzlXmWxwfbLOGBFXd1+cgcZ54QlMwv6/ofQ9xhrBIhCFjNh97WQ5pytEeTdcAGwLQ/QA==
+i18next-chained-backend@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/i18next-chained-backend/-/i18next-chained-backend-4.0.0.tgz#97679ee4b6e04e1ad96e49b3c4ab755ff62238eb"
+  integrity sha512-gOfkl2tvRDSMKQ2vaYbP+n5fsHeYM/836/Co8/NVP8LplRE8Ck7IrKWswp4vKw4D5Ji7cEdzA4drrG4ssgsXIg==
   dependencies:
   dependencies:
-    "@babel/runtime" "^7.14.0"
+    "@babel/runtime" "^7.19.4"
 
 
-i18next-fs-backend@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.1.4.tgz#d0e9b9ed2fa7a0f11002d82b9fa69c3c3d6482da"
-  integrity sha512-/MfAGMP0jHonV966uFf9PkWWuDjPYLIcsipnSO3NxpNtAgRUKLTwvm85fEmsF6hGeu0zbZiCQ3W74jwO6K9uXA==
+i18next-fs-backend@^1.1.5:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.2.0.tgz#c498c68c8e6a8ae5ed59bea5e5392a11991de696"
+  integrity sha512-pUx3AcgXCbur0jpFA7U67Z2RJflAcIi698Y8VL+phdOqUchahxriV3Cs+M6UkPNQSS/zPEzWLfdJ8EgjB7HVxg==
 
 
-i18next-hmr@^1.7.7:
-  version "1.7.7"
-  resolved "https://registry.yarnpkg.com/i18next-hmr/-/i18next-hmr-1.7.7.tgz#8288697ff5595d1201990d6d0de65c4a58e0ffd5"
-  integrity sha512-jZuRSyJ9IfZUGENlTnYlqsSk+Cv/rGo//udrz3lxu/yGCxPW9A8dHS1HSs6fJVXgdHtiV4CuNN5+uRqCFb+y3g==
+i18next-hmr@^1.11.0:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/i18next-hmr/-/i18next-hmr-1.11.0.tgz#2c474f68910f2f45d10ce7c76402a99bb0dc589f"
+  integrity sha512-OUKJ9oCwLjlBQ4rbB8PAaYVzsOcl6FjeRM1yA6kqyzfpS7uSNgk0aGhSIZ6vexu1Wu6Ymi3dTKM9rseUG+5Mog==
 
 
-i18next-http-backend@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz#d8d308e7d8c5b89988446d0b83f469361e051bc0"
-  integrity sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==
+i18next-http-backend@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.0.0.tgz#7be736eb4c592e110b9ee54a985b737248d1c43f"
+  integrity sha512-6aFT5LcDOSxFyaoezruIxZDzpp6nu92j1iZc444nrz/OOaF7rsxQFNi1es19la53MQQFzG7uD2Koxi7Jav8khg==
   dependencies:
   dependencies:
     cross-fetch "3.1.5"
     cross-fetch "3.1.5"
 
 
-i18next-localstorage-backend@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/i18next-localstorage-backend/-/i18next-localstorage-backend-3.1.3.tgz#5eaad25a515bdadebeb13e1486acfa6fa1686cbe"
-  integrity sha512-tx8dxQTEsTnRC654IrXPFr94c3NH7bIVHGKHnGvbgefpLz13/uFT5ITsmhqhg/gOza0TIj8e5jTsGnQytIhh+A==
+i18next-localstorage-backend@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/i18next-localstorage-backend/-/i18next-localstorage-backend-4.0.0.tgz#bd1b4318fe0f97baa1121dbb31c0c57e61e45a5d"
+  integrity sha512-XErjf0Zvciw3fo9/vzU1hWQfwHViq8l31ahKEvf6lgtqysPCtCBxNlIdrSjVZWEe76LD/thox1ixmO9PmlsL/w==
   dependencies:
   dependencies:
-    "@babel/runtime" "^7.14.6"
+    "@babel/runtime" "^7.19.4"
 
 
-i18next@^21.8.13:
-  version "21.9.2"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.9.2.tgz#3f7c5594393eb27117c1db4c38f5ec766e68de0e"
-  integrity sha512-00fVrLQOwy45nm3OtC9l1WiLK3nJlIYSljgCt0qzTaAy65aciMdRy9GsuW+a2AtKtdg9/njUGfRH30LRupV7ZQ==
+i18next@^21.9.1:
+  version "21.10.0"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.10.0.tgz#85429af55fdca4858345d0e16b584ec29520197d"
+  integrity sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==
   dependencies:
   dependencies:
     "@babel/runtime" "^7.17.2"
     "@babel/runtime" "^7.17.2"
 
 
@@ -14161,15 +14150,6 @@ load-plugin@^4.0.0:
     import-meta-resolve "^1.0.0"
     import-meta-resolve "^1.0.0"
     libnpmconfig "^1.0.0"
     libnpmconfig "^1.0.0"
 
 
-loader-utils@^1.2.3:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
-  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
-  dependencies:
-    big.js "^5.2.2"
-    emojis-list "^3.0.0"
-    json5 "^1.0.1"
-
 loader-utils@^2.0.0:
 loader-utils@^2.0.0:
   version "2.0.2"
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
@@ -16270,18 +16250,18 @@ nested-error-stacks@^2.0.0:
   resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
   resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
   integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
   integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
 
 
-next-i18next@^11.3.0:
-  version "11.3.0"
-  resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-11.3.0.tgz#bfce51d8df07fb5cd61097423eeb7d744e09ae25"
-  integrity sha512-xl0oIRtiVrk9ZaWBRUbNk/prva4Htdu59o9rFWzd9ax/KemaDVuTTuBZTQMkmXohUQk/MJ7w1rV/mICL6TzyGw==
+next-i18next@^12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-12.1.0.tgz#70926fbe966bc4750d2f68573307bfe36eadba46"
+  integrity sha512-rhos/PVULmZPdC0jpec2MDBQMXdGZ3+Mbh/tZfrDtjgnVN3ucdq7k8BlwsJNww6FnqC8AC31n6dSYuqVzYsGsw==
   dependencies:
   dependencies:
-    "@babel/runtime" "^7.18.6"
+    "@babel/runtime" "^7.18.9"
     "@types/hoist-non-react-statics" "^3.3.1"
     "@types/hoist-non-react-statics" "^3.3.1"
     core-js "^3"
     core-js "^3"
     hoist-non-react-statics "^3.3.2"
     hoist-non-react-statics "^3.3.2"
-    i18next "^21.8.13"
-    i18next-fs-backend "^1.1.4"
-    react-i18next "^11.18.0"
+    i18next "^21.9.1"
+    i18next-fs-backend "^1.1.5"
+    react-i18next "^11.18.4"
 
 
 next-superjson@^0.0.4:
 next-superjson@^0.0.4:
   version "0.0.4"
   version "0.0.4"
@@ -18622,7 +18602,7 @@ react-hotkeys@^2.0.0:
   dependencies:
   dependencies:
     prop-types "^15.6.1"
     prop-types "^15.6.1"
 
 
-react-i18next@^11.18.0:
+react-i18next@^11.18.4:
   version "11.18.6"
   version "11.18.6"
   resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"
   resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"
   integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==
   integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==