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

Merge remote-tracking branch 'origin/dev/7.0.x' into feat/new-editor

Yuki Takei 2 лет назад
Родитель
Сommit
bf5b55fca7
100 измененных файлов с 371 добавлено и 259 удалено
  1. 1 1
      .github/release-drafter-dev-7.0.x.yml
  2. 2 0
      .github/workflows/ci-app-prod.yml
  3. 9 1
      CHANGELOG.md
  4. 1 1
      apps/app/bin/github-actions/update-readme.sh
  5. 2 6
      apps/app/docker/README.md
  6. 3 7
      apps/app/package.json
  7. 9 1
      apps/app/public/static/locales/en_US/admin.json
  8. 9 1
      apps/app/public/static/locales/ja_JP/admin.json
  9. 9 1
      apps/app/public/static/locales/zh_CN/admin.json
  10. 10 0
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  11. 1 1
      apps/app/src/client/services/AdminHomeContainer.js
  12. 1 2
      apps/app/src/client/util/toastr.ts
  13. 6 1
      apps/app/src/components/Admin/AuditLog/ActivityTable.tsx
  14. 1 1
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  15. 3 23
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  16. 4 31
      apps/app/src/components/Admin/ExportArchiveDataPage.tsx
  17. 17 0
      apps/app/src/components/Admin/ForbiddenPage.tsx
  18. 0 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  19. 3 21
      apps/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  20. 22 0
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  21. 1 1
      apps/app/src/components/Admin/UserManagement.module.scss
  22. 1 1
      apps/app/src/components/BookmarkButtons.module.scss
  23. 3 3
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  24. 3 3
      apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  25. 1 1
      apps/app/src/components/Fab.module.scss
  26. 2 2
      apps/app/src/components/Layout/Admin.module.scss
  27. 1 1
      apps/app/src/components/Layout/NoLoginLayout.module.scss
  28. 1 1
      apps/app/src/components/Layout/RawLayout.tsx
  29. 1 1
      apps/app/src/components/Layout/SearchResultLayout.module.scss
  30. 1 1
      apps/app/src/components/LikeButtons.module.scss
  31. 1 1
      apps/app/src/components/LoginForm.tsx
  32. 1 1
      apps/app/src/components/Navbar/AuthorInfo.module.scss
  33. 1 2
      apps/app/src/components/Navbar/AuthorInfo.tsx
  34. 1 1
      apps/app/src/components/Navbar/GlobalSearch.module.scss
  35. 1 2
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  36. 1 1
      apps/app/src/components/Navbar/GrowiNavbar.module.scss
  37. 1 1
      apps/app/src/components/Navbar/GrowiSubNavigation.module.scss
  38. 1 1
      apps/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss
  39. 1 1
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  40. 2 1
      apps/app/src/components/Navbar/PersonalDropdown.jsx
  41. 8 9
      apps/app/src/components/Page/PageView.tsx
  42. 1 1
      apps/app/src/components/Page/TagLabels.module.scss
  43. 1 2
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx
  44. 1 1
      apps/app/src/components/PageComment.module.scss
  45. 2 1
      apps/app/src/components/PageComment/Comment.module.scss
  46. 1 1
      apps/app/src/components/PageComment/CommentEditor.module.scss
  47. 3 9
      apps/app/src/components/PageComment/CommentEditor.tsx
  48. 1 1
      apps/app/src/components/PageComment/CommentPreview.module.scss
  49. 1 1
      apps/app/src/components/PageComment/_comment-inheritance.scss
  50. 1 1
      apps/app/src/components/PageContentFooter.module.scss
  51. 14 14
      apps/app/src/components/PageCreateModal.jsx
  52. 1 1
      apps/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  53. 1 1
      apps/app/src/components/PageEditor/Editor.module.scss
  54. 1 1
      apps/app/src/components/PageEditor/GridEditModal.module.scss
  55. 1 1
      apps/app/src/components/PageHistory/RevisionDiff.module.scss
  56. 10 10
      apps/app/src/components/PageRenameModal.tsx
  57. 3 3
      apps/app/src/components/PageSideContents.tsx
  58. 3 2
      apps/app/src/components/PageStatusAlert.module.scss
  59. 3 3
      apps/app/src/components/PageTimeline.module.scss
  60. 1 1
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss
  61. 3 5
      apps/app/src/components/SavePageControls.tsx
  62. 1 1
      apps/app/src/components/SearchForm.module.scss
  63. 2 1
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  64. 1 1
      apps/app/src/components/SearchTypeahead.module.scss
  65. 1 1
      apps/app/src/components/ShortcutsModal.module.scss
  66. 2 2
      apps/app/src/components/Sidebar.module.scss
  67. 36 35
      apps/app/src/components/SlackNotification.module.scss
  68. 1 1
      apps/app/src/components/SubscribeButton.module.scss
  69. 1 1
      apps/app/src/components/TableOfContents.module.scss
  70. 1 1
      apps/app/src/components/TemplateModal/TemplateModal.module.scss
  71. 1 1
      apps/app/src/components/User/SeenUserInfo.module.scss
  72. 1 1
      apps/app/src/components/User/UserInfo.module.scss
  73. 2 1
      apps/app/src/components/User/Username.tsx
  74. 0 0
      apps/app/src/components/UsersHomepageFooter.module.scss
  75. 4 4
      apps/app/src/components/UsersHomepageFooter.tsx
  76. 1 1
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss
  77. 5 0
      apps/app/src/pages/admin/[...path].page.tsx
  78. 5 1
      apps/app/src/pages/admin/app.page.tsx
  79. 5 1
      apps/app/src/pages/admin/audit-log.page.tsx
  80. 4 0
      apps/app/src/pages/admin/customize.page.tsx
  81. 5 0
      apps/app/src/pages/admin/data-transfer.page.tsx
  82. 5 0
      apps/app/src/pages/admin/export.page.tsx
  83. 4 0
      apps/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx
  84. 4 0
      apps/app/src/pages/admin/global-notification/new.page.tsx
  85. 5 0
      apps/app/src/pages/admin/importer.page.tsx
  86. 12 4
      apps/app/src/pages/admin/index.page.tsx
  87. 4 0
      apps/app/src/pages/admin/markdown.page.tsx
  88. 5 0
      apps/app/src/pages/admin/notification.page.tsx
  89. 5 0
      apps/app/src/pages/admin/plugins.page.tsx
  90. 5 0
      apps/app/src/pages/admin/search.page.tsx
  91. 5 0
      apps/app/src/pages/admin/security.page.tsx
  92. 5 1
      apps/app/src/pages/admin/slack-integration-legacy.page.tsx
  93. 5 0
      apps/app/src/pages/admin/slack-integration.page.tsx
  94. 5 0
      apps/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx
  95. 5 0
      apps/app/src/pages/admin/user-groups.page.tsx
  96. 4 0
      apps/app/src/pages/admin/users/external-accounts.page.tsx
  97. 4 1
      apps/app/src/pages/admin/users/index.page.tsx
  98. 7 0
      apps/app/src/pages/me/[[...path]].page.tsx
  99. 1 0
      apps/app/src/pages/utils/commons.ts
  100. 2 3
      apps/app/src/server/crowi/index.js

+ 1 - 1
.github/release-drafter-dev-6.2.x.yml → .github/release-drafter-dev-7.0.x.yml

@@ -4,4 +4,4 @@ prerelease: true
 
 # Filter previous releases to consider only those with the tags starts with 'v6.2'
 include-pre-releases: true
-tag-prefix: v6.2
+tag-prefix: v7.0

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

@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - master
+      - dev/7.*.x
       - dev/6.*.x
     paths:
       - .github/workflows/ci-app-prod.yml
@@ -19,6 +20,7 @@ on:
   pull_request:
     branches:
       - master
