Procházet zdrojové kódy

Merge branch 'fix/107120-fix-trash-page-list' of https://github.com/weseek/growi into fix/107120-fix-trash-page-list

Yuken Tezuka před 3 roky
rodič
revize
07366849e4
38 změnil soubory, kde provedl 209 přidání a 413 odebrání
  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. 0 0
      packages/app/_obsolete/src/components/MyDraftList/Draft.tsx
  12. 0 0
      packages/app/_obsolete/src/components/MyDraftList/MyDraftList.jsx
  13. 5 6
      packages/app/package.json
  14. 0 2
      packages/app/public/static/locales/en_US/admin.json
  15. 5 1
      packages/app/public/static/locales/en_US/translation.json
  16. 0 2
      packages/app/public/static/locales/ja_JP/admin.json
  17. 5 1
      packages/app/public/static/locales/ja_JP/translation.json
  18. 0 2
      packages/app/public/static/locales/zh_CN/admin.json
  19. 5 1
      packages/app/public/static/locales/zh_CN/translation.json
  20. 2 4
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  21. 0 86
      packages/app/src/components/Admin/Users/RemoveAdminButton.jsx
  22. 0 85
      packages/app/src/components/Admin/Users/StatusSuspendedButton.jsx
  23. 3 11
      packages/app/src/components/ArchiveCreateModal.jsx
  24. 1 1
      packages/app/src/components/IdenticalPathPage.tsx
  25. 4 1
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  26. 6 3
      packages/app/src/components/InstallerForm.tsx
  27. 7 4
      packages/app/src/components/Navbar/GlobalSearch.tsx
  28. 24 16
      packages/app/src/components/PageEditor/ConflictDiffModal.tsx
  29. 12 17
      packages/app/src/components/PageStatusAlert.jsx
  30. 2 2
      packages/app/src/components/Sidebar/PageTree.tsx
  31. 5 1
      packages/app/src/components/Sidebar/Tag.tsx
  32. 9 5
      packages/app/src/components/TagList.tsx
  33. 0 38
      packages/app/src/components/User/LikerList.jsx
  34. 1 2
      packages/app/src/pages/utils/commons.ts
  35. 36 33
      packages/app/src/server/models/page.ts
  36. 2 2
      packages/app/src/server/service/page.ts
  37. 2 1
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  38. 41 68
      yarn.lock

+ 4 - 2
.github/dependabot.yml

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

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

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

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

@@ -19,7 +19,7 @@ jobs:
       - uses: actions/checkout@v3
 
       - 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
 
       # Drafts your next Release notes as Pull Requests are merged into "master"
@@ -48,7 +48,7 @@ jobs:
         id: release-version
         run: |
           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
       - name: Create/Update Pull Request

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

@@ -23,10 +23,10 @@ jobs:
       run: |
         export SLACK_ATTACHMENTS_ILLEGAL=`node bin/github-actions/list-branches --illegal`
         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
       if: steps.list-branches.outputs.SLACK_ATTACHMENTS_LENGTH_ILLEGAL > 0

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

@@ -17,7 +17,7 @@ jobs:
         lfs: true
 
     - 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
 
     - name: Docker meta

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

@@ -17,7 +17,7 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
 
     - 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
       with:
         workingDir: packages/slackbot-proxy
@@ -115,7 +115,7 @@ jobs:
         yarn bump-versions:slackbot-proxy
 
     - 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
       with:
         workingDir: packages/slackbot-proxy

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

@@ -38,7 +38,7 @@ jobs:
         sh ./packages/app/bin/github-actions/update-readme.sh
 
     - 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
 
     - name: Update Changelog
@@ -99,7 +99,7 @@ jobs:
         yarn bump-versions:slackbot-proxy
 
     - 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
 
     - name: Commit
@@ -140,7 +140,7 @@ jobs:
       id: suffix
       run: |
         [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
-        echo "::set-output name=SUFFIX::$suffix"
+        echo "SUFFIX=$suffix" >> $GITHUB_OUTPUT
 
     - name: Docker meta
       id: meta

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

@@ -10,6 +10,9 @@ on:
         type: boolean
       cypress-report-artifact-name:
         type: string
+      cypress-config-video:
+        type: boolean
+        default: false
     secrets:
       SLACK_WEBHOOK_URL:
         required: true
@@ -70,7 +73,7 @@ jobs:
           packages/app/.env.production* \
           packages/*/package.json \
           packages/*/dist
-        echo ::set-output name=file::production.tar.gz
+        echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
     - name: Upload production files as artifact
       uses: actions/upload-artifact@v3
@@ -126,8 +129,8 @@ jobs:
     - name: Get Date
       id: get-date
       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)
       id: cache-dependencies
