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

Merge branch 'feat/115673-marp' into imprv/115673-115675-enable-from-adminsetting

reiji-h 2 лет назад
Родитель
Сommit
39a9775819
61 измененных файлов с 682 добавлено и 775 удалено
  1. 29 1
      CHANGELOG.md
  2. 2 0
      apps/app/config/logger/config.dev.js
  3. 5 7
      apps/app/package.json
  4. 6 1
      apps/app/public/static/locales/en_US/admin.json
  5. 6 1
      apps/app/public/static/locales/ja_JP/admin.json
  6. 6 1
      apps/app/public/static/locales/zh_CN/admin.json
  7. 10 0
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  8. 1 1
      apps/app/src/client/services/AdminHomeContainer.js
  9. 1 2
      apps/app/src/client/util/toastr.ts
  10. 6 1
      apps/app/src/components/Admin/AuditLog/ActivityTable.tsx
  11. 1 1
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  12. 3 23
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  13. 4 31
      apps/app/src/components/Admin/ExportArchiveDataPage.tsx
  14. 0 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  15. 3 21
      apps/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  16. 22 0
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  17. 3 3
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  18. 3 3
      apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  19. 14 23
      apps/app/src/components/Comments.tsx
  20. 1 2
      apps/app/src/components/Navbar/AuthorInfo.tsx
  21. 1 2
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  22. 2 1
      apps/app/src/components/Navbar/PersonalDropdown.jsx
  23. 8 9
      apps/app/src/components/Page/PageView.tsx
  24. 4 11
      apps/app/src/components/Page/RevisionLoader.tsx
  25. 1 2
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx
  26. 6 19
      apps/app/src/components/PageComment.tsx
  27. 17 9
      apps/app/src/components/PageComment/CommentEditor.tsx
  28. 14 14
      apps/app/src/components/PageCreateModal.jsx
  29. 10 10
      apps/app/src/components/PageRenameModal.tsx
  30. 3 3
      apps/app/src/components/PageSideContents.tsx
  31. 3 5
      apps/app/src/components/SavePageControls.tsx
  32. 50 76
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  33. 2 1
      apps/app/src/components/User/Username.tsx
  34. 0 0
      apps/app/src/components/UsersHomepageFooter.module.scss
  35. 4 4
      apps/app/src/components/UsersHomepageFooter.tsx
  36. 58 0
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  37. 5 2
      apps/app/src/pages/[[...path]].page.tsx
  38. 3 3
      apps/app/src/pages/admin/index.page.tsx
  39. 7 0
      apps/app/src/pages/me/[[...path]].page.tsx
  40. 5 1
      apps/app/src/pages/share/[[...path]].page.tsx
  41. 1 5
      apps/app/src/pages/utils/commons.ts
  42. 2 3
      apps/app/src/server/crowi/index.js
  43. 0 35
      apps/app/src/server/events/user.js
  44. 51 0
      apps/app/src/server/events/user.ts
  45. 4 0
      apps/app/src/server/models/config.ts
  46. 0 11
      apps/app/src/server/models/obsolete-page.js
  47. 25 3
      apps/app/src/server/models/page.ts
  48. 15 6
      apps/app/src/server/routes/apiv3/security-settings/index.js
  49. 29 12
      apps/app/src/server/routes/apiv3/users.js
  50. 11 9
      apps/app/src/server/routes/next.ts
  51. 1 1
      apps/app/src/server/service/config-loader.ts
  52. 92 0
      apps/app/src/server/service/page.ts
  53. 5 3
      apps/app/src/stores/search.tsx
  54. 0 3
      apps/app/src/styles/molecules/toastr.scss
  55. 4 19
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  56. 2 12
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--comments.cy.ts
  57. 15 0
      apps/app/test/cypress/support/commands.ts
  58. 1 0
      apps/app/test/cypress/support/index.ts
  59. 5 5
      packages/core/src/utils/page-path-utils/index.ts
  60. 1 3
      packages/ui/src/components/UserPicture.tsx
  61. 89 350
      yarn.lock

+ 29 - 1
CHANGELOG.md