+      - dev/7.*.x
       - dev/6.*.x
     types: [opened, reopened, synchronize]
     paths:

+ 9 - 1
CHANGELOG.md

@@ -1,9 +1,17 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.10...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.11...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.1.11](https://github.com/weseek/growi/compare/v6.1.10...v6.1.11) - 2023-08-07
+
+### 🐛 Bug Fixes
+
+- fix: Admin page permission when the user transit with next-routing (#7908) @WNomunomu
+- fix: Transitioning to a non-existent page under "/me" results in a 500 error (#7946) @miya
+- fix: Auto-scroll search result content 2 (#7943) @yuki-takei
+
 ## [v6.1.10](https://github.com/weseek/growi/compare/v6.1.9...v6.1.10) - 2023-08-01
 
 ### 🐛 Bug Fixes

+ 1 - 1
apps/app/bin/github-actions/update-readme.sh

@@ -2,4 +2,4 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`6\.2\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`7\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md

+ 2 - 6
apps/app/docker/README.md

@@ -10,13 +10,9 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.2.0`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.0/apps/app/docker/Dockerfile)
+* [`7.0.0`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.0/apps/app/docker/Dockerfile)
+* [`6.2.0`, `6.2`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.0/apps/app/docker/Dockerfile)
 * [`6.1.0`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.8/apps/app/docker/Dockerfile)
-* [`6.0.15`, `6.0` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.15/packages/app/docker/Dockerfile)
-* [`5.1.7`, `5.1`, `5` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
-* [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
-* [`4.5.23`, `4.5`, `4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
-* [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 
 
 What is GROWI?

+ 3 - 7
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.2.0-RC.0",
+  "version": "7.0.0-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -196,7 +196,6 @@
     "swagger-jsdoc": "^6.1.0",
     "swr": "^2.0.3",
     "throttle-debounce": "^5.0.0",
-    "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
@@ -225,10 +224,9 @@
     "@types/react-scroll": "^1.8.4",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
-    "bootstrap": "^4.6.1",
+    "bootstrap": "^5.3.1",
     "codemirror": "^5.64.0",
     "connect-browser-sync": "^2.1.0",
-    "core-js": "=2.6.9",
     "diff2html": "^3.4.35",
     "eazy-logger": "^3.1.0",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
@@ -242,8 +240,6 @@
     "jest": "^29.5.0",
     "jest-date-mock": "^1.0.8",
     "jest-localstorage-mock": "^2.4.14",
-    "jquery-slimscroll": "^1.3.8",
-    "jquery.cookie": "~1.4.1",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
     "mongodb-memory-server": "^8.12.2",
@@ -264,7 +260,7 @@
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "source-map-loader": "^4.0.1",
-    "swagger2openapi": "^5.3.1",
+    "swagger2openapi": "^7.0.8",
     "tsc-alias": "^1.2.9"
   }
 }

+ 9 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -3,7 +3,7 @@
     "display_name": "English"
   },
   "last_login": "Last login",
-  "wiki_management_home_page": "Wiki Management Home Page",
+  "wiki_management_homepage": "Wiki Management Homepage",
   "public": "Public",
   "anyone_with_the_link": "Anyone with the link",
   "specified_users": "Specified users",
@@ -45,6 +45,11 @@
     "admin_only": "Admin only",
     "admin_and_author": "Admin and author",
     "anyone": "Anyone",
+    "user_homepage_deletion": {
+      "user_homepage_deletion": "User homepage deletion",
+      "enable_user_homepage_deletion": "Enable user homepage deletion",
+      "when_deleting_a_user_the_user_homepage_is_also_deleted": "When deleting a user, the user homepage is also deleted."
+    },
     "session": "Session",
     "max_age": "Max age (msec)",
     "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
@@ -1055,5 +1060,8 @@
     "activate_plugin_success": "Succeeded to activating {{pluginName}}",
     "deactivate_plugin_success": "Succeeded to deactivate {{pluginName}}",
     "remove_plugin_success": "Succeeded to removing {{pluginName}}"
+  },
+  "forbidden_page": {
+    "do_not_have_admin_permission": "Users without administrative rights cannot access the administration screen."
   }
 }

+ 9 - 1
apps/app/public/static/locales/ja_JP/admin.json

@@ -11,7 +11,7 @@
   "Edit": "編集",
   "Description": "説明",
   "last_login": "最終ログイン",
-  "wiki_management_home_page": "Wiki管理トップ",
+  "wiki_management_homepage": "Wiki管理トップ",
   "public": "公開",
   "anyone_with_the_link": "リンクを知っている人のみ",
   "specified_users": "特定ユーザーのみ",
@@ -53,6 +53,11 @@
     "admin_only": "管理者のみ可能",
     "admin_and_author": "管理者とページ作者が可能",
     "anyone": "誰でも可能",
+    "user_homepage_deletion": {
+      "user_homepage_deletion": "ユーザーページの削除",
+      "enable_user_homepage_deletion": "ユーザーページの削除を有効化",
+      "when_deleting_a_user_the_user_homepage_is_also_deleted": "ユーザー削除時にユーザーページも削除します。"
+    },
     "session": "セッション",
     "max_age": "有効期間 (ミリ秒)",
     "max_age_desc": "ユーザーのセッション情報の有効期間をミリ秒で指定できます。<br>デフォルト値: 2592000000 (30日間)",
@@ -1063,5 +1068,8 @@
     "activate_plugin_success": "{{pluginName}}を有効化しました",
     "deactivate_plugin_success": "{{pluginName}}を無効化しました",
     "remove_plugin_success": "{{pluginName}}を削除しました"
+  },
+  "forbidden_page": {
+    "do_not_have_admin_permission": "管理者権限のないユーザーでは管理画面にはアクセスできません。"
   }
 }

+ 9 - 1
apps/app/public/static/locales/zh_CN/admin.json

@@ -11,7 +11,7 @@
   "Edit": "编辑",
   "Description": "描述",
   "last_login": "上次登录",
-  "wiki_management_home_page": "Wiki管理首页",
+  "wiki_management_homepage": "Wiki管理首页",
   "public": "公共",
   "anyone_with_the_link": "任何人",
   "specified_users": "仅指定用户",
@@ -53,6 +53,11 @@
 		"admin_only": "仅管理员",
 		"admin_and_author": "管理员|作者",
 		"anyone": "任何人",
+    "user_homepage_deletion": {
+      "user_homepage_deletion": "删除用户页面",
+      "enable_user_homepage_deletion": "启用删除用户页面",
+      "when_deleting_a_user_the_user_homepage_is_also_deleted": "当一个用户被删除时,用户页面也会被删除。"
+    },
     "session": "会议",
     "max_age": "有效期间  (msec)",
     "max_age_desc": "指定使用户会话过期的数量(以毫秒为单位)。<br>默认值: 2592000000 (30天)",
@@ -1063,5 +1068,8 @@
     "activate_plugin_success": "Succeeded to activating {{pluginName}}",
     "deactivate_plugin_success": "Succeeded to deactivate {{pluginName}}",
     "remove_plugin_success": "Succeeded to removing {{pluginName}}"
+  },
+  "forbidden_page": {
+    "do_not_have_admin_permission": "没有管理权限的用户无法访问管理屏幕。"
   }
 }

+ 10 - 0
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -38,6 +38,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
+      isUsersHomepageDeletionEnabled: false,
       isLocalEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
@@ -73,6 +74,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
+      isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
@@ -193,6 +195,13 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ isShowRestrictedByGroup:  !this.state.isShowRestrictedByGroup });
   }
 