@@ -247,7 +250,7 @@ jobs:
       id: determine-spec-exp
       run: |
         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
       working-directory: ./packages/app
@@ -274,6 +277,7 @@ jobs:
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         start: yarn server
         wait-on: 'http://localhost:3000'
+        config: video=${{ inputs.cypress-config-video }}
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
         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


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


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


+ 5 - 6
packages/app/package.json

@@ -116,9 +116,9 @@
     "hast-util-select": "^5.0.2",
     "helmet": "^4.6.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-iso-date": "^0.0.1",
     "lucene-query-parser": "^1.2.0",
@@ -133,7 +133,7 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "next": "^12.2.5",
-    "next-i18next": "^11.3.0",
+    "next-i18next": "^12.1.0",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
     "nocache": "^3.0.1",
@@ -205,7 +205,6 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@alienfast/i18next-loader": "^1.1.4",
     "@growi/ui": "^6.0.0-RC.7",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
@@ -232,7 +231,7 @@
     "eslint-plugin-regex": "^1.8.0",
     "font-awesome": "^4.7.0",
     "handsontable": "=6.2.2",
-    "i18next-hmr": "^1.7.7",
+    "i18next-hmr": "^1.11.0",
     "jquery-slimscroll": "^1.3.8",
     "jquery.cookie": "~1.4.1",
     "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>"
   },
   "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_note": "Note: You will lose unique constraints from the page paths.",
     "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:",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "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",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
@@ -863,5 +863,9 @@
   "footer": {
     "bookmarks": "Bookmarks",
     "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>"
   },
   "v5_page_migration": {
-    "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
-    "go_to_settings": "設定する",
     "migration_desc": "公開されているページに 古い v4 互換形式のものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページを v5 互換形式に変換してください。",
     "migration_note": "注意: ページパスからユニーク制約が失われます。",
     "upgrade_to_v5": "v5 互換形式 へ変換",

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

@@ -109,7 +109,7 @@
   "Create under": "ページを以下に作成",
   "V5 Page Migration": "V5 互換形式 への変換",
   "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設定",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
@@ -854,5 +854,9 @@
   "footer": {
     "bookmarks": "ブックマーク",
     "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>"
   },
   "v5_page_migration": {
-    "page_tree_not_avaliable": "Page Tree 功能不可用",
-    "go_to_settings": "进入设置,启用该功能",
     "migration_desc": "有一些页面具有旧的v4兼容性。为了利用新的功能,如页面树和容易重命名,请将您的所有页面转换为v5兼容性。",
     "migration_note": "注意:你将失去页面路径的唯一约束。",
     "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:",
   "V5 Page Migration": "转换为V5的兼容性",
   "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设置",
 	"Markdown Settings": "Markdown设置",
 	"Notification Settings": "通知设置",
@@ -909,5 +909,9 @@
   "footer": {
     "bookmarks": "书签",
     "recently_created": "最近创建页面"
+  },
+  "v5_page_migration": {
+    "page_tree_not_avaliable": "Page Tree 功能不可用",
+    "go_to_settings": "进入设置,启用该功能"
   }
 }

+ 2 - 4
packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import urljoin from 'url-join';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
@@ -45,7 +44,7 @@ const ManageGlobalNotification = (props) => {
     }
   }, [triggerEvents]);
 