@@ -1,9 +1,37 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.8...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.10...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v6.1.10](https://github.com/weseek/growi/compare/v6.1.9...v6.1.10) - 2023-08-01
+
+### 🐛 Bug Fixes
+
+- fix: Unsaved comments remain in the editor when transitioning to a new page  (#7912) @miya
+- fix: SWR settings for searching (#7940) @yuki-takei
+- fix: Auto-scroll search result content (#7935) @yuki-takei
+
+## [v6.1.9](https://github.com/weseek/growi/compare/v6.1.8...v6.1.9) - 2023-07-31
+
+### 💎 Features
+
+- feat: LightBox for enlargement of image (#7899) @WNomunomu
+
+### 🚀 Improvement
+
+- imprv: Do not use loadConfigs in skipSSR (#7929) @jam411
+- imprv: Improve default behavior of skipSSR (#7927) @jam411
+- imprv: Improved Design for Bookmarks Sidebar (#7886) @soumaeda
+
+### 🐛 Bug Fixes
+
+- fix: Page creation and update process (#7925) @yuki-takei
+- fix: Sidebar doesn't show the link to the administration panel when logged in as an Admin (#7914) @miya
+- fix: Improve page data mutation after renaming and deleting by GrowiContextualSubNavigation (#7926) @yuki-takei
+- fix: Revert dynamic import. (#7923) @TatsuyaIse
+- fix: Questionnaire wikiType (#7907) @TatsuyaIse
+
 ## [v6.1.8](https://github.com/weseek/growi/compare/v6.1.7...v6.1.8) - 2023-07-24
 ## [v6.1.8](https://github.com/weseek/growi/compare/v6.1.7...v6.1.8) - 2023-07-24
 
 
 ### 💎 Features
 ### 💎 Features

+ 2 - 0
apps/app/config/logger/config.dev.js

@@ -29,6 +29,8 @@ module.exports = {
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:g2g-transfer': 'debug',
   'growi:service:g2g-transfer': 'debug',
 
 
+  'growi:migration:add-installed-date-to-config': 'debug',
+
   /*
   /*
    * configure level for client
    * configure level for client
    */
    */

+ 5 - 7
apps/app/package.json

@@ -197,7 +197,6 @@
     "swagger-jsdoc": "^6.1.0",
     "swagger-jsdoc": "^6.1.0",
     "swr": "^2.0.3",
     "swr": "^2.0.3",
     "throttle-debounce": "^5.0.0",
     "throttle-debounce": "^5.0.0",
-    "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
     "unstated": "^2.1.1",
@@ -228,7 +227,6 @@
     "bootstrap": "^4.6.1",
     "bootstrap": "^4.6.1",
     "codemirror": "^5.64.0",
     "codemirror": "^5.64.0",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
-    "core-js": "=2.6.9",
     "diff2html": "^3.4.35",
     "diff2html": "^3.4.35",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
@@ -236,13 +234,13 @@
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "font-awesome": "^4.7.0",
     "font-awesome": "^4.7.0",
+    "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.11.0",
     "i18next-hmr": "^1.11.0",
     "jest": "^29.5.0",
     "jest": "^29.5.0",
     "jest-date-mock": "^1.0.8",
     "jest-date-mock": "^1.0.8",
     "jest-localstorage-mock": "^2.4.14",
     "jest-localstorage-mock": "^2.4.14",
-    "jquery-slimscroll": "^1.3.8",
-    "jquery.cookie": "~1.4.1",
+    "jquery": "^3.7.0",
     "load-css-file": "^1.0.0",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
     "material-icons": "^1.11.3",
     "mongodb-memory-server": "^8.12.2",
     "mongodb-memory-server": "^8.12.2",
@@ -250,6 +248,7 @@
     "null-loader": "^4.0.1",
     "null-loader": "^4.0.1",
     "penpal": "^4.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
+    "popper.js": "^1.16.1",
     "prettier": "^1.19.1",
     "prettier": "^1.19.1",
     "react-codemirror2": "^6.0.0",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
@@ -263,8 +262,7 @@
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "socket.io-client": "^4.2.0",
     "source-map-loader": "^4.0.1",
     "source-map-loader": "^4.0.1",
-    "swagger2openapi": "^5.3.1",
-    "tsc-alias": "^1.2.9",
-    "fslightbox-react": "^1.7.6"
+    "swagger2openapi": "^7.0.8",
+    "tsc-alias": "^1.2.9"
   }
   }
 }
 }

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

@@ -3,7 +3,7 @@
     "display_name": "English"
     "display_name": "English"
   },
   },
   "last_login": "Last login",
   "last_login": "Last login",
-  "wiki_management_home_page": "Wiki Management Home Page",
+  "wiki_management_homepage": "Wiki Management Homepage",
   "public": "Public",
   "public": "Public",
   "anyone_with_the_link": "Anyone with the link",
   "anyone_with_the_link": "Anyone with the link",
   "specified_users": "Specified users",
   "specified_users": "Specified users",
@@ -45,6 +45,11 @@
     "admin_only": "Admin only",
     "admin_only": "Admin only",
     "admin_and_author": "Admin and author",
     "admin_and_author": "Admin and author",
     "anyone": "Anyone",
     "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",
     "session": "Session",
     "max_age": "Max age (msec)",
     "max_age": "Max age (msec)",
     "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
     "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",

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

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

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

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

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

@@ -38,6 +38,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       expandOtherOptionsForCompleteDeletion: false,
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
       isShowRestrictedByGroup: false,
+      isUsersHomepageDeletionEnabled: false,
       isLocalEnabled: false,
       isLocalEnabled: false,
       isLdapEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
       isSamlEnabled: false,
@@ -73,6 +74,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
       currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
+      isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
       sessionMaxAge: generalSetting.sessionMaxAge,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       wikiMode: generalSetting.wikiMode,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
@@ -193,6 +195,13 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ isShowRestrictedByGroup:  !this.state.isShowRestrictedByGroup });
     this.setState({ isShowRestrictedByGroup:  !this.state.isShowRestrictedByGroup });
   }
   }
 
 
+  /**
+   * Switch isUsersHomepageDeletionEnabled
+   */
+  switchIsUsersHomepageDeletionEnabled() {
+    this.setState({ isUsersHomepageDeletionEnabled: !this.state.isUsersHomepageDeletionEnabled });
+  }
+
   /**
   /**
    * Update restrictGuestMode
    * Update restrictGuestMode
    * @memberOf AdminGeneralSecuritySContainer
    * @memberOf AdminGeneralSecuritySContainer
@@ -209,6 +218,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
       pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
       hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
       hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
+      isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
     };
     };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     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');
 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
  * @extends {Container} unstated Container
  */
  */
 export default class AdminHomeContainer extends 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 { toast, ToastContent, ToastOptions } from 'react-toastify';
-import * as toastrLegacy from 'toastr';
 
 
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 
@@ -34,5 +33,5 @@ export const toastWarningOption: ToastOptions = {
   closeButton: true,
   closeButton: true,
 };
 };
 export const toastWarning = (content: ToastContent, option: ToastOptions = toastWarningOption): void => {
 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 && (
                   { activity.user != null && (
                     <>
                     <>
                       <UserPicture user={activity.user} />
                       <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>
                 </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 '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 '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')} </>;
     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 */
       /* 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 {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
-import * as toastr from 'toastr';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
-
-// import { toastSuccess, toastError } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
 
 
 const GROUPS_PAGE = [
 const GROUPS_PAGE = [
@@ -72,37 +70,19 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
     try {
     try {
       // TODO: use apiv3Post
       // TODO: use apiv3Post
       const result = await apiPost<any>('/v3/export', { collections: Array.from(selectedCollections) });
       const result = await apiPost<any>('/v3/export', { collections: Array.from(selectedCollections) });
-      // TODO: toastSuccess, toastError
 
 
       if (!result.ok) {
       if (!result.ok) {
         throw new Error('Error occured.');
         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();
       onExportingRequested();
       onClose();
       onClose();
       uncheckAll();
       uncheckAll();
     }
     }
     catch (err) {
     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]);
   }, [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 React, { useCallback, useEffect, useState } from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import * as toastr from 'toastr';
 
 
 
 
 import { apiDelete } from '~/client/util/apiv1-client';
 import { apiDelete } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { useAdminSocket } from '~/stores/socket-io';
 import { useAdminSocket } from '~/stores/socket-io';
 
 
 import LabeledProgressBar from './Common/LabeledProgressBar';
 import LabeledProgressBar from './Common/LabeledProgressBar';
@@ -34,7 +34,6 @@ const ExportArchiveDataPage = (): JSX.Element => {
       apiv3Get<{collections: any[]}>('/mongo/collections', {}),
       apiv3Get<{collections: any[]}>('/mongo/collections', {}),
       apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
       apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
     ]);
     ]);
-    // TODO: toastSuccess, toastError
 
 
     // filter only not ignored collection names
     // filter only not ignored collection names
     const filteredCollections = collectionsData.collections.filter((collectionName) => {
     const filteredCollections = collectionsData.collections.filter((collectionName) => {
@@ -69,16 +68,7 @@ const ExportArchiveDataPage = (): JSX.Element => {
         setExported(true);
         setExported(true);
         setZipFileStats(prev => prev.concat([addedZipFileStat]));
         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]);
   }, [socket]);
@@ -89,27 +79,10 @@ const ExportArchiveDataPage = (): JSX.Element => {
 
 
       setZipFileStats(prev => prev.filter(stat => stat.fileName !== fileName));
       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) {
     catch (err) {
-      // TODO: toastSuccess, toastError
-      toastr.error(err, 'Error', {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '3000',
-      });
+      toastError(err);
     }
     }
   }, []);
   }, []);
 
 

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

@@ -32,7 +32,6 @@ class UploadForm extends React.Component {
 
 
     try {
     try {
       const { data } = await apiv3PostForm('/import/upload', formData);
       const { data } = await apiv3PostForm('/import/upload', formData);
-      // TODO: toastSuccess, toastError
       this.props.onUpload(data);
       this.props.onUpload(data);
     }
     }
     catch (err) {
     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 { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import * as toastr from 'toastr';
 
 
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 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 ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
 import UploadForm from './GrowiArchive/UploadForm';
@@ -59,27 +58,10 @@ class GrowiArchiveSection extends React.Component {
       await apiv3Delete('/import/all');
       await apiv3Delete('/import/all');
       this.resetState();
       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) {
     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]))
           ].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>
         <h4>{t('security_settings.session')}</h4>
         <div className="form-group row">
         <div className="form-group row">
           <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>
           <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>

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

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

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

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

+ 14 - 23
apps/app/src/components/Comments.tsx

@@ -1,10 +1,11 @@
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect, useMemo, useRef } from 'react';
 
 
 import type { IRevisionHasId } from '@growi/core';
 import type { IRevisionHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import { debounce } from 'throttle-debounce';
 
 
-import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
+import { type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage, useSWRMUTxPageInfo } from '~/stores/page';
 import { useIsTrashPage, useSWRMUTxPageInfo } from '~/stores/page';
 
 
@@ -39,30 +40,21 @@ export const Comments = (props: CommentsProps): JSX.Element => {
 
 
   const pageCommentParentRef = useRef<HTMLDivElement>(null);
   const pageCommentParentRef = useRef<HTMLDivElement>(null);
 
 
+  const onLoadedDebounced = useMemo(() => debounce(500, () => onLoaded?.()), [onLoaded]);
+
   useEffect(() => {
   useEffect(() => {
     const parent = pageCommentParentRef.current;
     const parent = pageCommentParentRef.current;
     if (parent == null) return;
     if (parent == null) return;
 
 
-    const observerCallback = (mutationRecords: MutationRecord[]) => {
-      mutationRecords.forEach((record: MutationRecord) => {
-        const target = record.target as HTMLElement;
-
-        for (const child of Array.from(target.children)) {
-          const childId = (child as HTMLElement).id;
-          if (childId === PageCommentRootElemId) {
-            onLoaded?.();
-            break;
-          }
-        }
-
-      });
-    };
-
-    const observer = new MutationObserver(observerCallback);
-    observer.observe(parent, { childList: true });
-    return () => {
-      observer.disconnect();
-    };
+    const observer = new MutationObserver(() => {
+      onLoadedDebounced();
+    });
+    observer.observe(parent, { childList: true, subtree: true });
+
+    // no cleanup function -- 2023.07.31 Yuki Takei
+    // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
+    // > You can call observe() multiple times on the same MutationObserver
+    // > to watch for changes to different parts of the DOM tree and/or different types of changes.
   }, [onLoaded]);
   }, [onLoaded]);
 
 
   const isTopPagePath = isTopPage(pagePath);
   const isTopPagePath = isTopPage(pagePath);
@@ -87,7 +79,6 @@ export const Comments = (props: CommentsProps): JSX.Element => {
             currentUser={currentUser}
             currentUser={currentUser}
             isReadOnly={false}
             isReadOnly={false}
             titleAlign="left"
             titleAlign="left"
-            hideIfEmpty={false}
           />
           />
         </div>
         </div>
         {!isDeleted && (
         {!isDeleted && (

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

+ 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 AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
-
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 
 
 
 
@@ -396,7 +395,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
               />
               />
             )}
             )}
           </div>
           </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`}>
             <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">
               <li className="pb-1">
                 {currentPage != null
                 {currentPage != null

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

@@ -1,5 +1,6 @@
 import { useRef, useState } from 'react';
 import { useRef, useState } from 'react';
 
 
+import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -64,7 +65,7 @@ const PersonalDropdown = () => {
 
 
           <div className="btn-group btn-block mt-2" role="group">
           <div className="btn-group btn-block mt-2" role="group">
             <Link
             <Link
-              href={`/user/${currentUser.username}`}
+              href={pagePathUtils.userHomepagePath(currentUser)}
               className="btn btn-sm btn-outline-secondary col"
               className="btn btn-sm btn-outline-secondary col"
               data-testid="grw-personal-dropdown-menu-user-home"
               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,
   useEffect, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
 
 
-
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 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 dynamic from 'next/dynamic';
 
 
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -23,7 +22,7 @@ import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageContentFooter } from '../PageContentFooter';
 import { PageContentFooter } from '../PageContentFooter';
 import type { PageSideContentsProps } from '../PageSideContents';
 import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
 import { UserInfo } from '../User/UserInfo';
-import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
+import type { UsersHomepageFooterProps } from '../UsersHomepageFooter';
 
 
 import RevisionRenderer from './RevisionRenderer';
 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 PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { 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 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 });
 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 page = pageBySWR ?? initialPage;
   const isNotFound = isNotFoundMeta || page?.revision == null;
   const isNotFound = isNotFoundMeta || page?.revision == null;
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const isUsersHomepagePath = isUsersHomepage(pagePath);
 
 
 
 
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
@@ -116,8 +115,8 @@ export const PageView = (props: Props): JSX.Element => {
             onLoaded={() => setCommentsLoaded(true)}
             onLoaded={() => setCommentsLoaded(true)}
           />
           />
         </div>
         </div>
-        {(isUsersHomePagePath && page.creator != null) && (
-          <UsersHomePageFooter creatorId={page.creator._id} />
+        {(isUsersHomepagePath && page.creator != null) && (
+          <UsersHomepageFooter creatorId={page.creator._id} />
         )}
         )}
         <PageContentFooter page={page} />
         <PageContentFooter page={page} />
       </>
       </>
@@ -150,7 +149,7 @@ export const PageView = (props: Props): JSX.Element => {
       {specialContents}
       {specialContents}
       {specialContents == null && (
       {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']}` : ''}`}>
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
             <Contents />
             <Contents />
           </div>
           </div>

+ 4 - 11
apps/app/src/components/Page/RevisionLoader.tsx

@@ -20,11 +20,6 @@ export type RevisionLoaderProps = {
 
 
 const logger = loggerFactory('growi:Page:RevisionLoader');
 const logger = loggerFactory('growi:Page:RevisionLoader');
 
 
-// Always render '#revision-loader' for MutationObserver of SearchResultContent
-const RevisionLoaderRoot = (props: React.HTMLAttributes<HTMLDivElement>): JSX.Element => (
-  <div id={ROOT_ELEM_ID} {...props}>{props.children}</div>
-);
-
 /**
 /**
  * Load data from server and render RevisionBody component
  * Load data from server and render RevisionBody component
  */
  */
@@ -76,11 +71,9 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <RevisionLoaderRoot>
-      <RevisionRenderer
-        rendererOptions={rendererOptions}
-        markdown={markdown}
-      />
-    </RevisionLoaderRoot>
+    <RevisionRenderer
+      rendererOptions={rendererOptions}
+      markdown={markdown}
+    />
   );
   );
 };
 };

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

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

+ 6 - 19
apps/app/src/components/PageComment.tsx

@@ -23,13 +23,6 @@ import { ReplyComments } from './PageComment/ReplyComments';
 
 
 import styles from './PageComment.module.scss';
 import styles from './PageComment.module.scss';
 
 
-export const ROOT_ELEM_ID = 'page-comments' as const;
-
-// Always render '#page-comments' for MutationObserver of SearchResultContent
-const PageCommentRoot = (props: React.HTMLAttributes<HTMLDivElement>): JSX.Element => (
-  <div id={ROOT_ELEM_ID} {...props}>{props.children}</div>
-);
-
 
 
 export type PageCommentProps = {
 export type PageCommentProps = {
   rendererOptions?: RendererOptions,
   rendererOptions?: RendererOptions,
@@ -39,14 +32,13 @@ export type PageCommentProps = {
   currentUser: any,
   currentUser: any,
   isReadOnly: boolean,
   isReadOnly: boolean,
   titleAlign?: 'center' | 'left' | 'right',
   titleAlign?: 'center' | 'left' | 'right',
-  hideIfEmpty?: boolean,
 }
 }
 
 
 export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps): JSX.Element => {
 export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps): JSX.Element => {
 
 
   const {
   const {
     rendererOptions: rendererOptionsByProps,
     rendererOptions: rendererOptionsByProps,
-    pageId, pagePath, revision, currentUser, isReadOnly, titleAlign, hideIfEmpty,
+    pageId, pagePath, revision, currentUser, isReadOnly, titleAlign,
   } = props;
   } = props;
 
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
   const { data: comments, mutate } = useSWRxPageComment(pageId);
@@ -116,8 +108,8 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
     mutatePageInfo();
     mutatePageInfo();
   }, [removeShowEditorId, mutate, mutatePageInfo]);
   }, [removeShowEditorId, mutate, mutatePageInfo]);
 
 
-  if (hideIfEmpty && comments?.length === 0) {
-    return <PageCommentRoot />;
+  if (comments?.length === 0) {
+    return <></>;
   }
   }
 
 
   let commentTitleClasses = 'border-bottom py-3 mb-3';
   let commentTitleClasses = 'border-bottom py-3 mb-3';
@@ -126,12 +118,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   const rendererOptions = rendererOptionsByProps ?? rendererOptionsForCurrentPage;
   const rendererOptions = rendererOptionsByProps ?? rendererOptionsForCurrentPage;
 
 
   if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
   if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
-    if (hideIfEmpty) {
-      return <PageCommentRoot />;
-    }
-    return (
-      <></>
-    );
+    return <></>;
   }
   }
 
 
   const revisionId = getIdForRef(revision);
   const revisionId = getIdForRef(revision);
@@ -168,7 +155,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   );
   );
 
 
   return (
   return (
-    <PageCommentRoot className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
+    <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
       <div className="container-lg">
       <div className="container-lg">
         <div className="page-comments">
         <div className="page-comments">
           <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
           <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
@@ -230,7 +217,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
           confirmToDelete={onDeleteComment}
           confirmToDelete={onDeleteComment}
         />
         />
       )}
       )}
-    </PageCommentRoot>
+    </div>
   );
   );
 });
 });
 
 

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