+  /**
+   * Switch isUsersHomepageDeletionEnabled
+   */
+  switchIsUsersHomepageDeletionEnabled() {
+    this.setState({ isUsersHomepageDeletionEnabled: !this.state.isUsersHomepageDeletionEnabled });
+  }
+
   /**
    * Update restrictGuestMode
    * @memberOf AdminGeneralSecuritySContainer
@@ -209,6 +218,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
       hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
+      isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);

+ 1 - 1
apps/app/src/client/services/AdminHomeContainer.js

@@ -9,7 +9,7 @@ import { apiv3Get } from '../util/apiv3-client';
 const logger = loggerFactory('growi:services:AdminHomeContainer');
 
 /**
- * Service container for admin home page (AdminHome.jsx)
+ * Service container for admin homepage (AdminHome.jsx)
  * @extends {Container} unstated Container
  */
 export default class AdminHomeContainer extends Container {

+ 1 - 2
apps/app/src/client/util/toastr.ts

@@ -1,5 +1,4 @@
 import { toast, ToastContent, ToastOptions } from 'react-toastify';
-import * as toastrLegacy from 'toastr';
 
 import { toArrayIfNot } from '~/utils/array-utils';
 
@@ -34,5 +33,5 @@ export const toastWarningOption: ToastOptions = {
   closeButton: true,
 };
 export const toastWarning = (content: ToastContent, option: ToastOptions = toastWarningOption): void => {
-  toastrLegacy.warning(content, option);
+  toast.warning(content, option);
 };

+ 6 - 1
apps/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -48,7 +48,12 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                   { activity.user != null && (
                     <>
                       <UserPicture user={activity.user} />
-                      <a className="ml-2" href={pagePathUtils.userPageRoot(activity.user)}>{activity.snapshot?.username}</a>
+                      <a
+                        className="ml-2"
+                        href={pagePathUtils.userHomepagePath(activity.user)}
+                      >
+                        {activity.snapshot?.username}
+                      </a>
                     </>
                   )}
                 </td>

+ 1 - 1
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -29,7 +29,7 @@ const MenuLabel = ({ menu }: { menu: string }) => {
     case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
     case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
     case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
-    default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
+    default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_homepage') }</>;
       /* eslint-enable no-multi-spaces, max-len */
   }
 };

+ 3 - 23
apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -4,11 +4,9 @@ import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
-import * as toastr from 'toastr';
 
 import { apiPost } from '~/client/util/apiv1-client';
-
-// import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 const GROUPS_PAGE = [
@@ -72,37 +70,19 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     try {
       // TODO: use apiv3Post
       const result = await apiPost<any>('/v3/export', { collections: Array.from(selectedCollections) });
-      // TODO: toastSuccess, toastError
 
       if (!result.ok) {
         throw new Error('Error occured.');
       }
 
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, 'Export process has requested.', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
-      });
+      toastSuccess('Export process has requested.');
 
       onExportingRequested();
       onClose();
       uncheckAll();
     }
     catch (err) {
-      // TODO: toastSuccess, toastError
-      toastr.error(err, 'Error', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
-      });
+      toastError(err);
     }
   }, [onClose, onExportingRequested, selectedCollections, uncheckAll]);
 

+ 4 - 31
apps/app/src/components/Admin/ExportArchiveDataPage.tsx

@@ -1,11 +1,11 @@
 import React, { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
-import * as toastr from 'toastr';
 
 
 import { apiDelete } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/stores/socket-io';
 
 import LabeledProgressBar from './Common/LabeledProgressBar';
@@ -34,7 +34,6 @@ const ExportArchiveDataPage = (): JSX.Element => {
       apiv3Get<{collections: any[]}>('/mongo/collections', {}),
       apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
     ]);
-    // TODO: toastSuccess, toastError
 
     // filter only not ignored collection names
     const filteredCollections = collectionsData.collections.filter((collectionName) => {
@@ -69,16 +68,7 @@ const ExportArchiveDataPage = (): JSX.Element => {
         setExported(true);
         setZipFileStats(prev => prev.concat([addedZipFileStat]));
 
-        // TODO: toastSuccess, toastError
-        toastr.success(undefined, `New Archive Data '${addedZipFileStat.fileName}' is added`, {
-          closeButton: true,
-          progressBar: true,
-          newestOnTop: false,
-          showDuration: '100',
-          hideDuration: '100',
-          timeOut: '1200',
-          extendedTimeOut: '150',
-        });
+        toastSuccess(`New Archive Data '${addedZipFileStat.fileName}' is added`);
       });
     }
   }, [socket]);
@@ -89,27 +79,10 @@ const ExportArchiveDataPage = (): JSX.Element => {
 
       setZipFileStats(prev => prev.filter(stat => stat.fileName !== fileName));
 
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, `Deleted ${fileName}`, {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
-      });
+      toastSuccess(`Deleted ${fileName}`);
     }
     catch (err) {
-      // TODO: toastSuccess, toastError
-      toastr.error(err, 'Error', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
-      });
+      toastError(err);
     }
   }, []);
 

+ 17 - 0
apps/app/src/components/Admin/ForbiddenPage.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+
+import DefaultErrorPage from 'next/error';
+import { useTranslation } from 'react-i18next';
+
+
+export const ForbiddenPage = (): JSX.Element => {
+  const { t } = useTranslation('admin');
+
+  const errorMessage = t('forbidden_page.do_not_have_admin_permission');
+
+  return (
+    <>
+      <DefaultErrorPage statusCode={403} title={errorMessage} />
+    </>
+  );
+};

+ 0 - 1
apps/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -32,7 +32,6 @@ class UploadForm extends React.Component {
 
     try {
       const { data } = await apiv3PostForm('/import/upload', formData);
-      // TODO: toastSuccess, toastError
       this.props.onUpload(data);
     }
     catch (err) {

+ 3 - 21
apps/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -2,11 +2,10 @@ import React, { Fragment } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import * as toastr from 'toastr';
 
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
-// import { toastSuccess, toastError } from '~/client/util/toastr';
 
 import ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
@@ -59,27 +58,10 @@ class GrowiArchiveSection extends React.Component {
       await apiv3Delete('/import/all');
       this.resetState();
 
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, `Deleted ${fileName}`, {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
-      });
+      toastSuccess(`Deleted ${fileName}`);
     }
     catch (err) {
-      // TODO: toastSuccess, toastError
-      toastr.error(err, 'Error', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
-      });
+      toastError(err);
     }
   }
 

+ 22 - 0
apps/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -453,6 +453,28 @@ class SecuritySetting extends React.Component {
           ].map(arr => this.renderPageDeletePermission(arr[0], arr[1], arr[2], arr[3]))
         }
 
+        <h4>{t('security_settings.user_homepage_deletion.user_homepage_deletion')}</h4>
+        <div className="row mb-4">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
+              <input
+                type="checkbox"
+                className="custom-control-input"
+                id="is-user-page-deletion-enabled"
+                checked={adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsUsersHomepageDeletionEnabled() }}
+              />
+              <label className="custom-control-label" htmlFor="is-user-page-deletion-enabled">
+                {t('security_settings.user_homepage_deletion.enable_user_homepage_deletion')}
+              </label>
+            </div>
+            <p
+              className="form-text text-muted small"
+              dangerouslySetInnerHTML={{ __html: t('security_settings.user_homepage_deletion.when_deleting_a_user_the_user_homepage_is_also_deleted') }}
+            />
+          </div>
+        </div>
+
         <h4>{t('security_settings.session')}</h4>
         <div className="form-group row">
           <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>