-  const submitHandler = useCallback(async() => {
+  const updateButtonClickedHandler = useCallback(async() => {
 
     const requestParams = {
       triggerPath,
@@ -62,7 +61,6 @@ const ManageGlobalNotification = (props) => {
       else {
         await apiv3Post('/notification-setting/global-notification', requestParams);
       }
-      window.location.href = urljoin(window.location.origin, '/admin/notification#global-notification');
     }
     catch (err) {
       toastError(err);
@@ -271,7 +269,7 @@ const ManageGlobalNotification = (props) => {
       </div>
 
       <AdminUpdateButtonRow
-        onClick={submitHandler}
+        onClick={updateButtonClickedHandler}
         disabled={adminNotificationContainer.state.retrieveError != null}
       />
     </>

+ 0 - 86
packages/app/src/components/Admin/Users/RemoveAdminButton.jsx

@@ -1,86 +0,0 @@
-import React, { Fragment } from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class RemoveAdminButton extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickRemoveAdminBtn = this.onClickRemoveAdminBtn.bind(this);
-  }
-
-  async onClickRemoveAdminBtn() {
-    const { t } = this.props;
-
-    try {
-      const username = await this.props.adminUsersContainer.removeUserAdmin(this.props.user._id);
-      toastSuccess(t('toaster.remove_user_admin', { username }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-
-  renderRemoveAdminBtn() {
-    const { t } = this.props;
-
-    return (
-      <button className="dropdown-item" type="button" onClick={() => { this.onClickRemoveAdminBtn() }}>
-        <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_management.user_table.remove_admin_access')}
-      </button>
-    );
-  }
-
-  renderRemoveAdminAlert() {
-    const { t } = this.props;
-
-    return (
-      <div className="px-4">
-        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('admin:user_management.user_table.remove_admin_access')}
-        <p className="alert alert-danger">{t('admin:user_management.user_table.cannot_remove')}</p>
-      </div>
-    );
-  }
-
-  render() {
-    const { user } = this.props;
-    const { currentUsername } = this.props.appContainer;
-
-    return (
-      <Fragment>
-        {user.username !== currentUsername ? this.renderRemoveAdminBtn()
-          : this.renderRemoveAdminAlert()}
-      </Fragment>
-    );
-  }
-
-}
-
-const RemoveAdminButtonWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <RemoveAdminButton t={t} {...props} />;
-};
-
-/**
-* Wrapper component for using unstated
-*/
-const RemoveAdminButtonWrapper = withUnstatedContainers(RemoveAdminButtonWrapperFC, [AppContainer, AdminUsersContainer]);
-
-RemoveAdminButton.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default RemoveAdminButtonWrapper;

+ 0 - 85
packages/app/src/components/Admin/Users/StatusSuspendedButton.jsx

@@ -1,85 +0,0 @@
-import React, { Fragment } from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class StatusSuspendedButton extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickDeactiveBtn = this.onClickDeactiveBtn.bind(this);
-  }
-
-  async onClickDeactiveBtn() {
-    const { t } = this.props;
-
-    try {
-      const username = await this.props.adminUsersContainer.deactivateUser(this.props.user._id);
-      toastSuccess(t('toaster.deactivate_user_success', { username }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  renderSuspendedBtn() {
-    const { t } = this.props;
-
-    return (
-      <button className="dropdown-item" type="button" onClick={() => { this.onClickDeactiveBtn() }}>
-        <i className="icon-fw icon-ban"></i> {t('admin:user_management.user_table.deactivate_account')}
-      </button>
-    );
-  }
-
-  renderSuspendedAlert() {
-    const { t } = this.props;
-
-    return (
-      <div className="px-4">
-        <i className="icon-fw icon-ban mb-2"></i>{t('admin:user_management.user_table.deactivate_account')}
-        <p className="alert alert-danger">{t('admin:user_management.user_table.your_own')}</p>
-      </div>
-    );
-  }
-
-  render() {
-    const { user } = this.props;
-    const { currentUsername } = this.props.appContainer;
-
-    return (
-      <Fragment>
-        {user.username !== currentUsername ? this.renderSuspendedBtn()
-          : this.renderSuspendedAlert()}
-      </Fragment>
-    );
-  }
-
-}
-
-const StatusSuspendedFormWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <StatusSuspendedButton t={t} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-const StatusSuspendedFormWrapper = withUnstatedContainers(StatusSuspendedFormWrapperFC, [AppContainer, AdminUsersContainer]);
-
-StatusSuspendedButton.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default StatusSuspendedFormWrapper;

+ 3 - 11
packages/app/src/components/ArchiveCreateModal.jsx

@@ -1,17 +1,14 @@
 import React, { useState, useCallback } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
 
 const ArchiveCreateModal = (props) => {
   const { t } = useTranslation();
@@ -235,7 +232,7 @@ const ArchiveCreateModal = (props) => {
 };
 
 ArchiveCreateModal.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func,
   path: PropTypes.string.isRequired,
@@ -243,9 +240,4 @@ ArchiveCreateModal.propTypes = {
   errorMessage: PropTypes.string,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const ArchiveCreateModalWrapper = withUnstatedContainers(ArchiveCreateModal, [AppContainer]);
-
-export default ArchiveCreateModalWrapper;
+export default ArchiveCreateModal;

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

@@ -40,7 +40,7 @@ const IdenticalPathAlert : FC<IdenticalPathAlertProps> = (props: IdenticalPathAl
           { path: _path, pageName: _pageName })}<br />
         <span
           // 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 className="mb-1">{t('duplicated_page_alert.select_page_to_see')}</p>

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

@@ -4,6 +4,7 @@ import React, {
 
 import { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui';
+import { useRouter } from 'next/router';
 
 import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import { IInAppNotification } from '~/interfaces/in-app-notification';
@@ -24,6 +25,8 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
     notification, actionMsg, actionIcon, actionUsers,
   } = props;
 
+  const router = useRouter();
+
   const snapshot = parseSnapshot(notification.snapshot);
 
   // publish open()
@@ -33,7 +36,7 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
         // jump to target page
         const targetPagePath = notification.target.path;
         if (targetPagePath != null) {
-          window.location.href = targetPagePath;
+          router.push(targetPagePath);
         }
       }
     },

+ 6 - 3
packages/app/src/components/InstallerForm.tsx

@@ -3,6 +3,7 @@ import {
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
@@ -12,6 +13,8 @@ import { apiv3Post } from '~/client/util/apiv3-client';
 const InstallerForm = memo((): JSX.Element => {
   const { t, i18n } = useTranslation();
 
+  const router = useRouter();
+
   const [isValidUserName, setValidUserName] = useState(true);
   const [isSubmittingDisabled, setSubmittingDisabled] = useState(false);
   const [currentLocale, setCurrentLocale] = useState('en_US');
@@ -70,7 +73,7 @@ const InstallerForm = memo((): JSX.Element => {
 
     try {
       await apiv3Post('/installer', data);
-      window.location.href = '/';
+      router.push('/');
     }
     catch (errs) {
       const err = errs[0];
@@ -78,12 +81,12 @@ const InstallerForm = memo((): JSX.Element => {
 
       if (code === 'failed_to_login_after_install') {
         toastError(t('installer.failed_to_login_after_install'));
-        setTimeout(() => { window.location.href = '/login' }, 700); // Wait 700 ms to show toastr
+        setTimeout(() => { router.push('/login') }, 700); // Wait 700 ms to show toastr
       }
 
       toastError(t('installer.failed_to_install'));
     }
-  }, [isSubmittingDisabled, t, currentLocale]);
+  }, [isSubmittingDisabled, currentLocale, router, t]);
 
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const unavailableUserId = isValidUserName

+ 7 - 4
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -3,6 +3,7 @@ import React, { useState, useCallback, useRef } from 'react';
 import assert from 'assert';
 
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IPageWithSearchMeta } from '~/interfaces/search';
@@ -25,6 +26,8 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
 
   const { dropup } = props;
 
+  const router = useRouter();
+
   const globalSearchFormRef = useRef<IFocusable>(null);
 
   useGlobalSearchFormRef(globalSearchFormRef);
@@ -45,9 +48,9 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
 
     // navigate to page
     if (page != null) {
-      window.location.href = `/${page._id}`;
+      router.push(`/${page._id}`);
     }
-  }, []);
+  }, [router]);
 
   const search = useCallback(() => {
     const url = new URL(window.location.href);
@@ -60,8 +63,8 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
     }
     url.searchParams.append('q', q);
 
-    window.location.href = url.href;
-  }, [currentPagePath, isScopeChildren, text]);
+    router.push(url.href);
+  }, [currentPagePath, isScopeChildren, router, text]);
 
   const scopeLabel = isScopeChildren
     ? t('header_search_box.label.This tree')

+ 24 - 16
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -14,7 +14,6 @@ import { IUser } from '~/interfaces/user';
 import { useCurrentUser } from '~/stores/context';
 import { useEditorMode } from '~/stores/ui';
 
-import PageContainer from '../../client/services/PageContainer';
 import { IRevisionOnConflict } from '../../interfaces/revision';
 import ExpandOrContractButton from '../ExpandOrContractButton';
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
@@ -29,7 +28,7 @@ Object.keys(DMP).forEach((key) => { window[key] = DMP[key] });
 type ConflictDiffModalProps = {
   isOpen?: boolean;
   onClose?: (() => void);
-  pageContainer: PageContainer;
+  // pageContainer: PageContainer;
   markdownOnEdit: string;
 };
 
@@ -38,7 +37,7 @@ type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'>
 }
 
 const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IUser }): JSX.Element => {
-  const { currentUser, pageContainer, onClose } = props;
+  const { currentUser, onClose } = props;
 
   const { data: editorMode } = useEditorMode();
 
@@ -59,16 +58,25 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
     user: currentUser,
   };
   const origin: IRevisionOnConflictWithStringDate = {
-    revisionId: pageContainer.state.revisionId || '',
-    revisionBody: pageContainer.state.markdown || '',
-    createdAt: pageContainer.state.updatedAt || '',
-    user: pageContainer.state.revisionAuthor,
+    // revisionId: pageContainer.state.revisionId || '',
+    // revisionBody: pageContainer.state.markdown || '',
+    // createdAt: pageContainer.state.updatedAt || '',
+    // user: pageContainer.state.revisionAuthor,
+    revisionId:  '',
+    revisionBody: '',
+    createdAt: '',
+    user: {} as IUser,
   };
   const latest: IRevisionOnConflictWithStringDate = {
-    revisionId: pageContainer.state.remoteRevisionId || '',
-    revisionBody: pageContainer.state.remoteRevisionBody || '',
-    createdAt: format(new Date(pageContainer.state.remoteRevisionUpdateAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
-    user: pageContainer.state.lastUpdateUser,
+    // revisionId: pageContainer.state.remoteRevisionId || '',
+    // revisionBody: pageContainer.state.remoteRevisionBody || '',
+    // createdAt: format(new Date(pageContainer.state.remoteRevisionUpdateAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
+    // user: pageContainer.state.lastUpdateUser,
+    revisionId: '',
+    revisionBody: '',
+    createdAt: format(new Date(''), 'yyyy/MM/dd HH:mm:ss'),
+    user: {} as IUser,
+
   };
 
   useEffect(() => {
@@ -101,15 +109,15 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
     const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue();
 
     try {
-      await pageContainer.resolveConflict(codeMirrorVal, editorMode);
-      close();
-      pageContainer.showSuccessToastr();
+      // await pageContainer.resolveConflict(codeMirrorVal, editorMode);
+      // close();
+      // pageContainer.showSuccessToastr();
     }
     catch (error) {
-      pageContainer.showErrorToastr(error);
+      // pageContainer.showErrorToastr(error);
     }
 
-  }, [editorMode, close, pageContainer]);
+  }, [editorMode, close]);
 
   const resizeAndCloseButtons = useMemo(() => (
     <div className="d-flex flex-nowrap">

+ 12 - 17
packages/app/src/components/PageStatusAlert.jsx

@@ -3,8 +3,8 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
+// import AppContainer from '~/client/services/AppContainer';
+// import PageContainer from '~/client/services/PageContainer';
 import Username from '~/components/User/Username';
 
 import { withUnstatedContainers } from './UnstatedUtils';
@@ -73,15 +73,15 @@ class PageStatusAlert extends React.Component {
   }
 
   getContentsForUpdatedAlert() {
-    const { t, appContainer, pageContainer } = this.props;
-    const pageEditor = appContainer.getComponentInstance('PageEditor');
+    const { t } = this.props;
+    // const pageEditor = appContainer.getComponentInstance('PageEditor');
 
-    let isConflictOnEdit = false;
+    const isConflictOnEdit = false;
 
-    if (pageEditor != null) {
-      const markdownOnEdit = pageEditor.getMarkdown();
-      isConflictOnEdit = markdownOnEdit !== pageContainer.state.markdown;
-    }
+    // if (pageEditor != null) {
+    //   const markdownOnEdit = pageEditor.getMarkdown();
+    //   isConflictOnEdit = markdownOnEdit !== pageContainer.state.markdown;
+    // }
 
     // TODO: re-impl with Next.js way
     // const usernameComponentToString = ReactDOMServer.renderToString(<Username user={pageContainer.state.lastUpdateUser} />);
@@ -165,8 +165,8 @@ class PageStatusAlert extends React.Component {
 PageStatusAlert.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  // pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 
 const PageStatusAlertWrapperFC = (props) => {
@@ -174,9 +174,4 @@ const PageStatusAlertWrapperFC = (props) => {
   return <PageStatusAlert t={t} {...props} />;
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PageStatusAlertWrapper = withUnstatedContainers(PageStatusAlertWrapperFC, [AppContainer, PageContainer]);
-
-export default PageStatusAlertWrapper;
+export default PageStatusAlertWrapperFC;

+ 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>
         </div>
         <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>
       </>
     );

+ 5 - 1
packages/app/src/components/Sidebar/Tag.tsx

@@ -1,6 +1,7 @@
 import React, { FC, useState, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 
 import { IDataTagCount } from '~/interfaces/tag';
 import { useSWRxTagsList } from '~/stores/tag';
@@ -13,6 +14,9 @@ const PAGING_LIMIT = 10;
 const TAG_CLOUD_LIMIT = 20;
 
 const Tag: FC = () => {
+
+  const router = useRouter();
+
   const [activePage, setActivePage] = useState<number>(1);
   const [offset, setOffset] = useState<number>(0);
 
@@ -74,7 +78,7 @@ const Tag: FC = () => {
         <button
           className="btn btn-primary rounded px-4"
           type="button"
-          onClick={() => { window.location.href = '/tags' }}
+          onClick={() => router.push('/tags')}
         >
           {t('Check All tags')}
         </button>

+ 9 - 5
packages/app/src/components/TagList.tsx

@@ -3,6 +3,7 @@ import React, {
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 
 import { IDataTagCount } from '~/interfaces/tag';
 
@@ -33,14 +34,17 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
       const tagListClasses: string = index === 0 ? 'list-group-item d-flex' : 'list-group-item d-flex border-top-0';
 
       return (
-        <a
+        <Link
           key={tag._id}
           href={`/_search?q=tag:${encodeURIComponent(tag.name)}`}
-          className={tagListClasses}
         >
-          <div className="text-truncate list-tag-name">{tag.name}</div>
-          <div className="ml-4 my-auto py-1 px-2 list-tag-count badge badge-secondary text-white">{tag.count}</div>
-        </a>
+          <a
+            className={tagListClasses}
+          >
+            <div className="text-truncate list-tag-name">{tag.name}</div>
+            <div className="ml-4 my-auto py-1 px-2 list-tag-count badge badge-secondary text-white">{tag.count}</div>
+          </a>
+        </Link>
       );
     });
   }, []);

+ 0 - 38
packages/app/src/components/User/LikerList.jsx

@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import UserPictureList from './UserPictureList';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import PageContainer from '~/client/services/PageContainer';
-
-class LikerList extends React.Component {
-
-  render() {
-    const { pageContainer } = this.props;
-    return (
-      <div className="user-list-content text-truncate text-muted text-right">
-        <span className="text-info">
-          <span className="liker-user-count">{pageContainer.state.sumOfLikers}</span>
-          <i className="fa fa-fw fa-heart-o"></i>
-        </span>
-        <span className="mr-1">
-          <UserPictureList users={pageContainer.state.likerUsers} />
-        </span>
-      </div>
-    );
-  }
-
-}
-
-LikerList.propTypes = {
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const LikerListWrapper = withUnstatedContainers(LikerList, [PageContainer]);
-
-export default (LikerListWrapper);

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

@@ -78,12 +78,11 @@ export const getNextI18NextConfig = async(
     ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
     ?? 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'];
   if (namespacesRequired != null) {
     namespaces.push(...namespacesRequired);
   }
+  // TODO: deprecate 'translation.json' in the future
   else {
     namespaces.push('translation');
   }

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

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

+ 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', () => {
-    cy.visit('/tags');
 
     // open sidebar
     cy.collapseSidebar(false);
+
+    cy.visit('/tags');
     // select tags
     cy.getByTestid('grw-sidebar-nav-primary-tags').click();
     cy.getByTestid('grw-sidebar-content-tags').should('be.visible');

+ 41 - 68
yarn.lock

@@ -10,16 +10,6 @@
     plantuml-encoder "^1.4.0"
     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":
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@@ -1448,7 +1438,7 @@
   dependencies:
     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"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
   integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
@@ -1462,10 +1452,10 @@
   dependencies:
     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:
     regenerator-runtime "^0.13.4"
 
@@ -11014,14 +11004,6 @@ glam@^5.0.1:
     fbjs "^0.8.16"
     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:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -11058,7 +11040,7 @@ glob2base@^0.0.12:
   dependencies:
     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"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -11935,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"
   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:
-    "@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:
     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:
-    "@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:
     "@babel/runtime" "^7.17.2"
 
@@ -14168,15 +14150,6 @@ load-plugin@^4.0.0:
     import-meta-resolve "^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:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
@@ -16277,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"
   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:
-    "@babel/runtime" "^7.18.6"
+    "@babel/runtime" "^7.18.9"
     "@types/hoist-non-react-statics" "^3.3.1"
     core-js "^3"
     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:
   version "0.0.4"
@@ -18629,7 +18602,7 @@ react-hotkeys@^2.0.0:
   dependencies:
     prop-types "^15.6.1"
 
-react-i18next@^11.18.0:
+react-i18next@^11.18.4:
   version "11.18.6"
   resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"
   integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==