@@ -4,12 +4,13 @@ import React, {
 
 
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
 import {
 import {
   Button, TabContent, TabPane,
   Button, TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
-import * as toastr from 'toastr';
 
 
 import { apiPostForm } from '~/client/util/apiv1-client';
 import { apiPostForm } from '~/client/util/apiv1-client';
+import { toastError } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import {
 import {
@@ -85,6 +86,20 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   const editorRef = useRef<IEditorMethods>(null);
   const editorRef = useRef<IEditorMethods>(null);
 
 
+  const router = useRouter();
+
+  // UnControlled CodeMirror value is not reset on page transition, so explicitly set the value to the initial value
+  const onRouterChangeComplete = useCallback(() => {
+    editorRef.current?.setValue('');
+  }, []);
+
+  useEffect(() => {
+    router.events.on('routeChangeComplete', onRouterChangeComplete);
+    return () => {
+      router.events.off('routeChangeComplete', onRouterChangeComplete);
+    };
+  }, [onRouterChangeComplete, router.events]);
+
   const handleSelect = useCallback((activeTab: string) => {
   const handleSelect = useCallback((activeTab: string) => {
     setActiveTab(activeTab);
     setActiveTab(activeTab);
   }, []);
   }, []);
@@ -185,14 +200,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   }, [postCommentHandler]);
   }, [postCommentHandler]);
 
 
   const apiErrorHandler = useCallback((error: Error) => {
   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) => {
   const uploadHandler = useCallback(async(file) => {

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

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

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

@@ -28,7 +28,7 @@ const isV5Compatible = (meta: unknown): boolean => {
 const PageRenameModal = (): JSX.Element => {
 const PageRenameModal = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const { isUsersHomePage } = pagePathUtils;
+  const { isUsersHomepage } = pagePathUtils;
   const { data: siteUrl } = useSiteUrl();
   const { data: siteUrl } = useSiteUrl();
   const { data: renameModalData, close: closeRenameModal } = usePageRenameModal();
   const { data: renameModalData, close: closeRenameModal } = usePageRenameModal();
   const { data: isReachable } = useIsSearchServiceReachable();
   const { data: isReachable } = useIsSearchServiceReachable();
@@ -54,7 +54,7 @@ const PageRenameModal = (): JSX.Element => {
   const [isRemainMetadata, setIsRemainMetadata] = useState(false);
   const [isRemainMetadata, setIsRemainMetadata] = useState(false);
   const [expandOtherOptions, setExpandOtherOptions] = useState(false);
   const [expandOtherOptions, setExpandOtherOptions] = useState(false);
   const [subordinatedError] = useState(null);
   const [subordinatedError] = useState(null);
-  const [isMatchedWithUserHomePagePath, setIsMatchedWithUserHomePagePath] = useState(false);
+  const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] = useState(false);
 
 
   const updateSubordinatedList = useCallback(async() => {
   const updateSubordinatedList = useCallback(async() => {
     if (page == null) {
     if (page == null) {
@@ -80,14 +80,14 @@ const PageRenameModal = (): JSX.Element => {
   }, [isOpened, page, updateSubordinatedList]);
   }, [isOpened, page, updateSubordinatedList]);
 
 
   const canRename = useMemo(() => {
   const canRename = useMemo(() => {
-    if (page == null || isMatchedWithUserHomePagePath || page.data.path === pageNameInput) {
+    if (page == null || isMatchedWithUserHomepagePath || page.data.path === pageNameInput) {
       return false;
       return false;
     }
     }
     if (isV5Compatible(page.meta)) {
     if (isV5Compatible(page.meta)) {
       return existingPaths.length === 0; // v5 data
       return existingPaths.length === 0; // v5 data
     }
     }
     return isRenameRecursively; // v4 data
     return isRenameRecursively; // v4 data
-  }, [existingPaths.length, isMatchedWithUserHomePagePath, isRenameRecursively, page, pageNameInput]);
+  }, [existingPaths.length, isMatchedWithUserHomepagePath, isRenameRecursively, page, pageNameInput]);
 
 
   const rename = useCallback(async() => {
   const rename = useCallback(async() => {
     if (page == null || !canRename) {
     if (page == null || !canRename) {
@@ -151,20 +151,20 @@ const PageRenameModal = (): JSX.Element => {
     return debounce(1000, checkExistPaths);
     return debounce(1000, checkExistPaths);
   }, [checkExistPaths]);
   }, [checkExistPaths]);
 
 
-  const checkIsUsersHomePageDebounce = useMemo(() => {
+  const checkIsUsersHomepageDebounce = useMemo(() => {
     const checkIsPagePathRenameable = () => {
     const checkIsPagePathRenameable = () => {
-      setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));
+      setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
     };
     };
 
 
     return debounce(1000, checkIsPagePathRenameable);
     return debounce(1000, checkIsPagePathRenameable);
-  }, [isUsersHomePage, pageNameInput]);
+  }, [isUsersHomepage, pageNameInput]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (isOpened && page != null && pageNameInput !== page.data.path) {
     if (isOpened && page != null && pageNameInput !== page.data.path) {
       checkExistPathsDebounce(page.data.path, pageNameInput);
       checkExistPathsDebounce(page.data.path, pageNameInput);
-      checkIsUsersHomePageDebounce(pageNameInput);
+      checkIsUsersHomepageDebounce(pageNameInput);
     }
     }
-  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomePageDebounce]);
+  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomepageDebounce]);
 
 
   function ppacInputChangeHandler(value) {
   function ppacInputChangeHandler(value) {
     setErrs(null);
     setErrs(null);
@@ -246,7 +246,7 @@ const PageRenameModal = (): JSX.Element => {
         { isTargetPageDuplicate && (
         { isTargetPageDuplicate && (
           <p className="text-danger">Error: Target path is duplicated.</p>
           <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>
           <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';
 import styles from './PageSideContents.module.scss';
 
 
 
 
-const { isTopPage, isUsersHomePage, isTrashPage } = pagePathUtils;
+const { isTopPage, isUsersHomepage, isTrashPage } = pagePathUtils;
 
 
 
 
 export type PageSideContentsProps = {
 export type PageSideContentsProps = {
@@ -35,7 +35,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
 
   const pagePath = page.path;
   const pagePath = page.path;
   const isTopPagePath = isTopPage(pagePath);
   const isTopPagePath = isTopPage(pagePath);
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const isUsersHomepagePath = isUsersHomepage(pagePath);
   const isTrash = isTrashPage(pagePath);
   const isTrash = isTrashPage(pagePath);
 
 
   return (
   return (
@@ -83,7 +83,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
 
       <div className="d-none d-lg-block">
       <div className="d-none d-lg-block">
         <TableOfContents />
         <TableOfContents />
-        {isUsersHomePagePath && <ContentLinkButtons author={page?.creator} />}
+        {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
       </div>
       </div>
     </>
     </>
   );
   );

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

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

+ 50 - 76
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -8,6 +8,7 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { animateScroll } from 'react-scroll';
 import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
+import { debounce } from 'throttle-debounce';
 
 
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
@@ -24,8 +25,8 @@ import { mutateSearching } from '~/stores/search';
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import type { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import type { SubNavButtonsProps } from '../Navbar/SubNavButtons';
 import type { SubNavButtonsProps } from '../Navbar/SubNavButtons';
-import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, type RevisionLoaderProps } from '../Page/RevisionLoader';
-import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '../PageComment';
+import { type RevisionLoaderProps } from '../Page/RevisionLoader';
+import { type PageCommentProps } from '../PageComment';
 import type { PageContentFooterProps } from '../PageContentFooter';
 import type { PageContentFooterProps } from '../PageContentFooter';
 
 
 import styles from './SearchResultContent.module.scss';
 import styles from './SearchResultContent.module.scss';
@@ -60,7 +61,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
 };
 };
 
 
 const SCROLL_OFFSET_TOP = 30;
 const SCROLL_OFFSET_TOP = 30;
-const MUTATION_OBSERVER_CONFIG = { childList: true }; // omit 'subtree: true'
+const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true }; // omit 'subtree: true'
 
 
 type Props ={
 type Props ={
   pageWithMeta : IPageWithSearchMeta,
   pageWithMeta : IPageWithSearchMeta,
@@ -69,72 +70,41 @@ type Props ={
   forceHideMenuItems?: ForceHideMenuItems,
   forceHideMenuItems?: ForceHideMenuItems,
 }
 }
 
 
-const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): boolean => {
+const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): void => {
   // use querySelector to intentionally get the first element found
   // use querySelector to intentionally get the first element found
   const toElem = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
   const toElem = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
   if (toElem == null) {
   if (toElem == null) {
-    return false;
+    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,
     containerId: scrollElement.id,
     duration: 200,
     duration: 200,
   });
   });
-  return true;
 };
 };
+const scrollToFirstHighlightedKeywordDebounced = debounce(500, scrollToFirstHighlightedKeyword);
 
 
 export const SearchResultContent: FC<Props> = (props: Props) => {
 export const SearchResultContent: FC<Props> = (props: Props) => {
 
 
   const scrollElementRef = useRef<HTMLDivElement|null>(null);
   const scrollElementRef = useRef<HTMLDivElement|null>(null);
 
 
-  const [isRevisionLoaded, setRevisionLoaded] = useState(false);
-  const [isPageCommentLoaded, setPageCommentLoaded] = useState(false);
-
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
   useEffect(() => {
     const scrollElement = scrollElementRef.current;
     const scrollElement = scrollElementRef.current;
-    if (scrollElement == null) return;
 
 
-    const observerCallback = (mutationRecords:MutationRecord[]) => {
-      mutationRecords.forEach((record:MutationRecord) => {
-        const target = record.target as HTMLElement;
-
-        // turn on boolean if loaded
-        Array.from(target.children).forEach((child) => {
-          const childId = (child as HTMLElement).id;
-          if (childId === RevisionLoaderRoomElemId) {
-            setRevisionLoaded(true);
-          }
-          else if (childId === PageCommentRootElemId) {
-            setPageCommentLoaded(true);
-          }
-        });
-      });
-    };
+    if (scrollElement == null) return;
 
 
-    const observer = new MutationObserver(observerCallback);
+    const observer = new MutationObserver(() => {
+      scrollToFirstHighlightedKeywordDebounced(scrollElement);
+    });
     observer.observe(scrollElement, MUTATION_OBSERVER_CONFIG);
     observer.observe(scrollElement, MUTATION_OBSERVER_CONFIG);
-    return () => {
-      observer.disconnect();
-    };
-  }, []);
-
-  useEffect(() => {
-    if (!isRevisionLoaded || !isPageCommentLoaded) {
-      return;
-    }
-    if (scrollElementRef.current == null) {
-      return;
-    }
 
 
-    const scrollElement = scrollElementRef.current;
-    const isScrollProcessed = scrollToFirstHighlightedKeyword(scrollElement);
-    // retry after 1000ms if highlighted element is absense
-    if (!isScrollProcessed) {
-      setTimeout(() => scrollToFirstHighlightedKeyword(scrollElement), 1000);
-    }
-
-  }, [isPageCommentLoaded, isRevisionLoaded]);
+    // no cleanup function -- 2023.07.31 Yuki Takei
+    // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
+    // > You can call observe() multiple times on the same MutationObserver
+    // > to watch for changes to different parts of the DOM tree and/or different types of changes.
+  });
   // *******************************  end  *******************************
   // *******************************  end  *******************************
 
 
   const {
   const {
@@ -233,40 +203,44 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   }, [page, isExpandContentWidth, showPageControlDropdown, forceHideMenuItems, isContainerFluid,
   }, [page, isExpandContentWidth, showPageControlDropdown, forceHideMenuItems, isContainerFluid,
       duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, switchContentWidthHandler]);
       duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, switchContentWidthHandler]);
 
 
-  // return if page or growiRenderer is null
-  if (page == null || rendererOptions == null) return <></>;
+  const isRenderable = page != null && rendererOptions != null;
 
 
   return (
   return (
     <div key={page._id} data-testid="search-result-content" className={`search-result-content ${styles['search-result-content']} d-flex flex-column`}>
     <div key={page._id} data-testid="search-result-content" className={`search-result-content ${styles['search-result-content']} d-flex flex-column`}>
       <div className="grw-page-path-text-muted-container">
       <div className="grw-page-path-text-muted-container">
-        <GrowiSubNavigation
-          pagePath={page.path}
-          pageId={page._id}
-          rightComponent={RightComponent}
-          isCompactMode
-          additionalClasses={['px-4']}
-        />
+        { isRenderable && (
+          <GrowiSubNavigation
+            pagePath={page.path}
+            pageId={page._id}
+            rightComponent={RightComponent}
+            isCompactMode
+            additionalClasses={['px-4']}
+          />
+        ) }
       </div>
       </div>
       <div id="search-result-content-body-container" className="search-result-content-body-container" ref={scrollElementRef}>
       <div id="search-result-content-body-container" className="search-result-content-body-container" ref={scrollElementRef}>
-        {/* RevisionLoader will render '#revision-loader' after loaded */}
-        <RevisionLoader
-          rendererOptions={rendererOptions}
-          pageId={page._id}
-          revisionId={page.revision}
-        />
-        {/* PageComment will render '#page-comment' after loaded */}
-        <PageComment
-          rendererOptions={rendererOptions}
-          pageId={page._id}
-          pagePath={page.path}
-          revision={page.revision}
-          currentUser={currentUser}
-          isReadOnly
-          hideIfEmpty
-        />
-        <PageContentFooter
-          page={page}
-        />
+        { isRenderable && (
+          <RevisionLoader
+            rendererOptions={rendererOptions}
+            pageId={page._id}
+            revisionId={page.revision}
+          />
+        )}
+        { isRenderable && (
+          <PageComment
+            rendererOptions={rendererOptions}
+            pageId={page._id}
+            pagePath={page.path}
+            revision={page.revision}
+            currentUser={currentUser}
+            isReadOnly
+          />
+        )}
+        { isRenderable && (
+          <PageContentFooter
+            page={page}
+          />
+        )}
       </div>
       </div>
     </div>
     </div>
   );
   );

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

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
 import Link from 'next/link';
 import Link from 'next/link';
 
 
 export const Username: React.FC<{ user?: IUser }> = ({ user }): JSX.Element => {
 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 name = user.name || '(no name)';
   const username = user.username;
   const username = user.username;
-  const href = `/user/${user.username}`;
+  const href = pagePathUtils.userHomepagePath(user);
 
 
   return (
   return (
     <Link href={href} prefetch={false}>
     <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 { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 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 { useCurrentUser } from '~/stores/context';
 
 
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 import { CompressIcon } from './Icons/CompressIcon';
 import { CompressIcon } from './Icons/CompressIcon';
 import { ExpandIcon } from './Icons/ExpandIcon';
 import { ExpandIcon } from './Icons/ExpandIcon';
 
 
-export type UsersHomePageFooterProps = {
+export type UsersHomepageFooterProps = {
   creatorId: string,
   creatorId: string,
 }
 }
 
 
-export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Element => {
+export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { creatorId } = props;
   const { creatorId } = props;
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
@@ -43,7 +43,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
         </h2>
         </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. */}
         {/* 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']}`}`}>
         <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>
       </div>
       <div className="grw-user-page-list-m mt-5 d-edit-none">
       <div className="grw-user-page-list-m mt-5 d-edit-none">

+ 58 - 0
apps/app/src/migrations/20230731075753-add_installed_date_to_config.js

@@ -0,0 +1,58 @@
+// eslint-disable-next-line import/no-named-as-default
+import ConfigModel from '~/server/models/config';
+import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migration:add-installed-date-to-config');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+    const Config = getModelSafely('Config') || ConfigModel;
+    const User = getModelSafely('User') || require('~/server/models/user')();
+
+    const appInstalled = await Config.findOne({ key: 'app:installed' });
+    if (appInstalled != null && appInstalled.createdAt == null) {
+      // Get the oldest user who probably installed this GROWI.
+      const users = await User.find().limit(1).sort({ createdAt: 1 });
+      const initialUserCreatedAt = users[0].createdAt;
+      logger.debug('initialUserCreatedAt: ', initialUserCreatedAt);
+
+      // Set app:installed date.
+      // refs: https://mongoosejs.com/docs/6.x/docs/timestamps.html#disabling-timestamps
+      //       Read the section after "Disabling timestamps also lets you set timestamps yourself..."
+      const updatedConfig = await Config.findOneAndUpdate({ _id: appInstalled._id }, { createdAt: initialUserCreatedAt }, {
+        new: true,
+        timestamps: false,
+        strict: false,
+      });
+      logger.debug('updatedConfig: ', updatedConfig);
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    logger.info('Rollback migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+    const Config = getModelSafely('Config') || ConfigModel;
+
+    const appInstalled = await Config.findOne({ key: 'app:installed' });
+    if (appInstalled != null) {
+
+      // Unset app:installed date.
+      const updatedConfig = await Config.findOneAndUpdate({ _id: appInstalled._id }, { $unset: { createdAt: 1 } }, {
+        new: true,
+        timestamps: false,
+        strict: false,
+      });
+      logger.debug('updatedConfig: ', updatedConfig);
+    }
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

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

@@ -171,6 +171,7 @@ type Props = CommonProps & {
   isIndentSizeForced: boolean,
   isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
   skipSSR: boolean,
   skipSSR: boolean,
+  ssrMaxRevisionBodyLength: number,
 
 
   grantData?: IPageGrantData,
   grantData?: IPageGrantData,
 
 
@@ -440,7 +441,7 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
 
 
   const Page = crowi.model('Page') as PageModel;
   const Page = crowi.model('Page') as PageModel;
   const PageRedirect = mongooseModel('PageRedirect') as PageRedirectModel;
   const PageRedirect = mongooseModel('PageRedirect') as PageRedirectModel;
-  const { pageService } = crowi;
+  const { pageService, configManager } = crowi;
 
 
   let currentPathname = props.currentPathname;
   let currentPathname = props.currentPathname;
 
 
@@ -479,7 +480,8 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   if (page != null) {
   if (page != null) {
     page.initLatestRevisionField(revisionId);
     page.initLatestRevisionField(revisionId);
     props.isLatestRevision = page.isLatestRevision();
     props.isLatestRevision = page.isLatestRevision();
-    props.skipSSR = await skipSSR(page);
+    const ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+    props.skipSSR = await skipSSR(page, ssrMaxRevisionBodyLength);
     await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
     await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
   }
   }
 
 
@@ -606,6 +608,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
   };
 
 
+  props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
 }
 }
 
 
 /**
 /**

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

@@ -28,14 +28,14 @@ type Props = CommonProps & {
 };
 };
 
 
 
 
-const AdminHomePage: NextPage<Props> = (props) => {
+const AdminHomepage: NextPage<Props> = (props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
   useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
 
 
   const { t } = useTranslation('admin');
   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>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
@@ -82,4 +82,4 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 };
 };
 
 
 
 
-export default AdminHomePage;
+export default AdminHomepage;

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

@@ -73,6 +73,13 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
 
 
   const getTargetPageToRender = (pagesMap, keys): {title: string, component: JSX.Element} => {
   const getTargetPageToRender = (pagesMap, keys): {title: string, component: JSX.Element} => {
     return keys.reduce((pagesMap, key) => {
     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];
       return pagesMap[key];
     }, pagesMap);
     }, pagesMap);
   };
   };

+ 5 - 1
apps/app/src/pages/share/[[...path]].page.tsx

@@ -45,6 +45,7 @@ type Props = CommonProps & {
   drawioUri: string | null,
   drawioUri: string | null,
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
   skipSSR: boolean,
   skipSSR: boolean,
+  ssrMaxRevisionBodyLength: number,
 };
 };
 
 
 type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
 type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
@@ -181,6 +182,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
     highlightJsStyleBorder: configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
   };
+
+  props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
 }
 }
 
 
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
@@ -237,7 +240,8 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     }
     }
     else {
     else {
       props.isNotFound = false;
       props.isNotFound = false;
-      props.skipSSR = await skipSSR(shareLink.relatedPage);
+      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+      props.skipSSR = await skipSSR(shareLink.relatedPage, ssrMaxRevisionBodyLength);
       props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
       props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
       props.isExpired = shareLink.isExpired();
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
       props.shareLink = shareLink.toObject();

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

@@ -172,7 +172,7 @@ export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettin
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 };
 };
 
 
-export const skipSSR = async(page: PageDocument): Promise<boolean> => {
+export const skipSSR = async(page: PageDocument, ssrMaxRevisionBodyLength: number): Promise<boolean> => {
   if (!isServer()) {
   if (!isServer()) {
     throw new Error('This method is not available on the client-side');
     throw new Error('This method is not available on the client-side');
   }
   }
@@ -183,9 +183,5 @@ export const skipSSR = async(page: PageDocument): Promise<boolean> => {
     return true;
     return true;
   }
   }
 
 
-  const { configManager } = await import('~/server/service/config-manager');
-  await configManager.loadConfigs();
-  const ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
-
   return ssrMaxRevisionBodyLength < latestRevisionBodyLength;
   return ssrMaxRevisionBodyLength < latestRevisionBodyLength;
 };
 };

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

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

+ 0 - 35
apps/app/src/server/events/user.js

@@ -1,35 +0,0 @@
-const debug = require('debug')('growi:events:user');
-const util = require('util');
-const events = require('events');
-
-function UserEvent(crowi) {
-  this.crowi = crowi;
-
-  events.EventEmitter.call(this);
-}
-util.inherits(UserEvent, events.EventEmitter);
-
-UserEvent.prototype.onActivated = async function(user) {
-  const Page = this.crowi.model('Page');
-
-  const userPagePath = Page.getUserPagePath(user);
-
-  const page = await Page.findByPath(userPagePath, user);
-
-  if (page == null) {
-    const body = `# ${user.username}\nThis is ${user.username}'s page`;
-
-    // create user page
-    try {
-      await this.crowi.pageService.create(userPagePath, body, user, {});
-
-      // page created
-      debug('User page created', page);
-    }
-    catch (err) {
-      debug('Failed to create user page', err);
-    }
-  }
-};
-
-module.exports = UserEvent;

+ 51 - 0
apps/app/src/server/events/user.ts

@@ -0,0 +1,51 @@
+import EventEmitter from 'events';
+
+import type { IUserHasId } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:events:user');
+
+class UserEvent extends EventEmitter {
+
+  crowi: any;
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  constructor(crowi: any) {
+    super();
+    this.crowi = crowi;
+  }
+
+  async onActivated(user: IUserHasId): Promise<void> {
+    if (this.crowi.pageService === null) {
+      logger.warn('crowi pageService is null');
+      return;
+    }
+
+    const Page = this.crowi.model('Page');
+    const userHomepagePath = pagePathUtils.userHomepagePath(user);
+
+    let page = await Page.findByPath(userHomepagePath, true);
+
+    if (page != null && page.creator != null && page.creator.toString() !== user._id.toString()) {
+      await this.crowi.pageService.deleteCompletelyUserHomeBySystem(userHomepagePath);
+      page = null;
+    }
+
+    if (page == null) {
+      const body = `# ${user.username}\nThis is ${user.username}'s page`;
+
+      try {
+        await this.crowi.pageService.create(userHomepagePath, body, user, {});
+        logger.debug('User page created', page);
+      }
+      catch (err) {
+        logger.error('Failed to create user page', err);
+      }
+    }
+  }
+
+}
+
+export default UserEvent;

+ 4 - 0
apps/app/src/server/models/config.ts

@@ -23,7 +23,10 @@ const schema = new Schema<Config>({
   ns: { type: String, required: true },
   ns: { type: String, required: true },
   key: { type: String, required: true },
   key: { type: String, required: true },
   value: { type: String, required: true },
   value: { type: String, required: true },
+}, {
+  timestamps: true,
 });
 });
+
 // define unique compound index
 // define unique compound index
 schema.index({ ns: 1, key: 1 }, { unique: true });
 schema.index({ ns: 1, key: 1 }, { unique: true });
 schema.plugin(uniqueValidator);
 schema.plugin(uniqueValidator);
@@ -67,6 +70,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'security:pageRecursiveDeletionAuthority' : undefined,
   'security:pageRecursiveDeletionAuthority' : undefined,
   'security:pageRecursiveCompleteDeletionAuthority' : undefined,
   'security:pageRecursiveCompleteDeletionAuthority' : undefined,
   'security:disableLinkSharing' : false,
   'security:disableLinkSharing' : false,
+  'security:isUsersHomepageDeletionEnabled': false,
 
 
   'security:passport-local:isEnabled' : true,
   'security:passport-local:isEnabled' : true,
   'security:passport-ldap:isEnabled' : false,
   'security:passport-ldap:isEnabled' : false,

+ 0 - 11
apps/app/src/server/models/obsolete-page.js

@@ -288,10 +288,6 @@ export const getPageSchema = (crowi) => {
       });
       });
   };
   };
 
 
-  pageSchema.statics.getUserPagePath = function(user) {
-    return `/user/${user.username}`;
-  };
-
   pageSchema.statics.getDeletedPageName = function(path) {
   pageSchema.statics.getDeletedPageName = function(path) {
     if (path.match('/')) {
     if (path.match('/')) {
       // eslint-disable-next-line no-param-reassign
       // eslint-disable-next-line no-param-reassign
@@ -673,13 +669,6 @@ export const getPageSchema = (crowi) => {
 
 
   };
   };
 
 
-  pageSchema.statics.removeByPath = function(path) {
-    if (path == null) {
-      throw new Error('path is required');
-    }
-    return this.findOneAndRemove({ path }).exec();
-  };
-
   pageSchema.statics.findListByPathsArray = async function(paths, includeEmpty = false) {
   pageSchema.statics.findListByPathsArray = async function(paths, includeEmpty = false) {
     const queryBuilder = new this.PageQueryBuilder(this.find(), includeEmpty);
     const queryBuilder = new this.PageQueryBuilder(this.find(), includeEmpty);
     queryBuilder.addConditionToListByPathsArray(paths);
     queryBuilder.addConditionToListByPathsArray(paths);

+ 25 - 3
apps/app/src/server/models/page.ts

@@ -1,11 +1,10 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
 
+import assert from 'assert';
 import nodePath from 'path';
 import nodePath from 'path';
 
 
-import {
-  isPopulated,
-} from '@growi/core';
 import type { IPage, HasObjectId } from '@growi/core';
 import type { IPage, HasObjectId } from '@growi/core';
+import { isPopulated } from '@growi/core/dist/interfaces';
 import { isTopPage, hasSlash, collectAncestorPaths } from '@growi/core/dist/utils/page-path-utils';
 import { isTopPage, hasSlash, collectAncestorPaths } from '@growi/core/dist/utils/page-path-utils';
 import { addTrailingSlash, normalizePath } from '@growi/core/dist/utils/path-utils';
 import { addTrailingSlash, normalizePath } from '@growi/core/dist/utils/path-utils';
 import escapeStringRegexp from 'escape-string-regexp';
 import escapeStringRegexp from 'escape-string-regexp';
@@ -375,6 +374,12 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
+  addConditionForSystemDeletion(): PageQueryBuilder {
+    const condition = generateGrantConditionForSystemDeletion();
+    this.query = this.query.and(condition);
+    return this;
+  }
+
   addConditionToPagenate(offset, limit, sortOpt?): PageQueryBuilder {
   addConditionToPagenate(offset, limit, sortOpt?): PageQueryBuilder {
     this.query = this.query
     this.query = this.query
       .sort(sortOpt).skip(offset).limit(limit); // eslint-disable-line newline-per-chained-call
       .sort(sortOpt).skip(offset).limit(limit); // eslint-disable-line newline-per-chained-call
@@ -941,6 +946,23 @@ export function generateGrantCondition(
 
 
 schema.statics.generateGrantCondition = generateGrantCondition;
 schema.statics.generateGrantCondition = generateGrantCondition;
 
 
+function generateGrantConditionForSystemDeletion(): { $or: any[] } {
+  const grantCondition: AnyObject[] = [
+    { grant: null },
+    { grant: GRANT_PUBLIC },
+    { grant: GRANT_RESTRICTED },
+    { grant: GRANT_SPECIFIED },
+    { grant: GRANT_OWNER },
+    { grant: GRANT_USER_GROUP },
+  ];
+
+  return {
+    $or: grantCondition,
+  };
+}
+
+schema.statics.generateGrantConditionForSystemDeletion = generateGrantConditionForSystemDeletion;
+
 // find ancestor page with isEmpty: false. If parameter path is '/', return undefined
 // find ancestor page with isEmpty: false. If parameter path is '/', return undefined
 schema.statics.findNonEmptyClosestAncestor = async function(path: string): Promise<PageDocument | undefined> {
 schema.statics.findNonEmptyClosestAncestor = async function(path: string): Promise<PageDocument | undefined> {
   if (path === '/') {
   if (path === '/') {

+ 15 - 6
apps/app/src/server/routes/apiv3/security-settings/index.js

@@ -1,4 +1,5 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
+import xss from 'xss';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
@@ -28,6 +29,7 @@ const validator = {
     body('pageCompleteDeletionAuthority').if(value => value != null).isString().isIn(Object.values(PageDeleteConfigValue)),
     body('pageCompleteDeletionAuthority').if(value => value != null).isString().isIn(Object.values(PageDeleteConfigValue)),
     body('hideRestrictedByOwner').if(value => value != null).isBoolean(),
     body('hideRestrictedByOwner').if(value => value != null).isBoolean(),
     body('hideRestrictedByGroup').if(value => value != null).isBoolean(),
     body('hideRestrictedByGroup').if(value => value != null).isBoolean(),
+    body('isUsersHomepageDeletionEnabled').if(value => value != null).isBoolean(),
   ],
   ],
   shareLinkSetting: [
   shareLinkSetting: [
     body('disableLinkSharing').if(value => value != null).isBoolean(),
     body('disableLinkSharing').if(value => value != null).isBoolean(),
@@ -355,6 +357,7 @@ module.exports = (crowi) => {
         pageRecursiveCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
         pageRecursiveCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
         hideRestrictedByOwner: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByOwner: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
         hideRestrictedByGroup: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
+        isUsersHomepageDeletionEnabled: await configManager.getConfig('crowi', 'security:isUsersHomepageDeletionEnabled'),
         wikiMode: await configManager.getConfig('crowi', 'security:wikiMode'),
         wikiMode: await configManager.getConfig('crowi', 'security:wikiMode'),
         sessionMaxAge: await configManager.getConfig('crowi', 'security:sessionMaxAge'),
         sessionMaxAge: await configManager.getConfig('crowi', 'security:sessionMaxAge'),
       },
       },
@@ -622,6 +625,7 @@ module.exports = (crowi) => {
       'security:pageRecursiveCompleteDeletionAuthority': req.body.pageRecursiveCompleteDeletionAuthority,
       'security:pageRecursiveCompleteDeletionAuthority': req.body.pageRecursiveCompleteDeletionAuthority,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
       'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
       'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
+      'security:isUsersHomepageDeletionEnabled': req.body.isUsersHomepageDeletionEnabled,
     };
     };
 
 
     // Validate delete config
     // Validate delete config
@@ -650,6 +654,7 @@ module.exports = (crowi) => {
         pageRecursiveCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
         pageRecursiveCompleteDeletionAuthority: await configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
         hideRestrictedByOwner: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByOwner: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
         hideRestrictedByGroup: await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
+        isUsersHomepageDeletionEnabled: await configManager.getConfig('crowi', 'security:isUsersHomepageDeletionEnabled'),
       };
       };
 
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_SECURITY_SETTINGS_UPDATE };
       const parameters = { action: SupportedAction.ACTION_ADMIN_SECURITY_SETTINGS_UPDATE };
@@ -799,13 +804,17 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/LocalSetting'
    *                  $ref: '#/components/schemas/LocalSetting'
    */
    */
   router.put('/local-setting', loginRequiredStrictly, adminRequired, addActivity, validator.localSetting, apiV3FormValidator, async(req, res) => {
   router.put('/local-setting', loginRequiredStrictly, adminRequired, addActivity, validator.localSetting, apiV3FormValidator, async(req, res) => {
-    const requestParams = {
-      'security:registrationMode': req.body.registrationMode,
-      'security:registrationWhitelist': req.body.registrationWhitelist,
-      'security:passport-local:isPasswordResetEnabled': req.body.isPasswordResetEnabled,
-      'security:passport-local:isEmailAuthenticationEnabled': req.body.isEmailAuthenticationEnabled,
-    };
     try {
     try {
+      const sanitizedRegistrationWhitelist = req.body.registrationWhitelist
+        .map(line => xss(line, { stripIgnoreTag: true }));
+
+      const requestParams = {
+        'security:registrationMode': req.body.registrationMode,
+        'security:registrationWhitelist': sanitizedRegistrationWhitelist,
+        'security:passport-local:isPasswordResetEnabled': req.body.isPasswordResetEnabled,
+        'security:passport-local:isEmailAuthenticationEnabled': req.body.isEmailAuthenticationEnabled,
+      };
+
       await updateAndReloadStrategySettings('local', requestParams);
       await updateAndReloadStrategySettings('local', requestParams);
 
 
       const localSettingParams = {
       const localSettingParams = {

+ 29 - 12
apps/app/src/server/routes/apiv3/users.js

@@ -1,4 +1,6 @@
+
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
+import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import Activity from '~/server/models/activity';
@@ -369,7 +371,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3('find-user-is-not-found'));
       return res.apiv3Err(new ErrorV3('find-user-is-not-found'));
     }
     }
 
 
-    const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
+    const limit = parseInt(req.query.limit) || await configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
     const page = req.query.page;
     const page = req.query.page;
     const offset = (page - 1) * limit;
     const offset = (page - 1) * limit;
     const queryOptions = { offset, limit };
     const queryOptions = { offset, limit };
@@ -765,6 +767,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(err));
       return res.apiv3Err(new ErrorV3(err));
     }
     }
   });
   });
+
   /**
   /**
    * @swagger
    * @swagger
    *
    *
@@ -774,7 +777,7 @@ module.exports = (crowi) => {
    *        tags: [Users]
    *        tags: [Users]
    *        operationId: removeUser
    *        operationId: removeUser
    *        summary: /users/{id}/remove
    *        summary: /users/{id}/remove
-   *        description: Delete user
+   *        description: Delete user and if isUsersHomepageDeletionEnabled delete user homepage and subpages
    *        parameters:
    *        parameters:
    *          - name: id
    *          - name: id
    *            in: path
    *            in: path
@@ -784,30 +787,44 @@ module.exports = (crowi) => {
    *              type: string
    *              type: string
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Deleting user success
+   *            description: Deleting user success and if isUsersHomepageDeletionEnabled delete user homepage and subpages success
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
-   *                    userData:
+   *                    user:
    *                      type: object
    *                      type: object
-   *                      description: data of delete user
+   *                      description: data of deleted user
+   *                    userHomepagePath:
+   *                      type: string
+   *                      description: a user homepage path
+   *                    isUsersHomepageDeletionEnabled:
+   *                      type: boolean
+   *                      description: is users homepage deletion enabled
    */
    */
   router.delete('/:id/remove', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
   router.delete('/:id/remove', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
     const { id } = req.params;
     const { id } = req.params;
+    const isUsersHomepageDeletionEnabled = configManager.getConfig('crowi', 'security:isUsersHomepageDeletionEnabled');
 
 
     try {
     try {
-      const userData = await User.findById(id);
-      await UserGroupRelation.remove({ relatedUser: userData });
-      await userData.statusDelete();
-      await ExternalAccount.remove({ user: userData });
-      await Page.removeByPath(`/user/${userData.username}`);
+      const user = await User.findById(id);
+      // !! DO NOT MOVE homepagePath FROM THIS POSITION !! -- 05.31.2023
+      // catch username before delete user because username will be change to deleted_at_*
+      const homepagePath = userHomepagePath(user);
 
 
-      const serializedUserData = serializeUserSecurely(userData);
+      await UserGroupRelation.remove({ relatedUser: user });
+      await user.statusDelete();
+      await ExternalAccount.remove({ user });
+
+      const serializedUser = serializeUserSecurely(user);
 
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
 
 
-      return res.apiv3({ userData: serializedUserData });
+      if (isUsersHomepageDeletionEnabled) {
+        crowi.pageService.deleteCompletelyUserHomeBySystem(homepagePath);
+      }
+
+      return res.apiv3({ user: serializedUser });
     }
     }
     catch (err) {
     catch (err) {
       logger.error('Error', err);
       logger.error('Error', err);

+ 11 - 9
apps/app/src/server/routes/next.ts

@@ -1,23 +1,25 @@
-import {
-  Request, Response,
-} from 'express';
+import type { IncomingMessage } from 'http';
+
+import type { NextServer, RequestHandler } from 'next/dist/server/next';
 
 
 type Crowi = {
 type Crowi = {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  nextApp: any,
+  nextApp: NextServer,
 }
 }
 
 
-type CrowiReq = Request & {
+type CrowiReq = IncomingMessage & {
   crowi: Crowi,
   crowi: Crowi,
 }
 }
 
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const delegator = (crowi: Crowi) => {
+type NextDelegatorResult = {
+  delegateToNext: RequestHandler,
+};
+
+const delegator = (crowi: Crowi): NextDelegatorResult => {
 
 
   const { nextApp } = crowi;
   const { nextApp } = crowi;
   const handle = nextApp.getRequestHandler();
   const handle = nextApp.getRequestHandler();
 
 
-  const delegateToNext = (req: CrowiReq, res: Response): void => {
+  const delegateToNext: RequestHandler = (req: CrowiReq, res): Promise<void> => {
     req.crowi = crowi;
     req.crowi = crowi;
     return handle(req, res);
     return handle(req, res);
   };
   };

+ 1 - 1
apps/app/src/server/service/config-loader.ts

@@ -782,7 +782,7 @@ export default class ConfigLoader {
   /**
   /**
    * get config from the environment variables for display admin page
    * get config from the environment variables for display admin page
    *
    *
-   * **use this only admin home page.**
+   * **use this only admin homepage.**
    */
    */
   static getEnvVarsForDisplay(avoidSecurity = false): any {
   static getEnvVarsForDisplay(avoidSecurity = false): any {
     const config = {};
     const config = {};

+ 92 - 0
apps/app/src/server/service/page.ts

@@ -1963,6 +1963,98 @@ class PageService {
     }
     }
   }
   }
 
 
+  /**
+   * @description This function is intended to be used exclusively for forcibly deleting the user homepage by the system.
+   * It should only be called from within the appropriate context and with caution as it performs a system-level operation.
+   *
+   * @param {string} userHomepagePath - The path of the user's homepage.
+   * @returns {Promise<void>} - A Promise that resolves when the deletion is complete.
+   * @throws {Error} - If an error occurs during the deletion process.
+   */
+  async deleteCompletelyUserHomeBySystem(userHomepagePath: string): Promise<void> {
+    const Page = this.crowi.model('Page');
+    const userHomepage = await Page.findByPath(userHomepagePath, true);
+
+    if (userHomepage == null) {
+      logger.error('user homepage is not found.');
+      return;
+    }
+
+    const shouldUseV4Process = this.shouldUseV4Process(userHomepage);
+
+    const ids = [userHomepage._id];
+    const paths = [userHomepage.path];
+
+    try {
+      if (!shouldUseV4Process) {
+        // Ensure consistency of ancestors
+        const inc = userHomepage.isEmpty ? -userHomepage.descendantCount : -(userHomepage.descendantCount + 1);
+        await this.updateDescendantCountOfAncestors(userHomepage.parent, inc, true);
+      }
+
+      // Delete the user's homepage
+      await this.deleteCompletelyOperation(ids, paths);
+
+      if (!shouldUseV4Process) {
+        // Remove leaf empty pages
+        await Page.removeLeafEmptyPagesRecursively(userHomepage.parent);
+      }
+
+      if (!userHomepage.isEmpty) {
+        // Emit an event for the search service
+        this.pageEvent.emit('deleteCompletely', userHomepage);
+      }
+
+      const { PageQueryBuilder } = Page;
+
+      // Find descendant pages with system deletion condition
+      const builder = new PageQueryBuilder(Page.find(), true)
+        .addConditionForSystemDeletion()
+        .addConditionToListOnlyDescendants(userHomepage.path);
+
+      // Stream processing to delete descendant pages
+      // ────────┤ start │─────────
+      const readStream = await builder
+        .query
+        .lean()
+        .cursor({ batchSize: BULK_REINDEX_SIZE });
+
+      let count = 0;
+
+      const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
+      const writeStream = new Writable({
+        objectMode: true,
+        async write(batch, encoding, callback) {
+          try {
+            count += batch.length;
+            // Delete multiple pages completely
+            await deleteMultipleCompletely(batch, null, {});
+            logger.debug(`Adding pages progressing: (count=${count})`);
+          }
+          catch (err) {
+            logger.error('addAllPages error on add anyway: ', err);
+          }
+          callback();
+        },
+        final(callback) {
+          logger.debug(`Adding pages has completed: (totalCount=${count})`);
+          callback();
+        },
+      });
+
+      readStream
+        .pipe(createBatchStream(BULK_REINDEX_SIZE))
+        .pipe(writeStream);
+
+      await streamToPromise(writeStream);
+      // ────────┤ end │─────────
+    }
+    catch (err) {
+      logger.error('Error occurred while deleting user homepage and subpages.', err);
+      throw err;
+    }
+  }
+
   // use the same process in both v4 and v5
   // use the same process in both v4 and v5
   private async revertDeletedDescendants(pages, user) {
   private async revertDeletedDescendants(pages, user) {
     const Page = this.crowi.model('Page');
     const Page = this.crowi.model('Page');

+ 5 - 3
apps/app/src/stores/search.tsx

@@ -1,5 +1,4 @@
-import { mutate, SWRResponse } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+import useSWR, { mutate, SWRResponse } from 'swr';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
@@ -67,7 +66,7 @@ export const useSWRxSearch = (
 
 
   const isKeywordValid = keyword != null && keyword.length > 0;
   const isKeywordValid = keyword != null && keyword.length > 0;
 
 
-  const swrResult = useSWRImmutable(
+  const swrResult = useSWR(
     isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     ([endpoint, , fixedConfigurations]) => {
     ([endpoint, , fixedConfigurations]) => {
       const {
       const {
@@ -86,6 +85,9 @@ export const useSWRxSearch = (
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       ).then(result => result as IFormattedSearchResult);
       ).then(result => result as IFormattedSearchResult);
     },
     },
+    {
+      revalidateOnFocus: false,
+    },
   );
   );
 
 
   return {
   return {

+ 0 - 3
apps/app/src/styles/molecules/toastr.scss

@@ -1,4 +1 @@
-:root {
-  @import '~toastr/build/toastr';
-}
 @import '~react-toastify/scss/main';
 @import '~react-toastify/scss/main';

+ 4 - 19
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts

@@ -11,21 +11,6 @@ function openEditor() {
   cy.get('.CodeMirror').should('be.visible');
   cy.get('.CodeMirror').should('be.visible');
 }
 }
 
 
-function appendTextToEditorUntilContains(inputText: string) {
-  const lines: string[] = [];
-  cy.waitUntil(() => {
-    // do
-    cy.get('.CodeMirror textarea').type(inputText, { force: true });
-    // until
-    return cy.get('.CodeMirror-line')
-      .each(($item) => {
-        lines.push($item.text());
-      }).then(() => {
-        return lines.join('\n').endsWith(inputText);
-      });
-  });
-}
-
 context('Access to page', () => {
 context('Access to page', () => {
   const ssPrefix = 'access-to-page-';
   const ssPrefix = 'access-to-page-';
 
 
@@ -107,7 +92,7 @@ context('Access to page', () => {
     openEditor();
     openEditor();
 
 
     // check edited contents after save
     // check edited contents after save
-    appendTextToEditorUntilContains(body1);
+    cy.appendTextToEditorUntilContains(body1);
     cy.get('.page-editor-preview-body').should('contain.text', body1);
     cy.get('.page-editor-preview-body').should('contain.text', body1);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();
     cy.getByTestid('save-page-btn').click();
@@ -124,7 +109,7 @@ context('Access to page', () => {
     openEditor();
     openEditor();
 
 
     // check editing contents with shortcut key
     // check editing contents with shortcut key
-    appendTextToEditorUntilContains(body2);
+    cy.appendTextToEditorUntilContains(body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
     cy.get('.CodeMirror').click().type(savePageShortcutKey);
     cy.get('.CodeMirror').click().type(savePageShortcutKey);
     cy.get('.CodeMirror-code').should('contain.text', body1+body2);
     cy.get('.CodeMirror-code').should('contain.text', body1+body2);
@@ -295,7 +280,7 @@ context('Access to Template Editing Mode', () => {
       cy.screenshot(`${ssPrefix}-open-template-page-for-children-in-editor-mode`);
       cy.screenshot(`${ssPrefix}-open-template-page-for-children-in-editor-mode`);
     });
     });
 
 
-    appendTextToEditorUntilContains(templateBody1);
+    cy.appendTextToEditorUntilContains(templateBody1);
     cy.get('.page-editor-preview-body').should('contain.text', templateBody1);
     cy.get('.page-editor-preview-body').should('contain.text', templateBody1);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();
     cy.getByTestid('save-page-btn').click();
@@ -329,7 +314,7 @@ context('Access to Template Editing Mode', () => {
       cy.screenshot(`${ssPrefix}-open-template-page-for-descendants-in-editor-mode`);
       cy.screenshot(`${ssPrefix}-open-template-page-for-descendants-in-editor-mode`);
     })
     })
 
 
-    appendTextToEditorUntilContains(templateBody2);
+    cy.appendTextToEditorUntilContains(templateBody2);
     cy.get('.page-editor-preview-body').should('contain.text', templateBody2);
     cy.get('.page-editor-preview-body').should('contain.text', templateBody2);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();
     cy.getByTestid('save-page-btn').click();

+ 2 - 12
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--comments.cy.ts

@@ -103,12 +103,7 @@ context('Comment', () => {
       return cy.get('.comment-write').then($elem => $elem.is(':visible'));
       return cy.get('.comment-write').then($elem => $elem.is(':visible'));
     });
     });
 
 
-    cy.waitUntil(() => {
-      // do
-      cy.get('.CodeMirror').type(username);
-      // wait until
-      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
-    });
+    cy.appendTextToEditorUntilContains(username);
 
 
     cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-mention-username-found`) });
     cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-mention-username-found`) });
     // Click on mentioned username
     // Click on mentioned username
@@ -129,12 +124,7 @@ context('Comment', () => {
       return cy.get('.comment-write').then($elem => $elem.is(':visible'));
       return cy.get('.comment-write').then($elem => $elem.is(':visible'));
     });
     });
 
 
-    cy.waitUntil(() => {
-      // do
-      cy.get('.CodeMirror').type(username);
-      // wait until
-      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
-    });
+    cy.appendTextToEditorUntilContains(username);
 
 
     cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}6-mention-username-not-found`) });
     cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}6-mention-username-not-found`) });
     // Click on username not found hint
     // Click on username not found hint

+ 15 - 0
apps/app/test/cypress/support/commands.ts

@@ -101,3 +101,18 @@ Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving =
   });
   });
 
 
 });
 });
+
+Cypress.Commands.add('appendTextToEditorUntilContains', (inputText: string) => {
+  const lines: string[] = [];
+  cy.waitUntil(() => {
+    // do
+    cy.get('.CodeMirror textarea').type(inputText, { force: true });
+    // until
+    return cy.get('.CodeMirror-line')
+      .each(($item) => {
+        lines.push($item.text());
+      }).then(() => {
+        return lines.join('\n').endsWith(inputText);
+      });
+  });
+});

+ 1 - 0
apps/app/test/cypress/support/index.ts

@@ -40,6 +40,7 @@ declare global {
        collapseSidebar(isCollapsed: boolean, waitUntilSaving?: boolean): Chainable<void>,
        collapseSidebar(isCollapsed: boolean, waitUntilSaving?: boolean): Chainable<void>,
        waitUntilSkeletonDisappear(): Chainable<void>,
        waitUntilSkeletonDisappear(): Chainable<void>,
        waitUntilSpinnerDisappear(): Chainable<void>,
        waitUntilSpinnerDisappear(): Chainable<void>,
+       appendTextToEditorUntilContains(inputText: string): Chainable<void>
     }
     }
   }
   }
 }
 }

+ 5 - 5
packages/core/src/utils/page-path-utils/index.ts

@@ -25,10 +25,10 @@ export const isPermalink = (path: string): boolean => {
 };
 };
 
 
 /**
 /**
- * Whether path is user's home page
+ * Whether path is user's homepage
  * @param path
  * @param path
  */
  */
-export const isUsersHomePage = (path: string): boolean => {
+export const isUsersHomepage = (path: string): boolean => {
   // https://regex101.com/r/utVQct/1
   // https://regex101.com/r/utVQct/1
   if (path.match(/^\/user\/[^/]+$/)) {
   if (path.match(/^\/user\/[^/]+$/)) {
     return true;
     return true;
@@ -41,7 +41,7 @@ export const isUsersHomePage = (path: string): boolean => {
  * @param path
  * @param path
  */
  */
 export const isUsersProtectedPages = (path: string): boolean => {
 export const isUsersProtectedPages = (path: string): boolean => {
-  return isUsersTopPage(path) || isUsersHomePage(path);
+  return isUsersTopPage(path) || isUsersHomepage(path);
 };
 };
 
 
 /**
 /**
@@ -121,11 +121,11 @@ export const isCreatablePage = (path: string): boolean => {
 };
 };
 
 
 /**
 /**
- * return user path
+ * return user's homepage path
  * @param user
  * @param user
  */
  */
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const userPageRoot = (user: any): string => {
+export const userHomepagePath = (user: any): string => {
   if (!user || !user.username) {
   if (!user || !user.username) {
     return '';
     return '';
   }
   }

+ 1 - 3
packages/ui/src/components/UserPicture.tsx

@@ -11,8 +11,6 @@ import type { UncontrolledTooltipProps } from 'reactstrap';
 
 
 const UncontrolledTooltip = dynamic<UncontrolledTooltipProps>(() => import('reactstrap').then(mod => mod.UncontrolledTooltip), { ssr: false });
 const UncontrolledTooltip = dynamic<UncontrolledTooltipProps>(() => import('reactstrap').then(mod => mod.UncontrolledTooltip), { ssr: false });
 
 
-const { userPageRoot } = pagePathUtils;
-
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 
 
 
 
@@ -30,7 +28,7 @@ const UserPictureRootWithLink = forwardRef<HTMLSpanElement, UserPictureRootProps
   const router = useRouter();
   const router = useRouter();
 
 
   const { user } = props;
   const { user } = props;
-  const href = userPageRoot(user);
+  const href = pagePathUtils.userHomepagePath(user);
 
 
   const clickHandler = useCallback(() => {
   const clickHandler = useCallback(() => {
     router.push(href);
     router.push(href);

+ 89 - 350
yarn.lock

@@ -2429,6 +2429,11 @@
   resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
   resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
   integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
   integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
 
 
+"@exodus/schemasafe@^1.0.0-rc.2":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.1.1.tgz#006ab8b33b1aec6d2992c75e5918c65197388aa2"
+  integrity sha512-Pd7+aGvWIaTDL5ecV4ZBEtBrjXnk8/ly5xyHbikxVhgcq7qhihzHWHbcYmFupQBT2A5ggNZGvT7Bpj0M6AKHjA==
+
 "@godaddy/terminus@^4.9.0":
 "@godaddy/terminus@^4.9.0":
   version "4.9.0"
   version "4.9.0"
   resolved "https://registry.yarnpkg.com/@godaddy/terminus/-/terminus-4.9.0.tgz#c7de0b45ede05116854d1461832dd05df169f689"
   resolved "https://registry.yarnpkg.com/@godaddy/terminus/-/terminus-4.9.0.tgz#c7de0b45ede05116854d1461832dd05df169f689"
@@ -2534,8 +2539,6 @@
 
 
 "@growi/remark-drawio@link:packages/remark-drawio":
 "@growi/remark-drawio@link:packages/remark-drawio":
   version "6.2.0-RC.0"
   version "6.2.0-RC.0"
-  dependencies:
-    pako "^2.1.0"
 
 
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
   version "6.2.0-RC.0"
   version "6.2.0-RC.0"
@@ -4624,15 +4627,6 @@ ajv-keywords@^3.5.2:
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
   integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
   integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
 
 
-ajv@^5.5.2:
-  version "5.5.2"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
-  dependencies:
-    co "^4.6.0"
-    fast-deep-equal "^1.0.0"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.3.0"
-
 ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5, ajv@~6.12.6:
 ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5, ajv@~6.12.6:
   version "6.12.6"
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -4670,10 +4664,6 @@ ansi-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
   integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
   integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
 
 
-ansi-regex@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
-
 ansi-regex@^5.0.1:
 ansi-regex@^5.0.1:
   version "5.0.1"
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@@ -5217,32 +5207,6 @@ bcryptjs@^2.4.0:
   version "2.4.3"
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
   resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
 
 
-better-ajv-errors@^0.5.2:
-  version "0.5.7"
-  resolved "https://registry.yarnpkg.com/better-ajv-errors/-/better-ajv-errors-0.5.7.tgz#246123954161cc0ef124761c55a121c96b0cdce0"
-  integrity sha512-O7tpXektKWVwYCH5g6Vs3lKD+sJs7JHh5guapmGJd+RTwxhFZEf4FwvbHBURUnoXsTeFaMvGuhTTmEGiHpNi6w==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    "@babel/runtime" "^7.0.0"
-    chalk "^2.4.1"
-    core-js "^2.5.7"
-    json-to-ast "^2.0.3"
-    jsonpointer "^4.0.1"
-    leven "^2.1.0"
-
-better-ajv-errors@^0.6.1:
-  version "0.6.6"
-  resolved "https://registry.yarnpkg.com/better-ajv-errors/-/better-ajv-errors-0.6.6.tgz#967f3075ca43455021c4802c92761dfbf843188e"
-  integrity sha512-CD5Xb75GtFpwcPnGH60MFlqwhMt0uUhKEjCTaWNXa3btSEQ0dbokn4WbyuMy/ykpfa0miJ2fEU5iQWIVSXIXgw==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    "@babel/runtime" "^7.0.0"
-    chalk "^2.4.1"
-    core-js "^3.2.1"
-    json-to-ast "^2.0.3"
-    jsonpointer "^4.0.1"
-    leven "^3.1.0"
-
 big-integer@^1.6.17:
 big-integer@^1.6.17:
   version "1.6.44"
   version "1.6.44"
   resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.44.tgz#4ee9ae5f5839fc11ade338fea216b4513454a539"
   resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.44.tgz#4ee9ae5f5839fc11ade338fea216b4513454a539"
@@ -5617,7 +5581,7 @@ camelcase@^2.0.0:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
   integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
   integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
 
 
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.3.1:
   version "5.3.1"
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -5919,14 +5883,6 @@ client-only@0.0.1:
   resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
   resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
   integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
   integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
 
 
-cliui@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
-  dependencies:
-    string-width "^2.1.1"
-    strip-ansi "^4.0.0"
-    wrap-ansi "^2.0.0"
-
 cliui@^7.0.2:
 cliui@^7.0.2:
   version "7.0.4"
   version "7.0.4"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
@@ -5971,16 +5927,6 @@ code-block-writer@^12.0.0:
   resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
   resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
   integrity sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==
   integrity sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==
 
 
-code-error-fragment@0.0.230:
-  version "0.0.230"
-  resolved "https://registry.yarnpkg.com/code-error-fragment/-/code-error-fragment-0.0.230.tgz#d736d75c832445342eca1d1fedbf17d9618b14d7"
-  integrity sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==
-
-code-point-at@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-  integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
-
 codemirror@^5.64.0:
 codemirror@^5.64.0:
   version "5.64.0"
   version "5.64.0"
   resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.64.0.tgz#182eec65b62178e3cd1de8f9d88ab819cfe5f625"
   resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.64.0.tgz#182eec65b62178e3cd1de8f9d88ab819cfe5f625"
@@ -6323,16 +6269,6 @@ core-js-pure@^3.20.2:
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.1.tgz#0b27e4c3ad46178b84e790dbbb81987218ab82ad"
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.1.tgz#0b27e4c3ad46178b84e790dbbb81987218ab82ad"
   integrity sha512-3qNgf6TqI3U1uhuSYRzJZGfFd4T+YlbyVPl+jgRiKjdZopvG4keZQwWZDAWpu1UH9nCgTpUzIV3GFawC7cJsqg==
   integrity sha512-3qNgf6TqI3U1uhuSYRzJZGfFd4T+YlbyVPl+jgRiKjdZopvG4keZQwWZDAWpu1UH9nCgTpUzIV3GFawC7cJsqg==
 
 
-core-js@=2.6.9:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
-  integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
-
-core-js@^2.5.7:
-  version "2.6.12"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
-  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
-
 core-js@^3, core-js@^3.0.1, core-js@^3.2.1:
 core-js@^3, core-js@^3.0.1, core-js@^3.2.1:
   version "3.23.3"
   version "3.23.3"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
@@ -6435,7 +6371,7 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, c
     shebang-command "^2.0.0"
     shebang-command "^2.0.0"
     which "^2.0.1"
     which "^2.0.1"
 
 
-cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+cross-spawn@^6.0.5:
   version "6.0.5"
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -7933,18 +7869,6 @@ execa@4.1.0:
     signal-exit "^3.0.2"
     signal-exit "^3.0.2"
     strip-final-newline "^2.0.0"
     strip-final-newline "^2.0.0"
 
 
-execa@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
-  dependencies:
-    cross-spawn "^6.0.0"
-    get-stream "^4.0.0"
-    is-stream "^1.1.0"
-    npm-run-path "^2.0.0"
-    p-finally "^1.0.0"
-    signal-exit "^3.0.0"
-    strip-eof "^1.0.0"
-
 execa@^5.0.0:
 execa@^5.0.0:
   version "5.0.0"
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
   resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
@@ -8115,10 +8039,6 @@ extsprintf@^1.2.0:
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
   integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
   integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
 
 
-fast-deep-equal@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
-
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -8149,6 +8069,11 @@ fast-levenshtein@^2.0.6:
   version "2.0.6"
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
 
 
+fast-safe-stringify@^2.0.7:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
+  integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
+
 fast-text-encoding@^1.0.0:
 fast-text-encoding@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef"
   resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef"
@@ -8295,12 +8220,6 @@ find-up@^2.1.0:
   dependencies:
   dependencies:
     locate-path "^2.0.0"
     locate-path "^2.0.0"
 
 
-find-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
-  dependencies:
-    locate-path "^3.0.0"
-
 find-up@^4.0.0, find-up@^4.1.0:
 find-up@^4.0.0, find-up@^4.1.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@@ -8591,7 +8510,7 @@ gensync@^1.0.0-beta.2:
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
   integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
   integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 
 
-get-caller-file@^1.0.1, get-caller-file@^1.0.2:
+get-caller-file@^1.0.2:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
   integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
   integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
@@ -8635,12 +8554,6 @@ get-stdin@^8.0.0:
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
   integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
   integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
 
 
-get-stream@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
-  dependencies:
-    pump "^3.0.0"
-
 get-stream@^5.0.0, get-stream@^5.1.0:
 get-stream@^5.0.0, get-stream@^5.1.0:
   version "5.2.0"
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
@@ -9615,10 +9528,6 @@ invariant@^2.2.1, invariant@^2.2.4:
   dependencies:
   dependencies:
     loose-envify "^1.0.0"
     loose-envify "^1.0.0"
 
 
-invert-kv@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
-
 ip@^2.0.0:
 ip@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
   resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
@@ -9768,17 +9677,6 @@ is-finite@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
   resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
   integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
   integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
 
 
-is-fullwidth-code-point@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
-  integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
-  dependencies:
-    number-is-nan "^1.0.0"
-
-is-fullwidth-code-point@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
-
 is-fullwidth-code-point@^3.0.0:
 is-fullwidth-code-point@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -10481,20 +10379,10 @@ jpeg-js@^0.4.0, jpeg-js@^0.4.2:
   resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
   resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
   integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
   integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
 
 
-jquery-slimscroll@^1.3.8:
-  version "1.3.8"
-  resolved "https://registry.yarnpkg.com/jquery-slimscroll/-/jquery-slimscroll-1.3.8.tgz#8481c44e7a47687653908a28f7f70aed64c84e36"
-  dependencies:
-    jquery ">= 1.7"
-
-jquery.cookie@~1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/jquery.cookie/-/jquery.cookie-1.4.1.tgz#d63dce209eab691fe63316db08ca9e47e0f9385b"
-
-"jquery@>= 1.7", jquery@>=1.12.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
-  integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
+jquery@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.0.tgz#fe2c01a05da500709006d8790fe21c8a39d75612"
+  integrity sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==
 
 
 js-string-escape@^1.0.1:
 js-string-escape@^1.0.1:
   version "1.0.1"
   version "1.0.1"
@@ -10545,10 +10433,6 @@ json-parse-even-better-errors@^2.3.0:
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
 
-json-schema-traverse@^0.3.0:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
-
 json-schema-traverse@^0.4.1:
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -10583,14 +10467,6 @@ json-stringify-safe@~5.0.1:
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
 
-json-to-ast@^2.0.3:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/json-to-ast/-/json-to-ast-2.1.0.tgz#041a9fcd03c0845036acb670d29f425cea4faaf9"
-  integrity sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==
-  dependencies:
-    code-error-fragment "0.0.230"
-    grapheme-splitter "^1.0.4"
-
 json5@^1.0.1:
 json5@^1.0.1:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
   resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
@@ -10628,11 +10504,6 @@ jsonparse@^1.2.0:
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
   integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
 
 
-jsonpointer@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
-  integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk=
-
 jsonwebtoken@^8.5.1:
 jsonwebtoken@^8.5.1:
   version "8.5.1"
   version "8.5.1"
   resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
   resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@@ -10808,12 +10679,6 @@ lazystream@^1.0.0:
   dependencies:
   dependencies:
     readable-stream "^2.0.5"
     readable-stream "^2.0.5"
 
 
-lcid@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
-  dependencies:
-    invert-kv "^2.0.0"
-
 ldap-filter@^0.3.3:
 ldap-filter@^0.3.3:
   version "0.3.3"
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/ldap-filter/-/ldap-filter-0.3.3.tgz#2b14c68a2a9d4104dbdbc910a1ca85fd189e9797"
   resolved "https://registry.yarnpkg.com/ldap-filter/-/ldap-filter-0.3.3.tgz#2b14c68a2a9d4104dbdbc910a1ca85fd189e9797"
@@ -10861,11 +10726,6 @@ less@^3.12.2:
     native-request "^1.0.5"
     native-request "^1.0.5"
     source-map "~0.6.0"
     source-map "~0.6.0"
 
 
-leven@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
-  integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
-
 leven@^3.1.0:
 leven@^3.1.0:
   version "3.1.0"
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -10955,13 +10815,6 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     p-locate "^2.0.0"
     path-exists "^3.0.0"
     path-exists "^3.0.0"
 
 
-locate-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
-  dependencies:
-    p-locate "^3.0.0"
-    path-exists "^3.0.0"
-
 locate-path@^5.0.0:
 locate-path@^5.0.0:
   version "5.0.0"
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@@ -11245,13 +11098,6 @@ makeerror@1.0.12:
   dependencies:
   dependencies:
     tmpl "1.0.5"
     tmpl "1.0.5"
 
 
-map-age-cleaner@^0.1.1:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
-  integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
-  dependencies:
-    p-defer "^1.0.0"
-
 map-obj@^1.0.0, map-obj@^1.0.1:
 map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
@@ -11540,14 +11386,6 @@ media-typer@0.3.0:
   version "0.3.0"
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
 
 
-mem@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"
-  dependencies:
-    map-age-cleaner "^0.1.1"
-    mimic-fn "^1.0.0"
-    p-is-promise "^2.0.0"
-
 memory-pager@^1.0.2:
 memory-pager@^1.0.2:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.1.0.tgz#9308915e0e972849fefbae6f8bc95d6b350e7344"
   resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.1.0.tgz#9308915e0e972849fefbae6f8bc95d6b350e7344"
@@ -12014,10 +11852,6 @@ mime@>=2.4.6, mime@^3.0.0:
   resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
   resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
   integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
   integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
 
 
-mimic-fn@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
-
 mimic-fn@^2.1.0:
 mimic-fn@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -12638,12 +12472,6 @@ npm-run-all@^4.1.5:
     shell-quote "^1.6.1"
     shell-quote "^1.6.1"
     string.prototype.padend "^3.0.0"
     string.prototype.padend "^3.0.0"
 
 
-npm-run-path@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
-  dependencies:
-    path-key "^2.0.0"
-
 npm-run-path@^4.0.0, npm-run-path@^4.0.1:
 npm-run-path@^4.0.0, npm-run-path@^4.0.1:
   version "4.0.1"
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -12680,11 +12508,6 @@ num2fraction@^1.2.2:
   version "1.2.2"
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
   resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
 
 
-number-is-nan@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
-  integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
-
 numbro@^2.0.6:
 numbro@^2.0.6:
   version "2.1.2"
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.2.tgz#2d51104f09b5d69aef7e15bb565d7795e47ecfd6"
   resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.2.tgz#2d51104f09b5d69aef7e15bb565d7795e47ecfd6"
@@ -12692,52 +12515,51 @@ numbro@^2.0.6:
   dependencies:
   dependencies:
     bignumber.js "^8.0.1"
     bignumber.js "^8.0.1"
 
 
-oas-kit-common@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.7.tgz#de67dc19a572d82bd5f9ba2e1f606ad02d1fb30e"
-  integrity sha512-8+P8gBjN9bGfa5HPgyefO78o394PUwHoQjuD4hM0Bpl56BkcxoyW4MpWMPM6ATm+yIIz4qT1igmuVukUtjP/pQ==
+oas-kit-common@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535"
+  integrity sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==
   dependencies:
   dependencies:
-    safe-json-stringify "^1.2.0"
+    fast-safe-stringify "^2.0.7"
 
 
-oas-linter@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/oas-linter/-/oas-linter-3.0.1.tgz#41e577549a01c93a0c9fe8422f499d1ff0a9acfd"
-  integrity sha512-vk8Pzqq8iZM8V0/8NJMHAbf4CMyAUnLTJPNKwCkFl6g2W7omomL3yPpseNqihwU7KgqwYDTjxJ31qavmYbeDbg==
+oas-linter@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/oas-linter/-/oas-linter-3.2.2.tgz#ab6a33736313490659035ca6802dc4b35d48aa1e"
+  integrity sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==
   dependencies:
   dependencies:
+    "@exodus/schemasafe" "^1.0.0-rc.2"
     should "^13.2.1"
     should "^13.2.1"
-    yaml "^1.3.1"
+    yaml "^1.10.0"
 
 
-oas-resolver@^2.2.5:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/oas-resolver/-/oas-resolver-2.2.5.tgz#9006e66ee3c0542f3b507ae61afc3e5328d7116c"
-  integrity sha512-AwARII3hmdXtDAGccvjVsRLked0PNJycIG/koD6lYoGspJjxnQ3a8AmDgp7kHYnG148zusfsl8GM0cfwGmd7EA==
+oas-resolver@^2.5.6:
+  version "2.5.6"
+  resolved "https://registry.yarnpkg.com/oas-resolver/-/oas-resolver-2.5.6.tgz#10430569cb7daca56115c915e611ebc5515c561b"
+  integrity sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==
   dependencies:
   dependencies:
     node-fetch-h2 "^2.3.0"
     node-fetch-h2 "^2.3.0"
-    oas-kit-common "^1.0.7"
-    reftools "^1.0.8"
-    yaml "^1.3.1"
-    yargs "^12.0.5"
+    oas-kit-common "^1.0.8"
+    reftools "^1.1.9"
+    yaml "^1.10.0"
+    yargs "^17.0.1"
 
 
-oas-schema-walker@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/oas-schema-walker/-/oas-schema-walker-1.1.2.tgz#0ad6b78a01421cb9fda9dd820f23f5db51d51b86"
-  integrity sha512-Q9xqeUtc17ccP/dpUfARci4kwFFszyJAgR/wbDhrRR/73GqsY5uSmKaIK+RmBqO8J4jVYrrDPjQKvt1IcpQdGw==
+oas-schema-walker@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz#74c3cd47b70ff8e0b19adada14455b5d3ac38a22"
+  integrity sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==
 
 
-oas-validator@^3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/oas-validator/-/oas-validator-3.3.1.tgz#4f93207967837fb86efeab86a119fd0342dbd432"
-  integrity sha512-WFKafxpH2KrxHG6drJiJ7M0mzGZER3XDkLtbeX8z9YNR4JvCMDlhQL7J2i+rnCxyVC8riRZGGeZpxQ0000w2HA==
+oas-validator@^5.0.8:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/oas-validator/-/oas-validator-5.0.8.tgz#387e90df7cafa2d3ffc83b5fb976052b87e73c28"
+  integrity sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==
   dependencies:
   dependencies:
-    ajv "^5.5.2"
-    better-ajv-errors "^0.5.2"
     call-me-maybe "^1.0.1"
     call-me-maybe "^1.0.1"
-    oas-kit-common "^1.0.7"
-    oas-linter "^3.0.1"
-    oas-resolver "^2.2.5"
-    oas-schema-walker "^1.1.2"
-    reftools "^1.0.8"
+    oas-kit-common "^1.0.8"
+    oas-linter "^3.2.2"
+    oas-resolver "^2.5.6"
+    oas-schema-walker "^1.1.5"
+    reftools "^1.1.9"
     should "^13.2.1"
     should "^13.2.1"
-    yaml "^1.3.1"
+    yaml "^1.10.0"
 
 
 oauth-sign@~0.9.0:
 oauth-sign@~0.9.0:
   version "0.9.0"
   version "0.9.0"
@@ -12923,14 +12745,6 @@ ora@^5.4.1:
     strip-ansi "^6.0.0"
     strip-ansi "^6.0.0"
     wcwidth "^1.0.1"
     wcwidth "^1.0.1"
 
 
-os-locale@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
-  dependencies:
-    execa "^1.0.0"
-    lcid "^2.0.0"
-    mem "^4.0.0"
-
 os-tmpdir@~1.0.2:
 os-tmpdir@~1.0.2:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -12940,10 +12754,6 @@ ospath@^1.2.2:
   resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
   resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
   integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
   integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
 
 
-p-defer@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
-
 p-each-series@^2.2.0:
 p-each-series@^2.2.0:
   version "2.2.0"
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a"
   resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a"
@@ -12960,17 +12770,13 @@ p-finally@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
 
 
-p-is-promise@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5"
-
 p-limit@^1.1.0:
 p-limit@^1.1.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
   dependencies:
   dependencies:
     p-try "^1.0.0"
     p-try "^1.0.0"
 
 
-p-limit@^2.0.0, p-limit@^2.2.0:
+p-limit@^2.2.0:
   version "2.2.1"
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537"
   integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==
   integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==
@@ -12997,12 +12803,6 @@ p-locate@^2.0.0:
   dependencies:
   dependencies:
     p-limit "^1.1.0"
     p-limit "^1.1.0"
 
 
-p-locate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
-  dependencies:
-    p-limit "^2.0.0"
-
 p-locate@^4.1.0:
 p-locate@^4.1.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
@@ -13278,7 +13078,7 @@ path-is-absolute@^1.0.0:
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
 
-path-key@^2.0.0, path-key@^2.0.1:
+path-key@^2.0.1:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
 
 
@@ -13455,6 +13255,11 @@ popper.js@^1.14.4:
   version "1.14.7"
   version "1.14.7"
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
 
 
+popper.js@^1.16.1:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
+  integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
+
 postcss-media-query-parser@^0.2.3:
 postcss-media-query-parser@^0.2.3:
   version "0.2.3"
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
   resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
@@ -14196,10 +14001,10 @@ refractor@^3.6.0:
     parse-entities "^2.0.0"
     parse-entities "^2.0.0"
     prismjs "~1.27.0"
     prismjs "~1.27.0"
 
 
-reftools@^1.0.8:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.0.8.tgz#ec26941f780044420c1d1bb48836112f199e520b"
-  integrity sha512-hERpM8J+L0q8dzKFh/QqcLlKZYmTgzGZM7m8b1ptS66eg4NA/iMPm7GNw3TKZ876ndVjGpiLt0BCIfAWsUgwGg==
+reftools@^1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.1.9.tgz#e16e19f662ccd4648605312c06d34e5da3a2b77e"
+  integrity sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==
 
 
 reg-cli@^0.17.0:
 reg-cli@^0.17.0:
   version "0.17.4"
   version "0.17.4"
@@ -14595,11 +14400,6 @@ require-from-string@^2.0.2:
   version "2.0.2"
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
 
 
-require-main-filename@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
-  integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
-
 resolve-cwd@^3.0.0:
 resolve-cwd@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@@ -14797,7 +14597,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
 
-safe-json-stringify@^1.2.0, safe-json-stringify@~1:
+safe-json-stringify@~1:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd"
   resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd"
   integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==
   integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==
@@ -15492,22 +15292,6 @@ string-width@=4.2.2:
     is-fullwidth-code-point "^3.0.0"
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
     strip-ansi "^6.0.0"
 
 
-string-width@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
-  integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
-  dependencies:
-    code-point-at "^1.0.0"
-    is-fullwidth-code-point "^1.0.0"
-    strip-ansi "^3.0.0"
-
-string-width@^2.0.0, string-width@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
-  dependencies:
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^4.0.0"
-
 string-width@^5.0.1, string-width@^5.1.2:
 string-width@^5.0.1, string-width@^5.1.2:
   version "5.1.2"
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -15597,19 +15381,13 @@ stringify-entities@^4.0.0:
   dependencies:
   dependencies:
     ansi-regex "^5.0.1"
     ansi-regex "^5.0.1"
 
 
-strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@^3.0.0:
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
   integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
   integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
   dependencies:
   dependencies:
     ansi-regex "^2.0.0"
     ansi-regex "^2.0.0"
 
 
-strip-ansi@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
-  dependencies:
-    ansi-regex "^3.0.0"
-
 strip-ansi@^7.0.1:
 strip-ansi@^7.0.1:
   version "7.1.0"
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -15633,10 +15411,6 @@ strip-bom@^4.0.0:
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
   integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
   integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
 
 
-strip-eof@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
-
 strip-final-newline@^2.0.0:
 strip-final-newline@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@@ -15845,22 +15619,22 @@ swagger-ui-dist@^3.46.0:
   resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.46.0.tgz#f08d2c9b4a2dce922ba363c598e4795b5ccf0b80"
   resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.46.0.tgz#f08d2c9b4a2dce922ba363c598e4795b5ccf0b80"
   integrity sha512-ueaZ45OHhHvGKmocvCkxFY8VCfbP5PgcxutoQxy9j8/VZeDoLDvg8FBf4SO6NxHhieNAdYPUd0O6G9FjJO2fqw==
   integrity sha512-ueaZ45OHhHvGKmocvCkxFY8VCfbP5PgcxutoQxy9j8/VZeDoLDvg8FBf4SO6NxHhieNAdYPUd0O6G9FjJO2fqw==
 
 
-swagger2openapi@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-5.3.1.tgz#a60f6ade642c867b13300e1a3c4f265fa5c74685"
-  integrity sha512-2EIs1gJs9LH4NjrxHPJs6N0Kh9pg66He+H9gIcfn1Q9dvdqPPVTC2NRdXalqT+98rIoV9kSfAtNBD4ASC0Q1mg==
+swagger2openapi@^7.0.8:
+  version "7.0.8"
+  resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59"
+  integrity sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==
   dependencies:
   dependencies:
-    better-ajv-errors "^0.6.1"
     call-me-maybe "^1.0.1"
     call-me-maybe "^1.0.1"
+    node-fetch "^2.6.1"
     node-fetch-h2 "^2.3.0"
     node-fetch-h2 "^2.3.0"
     node-readfiles "^0.2.0"
     node-readfiles "^0.2.0"
-    oas-kit-common "^1.0.7"
-    oas-resolver "^2.2.5"
-    oas-schema-walker "^1.1.2"
-    oas-validator "^3.3.1"
-    reftools "^1.0.8"
-    yaml "^1.3.1"
-    yargs "^12.0.5"
+    oas-kit-common "^1.0.8"
+    oas-resolver "^2.5.6"
+    oas-schema-walker "^1.1.5"
+    oas-validator "^5.0.8"
+    reftools "^1.1.9"
+    yaml "^1.10.0"
+    yargs "^17.0.1"
 
 
 swr@^2.0.3:
 swr@^2.0.3:
   version "2.0.3"
   version "2.0.3"
@@ -16083,12 +15857,6 @@ to-vfile@^7.0.0:
     is-buffer "^2.0.0"
     is-buffer "^2.0.0"
     vfile "^5.1.0"
     vfile "^5.1.0"
 
 
-toastr@^2.1.2:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/toastr/-/toastr-2.1.4.tgz#8b43be64fb9d0c414871446f2db8e8ca4e95f181"
-  dependencies:
-    jquery ">=1.12.0"
-
 toggle-selection@^1.0.6:
 toggle-selection@^1.0.6:
   version "1.0.6"
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
   resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
@@ -17050,10 +16818,6 @@ which-collection@^1.0.1:
     is-weakmap "^2.0.1"
     is-weakmap "^2.0.1"
     is-weakset "^2.0.1"
     is-weakset "^2.0.1"
 
 
-which-module@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-
 which-typed-array@^1.1.2:
 which-typed-array@^1.1.2:
   version "1.1.8"
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f"
   resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f"
@@ -17114,14 +16878,6 @@ word-wrap@^1.2.3:
     string-width "^4.1.0"
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
     strip-ansi "^6.0.0"
 
 
-wrap-ansi@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
-  integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
-  dependencies:
-    string-width "^1.0.1"
-    strip-ansi "^3.0.1"
-
 wrap-ansi@^6.2.0:
 wrap-ansi@^6.2.0:
   version "6.2.0"
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -17270,11 +17026,6 @@ xtend@~2.1.1:
   dependencies:
   dependencies:
     object-keys "~0.4.0"
     object-keys "~0.4.0"
 
 
-"y18n@^3.2.1 || ^4.0.0":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
-  integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
-
 y18n@^5.0.5:
 y18n@^5.0.5:
   version "5.0.8"
   version "5.0.8"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@@ -17300,7 +17051,7 @@ yaml@2.0.0-1:
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18"
   integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==
   integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==
 
 
-yaml@^1.10.0, yaml@^1.3.1:
+yaml@^1.10.0:
   version "1.10.2"
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
   integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
   integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
@@ -17314,13 +17065,6 @@ yargonaut@^1.1.4:
     figlet "^1.1.1"
     figlet "^1.1.1"
     parent-require "^1.0.0"
     parent-require "^1.0.0"
 
 
-yargs-parser@^11.1.1:
-  version "11.1.1"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
 yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^20.2.9:
 yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^20.2.9:
   version "20.2.9"
   version "20.2.9"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
@@ -17344,24 +17088,6 @@ yargs@17.0.1:
     y18n "^5.0.5"
     y18n "^5.0.5"
     yargs-parser "^20.2.2"
     yargs-parser "^20.2.2"
 
 
-yargs@^12.0.5:
-  version "12.0.5"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
-  integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
-  dependencies:
-    cliui "^4.0.0"
-    decamelize "^1.2.0"
-    find-up "^3.0.0"
-    get-caller-file "^1.0.1"
-    os-locale "^3.0.0"
-    require-directory "^2.1.1"
-    require-main-filename "^1.0.1"
-    set-blocking "^2.0.0"
-    string-width "^2.0.0"
-    which-module "^2.0.0"
-    y18n "^3.2.1 || ^4.0.0"
-    yargs-parser "^11.1.1"
-
 yargs@^16.0.0, yargs@^16.2.0:
 yargs@^16.0.0, yargs@^16.2.0:
   version "16.2.0"
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
@@ -17375,6 +17101,19 @@ yargs@^16.0.0, yargs@^16.2.0:
     y18n "^5.0.5"
     y18n "^5.0.5"
     yargs-parser "^20.2.2"
     yargs-parser "^20.2.2"
 
 
+yargs@^17.0.1:
+  version "17.7.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
+  dependencies:
+    cliui "^8.0.1"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.3"
+    y18n "^5.0.5"
+    yargs-parser "^21.1.1"
+
 yargs@^17.3.1, yargs@^17.7.1:
 yargs@^17.3.1, yargs@^17.7.1:
   version "17.7.1"
   version "17.7.1"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"