+ 1 - 1
apps/app/src/components/Admin/UserManagement.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 // styles for admin user search
 .search-typeahead :global {

+ 1 - 1
apps/app/src/components/BookmarkButtons.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .btn-group-bookmark :global {
   .btn-bookmark {

+ 3 - 3
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -29,7 +29,7 @@ type BookmarkFolderItemProps = {
   isOperable: boolean,
   level: number
   root: string
-  isUserHomePage?: boolean
+  isUserHomepage?: boolean
   onClickDeleteMenuItemHandler: (pageToDelete: IPageToDeleteWithMeta) => void
   bookmarkFolderTreeMutation: () => void
 }
@@ -38,7 +38,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   const BASE_FOLDER_PADDING = 15;
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const {
-    isReadOnlyUser, bookmarkFolder, isOpen: _isOpen = false, isOperable, level, root, isUserHomePage,
+    isReadOnlyUser, bookmarkFolder, isOpen: _isOpen = false, isOperable, level, root, isUserHomepage,
     onClickDeleteMenuItemHandler, bookmarkFolderTreeMutation,
   } = props;
 
@@ -155,7 +155,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
             bookmarkFolder={childFolder}
             level={level + 1}
             root={root}
-            isUserHomePage={isUserHomePage}
+            isUserHomepage={isUserHomepage}
             onClickDeleteMenuItemHandler={onClickDeleteMenuItemHandler}
             bookmarkFolderTreeMutation={bookmarkFolderTreeMutation}
           />

+ 3 - 3
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -27,13 +27,13 @@ import styles from './BookmarkFolderTree.module.scss';
 //  } & IPageHasId
 
 type Props = {
-  isUserHomePage?: boolean,
+  isUserHomepage?: boolean,
   userId?: string,
   isOperable: boolean,
 }
 
 export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
-  const { isUserHomePage, userId } = props;
+  const { isUserHomepage, userId } = props;
 
   // const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const { t } = useTranslation();
@@ -114,7 +114,7 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
               isOpen={false}
               level={0}
               root={bookmarkFolder._id}
-              isUserHomePage={isUserHomePage}
+              isUserHomepage={isUserHomepage}
               onClickDeleteMenuItemHandler={onClickDeleteMenuItemHandler}
               bookmarkFolderTreeMutation={bookmarkFolderTreeMutation}
             />

+ 1 - 1
apps/app/src/components/Fab.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-fab :global {
   position: fixed;

+ 2 - 2
apps/app/src/components/Layout/Admin.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as *;
+@use '@growi/core/scss/bootstrap/init' as *;
 @use '~/styles/mixins';
 
 $slack-work-space-name-card-background: #fff5ff;
@@ -264,7 +264,7 @@ $slack-work-space-name-card-border: #efc1f6;
 
     // style
     .theme-option-container a {
-      background-color: $gray-50;
+      background-color: $gray-100;
       border: 1px solid $border-color;
     }
     .theme-option-name, .theme-option-badge {

+ 1 - 1
apps/app/src/components/Layout/NoLoginLayout.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as *;
+@use '@growi/core/scss/bootstrap/init' as *;
 
 
 .nologin :global {

+ 1 - 1
apps/app/src/components/Layout/RawLayout.tsx

@@ -39,7 +39,7 @@ export const RawLayout = ({ children, className }: Props): JSX.Element => {
         <meta name="viewport" content="initial-scale=1.0, width=device-width" />
       </Head>
       <NextThemesProvider>
-        <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
+        <div className={classNames.join(' ')}>
           {children}
           <ToastContainer theme={colorScheme} />
         </div>

+ 1 - 1
apps/app/src/components/Layout/SearchResultLayout.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .on-search :global {
   .page-wrapper {

+ 1 - 1
apps/app/src/components/LikeButtons.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .btn-group-like :global {
   .btn-like {

+ 1 - 1
apps/app/src/components/LoginForm.tsx

@@ -18,7 +18,6 @@ import { CompleteUserRegistration } from './CompleteUserRegistration';
 
 import styles from './LoginForm.module.scss';
 
-
 type LoginFormProps = {
   username?: string,
   name?: string,
@@ -335,6 +334,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       resetRegisterErrors();
 
       const { redirectTo } = res.data;
+
       if (redirectTo != null) {
         router.push(redirectTo);
       }

+ 1 - 1
apps/app/src/components/Navbar/AuthorInfo.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 $author-font-size: 12px;
 $date-font-size: 11px;

+ 1 - 2
apps/app/src/components/Navbar/AuthorInfo.tsx

@@ -18,7 +18,6 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
     date, user, mode = 'create', locate = 'subnav',
   } = props;
 
-  const { userPageRoot } = pagePathUtils;
   const formatType = 'yyyy/MM/dd HH:mm';
 
   const infoLabelForSubNav = mode === 'create'
@@ -32,7 +31,7 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
     : 'Last revision posted at';
   const userLabel = user != null
     ? (
-      <Link href={userPageRoot(user)} prefetch={false}>
+      <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
         {user.name}
       </Link>
     )

+ 1 - 1
apps/app/src/components/Navbar/GlobalSearch.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 // input styles
 .grw-global-search :global {

+ 1 - 2
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -48,7 +48,6 @@ import type { SubNavButtonsProps } from './SubNavButtons';
 import AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
-
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 
 
@@ -396,7 +395,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
               />
             )}
           </div>
-          {(isAbleToShowPageAuthors && !isCompactMode && !pagePathUtils.isUsersHomePage(path ?? '')) && (
+          {(isAbleToShowPageAuthors && !isCompactMode && !pagePathUtils.isUsersHomepage(path ?? '')) && (
             <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
               <li className="pb-1">
                 {currentPage != null

+ 1 - 1
apps/app/src/components/Navbar/GrowiNavbar.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use '~/styles/mixins';
 
 .grw-navbar :global {

+ 1 - 1
apps/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use '~/styles/mixins';
 
 %subnav-buttons-height {

+ 1 - 1
apps/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 /*
  * Fixed ver

+ 1 - 1
apps/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -1,5 +1,5 @@
 // @mixin page-editor-mode-manager($textColor, $borderColor, $bgColorHoverAndActive, $bgColor: white) {
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use '~/styles/mixins';
 
 $btn-line-height: 1.2rem;

+ 2 - 1
apps/app/src/components/Navbar/PersonalDropdown.jsx

@@ -1,5 +1,6 @@
 import { useRef, useState } from 'react';
 
+import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -64,7 +65,7 @@ const PersonalDropdown = () => {
 
           <div className="btn-group btn-block mt-2" role="group">
             <Link
-              href={`/user/${currentUser.username}`}
+              href={pagePathUtils.userHomepagePath(currentUser)}
               className="btn btn-sm btn-outline-secondary col"
               data-testid="grw-personal-dropdown-menu-user-home"
             >

+ 8 - 9
apps/app/src/components/Page/PageView.tsx

@@ -2,9 +2,8 @@ import React, {
   useEffect, useMemo, useRef, useState,
 } from 'react';
 
-
 import type { IPagePopulatedToShowRevision } from '@growi/core';
-import { isUsersHomePage } from '@growi/core/dist/utils/page-path-utils';
+import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import dynamic from 'next/dynamic';
 
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -23,7 +22,7 @@ import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageContentFooter } from '../PageContentFooter';
 import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
-import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
+import type { UsersHomepageFooterProps } from '../UsersHomepageFooter';
 
 import RevisionRenderer from './RevisionRenderer';
 
@@ -36,8 +35,8 @@ const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
 const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
-const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../UsersHomePageFooter')
-  .then(mod => mod.UsersHomePageFooter), { ssr: false });
+const UsersHomepageFooter = dynamic<UsersHomepageFooterProps>(() => import('../UsersHomepageFooter')
+  .then(mod => mod.UsersHomepageFooter), { ssr: false });
 const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
 
 
@@ -68,7 +67,7 @@ export const PageView = (props: Props): JSX.Element => {
 
   const page = pageBySWR ?? initialPage;
   const isNotFound = isNotFoundMeta || page?.revision == null;
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const isUsersHomepagePath = isUsersHomepage(pagePath);
 
 
   // ***************************  Auto Scroll  ***************************
@@ -116,8 +115,8 @@ export const PageView = (props: Props): JSX.Element => {
             onLoaded={() => setCommentsLoaded(true)}
           />
         </div>
-        {(isUsersHomePagePath && page.creator != null) && (
-          <UsersHomePageFooter creatorId={page.creator._id} />
+        {(isUsersHomepagePath && page.creator != null) && (
+          <UsersHomepageFooter creatorId={page.creator._id} />
         )}
         <PageContentFooter page={page} />
       </>
@@ -150,7 +149,7 @@ export const PageView = (props: Props): JSX.Element => {
       {specialContents}
       {specialContents == null && (
         <>
-          {(isUsersHomePagePath && page?.creator != null) && <UserInfo author={page.creator} />}
+          {(isUsersHomepagePath && page?.creator != null) && <UserInfo author={page.creator} />}
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
             <Contents />
           </div>

+ 1 - 1
apps/app/src/components/Page/TagLabels.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 $grw-tag-label-font-size: 12px;
 

+ 1 - 2
apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx

@@ -1,6 +1,5 @@
 import React, { FC, useState, useCallback } from 'react';
 
-import { isInteger } from 'core-js/fn/number';
 import {
   format, parse, addDays, set,
 } from 'date-fns';
@@ -63,7 +62,7 @@ export const ShareLinkForm: FC<Props> = (props: Props) => {
     }
 
     if (expirationType === ExpirationType.NUMBER_OF_DAYS) {
-      if (!isInteger(Number(numberOfDays))) {
+      if (!Number.isInteger(numberOfDays)) {
         throw new Error(t('share_links.Invalid_Number_of_Date'));
       }
       return addDays(new Date(), numberOfDays);

+ 1 - 1
apps/app/src/components/PageComment.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .page-comment-styles :global {
   .page-comments {

+ 2 - 1
apps/app/src/components/PageComment/Comment.module.scss

@@ -1,5 +1,6 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
 @use '../../styles/variables' as var;
-@use '../../styles/bootstrap/init' as bs;
 @use './_comment-inheritance';
 
 .comment-styles :global {

+ 1 - 1
apps/app/src/components/PageComment/CommentEditor.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use './comment-inheritance';
 @use '../PageEditor/page-editor-inheritance';
 

+ 3 - 9
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -4,12 +4,13 @@ import React, {
 
 import { UserPicture } from '@growi/ui/dist/components';
 import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
 import {
   Button, TabContent, TabPane,
 } from 'reactstrap';
-import * as toastr from 'toastr';
 
 import { apiPostForm } from '~/client/util/apiv1-client';
+import { toastError } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import {
@@ -199,14 +200,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   }, [postCommentHandler]);
 
   const apiErrorHandler = useCallback((error: Error) => {
-    toastr.error(error.message, 'Error occured', {
-      closeButton: true,
-      progressBar: true,
-      newestOnTop: false,
-      showDuration: '100',
-      hideDuration: '100',
-      timeOut: '3000',
-    });
+    toastError(error.message);
   }, []);
 
   const uploadHandler = useCallback(async(file) => {

+ 1 - 1
apps/app/src/components/PageComment/CommentPreview.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use './comment-inheritance';
 @use '../PageEditor/page-editor-inheritance';
 

+ 1 - 1
apps/app/src/components/PageComment/_comment-inheritance.scss

@@ -1,4 +1,4 @@
-@use '../../styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 %comment-section {
   position: relative;

+ 1 - 1
apps/app/src/components/PageContentFooter.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .page-content-footer :global {
   border-top: solid 1px transparent;

+ 14 - 14
apps/app/src/components/PageCreateModal.jsx

@@ -21,7 +21,7 @@ import PagePathAutoComplete from './PagePathAutoComplete';
 import styles from './PageCreateModal.module.scss';
 
 const {
-  userPageRoot, isCreatablePage, generateEditorPath, isUsersHomePage,
+  isCreatablePage, generateEditorPath, isUsersHomepage,
 } = pagePathUtils;
 
 const PageCreateModal = () => {
@@ -35,8 +35,8 @@ const PageCreateModal = () => {
 
   const { data: isReachable } = useIsSearchServiceReachable();
   const pathname = path || '';
-  const userPageRootPath = userPageRoot(currentUser);
-  const isCreatable = isCreatablePage(pathname) || isUsersHomePage(pathname);
+  const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
+  const isCreatable = isCreatablePage(pathname) || isUsersHomepage(pathname);
   const pageNameInputInitialValue = isCreatable ? pathUtils.addTrailingSlash(pathname) : '/';
   const now = format(new Date(), 'yyyy/MM/dd');
 
@@ -46,7 +46,7 @@ const PageCreateModal = () => {
   const [todayInput2, setTodayInput2] = useState('');
   const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
-  const [isMatchedWithUserHomePagePath, setIsMatchedWithUserHomePagePath] = useState(false);
+  const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] = useState(false);
 
   // ensure pageNameInput is synced with selectedPagePath || currentPagePath
   useEffect(() => {
@@ -59,19 +59,19 @@ const PageCreateModal = () => {
     setTodayInput1(t('Memo'));
   }, [t]);
 
-  const checkIsUsersHomePageDebounce = useMemo(() => {
-    const checkIsUsersHomePage = () => {
-      setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));
+  const checkIsUsersHomepageDebounce = useMemo(() => {
+    const checkIsUsersHomepage = () => {
+      setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
     };
 
-    return debounce(1000, checkIsUsersHomePage);
+    return debounce(1000, checkIsUsersHomepage);
   }, [pageNameInput]);
 
   useEffect(() => {
     if (isOpened) {
-      checkIsUsersHomePageDebounce(pageNameInput);
+      checkIsUsersHomepageDebounce(pageNameInput);
     }
-  }, [isOpened, checkIsUsersHomePageDebounce, pageNameInput]);
+  }, [isOpened, checkIsUsersHomepageDebounce, pageNameInput]);
 
   function transitBySubmitEvent(e, transitHandler) {
     // prevent page transition by submit
@@ -129,7 +129,7 @@ const PageCreateModal = () => {
     if (tmpTodayInput1 === '') {
       tmpTodayInput1 = t('Memo');
     }
-    redirectToEditor(userPageRootPath, tmpTodayInput1, now, todayInput2);
+    redirectToEditor(userHomepagePath, tmpTodayInput1, now, todayInput2);
   }
 
   /**
@@ -164,7 +164,7 @@ const PageCreateModal = () => {
 
             <div className="d-flex align-items-center flex-fill flex-wrap flex-lg-nowrap">
               <div className="d-flex align-items-center">
-                <span>{userPageRootPath}/</span>
+                <span>{userHomepagePath}/</span>
                 <form onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
                   <input
                     type="text"
@@ -246,14 +246,14 @@ const PageCreateModal = () => {
                 data-testid="btn-create-page-under-below"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3"
                 onClick={createInputPage}
-                disabled={isMatchedWithUserHomePagePath}
+                disabled={isMatchedWithUserHomepagePath}
               >
                 <i className="icon-fw icon-doc"></i>{t('Create')}
               </button>
             </div>
 
           </div>
-          { isMatchedWithUserHomePagePath && (
+          { isMatchedWithUserHomepagePath && (
             <p className="text-danger mt-2">Error: Cannot create page under /user page directory.</p>
           ) }
 

+ 1 - 1
apps/app/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-codemirror-editor :global {
   @import '~codemirror/lib/codemirror';

+ 1 - 1
apps/app/src/components/PageEditor/Editor.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/mixins' as ms;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use './page-editor-inheritance';
 
 

+ 1 - 1
apps/app/src/components/PageEditor/GridEditModal.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-grid-edit-modal :global {
   .desktop-preview,

+ 1 - 1
apps/app/src/components/PageHistory/RevisionDiff.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .revision-diff-container :global {
   .comparison-header {

+ 10 - 10
apps/app/src/components/PageRenameModal.tsx

@@ -28,7 +28,7 @@ const isV5Compatible = (meta: unknown): boolean => {
 const PageRenameModal = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { isUsersHomePage } = pagePathUtils;
+  const { isUsersHomepage } = pagePathUtils;
   const { data: siteUrl } = useSiteUrl();
   const { data: renameModalData, close: closeRenameModal } = usePageRenameModal();
   const { data: isReachable } = useIsSearchServiceReachable();
@@ -54,7 +54,7 @@ const PageRenameModal = (): JSX.Element => {
   const [isRemainMetadata, setIsRemainMetadata] = useState(false);
   const [expandOtherOptions, setExpandOtherOptions] = useState(false);
   const [subordinatedError] = useState(null);
-  const [isMatchedWithUserHomePagePath, setIsMatchedWithUserHomePagePath] = useState(false);
+  const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] = useState(false);
 
   const updateSubordinatedList = useCallback(async() => {
     if (page == null) {
@@ -80,14 +80,14 @@ const PageRenameModal = (): JSX.Element => {
   }, [isOpened, page, updateSubordinatedList]);
 
   const canRename = useMemo(() => {
-    if (page == null || isMatchedWithUserHomePagePath || page.data.path === pageNameInput) {
+    if (page == null || isMatchedWithUserHomepagePath || page.data.path === pageNameInput) {
       return false;
     }
     if (isV5Compatible(page.meta)) {
       return existingPaths.length === 0; // v5 data
     }
     return isRenameRecursively; // v4 data
-  }, [existingPaths.length, isMatchedWithUserHomePagePath, isRenameRecursively, page, pageNameInput]);
+  }, [existingPaths.length, isMatchedWithUserHomepagePath, isRenameRecursively, page, pageNameInput]);
 
   const rename = useCallback(async() => {
     if (page == null || !canRename) {
@@ -151,20 +151,20 @@ const PageRenameModal = (): JSX.Element => {
     return debounce(1000, checkExistPaths);
   }, [checkExistPaths]);
 
-  const checkIsUsersHomePageDebounce = useMemo(() => {
+  const checkIsUsersHomepageDebounce = useMemo(() => {
     const checkIsPagePathRenameable = () => {
-      setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));
+      setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
     };
 
     return debounce(1000, checkIsPagePathRenameable);
-  }, [isUsersHomePage, pageNameInput]);
+  }, [isUsersHomepage, pageNameInput]);
 
   useEffect(() => {
     if (isOpened && page != null && pageNameInput !== page.data.path) {
       checkExistPathsDebounce(page.data.path, pageNameInput);
-      checkIsUsersHomePageDebounce(pageNameInput);
+      checkIsUsersHomepageDebounce(pageNameInput);
     }
-  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomePageDebounce]);
+  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomepageDebounce]);
 
   function ppacInputChangeHandler(value) {
     setErrs(null);
@@ -246,7 +246,7 @@ const PageRenameModal = (): JSX.Element => {
         { isTargetPageDuplicate && (
           <p className="text-danger">Error: Target path is duplicated.</p>
         ) }
-        { isMatchedWithUserHomePagePath && (
+        { isMatchedWithUserHomepagePath && (
           <p className="text-danger">Error: Cannot move to directory under /user page.</p>
         ) }
 

+ 3 - 3
apps/app/src/components/PageSideContents.tsx

@@ -16,7 +16,7 @@ import TableOfContents from './TableOfContents';
 import styles from './PageSideContents.module.scss';
 
 
-const { isTopPage, isUsersHomePage, isTrashPage } = pagePathUtils;
+const { isTopPage, isUsersHomepage, isTrashPage } = pagePathUtils;
 
 
 export type PageSideContentsProps = {
@@ -35,7 +35,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
   const pagePath = page.path;
   const isTopPagePath = isTopPage(pagePath);
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const isUsersHomepagePath = isUsersHomepage(pagePath);
   const isTrash = isTrashPage(pagePath);
 
   return (
@@ -83,7 +83,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
       <div className="d-none d-lg-block">
         <TableOfContents />
-        {isUsersHomePagePath && <ContentLinkButtons author={page?.creator} />}
+        {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
       </div>
     </>
   );

+ 3 - 2
apps/app/src/components/PageStatusAlert.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-page-status-alert :global {
   $margin-bottom: var.$grw-navbar-bottom-height + 10px;
@@ -17,7 +17,8 @@
       text-align: center;
 
       .btn {
-        @include bs.button-size(bs.$btn-padding-y-lg, bs.$btn-padding-x-lg, bs.$btn-font-size-lg, bs.$btn-line-height-lg, bs.$btn-border-radius-lg);
+        // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
+        // @include bs.button-size(bs.$btn-padding-y-lg, bs.$btn-padding-x-lg, bs.$btn-font-size-lg, bs.$btn-line-height-lg, bs.$btn-border-radius-lg);
       }
     }
   }

+ 3 - 3
apps/app/src/components/PageTimeline.module.scss

@@ -1,11 +1,11 @@
-@use '../styles/bootstrap/variables' as var;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .card-timeline :global {
   margin-bottom: 3rem;
-  border: 1px solid var.$gray-300;
+  border: 1px solid bs.$gray-300;
 
   .card-header {
-    background-color: var.$gray-300;
+    background-color: bs.$gray-300;
   }
 
   .card-body {

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss

@@ -1,5 +1,5 @@
 @use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .code-highlighted-title {
   position: absolute;

+ 3 - 5
apps/app/src/components/SavePageControls.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import EventEmitter from 'events';
 
-import { pagePathUtils } from '@growi/core/dist/utils';
+import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import { useTranslation } from 'next-i18next';
 import {
   UncontrolledButtonDropdown, Button,
@@ -29,8 +29,6 @@ declare global {
 
 const logger = loggerFactory('growi:SavePageControls');
 
-const { isTopPage } = pagePathUtils;
-
 export type SavePageControlsProps = {
   slackChannels: string
 }
@@ -71,7 +69,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
 
   const { grant, grantedGroup } = grantData;
 
-  const isRootPage = isTopPage(currentPage?.path ?? '');
+  const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const labelSubmitButton = (currentPage != null && !currentPage.isEmpty) ? t('Update') : t('Create');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
 
@@ -83,7 +81,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
           <div className="mr-2">
             <GrantSelector
               grant={grant}
-              disabled={isRootPage}
+              disabled={isGrantSelectorDisabledPage}
               grantGroupId={grantedGroup?.id}
               grantGroupName={grantedGroup?.name}
               onUpdateGrant={updateGrantHandler}

+ 1 - 1
apps/app/src/components/SearchForm.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-search-table {
   caption {

+ 2 - 1
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -77,7 +77,8 @@ const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): void => {
     return;
   }
 
-  animateScroll.scrollTo(toElem.offsetTop - SCROLL_OFFSET_TOP, {
+  const distance = toElem.getBoundingClientRect().top - scrollElement.getBoundingClientRect().top - SCROLL_OFFSET_TOP;
+  animateScroll.scrollMore(distance, {
     containerId: scrollElement.id,
     duration: 200,
   });

+ 1 - 1
apps/app/src/components/SearchTypeahead.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .search-typeahead :global {
   position: relative;

+ 1 - 1
apps/app/src/components/ShortcutsModal.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .shortcuts-modal :global {
   h3 {

+ 2 - 2
apps/app/src/components/Sidebar.module.scss

@@ -1,6 +1,6 @@
 @use '~/styles/variables' as var;
 @use '~/styles/mixins';
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-sidebar :global {
   // sticky
@@ -128,7 +128,7 @@
         }
         .hitarea {
           position: absolute;
-          border-radius: bs.$rounded-pill;
+          border-radius: bs.$border-radius-pill;
 
           @include hitarea(30px);
         }

+ 36 - 35
apps/app/src/components/SlackNotification.module.scss

@@ -1,43 +1,44 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
+// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
 .grw-slack-notification :global {
-  $input-height-slack: bs.$custom-control-indicator-size * 1.5;
-  border-color: bs.$gray-200;
+  // $input-height-slack: bs.$custom-control-indicator-size * 1.5;
+  // border-color: bs.$gray-200;
 
-  border-style: solid;
-  border-width: 1px;
-  border-radius: $input-height-slack/2 2px 2px $input-height-slack/2;
+  // border-style: solid;
+  // border-width: 1px;
+  // border-radius: $input-height-slack/2 2px 2px $input-height-slack/2;
 
-  .form-control {
-    height: $input-height-slack;
-    border: transparent;
-    @include bs.media-breakpoint-up(sm) {
-      width: 130px;
-    }
-    @include bs.media-breakpoint-up(md) {
-      width: 180px;
-    }
-  }
-  // height settings for slack button's responsive design
-  // in the input and form-control element
-  .grw-form-control-slack-notification.form-control {
-    height: $input-height-slack;
-  }
-  .grw-input-group-slack-notification {
-    height: $input-height-slack;
-    label {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      margin-bottom: 0;
-    }
-  }
+  // .form-control {
+  //   height: $input-height-slack;
+  //   border: transparent;
+  //   @include bs.media-breakpoint-up(sm) {
+  //     width: 130px;
+  //   }
+  //   @include bs.media-breakpoint-up(md) {
+  //     width: 180px;
+  //   }
+  // }
+  // // height settings for slack button's responsive design
+  // // in the input and form-control element
+  // .grw-form-control-slack-notification.form-control {
+  //   height: $input-height-slack;
+  // }
+  // .grw-input-group-slack-notification {
+  //   height: $input-height-slack;
+  //   label {
+  //     display: flex;
+  //     align-items: center;
+  //     justify-content: center;
+  //     margin-bottom: 0;
+  //   }
+  // }
 
-  .custom-control-label {
-    &::before {
-      border: transparent;
-    }
-  }
+  // .custom-control-label {
+  //   &::before {
+  //     border: transparent;
+  //   }
+  // }
 }
 // TODO デザインの使用が確定して実装、本タスクのスコープ外
 // .grw-slack-notification-xd {

+ 1 - 1
apps/app/src/components/SubscribeButton.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .btn-subscribe {
   &:global {

+ 1 - 1
apps/app/src/components/TableOfContents.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .revision-toc :global {
   // to get on the Attachment row

+ 1 - 1
apps/app/src/components/TemplateModal/TemplateModal.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .dm-templates :global {
   .dropdown-item:not(:first-child) {

+ 1 - 1
apps/app/src/components/User/SeenUserInfo.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 @use '~/styles/atoms/mixins/buttons' as mixins-buttons;
 
 .grw-seen-user-info :global {

+ 1 - 1
apps/app/src/components/User/UserInfo.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-users-info :global {
   .users-meta {

+ 2 - 1
apps/app/src/components/User/Username.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import type { IUser } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
 import Link from 'next/link';
 
 export const Username: React.FC<{ user?: IUser }> = ({ user }): JSX.Element => {
@@ -11,7 +12,7 @@ export const Username: React.FC<{ user?: IUser }> = ({ user }): JSX.Element => {
 
   const name = user.name || '(no name)';
   const username = user.username;
-  const href = `/user/${user.username}`;
+  const href = pagePathUtils.userHomepagePath(user);
 
   return (
     <Link href={href} prefetch={false}>

+ 0 - 0
apps/app/src/components/UsersHomePageFooter.module.scss → apps/app/src/components/UsersHomepageFooter.module.scss


+ 4 - 4
apps/app/src/components/UsersHomePageFooter.tsx → apps/app/src/components/UsersHomepageFooter.tsx

@@ -5,18 +5,18 @@ import { useTranslation } from 'next-i18next';
 
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
-import styles from '~/components/UsersHomePageFooter.module.scss';
+import styles from '~/components/UsersHomepageFooter.module.scss';
 import { useCurrentUser } from '~/stores/context';
 
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 import { CompressIcon } from './Icons/CompressIcon';
 import { ExpandIcon } from './Icons/ExpandIcon';
 
-export type UsersHomePageFooterProps = {
+export type UsersHomepageFooterProps = {
   creatorId: string,
 }
 
-export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Element => {
+export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Element => {
   const { t } = useTranslation();
   const { creatorId } = props;
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
@@ -43,7 +43,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
         </h2>
         {/* TODO: In bookmark folders v1, the button to create a new folder does not exist. The button should be included in the bookmark component. */}
         <div className={`${isExpanded ? `${styles['grw-bookarks-contents-expanded']}` : `${styles['grw-bookarks-contents-compressed']}`}`}>
-          <BookmarkFolderTree isUserHomePage={true} isOperable={isOperable} userId={creatorId} />
+          <BookmarkFolderTree isUserHomepage={true} isOperable={isOperable} userId={creatorId} />
         </div>
       </div>
       <div className="grw-user-page-list-m mt-5 d-edit-none">

+ 1 - 1
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss

@@ -1,4 +1,4 @@
-@use '~/styles/bootstrap/init' as bs;
+@use '@growi/core/scss/bootstrap/init' as bs;
 
 .grw-questionnaire-toasts :global {
   position: fixed;

+ 5 - 0
apps/app/src/pages/admin/[...path].page.tsx

@@ -11,12 +11,17 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AdminNotFoundPage = dynamic(() => import('~/components/Admin/NotFoundPage').then(mod => mod.AdminNotFoundPage), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
   useCurrentUser(props.currentUser ?? null);
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <AdminLayout>
       <AdminNotFoundPage />

+ 5 - 1
apps/app/src/pages/admin/app.page.tsx

@@ -7,7 +7,6 @@ import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 
-
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
@@ -18,6 +17,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AppSettingsPageContents = dynamic(() => import('~/components/Admin/App/AppSettingsPageContents'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
@@ -34,6 +34,10 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
 
   const title = generateCustomTitle(props, t('headers.app_settings'));
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={title}>

+ 5 - 1
apps/app/src/pages/admin/audit-log.page.tsx

@@ -13,8 +13,8 @@ import { useCurrentUser, useAuditLogEnabled, useAuditLogAvailableActions } from
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-
 const AuditLogManagement = dynamic(() => import('~/components/Admin/AuditLogManagement').then(mod => mod.AuditLogManagement), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -32,6 +32,10 @@ const AdminAuditLogPage: NextPage<Props> = (props) => {
   const title = t('audit_log_management.audit_log');
   const headTitle = generateCustomTitle(props, title);
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <AdminLayout componentTitle={title}>
       <Head>

+ 4 - 0
apps/app/src/pages/admin/customize.page.tsx

@@ -16,6 +16,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const CustomizeSettingContents = dynamic(() => import('~/components/Admin/Customize/Customize'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -40,6 +41,9 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
     injectableContainers.push(adminCustomizeContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 5 - 0
apps/app/src/pages/admin/data-transfer.page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const G2GDataTransferPage = dynamic(() => import('~/components/Admin/G2GDataTransfer'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps;
@@ -32,6 +33,10 @@ const DataTransferPage: NextPage<Props> = (props) => {
     injectableContainers.push(adminAppContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={title}>

+ 5 - 0
apps/app/src/pages/admin/export.page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const ExportArchiveDataPage = dynamic(() => import('~/components/Admin/ExportArchiveDataPage'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
@@ -30,6 +31,10 @@ const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminAppContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={componentTitle}>

+ 4 - 0
apps/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx

@@ -21,6 +21,7 @@ import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const ManageGlobalNotification = dynamic(() => import('~/components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
@@ -54,6 +55,9 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminNotificationContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    <ForbiddenPage />;
+  }
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 4 - 0
apps/app/src/pages/admin/global-notification/new.page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const ManageGlobalNotification = dynamic(() => import('~/components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
@@ -29,6 +30,9 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminNotificationContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    <ForbiddenPage />;
+  }
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 5 - 0
apps/app/src/pages/admin/importer.page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const DataImportPageContents = dynamic(() => import('~/components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminDataImportPage: NextPage<CommonProps> = (props) => {
@@ -30,6 +31,10 @@ const AdminDataImportPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminImportContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 12 - 4
apps/app/src/pages/admin/index.page.tsx

@@ -10,12 +10,16 @@ import { Container, Provider } from 'unstated';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores/context';
+import {
+  useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud,
+} from '~/stores/context';
+
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AdminHome = dynamic(() => import('~/components/Admin/AdminHome/AdminHome'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -28,14 +32,14 @@ type Props = CommonProps & {
 };
 
 
-const AdminHomePage: NextPage<Props> = (props) => {
+const AdminHomepage: NextPage<Props> = (props) => {
   useCurrentUser(props.currentUser ?? null);
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
 
   const { t } = useTranslation('admin');
 
-  const title = generateCustomTitle(props, t('wiki_management_home_page'));
+  const title = generateCustomTitle(props, t('wiki_management_homepage'));
 
   const injectableContainers: Container<any>[] = [];
 
@@ -45,6 +49,10 @@ const AdminHomePage: NextPage<Props> = (props) => {
     injectableContainers.push(adminHomeContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
 
   return (
     <Provider inject={[...injectableContainers]}>
@@ -82,4 +90,4 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 };
 
 
-export default AdminHomePage;
+export default AdminHomepage;

+ 4 - 0
apps/app/src/pages/admin/markdown.page.tsx

@@ -16,6 +16,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const MarkDownSettingContents = dynamic(() => import('~/components/Admin/MarkdownSetting/MarkDownSettingContents'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
@@ -32,6 +33,9 @@ const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminMarkDownContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 5 - 0
apps/app/src/pages/admin/notification.page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const NotificationSetting = dynamic(() => import('~/components/Admin/Notification/NotificationSetting'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
@@ -31,6 +32,10 @@ const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminNotificationContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 5 - 0
apps/app/src/pages/admin/plugins.page.tsx

@@ -21,6 +21,7 @@ const PluginsExtensionPageContents = dynamic(
   () => import('~/features/growi-plugin/client/components/Admin').then(mod => mod.PluginsExtensionPageContents),
   { ssr: false },
 );
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
@@ -36,6 +37,10 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminAppContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={title} >

+ 5 - 0
apps/app/src/pages/admin/search.page.tsx

@@ -15,6 +15,7 @@ const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { s
 const FullTextSearchManagement = dynamic(
   () => import('~/components/Admin//FullTextSearchManagement').then(mod => mod.FullTextSearchManagement), { ssr: false },
 );
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -30,6 +31,10 @@ const AdminFullTextSearchManagementPage: NextPage<Props> = (props) => {
   const title = t('full_text_search_management.full_text_search_management');
   const headTitle = generateCustomTitle(props, title);
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <AdminLayout componentTitle={title}>
       <Head>

+ 5 - 0
apps/app/src/pages/admin/security.page.tsx

@@ -22,6 +22,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const SecurityManagement = dynamic(() => import('~/components/Admin/Security/SecurityManagement'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -64,6 +65,10 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
     }
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <Provider inject={[...adminSecurityContainers]}>
       <AdminLayout componentTitle={componentTitle}>

+ 5 - 1
apps/app/src/pages/admin/slack-integration-legacy.page.tsx

@@ -15,7 +15,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const LegacySlackIntegration = dynamic(() => import('~/components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
-
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -30,6 +30,10 @@ const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminSlackIntegrationLegacyContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 5 - 0
apps/app/src/pages/admin/slack-integration.page.tsx

@@ -14,6 +14,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const SlackIntegration = dynamic(() => import('~/components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -29,6 +30,10 @@ const AdminSlackIntegrationPage: NextPage<Props> = (props) => {
   const componentTitle = t('slack_integration.slack_integration');
   const pageTitle = generateCustomTitle(props, componentTitle);
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <AdminLayout componentTitle={componentTitle}>
       <Head>

+ 5 - 0
apps/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const UserGroupDetailPage = dynamic(() => import('~/components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 type Props = CommonProps & {
   isAclEnabled: boolean
@@ -34,6 +35,10 @@ const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
 
   useIsAclEnabled(props.isAclEnabled);
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <AdminLayout componentTitle={title}>
       <Head>

+ 5 - 0
apps/app/src/pages/admin/user-groups.page.tsx

@@ -13,6 +13,7 @@ import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const UserGroupPage = dynamic(() => import('~/components/Admin/UserGroup/UserGroupPage').then(mod => mod.UserGroupPage), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -28,6 +29,10 @@ const AdminUserGroupPage: NextPage<Props> = (props) => {
   const title = t('user_group_management.user_group_management');
   const headTitle = generateCustomTitle(props, title);
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
+
   return (
     <AdminLayout componentTitle={title}>
       <Head>

+ 4 - 0
apps/app/src/pages/admin/users/external-accounts.page.tsx

@@ -15,6 +15,7 @@ import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const ManageExternalAccount = dynamic(() => import('~/components/Admin/ManageExternalAccount'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 const AdminUserManagementPage: NextPage<CommonProps> = (props) => {
@@ -32,6 +33,9 @@ const AdminUserManagementPage: NextPage<CommonProps> = (props) => {
     );
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 4 - 1
apps/app/src/pages/admin/users/index.page.tsx

@@ -15,8 +15,8 @@ import { useCurrentUser, useIsMailerSetup } from '~/stores/context';
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-
 const UserManagement = dynamic(() => import('~/components/Admin/UserManagement'), { ssr: false });
+const ForbiddenPage = dynamic(() => import('~/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
 
 
 type Props = CommonProps & {
@@ -39,6 +39,9 @@ const AdminUserManagementPage: NextPage<Props> = (props) => {
     injectableContainers.push(adminUsersContainer);
   }
 
+  if (props.isAccessDeniedForNonAdminUser) {
+    return <ForbiddenPage />;
+  }
 
   return (
     <Provider inject={[...injectableContainers]}>

+ 7 - 0
apps/app/src/pages/me/[[...path]].page.tsx

@@ -72,6 +72,13 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
 
   const getTargetPageToRender = (pagesMap, keys): {title: string, component: JSX.Element} => {
     return keys.reduce((pagesMap, key) => {
+      const page = pagesMap[key];
+      if (page == null) {
+        return {
+          title: 'NotFoundPage',
+          component: <h2>{t('commons:not_found_page.page_not_exist')}</h2>,
+        };
+      }
       return pagesMap[key];
     }, pagesMap);
   };

+ 1 - 0
apps/app/src/pages/utils/commons.ts

@@ -31,6 +31,7 @@ export type CommonProps = {
   redirectDestination: string | null,
   isDefaultLogo: boolean,
   growiCloudUri: string,
+  isAccessDeniedForNonAdminUser?: boolean,
   currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
   sidebarConfig: ISidebarConfig,

+ 2 - 3
apps/app/src/server/crowi/index.js

@@ -17,7 +17,7 @@ import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
-
+import UserEvent from '../events/user';
 import Activity from '../models/activity';
 import PageRedirect from '../models/page-redirect';
 import Tag from '../models/tag';
@@ -36,7 +36,6 @@ import { SlackIntegrationService } from '../service/slack-integration';
 import { UserNotificationService } from '../service/user-notification';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
-
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 const models = require('../models');
@@ -97,7 +96,7 @@ function Crowi() {
   this.port = this.env.PORT || 3000;
 
   this.events = {
-    user: new (require('../events/user'))(this),
+    user: new UserEvent(this),
     page: new (require('../events/page'))(this),
     activity: new (require('../events/activity'))(this),
     bookmark: new (require('../events/bookmark'))(this),

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